fix: 完善资料审核与 Worker 状态一致性

This commit is contained in:
root
2026-09-15 08:30:39 +08:00
parent f455552e92
commit ec573b5ae2
10 changed files with 639 additions and 101 deletions
+25
View File
@@ -129,6 +129,31 @@ describe("bottles with real PostgreSQL", () => {
expect((await prisma.dailyUsage.findFirstOrThrow()).bottlesCreated).toBe(1);
});
it("returns idempotency conflict for a reused key with different normalized content", async () => {
const key = randomUUID();
const first = await create(key, " same content ").expect(201);
const retry = await create(key, "same content").expect(201);
expect(retry.body.data.id).toBe(first.body.data.id);
const conflict = await create(key, "different content").expect(409);
expect(conflict.body.code).toBe("IDEMPOTENCY_CONFLICT");
expect(await prisma.bottle.count()).toBe(1);
expect((await prisma.dailyUsage.findFirstOrThrow()).bottlesCreated).toBe(1);
});
it("allows one concurrent writer and conflicts the other for different content on one key", async () => {
const key = randomUUID();
const results = await Promise.all([
create(key, "first contender"),
create(key, "second contender"),
]);
expect(results.map((result) => result.status).sort()).toEqual([201, 409]);
expect(results.find((result) => result.status === 409)?.body.code).toBe(
"IDEMPOTENCY_CONFLICT",
);
expect(await prisma.bottle.count()).toBe(1);
expect((await prisma.dailyUsage.findFirstOrThrow()).bottlesCreated).toBe(1);
});
it("atomically permits exactly ten of twelve concurrent requests", async () => {
const results = await Promise.all(
Array.from({ length: 12 }, (_, index) =>
+22 -6
View File
@@ -16,22 +16,24 @@ export class BottleService {
authorId_clientRequestId: { authorId, clientRequestId: requestId },
},
});
if (existing) return existing;
if (existing) return this.resolveIdempotent(existing, contentText);
try {
return await this.prisma.$transaction(async (tx) => {
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${`${authorId}:${requestId}`}, 0))`;
const duplicate = await tx.bottle.findUnique({
where: {
authorId_clientRequestId: { authorId, clientRequestId: requestId },
},
});
if (duplicate) return duplicate;
if (duplicate) return this.resolveIdempotent(duplicate, contentText);
const now = new Date();
const sanctioned = await tx.sanction.findFirst({
where: {
accountId: authorId,
type: { in: ["SUSPENSION", "BAN"] },
revokedAt: null,
startsAt: { lte: new Date() },
OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
startsAt: { lte: now },
OR: [{ expiresAt: null }, { expiresAt: { gt: now } }],
},
select: { id: true },
});
@@ -41,7 +43,7 @@ export class BottleService {
"Account sanctioned",
HttpStatus.FORBIDDEN,
);
const usageDate = utc8UsageDate(new Date());
const usageDate = utc8UsageDate(now);
const rows = await tx.$queryRaw<Array<{ bottles_created: number }>>`
INSERT INTO "daily_usage" ("id", "account_id", "usage_date", "bottles_created", "updated_at")
VALUES (gen_random_uuid(), ${authorId}::uuid, ${usageDate}::date, 1, now())
@@ -89,16 +91,30 @@ export class BottleService {
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === "P2002"
) {
return this.prisma.bottle.findUniqueOrThrow({
const duplicate = await this.prisma.bottle.findUniqueOrThrow({
where: {
authorId_clientRequestId: { authorId, clientRequestId: requestId },
},
});
return this.resolveIdempotent(duplicate, contentText);
}
throw error;
}
}
private resolveIdempotent<T extends { contentText: string }>(
bottle: T,
contentText: string,
): T {
if (bottle.contentText !== contentText)
throw new DomainException(
ErrorCode.IDEMPOTENCY_CONFLICT,
"Idempotency key reused with different content",
HttpStatus.CONFLICT,
);
return bottle;
}
async get(authorId: string, id: string) {
const bottle = await this.prisma.bottle.findFirst({
where: { id, authorId },