fix: 非事务多写包裹 \$transaction(plan/elder-svc/record/abnormal/qc)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6ea9ad097b
commit
292300c344
@ -30,16 +30,17 @@ function makeService(
|
|||||||
const orderUpdate = overrides.orderUpdate ?? vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'not_met' });
|
const orderUpdate = overrides.orderUpdate ?? vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'not_met' });
|
||||||
const orderFindUnique = overrides.orderFindUnique ?? vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'assigned' });
|
const orderFindUnique = overrides.orderFindUnique ?? vi.fn().mockResolvedValue({ id: 'o1', orderStatus: 'assigned' });
|
||||||
|
|
||||||
const prisma = {
|
const client: Record<string, any> = {
|
||||||
client: {
|
recordAbnormal: {
|
||||||
recordAbnormal: {
|
create: recordAbnormalCreate,
|
||||||
create: recordAbnormalCreate,
|
findMany: recordAbnormalFindMany,
|
||||||
findMany: recordAbnormalFindMany,
|
count: recordAbnormalCount,
|
||||||
count: recordAbnormalCount,
|
|
||||||
},
|
|
||||||
order: { update: orderUpdate, findUnique: orderFindUnique },
|
|
||||||
},
|
},
|
||||||
|
order: { update: orderUpdate, findUnique: orderFindUnique },
|
||||||
};
|
};
|
||||||
|
// $transaction:交互式回调以 client 自身作为 tx(事务内写仍命中同一 mock)。
|
||||||
|
client.$transaction = vi.fn(async (arg: any) => (typeof arg === 'function' ? arg(client) : Promise.all(arg)));
|
||||||
|
const prisma = { client };
|
||||||
const service = new AbnormalService(prisma as unknown as PrismaService);
|
const service = new AbnormalService(prisma as unknown as PrismaService);
|
||||||
return { service, recordAbnormalCreate, recordAbnormalFindMany, recordAbnormalCount, orderUpdate, orderFindUnique };
|
return { service, recordAbnormalCreate, recordAbnormalFindMany, recordAbnormalCount, orderUpdate, orderFindUnique };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,35 +39,40 @@ export class AbnormalService {
|
|||||||
this.assertNotMetEvidence(dto);
|
this.assertNotMetEvidence(dto);
|
||||||
// 扑空:工单须存在,再转 not_met(原因 + 到访时间)。
|
// 扑空:工单须存在,再转 not_met(原因 + 到访时间)。
|
||||||
await this.requireOrder(dto.orderId!);
|
await this.requireOrder(dto.orderId!);
|
||||||
await this.prisma.client.order.update({
|
|
||||||
where: { id: dto.orderId! },
|
|
||||||
data: {
|
|
||||||
orderStatus: 'not_met',
|
|
||||||
// not_met_reason 为 VarChar(50),abnormalDesc 为 Text,须截断避免超长 500。
|
|
||||||
notMetReason: (dto.abnormalDesc ?? '').slice(0, 50),
|
|
||||||
notMetAt: toDateTime(dto.visitTime) ?? new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await this.prisma.client.recordAbnormal.create({
|
// 扑空的「工单转 not_met + 写 record_abnormal」须原子落库,防止工单已转 not_met 而异常未写入。
|
||||||
data: {
|
const created = await this.prisma.client.$transaction(async (tx) => {
|
||||||
tenantCode,
|
if (dto.abnormalType === NOT_MET) {
|
||||||
elderId: dto.elderId,
|
await tx.order.update({
|
||||||
reporterId,
|
where: { id: dto.orderId! },
|
||||||
createdBy,
|
data: {
|
||||||
abnormalType: dto.abnormalType,
|
orderStatus: 'not_met',
|
||||||
abnormalDesc: dto.abnormalDesc ?? null,
|
// not_met_reason 为 VarChar(50),abnormalDesc 为 Text,须截断避免超长 500。
|
||||||
voiceFile: dto.voiceFile ?? null,
|
notMetReason: (dto.abnormalDesc ?? '').slice(0, 50),
|
||||||
hasPhoto: dto.hasPhoto ?? false,
|
notMetAt: toDateTime(dto.visitTime) ?? new Date(),
|
||||||
orderId: dto.orderId ?? null,
|
},
|
||||||
recordId: dto.recordId ?? null,
|
});
|
||||||
visitTime: toDateTime(dto.visitTime) ?? null,
|
}
|
||||||
visitLat: dto.visitLat ?? null,
|
return tx.recordAbnormal.create({
|
||||||
visitLng: dto.visitLng ?? null,
|
data: {
|
||||||
notifyStatus: 'pending',
|
tenantCode,
|
||||||
handleStatus: 'open',
|
elderId: dto.elderId,
|
||||||
},
|
reporterId,
|
||||||
|
createdBy,
|
||||||
|
abnormalType: dto.abnormalType,
|
||||||
|
abnormalDesc: dto.abnormalDesc ?? null,
|
||||||
|
voiceFile: dto.voiceFile ?? null,
|
||||||
|
hasPhoto: dto.hasPhoto ?? false,
|
||||||
|
orderId: dto.orderId ?? null,
|
||||||
|
recordId: dto.recordId ?? null,
|
||||||
|
visitTime: toDateTime(dto.visitTime) ?? null,
|
||||||
|
visitLat: dto.visitLat ?? null,
|
||||||
|
visitLng: dto.visitLng ?? null,
|
||||||
|
notifyStatus: 'pending',
|
||||||
|
handleStatus: 'open',
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
// Prisma Decimal(visit_lat/visit_lng)转字符串,避免经 TransformInterceptor 泄漏为 {s,e,d}。
|
// Prisma Decimal(visit_lat/visit_lng)转字符串,避免经 TransformInterceptor 泄漏为 {s,e,d}。
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -36,19 +36,20 @@ function makeService(
|
|||||||
const elderSvcUpdateMany = overrides.elderSvcUpdateMany ?? vi.fn().mockResolvedValue({ count: 2 });
|
const elderSvcUpdateMany = overrides.elderSvcUpdateMany ?? vi.fn().mockResolvedValue({ count: 2 });
|
||||||
const elderSvcCreate = overrides.elderSvcCreate ?? vi.fn().mockResolvedValue({ id: 'es1' });
|
const elderSvcCreate = overrides.elderSvcCreate ?? vi.fn().mockResolvedValue({ id: 'es1' });
|
||||||
|
|
||||||
const prisma = {
|
const client: Record<string, any> = {
|
||||||
client: {
|
elder: { findFirst: elderFindFirst },
|
||||||
elder: { findFirst: elderFindFirst },
|
svcOrg: { findFirst: svcOrgFindFirst },
|
||||||
svcOrg: { findFirst: svcOrgFindFirst },
|
svcNational: { findFirst: svcNationalFindFirst },
|
||||||
svcNational: { findFirst: svcNationalFindFirst },
|
svcLocal: { findFirst: svcLocalFindFirst },
|
||||||
svcLocal: { findFirst: svcLocalFindFirst },
|
elderSvc: {
|
||||||
elderSvc: {
|
findMany: elderSvcFindMany,
|
||||||
findMany: elderSvcFindMany,
|
updateMany: elderSvcUpdateMany,
|
||||||
updateMany: elderSvcUpdateMany,
|
create: elderSvcCreate,
|
||||||
create: elderSvcCreate,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// $transaction:交互式回调以 client 自身作为 tx(事务内写仍命中同一 mock)。
|
||||||
|
client.$transaction = vi.fn(async (arg: any) => (typeof arg === 'function' ? arg(client) : Promise.all(arg)));
|
||||||
|
const prisma = { client };
|
||||||
const service = new ElderSvcService(prisma as unknown as PrismaService);
|
const service = new ElderSvcService(prisma as unknown as PrismaService);
|
||||||
return {
|
return {
|
||||||
service,
|
service,
|
||||||
|
|||||||
@ -27,33 +27,35 @@ export class ElderSvcService {
|
|||||||
return this.prisma.client.elderSvc.findMany({ where: { elderId } });
|
return this.prisma.client.elderSvc.findMany({ where: { elderId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 整表覆盖保存个人项目库。 */
|
/** 整表覆盖保存个人项目库(软删旧行 + 写新行同一事务原子落库)。 */
|
||||||
async put(elderId: string, dto: PutElderSvcDto) {
|
async put(elderId: string, dto: PutElderSvcDto) {
|
||||||
await this.assertElderExists(elderId);
|
await this.assertElderExists(elderId);
|
||||||
const resolved = await this.resolveSvcItems(dto.svcIds);
|
const resolved = await this.resolveSvcItems(dto.svcIds);
|
||||||
// 软删旧行留档(care_mode 变更/重新保存均重制旧库)。
|
|
||||||
await this.prisma.client.elderSvc.updateMany({
|
|
||||||
where: { elderId },
|
|
||||||
data: { deletedAt: new Date() },
|
|
||||||
});
|
|
||||||
const ctx = getRequestContext()!;
|
const ctx = getRequestContext()!;
|
||||||
const rows = [];
|
return this.prisma.client.$transaction(async (tx) => {
|
||||||
for (const item of resolved) {
|
// 软删旧行留档(care_mode 变更/重新保存均重制旧库)。
|
||||||
rows.push(
|
await tx.elderSvc.updateMany({
|
||||||
await this.prisma.client.elderSvc.create({
|
where: { elderId },
|
||||||
data: {
|
data: { deletedAt: new Date() },
|
||||||
elderId,
|
});
|
||||||
tenantCode: ctx.tenantCode,
|
const rows = [];
|
||||||
svcId: item.svcId,
|
for (const item of resolved) {
|
||||||
svcNameSnapshot: item.svcName,
|
rows.push(
|
||||||
svcCodeSnapshot: item.svcCode,
|
await tx.elderSvc.create({
|
||||||
careMode: dto.careMode,
|
data: {
|
||||||
isActive: true,
|
elderId,
|
||||||
},
|
tenantCode: ctx.tenantCode,
|
||||||
}),
|
svcId: item.svcId,
|
||||||
);
|
svcNameSnapshot: item.svcName,
|
||||||
}
|
svcCodeSnapshot: item.svcCode,
|
||||||
return rows;
|
careMode: dto.careMode,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 校验每个 svc_id 落本机构 L4,并解析名称/代码快照。 */
|
/** 校验每个 svc_id 落本机构 L4,并解析名称/代码快照。 */
|
||||||
|
|||||||
@ -56,21 +56,22 @@ function makeService(
|
|||||||
const planItemCreate = overrides.planItemCreate ?? vi.fn().mockResolvedValue({ id: 'pi1' });
|
const planItemCreate = overrides.planItemCreate ?? vi.fn().mockResolvedValue({ id: 'pi1' });
|
||||||
const planItemFindMany = overrides.planItemFindMany ?? vi.fn().mockResolvedValue([]);
|
const planItemFindMany = overrides.planItemFindMany ?? vi.fn().mockResolvedValue([]);
|
||||||
|
|
||||||
const prisma = {
|
const client: Record<string, any> = {
|
||||||
client: {
|
svcOrg: { findFirst: svcOrgFindFirst },
|
||||||
svcOrg: { findFirst: svcOrgFindFirst },
|
svcNational: { findFirst: svcNationalFindFirst },
|
||||||
svcNational: { findFirst: svcNationalFindFirst },
|
svcLocal: { findFirst: svcLocalFindFirst },
|
||||||
svcLocal: { findFirst: svcLocalFindFirst },
|
elder: { findFirst: elderFindFirst },
|
||||||
elder: { findFirst: elderFindFirst },
|
plan: {
|
||||||
plan: {
|
create: planCreate,
|
||||||
create: planCreate,
|
update: planUpdate,
|
||||||
update: planUpdate,
|
findUnique: planFindUnique,
|
||||||
findUnique: planFindUnique,
|
findMany: planFindMany,
|
||||||
findMany: planFindMany,
|
|
||||||
},
|
|
||||||
planItem: { create: planItemCreate, findMany: planItemFindMany },
|
|
||||||
},
|
},
|
||||||
|
planItem: { create: planItemCreate, findMany: planItemFindMany },
|
||||||
};
|
};
|
||||||
|
// $transaction:交互式回调以 client 自身作为 tx(事务内写仍命中同一 mock)。
|
||||||
|
client.$transaction = vi.fn(async (arg: any) => (typeof arg === 'function' ? arg(client) : Promise.all(arg)));
|
||||||
|
const prisma = { client };
|
||||||
const service = new PlanService(prisma as unknown as PrismaService);
|
const service = new PlanService(prisma as unknown as PrismaService);
|
||||||
return {
|
return {
|
||||||
service,
|
service,
|
||||||
|
|||||||
@ -27,18 +27,25 @@ export class PlanService {
|
|||||||
|
|
||||||
/** 制定服务计划(active)。 */
|
/** 制定服务计划(active)。 */
|
||||||
create(dto: CreatePlanDto) {
|
create(dto: CreatePlanDto) {
|
||||||
return this.buildPlan(dto, { planStatus: 'active' });
|
return this.createPlan(dto, { planStatus: 'active' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 变更服务计划:新建 active 计划并指向旧计划,旧计划转 changed。 */
|
/** 变更服务计划:新建 active 计划并指向旧计划,旧计划转 changed(同一事务原子落库)。 */
|
||||||
async change(dto: ChangePlanDto) {
|
async change(dto: ChangePlanDto) {
|
||||||
const oldPlan = await this.requireExists(dto.changeFromPlanId);
|
const oldPlan = await this.requireExists(dto.changeFromPlanId);
|
||||||
if (oldPlan.elderId !== dto.elderId) {
|
if (oldPlan.elderId !== dto.elderId) {
|
||||||
throw new BusinessException(ErrorCodes.PLAN_NOT_FOUND, '被变更计划不属于该老人', HttpStatus.BAD_REQUEST);
|
throw new BusinessException(ErrorCodes.PLAN_NOT_FOUND, '被变更计划不属于该老人', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
const newPlan = await this.buildPlan(dto, { planStatus: 'active', changeFromPlanId: dto.changeFromPlanId });
|
const { planData, resolved } = await this.preparePlan(dto, {
|
||||||
await this.prisma.client.plan.update({ where: { id: dto.changeFromPlanId }, data: { planStatus: 'changed' } });
|
planStatus: 'active',
|
||||||
return newPlan;
|
changeFromPlanId: dto.changeFromPlanId,
|
||||||
|
});
|
||||||
|
return this.prisma.client.$transaction(async (tx) => {
|
||||||
|
const newPlan = await this.createPlanHead(tx, planData);
|
||||||
|
await this.createPlanItems(tx, newPlan.id, resolved);
|
||||||
|
await tx.plan.update({ where: { id: dto.changeFromPlanId }, data: { planStatus: 'changed' } });
|
||||||
|
return newPlan;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 确认计划:仅写确认时间与老人/家属布尔(不改变 plan_status)。 */
|
/** 确认计划:仅写确认时间与老人/家属布尔(不改变 plan_status)。 */
|
||||||
@ -67,13 +74,23 @@ export class PlanService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 制定/变更共用的建计划流程。 */
|
/** 制定/变更共用的建计划流程。 */
|
||||||
private async buildPlan(dto: CreatePlanDto, opts: { planStatus: string; changeFromPlanId?: string }) {
|
private async createPlan(dto: CreatePlanDto, opts: { planStatus: string; changeFromPlanId?: string }) {
|
||||||
|
const { planData, resolved } = await this.preparePlan(dto, opts);
|
||||||
|
return this.prisma.client.$transaction(async (tx) => {
|
||||||
|
const plan = await this.createPlanHead(tx, planData);
|
||||||
|
await this.createPlanItems(tx, plan.id, resolved);
|
||||||
|
return plan;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 制定/变更共用的前置校验与解析(读操作,事务外执行)。 */
|
||||||
|
private async preparePlan(dto: CreatePlanDto, opts: { planStatus: string; changeFromPlanId?: string }) {
|
||||||
await this.assertElderExists(dto.elderId);
|
await this.assertElderExists(dto.elderId);
|
||||||
const items = dto.items ?? [];
|
const items = dto.items ?? [];
|
||||||
const extraCount = dto.extraItemCount ?? 0;
|
const extraCount = dto.extraItemCount ?? 0;
|
||||||
const resolved = await this.resolvePlanItems(items);
|
const resolved = await this.resolvePlanItems(items);
|
||||||
const { ok, hint } = checkItemCount(dto.careMode, dto.careLevel, items.length, extraCount);
|
const { ok, hint } = checkItemCount(dto.careMode, dto.careLevel, items.length, extraCount);
|
||||||
const plan = await this.createPlanHead({
|
const planData: Record<string, any> = {
|
||||||
elderId: dto.elderId,
|
elderId: dto.elderId,
|
||||||
planPeriodStart: toDateOnly(dto.planPeriodStart),
|
planPeriodStart: toDateOnly(dto.planPeriodStart),
|
||||||
planPeriodEnd: toDateOnly(dto.planPeriodEnd),
|
planPeriodEnd: toDateOnly(dto.planPeriodEnd),
|
||||||
@ -88,26 +105,25 @@ export class PlanService {
|
|||||||
planStatus: opts.planStatus,
|
planStatus: opts.planStatus,
|
||||||
planBasis: dto.planBasis,
|
planBasis: dto.planBasis,
|
||||||
changeFromPlanId: opts.changeFromPlanId,
|
changeFromPlanId: opts.changeFromPlanId,
|
||||||
});
|
};
|
||||||
await this.createPlanItems(plan.id, resolved);
|
return { planData, resolved };
|
||||||
return plan;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 写计划头,注入 tenantCode/createdBy(plan 为租户级模型)。 */
|
/** 写计划头,注入 tenantCode/createdBy(plan 为租户级模型)。 */
|
||||||
private createPlanHead(data: Record<string, any>) {
|
private createPlanHead(tx: any, data: Record<string, any>) {
|
||||||
const ctx = getRequestContext();
|
const ctx = getRequestContext();
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
data.tenantCode = ctx.tenantCode;
|
data.tenantCode = ctx.tenantCode;
|
||||||
data.createdBy = ctx.userId;
|
data.createdBy = ctx.userId;
|
||||||
}
|
}
|
||||||
return this.prisma.client.plan.create({ data } as any);
|
return tx.plan.create({ data } as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 逐个写 plan_item(含 svc 名称/代码快照)。 */
|
/** 逐个写 plan_item(含 svc 名称/代码快照)。 */
|
||||||
private async createPlanItems(planId: string, items: ResolvedPlanItem[]) {
|
private async createPlanItems(tx: any, planId: string, items: ResolvedPlanItem[]) {
|
||||||
const ctx = getRequestContext()!;
|
const ctx = getRequestContext()!;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
await this.prisma.client.planItem.create({
|
await tx.planItem.create({
|
||||||
data: {
|
data: {
|
||||||
planId,
|
planId,
|
||||||
tenantCode: ctx.tenantCode,
|
tenantCode: ctx.tenantCode,
|
||||||
|
|||||||
@ -82,37 +82,41 @@ function makeService(
|
|||||||
|
|
||||||
const planChange = overrides.planChange ?? vi.fn().mockResolvedValue({ id: 'p-new' });
|
const planChange = overrides.planChange ?? vi.fn().mockResolvedValue({ id: 'p-new' });
|
||||||
|
|
||||||
|
const client: Record<string, any> = {
|
||||||
|
qc: {
|
||||||
|
findUnique: qcFindUnique,
|
||||||
|
findFirst: qcFindFirst,
|
||||||
|
findMany: qcFindMany,
|
||||||
|
update: qcUpdate,
|
||||||
|
create: qcCreate,
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
findUnique: recordFindUnique,
|
||||||
|
findFirst: recordFindFirst,
|
||||||
|
update: recordUpdate,
|
||||||
|
},
|
||||||
|
recordItem: { findMany: recordItemFindMany },
|
||||||
|
recordPhoto: { findMany: recordPhotoFindMany },
|
||||||
|
recordAbnormal: {
|
||||||
|
findMany: recordAbnormalFindMany,
|
||||||
|
count: recordAbnormalCount,
|
||||||
|
findUnique: recordAbnormalFindUnique,
|
||||||
|
update: recordAbnormalUpdate,
|
||||||
|
},
|
||||||
|
recordCorrection: { findMany: recordCorrectionFindMany, create: recordCorrectionCreate, count: recordCorrectionCount },
|
||||||
|
order: { findMany: orderFindMany, findFirst: orderFindFirst },
|
||||||
|
elder: { findMany: elderFindMany },
|
||||||
|
plan: { findFirst: planFindFirst },
|
||||||
|
};
|
||||||
|
// $transaction:交互式回调以 client 自身作为 tx(事务内写仍命中同一 mock)。
|
||||||
|
client.$transaction = vi.fn(async (arg: any) => (typeof arg === 'function' ? arg(client) : Promise.all(arg)));
|
||||||
|
|
||||||
const prisma = {
|
const prisma = {
|
||||||
raw: {
|
raw: {
|
||||||
record: { findMany: rawRecordFindMany },
|
record: { findMany: rawRecordFindMany },
|
||||||
qc: { findFirst: rawQcFindFirst, create: rawQcCreate },
|
qc: { findFirst: rawQcFindFirst, create: rawQcCreate },
|
||||||
},
|
},
|
||||||
client: {
|
client,
|
||||||
qc: {
|
|
||||||
findUnique: qcFindUnique,
|
|
||||||
findFirst: qcFindFirst,
|
|
||||||
findMany: qcFindMany,
|
|
||||||
update: qcUpdate,
|
|
||||||
create: qcCreate,
|
|
||||||
},
|
|
||||||
record: {
|
|
||||||
findUnique: recordFindUnique,
|
|
||||||
findFirst: recordFindFirst,
|
|
||||||
update: recordUpdate,
|
|
||||||
},
|
|
||||||
recordItem: { findMany: recordItemFindMany },
|
|
||||||
recordPhoto: { findMany: recordPhotoFindMany },
|
|
||||||
recordAbnormal: {
|
|
||||||
findMany: recordAbnormalFindMany,
|
|
||||||
count: recordAbnormalCount,
|
|
||||||
findUnique: recordAbnormalFindUnique,
|
|
||||||
update: recordAbnormalUpdate,
|
|
||||||
},
|
|
||||||
recordCorrection: { findMany: recordCorrectionFindMany, create: recordCorrectionCreate, count: recordCorrectionCount },
|
|
||||||
order: { findMany: orderFindMany, findFirst: orderFindFirst },
|
|
||||||
elder: { findMany: elderFindMany },
|
|
||||||
plan: { findFirst: planFindFirst },
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const service = new QcService(
|
const service = new QcService(
|
||||||
|
|||||||
@ -170,7 +170,7 @@ export class QcService {
|
|||||||
const qc = await this.requireQc(id);
|
const qc = await this.requireQc(id);
|
||||||
const record = await this.requireRecord(qc.recordId);
|
const record = await this.requireRecord(qc.recordId);
|
||||||
await this.assertPassable(qc, record, ctx.userId);
|
await this.assertPassable(qc, record, ctx.userId);
|
||||||
return this.markPassed(id, ctx.userId);
|
return this.markPassed(this.prisma.client, id, ctx.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 退回:幂等守卫 → qc_note 必填 → returned。 */
|
/** 退回:幂等守卫 → qc_note 必填 → returned。 */
|
||||||
@ -260,7 +260,7 @@ export class QcService {
|
|||||||
/** 批量放行:先全量预检(批量边界 + 逐条自检),全过才逐条落库(原子,不部分放行)。 */
|
/** 批量放行:先全量预检(批量边界 + 逐条自检),全过才逐条落库(原子,不部分放行)。 */
|
||||||
async batchPass(dto: BatchPassDto) {
|
async batchPass(dto: BatchPassDto) {
|
||||||
const ctx = getRequestContext()!;
|
const ctx = getRequestContext()!;
|
||||||
const pendingQcs = [];
|
const pendingQcs: Array<Record<string, any>> = [];
|
||||||
// 阶段一:全量预检,任一不过整体抛错,不落任何 pass。
|
// 阶段一:全量预检,任一不过整体抛错,不落任何 pass。
|
||||||
for (const recordId of dto.recordIds) {
|
for (const recordId of dto.recordIds) {
|
||||||
const record = await this.requireRecord(recordId);
|
const record = await this.requireRecord(recordId);
|
||||||
@ -289,18 +289,20 @@ export class QcService {
|
|||||||
await this.assertPassable(qc, record, ctx.userId);
|
await this.assertPassable(qc, record, ctx.userId);
|
||||||
pendingQcs.push(qc);
|
pendingQcs.push(qc);
|
||||||
}
|
}
|
||||||
// 阶段二:全部预检通过后逐条放行。
|
// 阶段二:全部预检通过后,同一事务内逐条放行(原子,不部分放行)。
|
||||||
for (const qc of pendingQcs) {
|
await this.prisma.client.$transaction(async (tx) => {
|
||||||
await this.markPassed(qc.id, ctx.userId);
|
for (const qc of pendingQcs) {
|
||||||
}
|
await this.markPassed(tx, qc.id, ctx.userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
return { passed: dto.recordIds.length };
|
return { passed: dto.recordIds.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 内部 ----
|
// ---- 内部 ----
|
||||||
|
|
||||||
/** 放行落库:qc_result=passed + qc_staff_id + qc_time + pass_time。 */
|
/** 放行落库:qc_result=passed + qc_staff_id + qc_time + pass_time。 */
|
||||||
private markPassed(id: string, userId: string) {
|
private markPassed(tx: any, id: string, userId: string) {
|
||||||
return this.prisma.client.qc.update({
|
return tx.qc.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { qcResult: 'passed', qcStaffId: userId, qcTime: new Date(), passTime: new Date() },
|
data: { qcResult: 'passed', qcStaffId: userId, qcTime: new Date(), passTime: new Date() },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -59,26 +59,27 @@ function makeService(
|
|||||||
const svcNationalFindFirst = overrides.svcNationalFindFirst ?? vi.fn().mockResolvedValue(null);
|
const svcNationalFindFirst = overrides.svcNationalFindFirst ?? vi.fn().mockResolvedValue(null);
|
||||||
const svcLocalFindFirst = overrides.svcLocalFindFirst ?? vi.fn().mockResolvedValue(null);
|
const svcLocalFindFirst = overrides.svcLocalFindFirst ?? vi.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
const prisma = {
|
const client: Record<string, any> = {
|
||||||
client: {
|
order: { findUnique: orderFindUnique },
|
||||||
order: { findUnique: orderFindUnique },
|
staff: { findFirst: staffFindFirst },
|
||||||
staff: { findFirst: staffFindFirst },
|
elder: { findFirst: elderFindFirst, findUnique: elderFindUnique },
|
||||||
elder: { findFirst: elderFindFirst, findUnique: elderFindUnique },
|
record: {
|
||||||
record: {
|
findUnique: recordFindUnique,
|
||||||
findUnique: recordFindUnique,
|
findFirst: recordFindFirst,
|
||||||
findFirst: recordFindFirst,
|
findMany: recordFindMany,
|
||||||
findMany: recordFindMany,
|
create: recordCreate,
|
||||||
create: recordCreate,
|
update: recordUpdate,
|
||||||
update: recordUpdate,
|
|
||||||
},
|
|
||||||
recordItem: { findMany: recordItemFindMany, create: recordItemCreate, deleteMany: recordItemDeleteMany },
|
|
||||||
recordPhoto: { findMany: recordPhotoFindMany, findFirst: recordPhotoFindFirst, create: recordPhotoCreate },
|
|
||||||
recordAbnormal: { findMany: recordAbnormalFindMany },
|
|
||||||
svcOrg: { findFirst: svcOrgFindFirst },
|
|
||||||
svcNational: { findFirst: svcNationalFindFirst },
|
|
||||||
svcLocal: { findFirst: svcLocalFindFirst },
|
|
||||||
},
|
},
|
||||||
|
recordItem: { findMany: recordItemFindMany, create: recordItemCreate, deleteMany: recordItemDeleteMany },
|
||||||
|
recordPhoto: { findMany: recordPhotoFindMany, findFirst: recordPhotoFindFirst, create: recordPhotoCreate },
|
||||||
|
recordAbnormal: { findMany: recordAbnormalFindMany },
|
||||||
|
svcOrg: { findFirst: svcOrgFindFirst },
|
||||||
|
svcNational: { findFirst: svcNationalFindFirst },
|
||||||
|
svcLocal: { findFirst: svcLocalFindFirst },
|
||||||
};
|
};
|
||||||
|
// $transaction:交互式回调以 client 自身作为 tx(事务内写仍命中同一 mock)。
|
||||||
|
client.$transaction = vi.fn(async (arg: any) => (typeof arg === 'function' ? arg(client) : Promise.all(arg)));
|
||||||
|
const prisma = { client };
|
||||||
const storage = {
|
const storage = {
|
||||||
buildKey: vi.fn().mockReturnValue('record/T001/1-a.jpg'),
|
buildKey: vi.fn().mockReturnValue('record/T001/1-a.jpg'),
|
||||||
upload: vi.fn().mockResolvedValue('record/T001/1-a.jpg'),
|
upload: vi.fn().mockResolvedValue('record/T001/1-a.jpg'),
|
||||||
|
|||||||
@ -94,27 +94,29 @@ export class RecordService {
|
|||||||
// 先全量校验(exec_result 枚举 + L4)并解析快照,任一失败不落库。
|
// 先全量校验(exec_result 枚举 + L4)并解析快照,任一失败不落库。
|
||||||
const resolved = await this.resolveItems(dto.items, order);
|
const resolved = await this.resolveItems(dto.items, order);
|
||||||
|
|
||||||
await this.prisma.client.recordItem.deleteMany({ where: { recordId: record.id } });
|
return this.prisma.client.$transaction(async (tx) => {
|
||||||
const rows = [];
|
await tx.recordItem.deleteMany({ where: { recordId: record.id } });
|
||||||
for (const item of resolved) {
|
const rows = [];
|
||||||
rows.push(
|
for (const item of resolved) {
|
||||||
await this.prisma.client.recordItem.create({
|
rows.push(
|
||||||
data: {
|
await tx.recordItem.create({
|
||||||
tenantCode: ctx.tenantCode,
|
data: {
|
||||||
recordId: record.id,
|
tenantCode: ctx.tenantCode,
|
||||||
svcId: item.svcId,
|
recordId: record.id,
|
||||||
svcNameSnapshot: item.svcName,
|
svcId: item.svcId,
|
||||||
execResult: item.execResult,
|
svcNameSnapshot: item.svcName,
|
||||||
undoReason: item.undoReason ?? null,
|
execResult: item.execResult,
|
||||||
extraFlag: item.extraFlag ?? false,
|
undoReason: item.undoReason ?? null,
|
||||||
photoRequired: item.photoRequired,
|
extraFlag: item.extraFlag ?? false,
|
||||||
isRefusedPhoto: item.isRefusedPhoto ?? false,
|
photoRequired: item.photoRequired,
|
||||||
refuseReason: item.refuseReason ?? null,
|
isRefusedPhoto: item.isRefusedPhoto ?? false,
|
||||||
},
|
refuseReason: item.refuseReason ?? null,
|
||||||
}),
|
},
|
||||||
);
|
}),
|
||||||
}
|
);
|
||||||
return rows;
|
}
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 上传照片:≤500KB → MinIO(record 前缀)→ record_photo。 */
|
/** 上传照片:≤500KB → MinIO(record 前缀)→ record_photo。 */
|
||||||
@ -340,63 +342,72 @@ export class RecordService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = [];
|
// 预读各垫付记录的 items/photos(读操作,事务外),再在同一事务内全量写入(原子,不部分结转)。
|
||||||
for (const rec of records) {
|
const snapshots = await Promise.all(
|
||||||
const [items, photos] = await Promise.all([
|
records.map(async (rec) => {
|
||||||
this.prisma.client.recordItem.findMany({ where: { recordId: rec.id } }),
|
const [items, photos] = await Promise.all([
|
||||||
this.prisma.client.recordPhoto.findMany({ where: { recordId: rec.id } }),
|
this.prisma.client.recordItem.findMany({ where: { recordId: rec.id } }),
|
||||||
]);
|
this.prisma.client.recordPhoto.findMany({ where: { recordId: rec.id } }),
|
||||||
const newRecord = await this.prisma.client.record.create({
|
]);
|
||||||
data: {
|
return { rec, items, photos };
|
||||||
tenantCode: ctx.tenantCode,
|
}),
|
||||||
createdBy: ctx.userId,
|
);
|
||||||
orderId: rec.orderId,
|
|
||||||
elderId: rec.elderId,
|
return this.prisma.client.$transaction(async (tx) => {
|
||||||
assigneeId: rec.assigneeId,
|
const created = [];
|
||||||
checkinTime: rec.checkinTime, // 原样复制,不改时间戳
|
for (const { rec, items, photos } of snapshots) {
|
||||||
checkoutTime: rec.checkoutTime,
|
const newRecord = await tx.record.create({
|
||||||
durationMin: rec.durationMin,
|
|
||||||
checkinLat: rec.checkinLat,
|
|
||||||
checkinLng: rec.checkinLng,
|
|
||||||
locateStatus: rec.locateStatus,
|
|
||||||
recordStatus: rec.recordStatus,
|
|
||||||
isPrepaid: false,
|
|
||||||
billable: true,
|
|
||||||
sourcePrepaidOf: rec.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
for (const item of items) {
|
|
||||||
await this.prisma.client.recordItem.create({
|
|
||||||
data: {
|
data: {
|
||||||
tenantCode: ctx.tenantCode,
|
tenantCode: ctx.tenantCode,
|
||||||
recordId: newRecord.id,
|
createdBy: ctx.userId,
|
||||||
svcId: item.svcId,
|
orderId: rec.orderId,
|
||||||
svcNameSnapshot: item.svcNameSnapshot,
|
elderId: rec.elderId,
|
||||||
execResult: item.execResult,
|
assigneeId: rec.assigneeId,
|
||||||
undoReason: item.undoReason,
|
checkinTime: rec.checkinTime, // 原样复制,不改时间戳
|
||||||
extraFlag: item.extraFlag,
|
checkoutTime: rec.checkoutTime,
|
||||||
photoRequired: item.photoRequired,
|
durationMin: rec.durationMin,
|
||||||
isRefusedPhoto: item.isRefusedPhoto,
|
checkinLat: rec.checkinLat,
|
||||||
refuseReason: item.refuseReason,
|
checkinLng: rec.checkinLng,
|
||||||
|
locateStatus: rec.locateStatus,
|
||||||
|
recordStatus: rec.recordStatus,
|
||||||
|
isPrepaid: false,
|
||||||
|
billable: true,
|
||||||
|
sourcePrepaidOf: rec.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
for (const item of items) {
|
||||||
|
await tx.recordItem.create({
|
||||||
|
data: {
|
||||||
|
tenantCode: ctx.tenantCode,
|
||||||
|
recordId: newRecord.id,
|
||||||
|
svcId: item.svcId,
|
||||||
|
svcNameSnapshot: item.svcNameSnapshot,
|
||||||
|
execResult: item.execResult,
|
||||||
|
undoReason: item.undoReason,
|
||||||
|
extraFlag: item.extraFlag,
|
||||||
|
photoRequired: item.photoRequired,
|
||||||
|
isRefusedPhoto: item.isRefusedPhoto,
|
||||||
|
refuseReason: item.refuseReason,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const photo of photos) {
|
||||||
|
await tx.recordPhoto.create({
|
||||||
|
data: {
|
||||||
|
tenantCode: ctx.tenantCode,
|
||||||
|
recordId: newRecord.id,
|
||||||
|
svcId: photo.svcId,
|
||||||
|
filePath: photo.filePath,
|
||||||
|
fileSize: photo.fileSize,
|
||||||
|
visibleToFamily: photo.visibleToFamily,
|
||||||
|
keepUntil: photo.keepUntil,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
created.push(this.serializeRecord(newRecord));
|
||||||
}
|
}
|
||||||
for (const photo of photos) {
|
return created;
|
||||||
await this.prisma.client.recordPhoto.create({
|
});
|
||||||
data: {
|
|
||||||
tenantCode: ctx.tenantCode,
|
|
||||||
recordId: newRecord.id,
|
|
||||||
svcId: photo.svcId,
|
|
||||||
filePath: photo.filePath,
|
|
||||||
fileSize: photo.fileSize,
|
|
||||||
visibleToFamily: photo.visibleToFamily,
|
|
||||||
keepUntil: photo.keepUntil,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
created.push(this.serializeRecord(newRecord));
|
|
||||||
}
|
|
||||||
return created;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 内部校验 ----
|
// ---- 内部校验 ----
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user