feat(c4): 质控异常处理端点(handle_status/handle_result)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
915745226f
commit
ba7f2397d7
@ -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' },
|
||||
];
|
||||
|
||||
/** 按钮资源(纯前端按钮,无独立受控接口)。 */
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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()!;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user