From 915745226ff1b05c1e038bf4d690e651c454c0e8 Mon Sep 17 00:00:00 2001 From: liuyun Date: Thu, 15 Oct 2026 01:37:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(c4):=20=E8=B4=A8=E6=8E=A7=E6=94=BE=E8=A1=8C?= =?UTF-8?q?=E8=87=AA=E6=A3=80=E9=A1=BA=E5=BA=8F=20+=20=E8=A1=A5=E6=AD=A3?= =?UTF-8?q?=E2=89=A53=E6=AC=A1=E8=BF=9B=E8=B4=A8=E6=8E=A7=E9=87=8D?= =?UTF-8?q?=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/qc/qc.service.spec.ts | 36 +++++++++++++++++++++++++++++++++--- src/qc/qc.service.ts | 21 +++++++++++++-------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/qc/qc.service.spec.ts b/src/qc/qc.service.spec.ts index 33ba2fe..1b9fbff 100644 --- a/src/qc/qc.service.spec.ts +++ b/src/qc/qc.service.spec.ts @@ -41,6 +41,7 @@ function makeService( recordAbnormalUpdate?: ReturnType; recordCorrectionFindMany?: ReturnType; recordCorrectionCreate?: ReturnType; + recordCorrectionCount?: ReturnType; // client.order / elder / plan orderFindMany?: ReturnType; orderFindFirst?: ReturnType; @@ -72,6 +73,7 @@ function makeService( const recordAbnormalUpdate = overrides.recordAbnormalUpdate ?? vi.fn().mockResolvedValue({ id: 'a1' }); const recordCorrectionFindMany = overrides.recordCorrectionFindMany ?? vi.fn().mockResolvedValue([]); const recordCorrectionCreate = overrides.recordCorrectionCreate ?? vi.fn().mockResolvedValue({ id: 'c1' }); + const recordCorrectionCount = overrides.recordCorrectionCount ?? vi.fn().mockResolvedValue(0); const orderFindMany = overrides.orderFindMany ?? vi.fn().mockResolvedValue([]); const orderFindFirst = overrides.orderFindFirst ?? vi.fn().mockResolvedValue(null); @@ -106,7 +108,7 @@ function makeService( findUnique: recordAbnormalFindUnique, update: recordAbnormalUpdate, }, - recordCorrection: { findMany: recordCorrectionFindMany, create: recordCorrectionCreate }, + recordCorrection: { findMany: recordCorrectionFindMany, create: recordCorrectionCreate, count: recordCorrectionCount }, order: { findMany: orderFindMany, findFirst: orderFindFirst }, elder: { findMany: elderFindMany }, plan: { findFirst: planFindFirst }, @@ -138,6 +140,7 @@ function makeService( recordAbnormalUpdate, recordCorrectionFindMany, recordCorrectionCreate, + recordCorrectionCount, orderFindMany, orderFindFirst, elderFindMany, @@ -199,15 +202,26 @@ describe('QcService.pass', () => { expect(qcUpdate).not.toHaveBeenCalled(); }); - it('已处理(非 pending)→ QC_ALREADY_HANDLED(幂等)', async () => { + it('已处理(非 pending)且要素齐全 → QC_ALREADY_HANDLED(幂等)', async () => { const qcFindUnique = vi.fn().mockResolvedValue({ id: 'qc1', recordId: 'r1', qcResult: 'passed' }); - const { service, qcUpdate } = makeService({ qcFindUnique }); + const recordFindUnique = vi.fn().mockResolvedValue(passableRecord); + const { service, qcUpdate } = makeService({ qcFindUnique, recordFindUnique }); await expect(runWithRequestContext(ctx, () => service.pass('qc1'))).rejects.toMatchObject({ response: { code: ErrorCodes.QC_ALREADY_HANDLED }, }); expect(qcUpdate).not.toHaveBeenCalled(); }); + it('已处理且缺要素 → QC_ELEMENT_MISSING(1004) 优先于 QC_ALREADY_HANDLED(顺序①在③前)', async () => { + const qcFindUnique = vi.fn().mockResolvedValue({ id: 'qc1', recordId: 'r1', qcResult: 'passed' }); + const recordFindUnique = vi.fn().mockResolvedValue({ ...passableRecord, checkoutTime: null }); + const { service, qcUpdate } = makeService({ qcFindUnique, recordFindUnique }); + await expect(runWithRequestContext(ctx, () => service.pass('qc1'))).rejects.toMatchObject({ + response: { code: ErrorCodes.QC_ELEMENT_MISSING, message: expect.stringContaining('1004') }, + }); + expect(qcUpdate).not.toHaveBeenCalled(); + }); + it('四要素缺 → QC_ELEMENT_MISSING(1004)', async () => { const qcFindUnique = vi.fn().mockResolvedValue(pendingQc); const recordFindUnique = vi.fn().mockResolvedValue({ ...passableRecord, checkoutTime: null }); @@ -526,9 +540,25 @@ describe('QcService.pendingList(五类聚合)', () => { expect(res.pendingQc.list[0]).toMatchObject({ id: 'qc1', record: { id: 'r1', elderId: 'e1', assigneeId: 'staff1' }, + correctionCount: 0, + focus: false, }); expect(res.pendingQc.list[0].waitMinutes).toBe(60); }); + + it('补正 ≥3 次 → 待放行队列项 focus=true(质控重点)', async () => { + const qcFindMany = vi.fn().mockResolvedValue([ + { id: 'qc1', recordId: 'r1', qcResult: 'pending', createdAt: new Date() }, + ]); + const recordFindUnique = vi.fn().mockResolvedValue(passableRecord); + const recordCorrectionCount = vi.fn().mockResolvedValue(3); + const { service } = makeService({ qcFindMany, recordFindUnique, recordCorrectionCount }); + + const res = await runWithRequestContext(ctx, () => service.pendingList()); + + expect(recordCorrectionCount).toHaveBeenCalledWith({ where: { recordId: 'r1' } }); + expect(res.pendingQc.list[0]).toMatchObject({ correctionCount: 3, focus: true }); + }); }); describe('QcService.detail', () => { diff --git a/src/qc/qc.service.ts b/src/qc/qc.service.ts index daa95cf..91c9101 100644 --- a/src/qc/qc.service.ts +++ b/src/qc/qc.service.ts @@ -109,7 +109,10 @@ export class QcService { const pendingQcList = []; for (const qc of pendingQcs) { - const record = await this.prisma.client.record.findUnique({ where: { id: qc.recordId } }); + const [record, correctionCount] = await Promise.all([ + this.prisma.client.record.findUnique({ where: { id: qc.recordId } }), + this.prisma.client.recordCorrection.count({ where: { recordId: qc.recordId } }), + ]); pendingQcList.push({ ...qc, record: record @@ -121,6 +124,8 @@ export class QcService { recordStatus: record.recordStatus, } : null, + correctionCount, + focus: correctionCount >= 3, waitMinutes: Math.max(0, Math.floor((now.getTime() - new Date(qc.createdAt).getTime()) / 60000)), }); } @@ -159,15 +164,12 @@ export class QcService { }; } - /** 放行:自检(幂等 → 四要素 1004 → 必拍 3001 → 自审 4001)→ passed。 */ + /** 放行:自检(四要素 1004 → 必拍 3001 → 幂等 → 自审 4001)→ passed。 */ async pass(id: string) { const ctx = getRequestContext()!; const qc = await this.requireQc(id); - if (qc.qcResult !== 'pending') { - throw new BusinessException(ErrorCodes.QC_ALREADY_HANDLED, '该质控已处理(幂等)', HttpStatus.BAD_REQUEST); - } const record = await this.requireRecord(qc.recordId); - await this.assertPassable(record, ctx.userId); + await this.assertPassable(qc, record, ctx.userId); return this.prisma.client.qc.update({ where: { id }, data: { qcResult: 'passed', qcStaffId: ctx.userId, qcTime: new Date(), passTime: new Date() }, @@ -291,8 +293,8 @@ export class QcService { return record; } - /** 放行自检:四要素(1004)→ 必拍已拍(3001)→ 自审自放行(4001)。 */ - private async assertPassable(record: Record, userId: string) { + /** 放行自检(按序):四要素(1004)→ 必拍已拍(3001)→ 幂等(46002)→ 自审自放行(4001)。 */ + private async assertPassable(qc: Record, record: Record, userId: string) { if (!record.checkinTime || !record.checkoutTime || record.durationMin == null || !record.assigneeId) { throw new BusinessException(ErrorCodes.QC_ELEMENT_MISSING, '留痕缺必填要素(1004)', HttpStatus.BAD_REQUEST); } @@ -312,6 +314,9 @@ export class QcService { } } } + if (qc.qcResult !== 'pending') { + throw new BusinessException(ErrorCodes.QC_ALREADY_HANDLED, '该质控已处理(幂等)', HttpStatus.BAD_REQUEST); + } if (record.createdBy === userId) { throw new BusinessException(ErrorCodes.QC_SELF_REVIEW, '不可自审自放行(4001)', HttpStatus.BAD_REQUEST); }