|
|
import { QUOTE_ID_PREFIX } from "@/lib/constants/quote";
|
|
|
import { prisma } from "@/lib/prisma";
|
|
|
import { writeAudit } from "@/modules/audit/service";
|
|
|
import { QuoteIdConflictError } from "@/modules/quote/types";
|
|
|
|
|
|
function formatDatePart(date: Date): string {
|
|
|
const y = date.getFullYear();
|
|
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
|
const d = String(date.getDate()).padStart(2, "0");
|
|
|
return `${y}${m}${d}`;
|
|
|
}
|
|
|
|
|
|
/** QTE_YYYYMMDD_NNNN 格式,DB 序号保证唯一性 */
|
|
|
export async function generateQuoteId(): Promise<string> {
|
|
|
const datePart = formatDatePart(new Date());
|
|
|
const prefix = `${QUOTE_ID_PREFIX}_${datePart}_`;
|
|
|
|
|
|
const latest = await prisma.quoteRecord.findFirst({
|
|
|
where: { quoteId: { startsWith: prefix } },
|
|
|
orderBy: { quoteId: "desc" },
|
|
|
select: { quoteId: true },
|
|
|
});
|
|
|
|
|
|
let seq = 1;
|
|
|
if (latest?.quoteId) {
|
|
|
const suffix = latest.quoteId.slice(prefix.length);
|
|
|
const parsed = Number.parseInt(suffix, 10);
|
|
|
if (!Number.isNaN(parsed)) {
|
|
|
seq = parsed + 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return `${prefix}${String(seq).padStart(4, "0")}`;
|
|
|
}
|
|
|
|
|
|
/** task-109:quote_id 唯一冲突 → 写 audit + 抛 INTERNAL_ERROR */
|
|
|
export async function handleQuoteIdConflict(
|
|
|
quoteId: string,
|
|
|
actorId: string,
|
|
|
): Promise<never> {
|
|
|
await writeAudit("QUOTE_ID_CONFLICT", actorId, quoteId, {
|
|
|
quoteId,
|
|
|
message: "quote_id 唯一键冲突",
|
|
|
});
|
|
|
throw new QuoteIdConflictError("系统繁忙,请稍后重试");
|
|
|
}
|
|
|
|
|
|
/** 检测 Prisma P2002 是否为 quote_id 冲突 */
|
|
|
export function isQuoteIdUniqueConflict(error: unknown): boolean {
|
|
|
const prismaError = error as { code?: string; meta?: { target?: string[] } };
|
|
|
if (prismaError.code !== "P2002") {
|
|
|
return false;
|
|
|
}
|
|
|
const target = prismaError.meta?.target;
|
|
|
return (
|
|
|
Array.isArray(target) &&
|
|
|
(target.includes("quote_id") || target.includes("quoteId"))
|
|
|
);
|
|
|
}
|