Skip to content

Commit c9c08da

Browse files
committed
feat: 重复创建工单保护
1 parent be6e99d commit c9c08da

File tree

1 file changed

+67
-40
lines changed

1 file changed

+67
-40
lines changed

next/api/src/router/ticket.ts

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -403,66 +403,93 @@ const ticketDuplicateCheckMiddleware: Middleware = async (ctx: Context, next) =>
403403
return;
404404
}
405405

406-
// We need the title from the request body
407-
const title = (ctx.request.body as any)?.title;
408-
if (!title || typeof title !== 'string') {
409-
// Title is required for the check, proceed if missing (validation should catch this later)
410-
console.warn(`[Duplicate Check] Title missing or invalid in request body for user ${currentUser.id}. Skipping check.`);
411-
await next();
412-
return;
413-
}
414-
406+
// Get potential identity sources from the body
407+
const body = ctx.request.body as any;
408+
const title = body?.title;
409+
const customFields = body?.customFields;
410+
411+
let checkType: 'title' | 'customFields' | null = null;
412+
let identityPayload: string | null = null;
413+
let payloadHash: string | null = null;
415414
let duplicateCheckKey: string | null = null;
416-
let titleHash: string | null = null;
417415

418-
if (redis) {
419-
console.log(`[Duplicate Check] Redis client is available for user ${currentUser.id}. Checking for duplicates...`);
416+
// Determine what to base the duplicate check on
417+
if (title && typeof title === 'string' && title.trim() !== '') {
418+
// Use non-empty title for check
419+
checkType = 'title';
420+
identityPayload = title; // Use the title directly for hashing
421+
console.log(`[Duplicate Check] Using TITLE for check: "${title}"`);
422+
} else if (customFields && Array.isArray(customFields) && customFields.length > 0) {
423+
// Fallback to custom fields if title is empty/missing and customFields exist
424+
checkType = 'customFields';
420425
try {
421-
titleHash = crypto.createHash('sha1').update(title).digest('hex');
422-
duplicateCheckKey = `duplicate_check:ticket:${currentUser.id}:${titleHash}`;
423-
console.log(`[Duplicate Check] User: ${currentUser.id}, Title: "${title}", Hash: ${titleHash}, Key: ${duplicateCheckKey}`);
424-
425-
const existingTicketId = await redis.get(duplicateCheckKey);
426-
console.log(`[Duplicate Check] Redis GET result for key ${duplicateCheckKey}: ${existingTicketId}`);
427-
428-
if (existingTicketId) {
429-
console.log(`[Duplicate Check] Duplicate ticket creation detected for user ${currentUser.id} with title hash ${titleHash}. Returning existing ticket ID: ${existingTicketId}`);
430-
ctx.status = 200; // Or maybe 303 See Other? 200 with flag is likely fine.
431-
ctx.body = { id: existingTicketId, duplicated: true };
432-
return; // Return early, do not proceed to create ticket
433-
}
434-
} catch (error: any) {
435-
console.error(`[Duplicate Check] Redis duplicate check failed for user ${currentUser.id}:`, error);
436-
// captureException(error, { extra: { component: 'TicketAPIV2', msg: 'Duplicate check failed', userId: currentUser.id } });
437-
// Fail open: If Redis fails, allow creation.
438-
duplicateCheckKey = null; // Ensure we don't try to set later if GET failed
426+
// Sort by field ID for consistent hashing regardless of submission order
427+
const sortedFields = [...customFields].sort((a, b) => String(a?.field).localeCompare(String(b?.field)));
428+
identityPayload = JSON.stringify(sortedFields);
429+
console.log(`[Duplicate Check] Title is empty/missing. Using CUSTOM FIELDS for check. Sorted JSON: ${identityPayload}`);
430+
} catch (e) {
431+
console.error('[Duplicate Check] Error processing custom fields for hashing:', e);
432+
// Don't perform check if processing fails
433+
identityPayload = null;
434+
checkType = null;
439435
}
440436
} else {
441-
console.warn(`[Duplicate Check] Redis client is not available. Skipping duplicate check for user ${currentUser.id}.`);
437+
// Cannot perform check if neither title nor customFields are usable
438+
console.warn(`[Duplicate Check] No usable title or custom fields found for user ${currentUser.id}. Skipping check.`);
439+
}
440+
441+
// Proceed with check only if we have a basis (checkType is set)
442+
if (checkType && identityPayload) {
443+
payloadHash = crypto.createHash('sha1').update(identityPayload).digest('hex');
444+
duplicateCheckKey = `duplicate_check:ticket:${currentUser.id}:${checkType}:${payloadHash}`;
445+
446+
if (redis) {
447+
console.log(`[Duplicate Check] Redis client available. Key: ${duplicateCheckKey}, Type: ${checkType}, Hash: ${payloadHash}`);
448+
try {
449+
const existingTicketId = await redis.get(duplicateCheckKey);
450+
console.log(`[Duplicate Check] Redis GET result for key ${duplicateCheckKey}: ${existingTicketId}`);
451+
452+
if (existingTicketId) {
453+
console.log(`[Duplicate Check] Duplicate ticket detected based on ${checkType} for user ${currentUser.id} with hash ${payloadHash}. Returning existing ID: ${existingTicketId}`);
454+
ctx.status = 200;
455+
ctx.body = { id: existingTicketId, duplicated: true };
456+
return; // Return early
457+
}
458+
} catch (error: any) {
459+
console.error(`[Duplicate Check] Redis GET failed for user ${currentUser.id}, key ${duplicateCheckKey}:`, error);
460+
// Fail open: If Redis GET fails, allow creation.
461+
duplicateCheckKey = null; // Prevent SET attempt later
462+
}
463+
} else {
464+
console.warn(`[Duplicate Check] Redis client not available. Skipping Redis GET/SET for user ${currentUser.id}.`);
465+
duplicateCheckKey = null; // Prevent SET attempt later
466+
}
442467
}
443468

444469
// Proceed to the actual ticket creation handler
445470
await next();
446471

447472
// ---- After ticket creation ----
448-
// Check if ticket creation was successful and we have an ID to store
449473
const createdTicketId = (ctx.body as any)?.id;
450-
const wasSuccessful = ctx.status === 201 && createdTicketId; // Assuming 201 Created is success status
451-
452-
if (wasSuccessful && redis && duplicateCheckKey) {
453-
console.log(`[Duplicate Set] Ticket creation successful (ID: ${createdTicketId}). Storing duplicate check key after creation. Key: ${duplicateCheckKey}, Ticket ID: ${createdTicketId}, TTL: ${DUPLICATE_CHECK_TTL_SECONDS}s`);
474+
const wasSuccessful = ctx.status === 201 && createdTicketId;
475+
476+
// Store in Redis only if:
477+
// 1. Ticket creation was successful
478+
// 2. We successfully generated a duplicateCheckKey earlier (based on title or customFields)
479+
// 3. Redis client is available
480+
if (wasSuccessful && duplicateCheckKey && redis) {
481+
console.log(`[Duplicate Set] Ticket creation successful (ID: ${createdTicketId}). Storing key based on ${checkType}. Key: ${duplicateCheckKey}, TTL: ${DUPLICATE_CHECK_TTL_SECONDS}s`);
454482
try {
455483
const setResult = await redis.set(duplicateCheckKey, createdTicketId, 'EX', DUPLICATE_CHECK_TTL_SECONDS);
456484
console.log(`[Duplicate Set] Redis SET result for key ${duplicateCheckKey}: ${setResult}`);
457485
} catch (error: any) {
458-
console.error(`[Duplicate Set] Redis set after creation failed for ticket ${createdTicketId}:`, error);
459-
// captureException(error, { extra: { component: 'TicketAPIV2', msg: 'Set duplicate key failed', ticketId: createdTicketId } });
486+
console.error(`[Duplicate Set] Redis SET failed for ticket ${createdTicketId}, key ${duplicateCheckKey}:`, error);
460487
// Log error but don't block response
461488
}
462489
} else if (wasSuccessful && !redis) {
463-
console.warn(`[Duplicate Set] Redis client not available. Cannot store duplicate check key for new ticket ${createdTicketId}.`);
490+
console.warn(`[Duplicate Set] Redis client not available. Cannot store duplicate check key for new ticket ${createdTicketId}.`);
464491
} else if (wasSuccessful && !duplicateCheckKey) {
465-
console.warn(`[Duplicate Set] Duplicate check key was not generated (likely due to earlier Redis error). Cannot store for new ticket ${createdTicketId}.`);
492+
console.warn(`[Duplicate Set] Duplicate check key was not generated/used. Cannot store for new ticket ${createdTicketId}.`);
466493
}
467494
};
468495

0 commit comments

Comments
 (0)