diff --git a/services/api/src/uploads/uploads.service.ts b/services/api/src/uploads/uploads.service.ts index dc72c80..ca1b8d3 100644 --- a/services/api/src/uploads/uploads.service.ts +++ b/services/api/src/uploads/uploads.service.ts @@ -594,8 +594,8 @@ export class UploadsService { const earliest = new Map(); for (const row of rows) { // bytes>0 的行只会经 complete 落定(同事务写 uploadedAt);null 仅理论 - // 形状(直改库等),跳过不参与映射——谓词命中与否不受影响(uploadedAt - // 只是回显元数据)。 + // 形状(直改库等,bytes>0 且 uploadedAt=null),跳过不参与映射——该形状 + // 下预检可能漏报而 init 仍会 409(偏差仅限于此理论形状,正常写路径不可达)。 if (row.uploadedAt === null) { continue; } diff --git a/services/api/test/uploads.e2e-spec.ts b/services/api/test/uploads.e2e-spec.ts index 39757fb..31ae24a 100644 --- a/services/api/test/uploads.e2e-spec.ts +++ b/services/api/test/uploads.e2e-spec.ts @@ -1117,6 +1117,46 @@ describe('uploads duplicates check (e2e)', () => { expect(res.body.duplicates).toEqual([]); }); + it('同 bytes 多条可见命中取最早 uploadedAt(force 允许的重复行)', async () => { + const bytes = 555_555; + await seedPhoto(memberAId, bytes, { + uploadedAt: new Date('2026-09-25T08:00:00.000Z'), + }); + await seedPhoto(memberAId, bytes, { + uploadedAt: new Date('2026-09-10T08:00:00.000Z'), // 更早,应回显此值 + }); + const res = await check(memberAToken, { + albumId, + items: [item('e.jpg', bytes)], + }); + expect(res.status).toBe(200); + expect(res.body.duplicates).toEqual([ + { index: 0, bytes, uploadedAt: '2026-09-10T08:00:00.000Z' }, + ]); + }); + + it('uploadEnabled=false:预检仍 200 且照常命中(只读预览刻意不校验开关;init 入队时仍 409 兜底)', async () => { + await prisma.albums.update({ + where: { id: albumId }, + data: { uploadEnabled: false }, + }); + try { + const res = await check(memberAToken, { + albumId, + items: [item('a.jpg', BYTES_A)], + }); + expect(res.status).toBe(200); + expect(res.body.duplicates).toEqual([ + { index: 0, bytes: BYTES_A, uploadedAt: UPLOADED_AT_A }, + ]); + } finally { + await prisma.albums.update({ + where: { id: albumId }, + data: { uploadEnabled: true }, + }); + } + }); + it('非成员 → 404 ALBUM_NOT_FOUND(防枚举,同 init)', async () => { const res = await check(outsiderToken, { albumId,