diff --git a/src/agency/agency.service.spec.ts b/src/agency/agency.service.spec.ts index 24892b8..338eb12 100644 --- a/src/agency/agency.service.spec.ts +++ b/src/agency/agency.service.spec.ts @@ -115,6 +115,35 @@ describe('AgencyService', () => { }); expect(update).not.toHaveBeenCalled(); }); + + it('R7:dto={qcThreshold:null} 过滤 null,创建用默认 24 而非 null', async () => { + const findUnique = vi.fn().mockResolvedValue(null); + const create = vi.fn().mockResolvedValue({ code: 'T001' }); + const update = vi.fn(); + const prisma = { client: { agency: { findUnique, create, update } } }; + const service = new AgencyService(prisma as unknown as PrismaService); + + await runWithRequestContext(ctx, () => service.update({ qcThreshold: null } as never)); + + expect(create).toHaveBeenCalledWith({ + data: { + code: 'T001', + orgName: '', + orgType: '', + creditCode: '', + locCode: '', + locName: '', + svcSource: '', + address: '', + contactName: '', + contactPhone: '', + qcThreshold: 24, + dockingTier: 'B', + newNationalNotify: true, + }, + }); + expect(update).not.toHaveBeenCalled(); + }); }); describe('UpdateAgencyDto.dockingTier', () => { diff --git a/src/agency/agency.service.ts b/src/agency/agency.service.ts index 0d3886b..5508068 100644 --- a/src/agency/agency.service.ts +++ b/src/agency/agency.service.ts @@ -22,6 +22,15 @@ function agencyDefaults(code: string): Record { }; } +/** R7:只保留 dto 中有值的字段,过滤 null/undefined,避免 null 覆盖 DB 默认值。 */ +function definedFields(dto: UpdateAgencyDto): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(dto)) { + if (value !== null && value !== undefined) out[key] = value; + } + return out; +} + /** * 机构(=租户)信息读写。Agency 是 TENANT_FREE 模型,主键 code = 当前租户 code, * 因此读写一律按 `ctx.tenantCode` 定位,而非 BaseCrudService 的 `findById(id)`。 @@ -44,12 +53,13 @@ export class AgencyService { async update(dto: UpdateAgencyDto) { const ctx = getRequestContext()!; const existing = await this.getCurrent(); + const data = definedFields(dto); if (!existing) { return this.prisma.client.agency.create({ - data: { ...agencyDefaults(ctx.tenantCode), ...dto } as any, + data: { ...agencyDefaults(ctx.tenantCode), ...data } as any, }); } - return this.prisma.client.agency.update({ where: { code: ctx.tenantCode }, data: dto }); + return this.prisma.client.agency.update({ where: { code: ctx.tenantCode }, data }); } } diff --git a/src/common/tenant-template.ts b/src/common/tenant-template.ts index b7ea892..21858ea 100644 --- a/src/common/tenant-template.ts +++ b/src/common/tenant-template.ts @@ -179,6 +179,7 @@ export const BUSINESS_ROLES: { code: string; name: string; permissions: string[] Permissions.qc.batchPass, Permissions.abnormal.list, Permissions.abnormal.handle, + Permissions.export.export, ], }, { code: 'leader', name: '组长', permissions: [Permissions.staff.list, Permissions.svcOrg.list] }, diff --git a/src/document/document.service.spec.ts b/src/document/document.service.spec.ts index 96dc8c1..b25cf5f 100644 --- a/src/document/document.service.spec.ts +++ b/src/document/document.service.spec.ts @@ -153,10 +153,10 @@ describe('DocumentService.export', () => { expect(recordItemFindMany).toHaveBeenCalledWith({ where: { recordId: { in: ['r1'] } } }); expect(recordAbnormalFindMany).toHaveBeenCalledWith({ where: { elderId: 'e1' } }); - // 水印:机构名 + 时间 + 操作人 + R4 文案 + // 水印:机构名 + 时间 + 操作人(username)+ R4 文案 expect(res.watermark).toMatchObject({ orgName: '测试机构', - operator: 'u1', + operator: 'admin', notice: EXPORT_NOTICE, }); expect(res.watermark.exportTime).toEqual(expect.any(String)); diff --git a/src/document/document.service.ts b/src/document/document.service.ts index 737cfdd..78a6836 100644 --- a/src/document/document.service.ts +++ b/src/document/document.service.ts @@ -94,7 +94,7 @@ export class DocumentService { watermark: { orgName: agency?.orgName ?? '', exportTime: new Date().toISOString(), - operator: ctx.userId, + operator: ctx.username, notice: EXPORT_NOTICE, }, elder: { diff --git a/src/plan/plan.service.spec.ts b/src/plan/plan.service.spec.ts index b237ee6..29dbdbc 100644 --- a/src/plan/plan.service.spec.ts +++ b/src/plan/plan.service.spec.ts @@ -166,6 +166,36 @@ describe('PlanService', () => { expect(planCreate).not.toHaveBeenCalled(); }); + it('R6:svc_org.photoTier=must 且客户端不传 → plan_item.photoTier=must', async () => { + const svcOrgFindFirst = vi + .fn() + .mockResolvedValue({ id: 'svc1', srcType: 'national', srcId: 'nat1', photoTier: 'must' }); + const planItemCreate = vi.fn().mockResolvedValue({ id: 'pi1' }); + const { service } = makeService({ svcOrgFindFirst, planItemCreate }); + + await runWithRequestContext(ctx, () => service.create({ ...baseDto, items: [{ svcId: 'svc1' }] })); + + expect(planItemCreate).toHaveBeenCalledWith({ + data: expect.objectContaining({ svcId: 'svc1', photoTier: 'must' }), + }); + }); + + it('R6:客户端 photoTier 覆盖 svc_org 默认(可加严)', async () => { + const svcOrgFindFirst = vi + .fn() + .mockResolvedValue({ id: 'svc1', srcType: 'national', srcId: 'nat1', photoTier: 'free' }); + const planItemCreate = vi.fn().mockResolvedValue({ id: 'pi1' }); + const { service } = makeService({ svcOrgFindFirst, planItemCreate }); + + await runWithRequestContext(ctx, () => + service.create({ ...baseDto, items: [{ svcId: 'svc1', photoTier: 'must' }] }), + ); + + expect(planItemCreate).toHaveBeenCalledWith({ + data: expect.objectContaining({ svcId: 'svc1', photoTier: 'must' }), + }); + }); + it('elder 不存在 → ELDER_NOT_FOUND', async () => { const elderFindFirst = vi.fn().mockResolvedValue(null); const { service, planCreate } = makeService({ elderFindFirst }); diff --git a/src/plan/plan.service.ts b/src/plan/plan.service.ts index cedb014..ce89bfa 100644 --- a/src/plan/plan.service.ts +++ b/src/plan/plan.service.ts @@ -126,18 +126,24 @@ export class PlanService { } } - /** 校验每个 svc_id 落本机构 L4,并解析 svc 名称/代码快照。 */ + /** 校验每个 svc_id 落本机构 L4,并解析 svc 名称/代码快照 + photo_tier 默认源。 */ private async resolvePlanItems(items: PlanItemInputDto[]): Promise { return Promise.all( items.map(async (item) => { const svcOrg = await this.requireSvcOrg(item.svcId); const snapshot = await this.resolveSvcSnapshot(svcOrg); - return { ...item, svcName: snapshot.svcName, svcCode: snapshot.svcCode }; + return { + ...item, + svcName: snapshot.svcName, + svcCode: snapshot.svcCode, + // R6:photo_tier 溯源 svc_org(客户端 DTO 仍可覆盖加严)。 + photoTier: item.photoTier ?? svcOrg.photoTier ?? 'free', + }; }), ); } - private async requireSvcOrg(svcId: string): Promise<{ srcType: string; srcId: string }> { + private async requireSvcOrg(svcId: string): Promise<{ srcType: string; srcId: string; photoTier: string }> { const svcOrg = await this.prisma.client.svcOrg.findFirst({ where: { id: svcId } }); if (!svcOrg) { throw new BusinessException(ErrorCodes.SVC_ORG_NOT_FOUND, '机构项目不存在', HttpStatus.NOT_FOUND);