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

@@ -61,16 +61,20 @@ export async function fetchActiveNotificationMap(): Promise<
async function insertNotification(
req: NotifyRequest,
operator: { id: string | null; name: string | null },
): Promise<NotificationRecord | { skipped: true }> {
// Check if a non-cancelled notification already exists for this pair
): Promise<NotificationRecord | { skipped: true; existingPlate: string }> {
// Business rule: each current vehicle (suggestion) can have AT MOST ONE
// active intervention at a time. Any non-cancelled record for the same
// suggestion_id blocks further interventions until it is cancelled.
const [existing] = (await pool.execute(
`SELECT id FROM tab_scheduling_notifications
WHERE suggestion_id = ? AND candidate_plate = ? AND status != 'cancelled'
`SELECT id, candidate_plate FROM tab_scheduling_notifications
WHERE suggestion_id = ? AND status != 'cancelled'
LIMIT 1`,
[req.suggestionId, req.candidatePlate],
[req.suggestionId],
)) as [any[], unknown];
if (existing.length > 0) return { skipped: true };
if (existing.length > 0) {
return { skipped: true, existingPlate: existing[0].candidate_plate as string };
}
const [result] = (await pool.execute(
`INSERT INTO tab_scheduling_notifications
@@ -110,7 +114,10 @@ app.post('/', async (c) => {
const result = await insertNotification(body, operator);
if ('skipped' in result) {
return c.json({ success: false, message: '该建议已处理' }, 409);
return c.json(
{ success: false, message: `此车已有干预(候选车 ${result.existingPlate}),请先解除` },
409,
);
}
console.log(