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.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.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.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 { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { ArrayMaxSize, IsArray, IsNotEmpty, IsString } from 'class-validator';
|
import { ArrayMaxSize, IsArray, IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
import { ChangePlanDto } from '../plan/dto.js';
|
import { ChangePlanDto } from '../plan/dto.js';
|
||||||
|
|
||||||
/** 退回质控入参(qc_note 必填)。 */
|
/** 退回质控入参(qc_note 必填)。 */
|
||||||
@ -40,3 +40,17 @@ export class BatchPassDto {
|
|||||||
@ArrayMaxSize(100)
|
@ArrayMaxSize(100)
|
||||||
recordIds!: string[];
|
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 { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||||
import { Permissions } from '../common/permissions.js';
|
import { Permissions } from '../common/permissions.js';
|
||||||
import { AuditLog, RequirePermissions } from '../auth/decorators.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';
|
import { QcService } from './qc.service.js';
|
||||||
|
|
||||||
@Controller('qc')
|
@Controller('qc')
|
||||||
@ -36,6 +36,13 @@ export class QcController {
|
|||||||
return this.service.batchPass(dto);
|
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')
|
@Get(':id')
|
||||||
@RequirePermissions(Permissions.qc.detail)
|
@RequirePermissions(Permissions.qc.detail)
|
||||||
detail(@Param('id') id: string) {
|
detail(@Param('id') id: string) {
|
||||||
|
|||||||
@ -648,3 +648,42 @@ describe('QcService.exceptionLink', () => {
|
|||||||
expect(res).toMatchObject({ id: 'p-new' });
|
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 { BusinessException } from '../common/business-exception.js';
|
||||||
import { ErrorCodes } from '../common/error-codes.js';
|
import { ErrorCodes } from '../common/error-codes.js';
|
||||||
import { PlanService } from '../plan/plan.service.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;
|
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。 */
|
/** 批量放行:安全边界(异常/未执行/加项/时长过短/已补正)任一命中 → QC_BATCH_UNSAFE;否则逐条 pass。 */
|
||||||
async batchPass(dto: BatchPassDto) {
|
async batchPass(dto: BatchPassDto) {
|
||||||
const ctx = getRequestContext()!;
|
const ctx = getRequestContext()!;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user