From ba7f2397d768d0ebc928b6af786fd4ae1819875f Mon Sep 17 00:00:00 2001 From: liuyun Date: Thu, 15 Oct 2026 01:50:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(c4):=20=E8=B4=A8=E6=8E=A7=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86=E7=AB=AF=E7=82=B9=EF=BC=88handle=5F?= =?UTF-8?q?status/handle=5Fresult=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/common/tenant-template.ts | 1 + src/qc/dto.ts | 18 ++++++++++++++-- src/qc/qc.controller.ts | 9 +++++++- src/qc/qc.service.spec.ts | 39 +++++++++++++++++++++++++++++++++++ src/qc/qc.service.ts | 14 ++++++++++++- 5 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/common/tenant-template.ts b/src/common/tenant-template.ts index a1a0f21..19d77a7 100644 --- a/src/common/tenant-template.ts +++ b/src/common/tenant-template.ts @@ -130,6 +130,7 @@ export const APIS: { code: string; name: string; method: string; path: string; p { code: Permissions.qc.exceptionLink, name: '异常联动改计划', method: 'POST', path: '/api/v1/qc/exception/link', parent: 'qc' }, { code: Permissions.qc.correct, name: '补正', method: 'POST', path: '/api/v1/qc/correct', parent: 'qc' }, { code: Permissions.qc.batchPass, name: '批量放行', method: 'POST', path: '/api/v1/qc/batch-pass', parent: 'qc' }, + { code: Permissions.abnormal.handle, name: '异常处理', method: 'POST', path: '/api/v1/qc/abnormal/:id/handle', parent: 'qc' }, ]; /** 按钮资源(纯前端按钮,无独立受控接口)。 */ diff --git a/src/qc/dto.ts b/src/qc/dto.ts index 34d6b51..b587f89 100644 --- a/src/qc/dto.ts +++ b/src/qc/dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMaxSize, IsArray, IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ArrayMaxSize, IsArray, IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { ChangePlanDto } from '../plan/dto.js'; /** 退回质控入参(qc_note 必填)。 */ @@ -40,3 +40,17 @@ export class BatchPassDto { @ArrayMaxSize(100) recordIds!: string[]; } + +/** 异常处理入参:质控员更新 handle_status/handle_result。 */ +export class HandleAbnormalDto { + @ApiProperty({ description: '处理状态', enum: ['handled', 'closed'] }) + @IsString() + @IsNotEmpty() + @IsIn(['handled', 'closed']) + handleStatus!: string; + + @ApiPropertyOptional({ description: '处理结果' }) + @IsOptional() + @IsString() + handleResult?: string; +} diff --git a/src/qc/qc.controller.ts b/src/qc/qc.controller.ts index fa725c7..7856c62 100644 --- a/src/qc/qc.controller.ts +++ b/src/qc/qc.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { Permissions } from '../common/permissions.js'; import { AuditLog, RequirePermissions } from '../auth/decorators.js'; -import { BatchPassDto, CorrectDto, ExceptionLinkDto, ReturnQcDto } from './dto.js'; +import { BatchPassDto, CorrectDto, ExceptionLinkDto, HandleAbnormalDto, ReturnQcDto } from './dto.js'; import { QcService } from './qc.service.js'; @Controller('qc') @@ -36,6 +36,13 @@ export class QcController { return this.service.batchPass(dto); } + @Post('abnormal/:id/handle') + @RequirePermissions(Permissions.abnormal.handle) + @AuditLog('qc', 'abnormal-handle') + handleAbnormal(@Param('id') id: string, @Body() dto: HandleAbnormalDto) { + return this.service.handleAbnormal(id, dto); + } + @Get(':id') @RequirePermissions(Permissions.qc.detail) detail(@Param('id') id: string) { diff --git a/src/qc/qc.service.spec.ts b/src/qc/qc.service.spec.ts index 1b9fbff..829c5f5 100644 --- a/src/qc/qc.service.spec.ts +++ b/src/qc/qc.service.spec.ts @@ -648,3 +648,42 @@ describe('QcService.exceptionLink', () => { expect(res).toMatchObject({ id: 'p-new' }); }); }); + +describe('QcService.handleAbnormal', () => { + it('abnormal 不存在 → ABNORMAL_NOT_FOUND', async () => { + const { service, recordAbnormalUpdate } = makeService(); + await expect( + runWithRequestContext(ctx, () => service.handleAbnormal('missing', { handleStatus: 'handled' })), + ).rejects.toMatchObject({ response: { code: ErrorCodes.ABNORMAL_NOT_FOUND } }); + expect(recordAbnormalUpdate).not.toHaveBeenCalled(); + }); + + it('处理 → 更新 handle_status/handle_result', async () => { + const recordAbnormalFindUnique = vi.fn().mockResolvedValue({ id: 'a1', handleStatus: 'open' }); + const recordAbnormalUpdate = vi.fn().mockResolvedValue({ id: 'a1' }); + const { service } = makeService({ recordAbnormalFindUnique, recordAbnormalUpdate }); + + await runWithRequestContext(ctx, () => + service.handleAbnormal('a1', { handleStatus: 'closed', handleResult: '已通知家属' }), + ); + + expect(recordAbnormalFindUnique).toHaveBeenCalledWith({ where: { id: 'a1' } }); + expect(recordAbnormalUpdate).toHaveBeenCalledWith({ + where: { id: 'a1' }, + data: { handleStatus: 'closed', handleResult: '已通知家属' }, + }); + }); + + it('handleResult 缺省 → 更新为 null', async () => { + const recordAbnormalFindUnique = vi.fn().mockResolvedValue({ id: 'a1', handleStatus: 'open' }); + const recordAbnormalUpdate = vi.fn().mockResolvedValue({ id: 'a1' }); + const { service } = makeService({ recordAbnormalFindUnique, recordAbnormalUpdate }); + + await runWithRequestContext(ctx, () => service.handleAbnormal('a1', { handleStatus: 'handled' })); + + expect(recordAbnormalUpdate).toHaveBeenCalledWith({ + where: { id: 'a1' }, + data: { handleStatus: 'handled', handleResult: null }, + }); + }); +}); diff --git a/src/qc/qc.service.ts b/src/qc/qc.service.ts index 91c9101..dd96bfb 100644 --- a/src/qc/qc.service.ts +++ b/src/qc/qc.service.ts @@ -5,7 +5,7 @@ import { getRequestContext } from '../common/context/request-context.js'; import { BusinessException } from '../common/business-exception.js'; import { ErrorCodes } from '../common/error-codes.js'; import { PlanService } from '../plan/plan.service.js'; -import { BatchPassDto, CorrectDto, ExceptionLinkDto, ReturnQcDto } from './dto.js'; +import { BatchPassDto, CorrectDto, ExceptionLinkDto, HandleAbnormalDto, ReturnQcDto } from './dto.js'; const DAY_MS = 24 * 60 * 60 * 1000; /** 批量放行安全边界的未执行枚举。 */ @@ -241,6 +241,18 @@ export class QcService { }); } + /** 异常处理:质控员更新 handle_status/handle_result。 */ + async handleAbnormal(id: string, dto: HandleAbnormalDto) { + const abnormal = await this.prisma.client.recordAbnormal.findUnique({ where: { id } }); + if (!abnormal) { + throw new BusinessException(ErrorCodes.ABNORMAL_NOT_FOUND, '异常上报不存在', HttpStatus.NOT_FOUND); + } + return this.prisma.client.recordAbnormal.update({ + where: { id }, + data: { handleStatus: dto.handleStatus, handleResult: dto.handleResult ?? null }, + }); + } + /** 批量放行:安全边界(异常/未执行/加项/时长过短/已补正)任一命中 → QC_BATCH_UNSAFE;否则逐条 pass。 */ async batchPass(dto: BatchPassDto) { const ctx = getRequestContext()!;