fix(scheduling): enforce one active intervention per current vehicle

Business rule: a running vehicle can hold AT MOST ONE active (sent|executed)
intervention. Switching to a different candidate requires cancelling the
prior one first.

- Server: insertNotification dedup key changes from (suggestion_id,
  candidate_plate) to just suggestion_id; 409 response includes the blocking
  candidate plate
- Detail modal: shows a banner naming the locked candidate; non-active
  candidates render a disabled "该车已有其他干预,请先解除" hint instead
  of the action button
- Batch: pickBestCandidate returns null for any suggestion already holding
  an active intervention — the whole suggestion is excluded

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-17 09:19:43 +08:00
parent 210db7f8ff
commit 1b2ad68743
3 changed files with 56 additions and 23 deletions

View File

@@ -147,10 +147,13 @@ function SkeletonPage() {
}
function pickBestCandidate(s: SchedulingSuggestion): CandidateVehicle | null {
// Prefer a candidate that can qualify and isn't already notified
const available = s.candidates.filter(c => !c.notificationStatus || c.notificationStatus === 'cancelled');
if (available.length === 0) return null;
return available.find(c => c.canQualifyAfterSwap) ?? available[0];
// Business rule: at most one active intervention per suggestion. If ANY
// candidate is already intervened, skip the whole suggestion in batch flow.
const hasActive = s.candidates.some(
c => c.notificationStatus === 'sent' || c.notificationStatus === 'executed',
);
if (hasActive) return null;
return s.candidates.find(c => c.canQualifyAfterSwap) ?? s.candidates[0] ?? null;
}
export default function SchedulingModule() {