借款出借人增加员工与公司组合数据库唯一约束(09-17-company-loan-principal-repayment)
- 新迁移 20261011010000_loan_lender_employee_unique:(legal_entity_id, employee_id) 唯一,归档计入、无员工出借人不冲突;生产只读排查重复 0 组 - 触到约束时返回原有 loan.employee_lender_conflict,不报 500 或误报请求重复;员工行锁与代码检查保留 - 测试改为断言数据库拒绝模糊旧数据,补空员工共存与归档占用;变异 12 项含去索引与数据库兜底两种运行 - PRD 补本迁移回退说明;独立只读复核无阻断,发布窗口需重跑重复计数(AC59) Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -297,6 +297,9 @@ async function companyLoansRoutes(fastify) {
|
||||
identityVersion: result.identity_version, employeeId: body.employeeId, externalPersonConfirmed: body.externalPersonConfirmed,
|
||||
});
|
||||
return result;
|
||||
}).catch((error) => {
|
||||
if (employeeLenders.isEmployeeLenderUniqueViolation(error)) throw buildBusinessValidationError("loan.employee_lender_conflict", {}, 409);
|
||||
throw error;
|
||||
});
|
||||
return sendData(reply, request, mapLender(fastify.config, item));
|
||||
});
|
||||
|
||||
@@ -32,6 +32,17 @@ async function linkedLenders(db, employeeId, legalEntityId) {
|
||||
include: LENDER_INCLUDE, orderBy: { id: "asc" }, take: 2 });
|
||||
}
|
||||
|
||||
// The database constraint backs the locked check below; a violation reports
|
||||
// the same conflict instead of a generic duplicate or a 500.
|
||||
const EMPLOYEE_LENDER_UNIQUE = "loan_lenders_legal_entity_id_employee_id_key";
|
||||
function isEmployeeLenderUniqueViolation(error) {
|
||||
if (error?.code !== "P2002") return false;
|
||||
const meta = error.meta ?? {};
|
||||
const fields = [meta.target, meta.driverAdapterError?.cause?.constraint?.fields].flat().filter(Boolean).map(String);
|
||||
return fields.includes("employee_id") || fields.includes(EMPLOYEE_LENDER_UNIQUE) ||
|
||||
String(meta.driverAdapterError?.cause?.originalMessage ?? "").includes(EMPLOYEE_LENDER_UNIQUE);
|
||||
}
|
||||
|
||||
async function assertUniqueEmployeeLender(tx, employeeId, legalEntityId, lenderId) {
|
||||
if (!employeeId) return;
|
||||
const links = await linkedLenders(tx, employeeId, legalEntityId);
|
||||
@@ -167,6 +178,7 @@ async function linkEmployeeLender(prisma, config, request, input) {
|
||||
});
|
||||
if (result) return result;
|
||||
} catch (error) {
|
||||
if (isEmployeeLenderUniqueViolation(error)) throw buildBusinessValidationError("loan.employee_lender_conflict", {}, 409);
|
||||
if (error?.code === "P2002") throw buildBusinessValidationError("loan.duplicate_registration", {}, 409);
|
||||
throw error;
|
||||
}
|
||||
@@ -199,6 +211,6 @@ async function refreshEmployeeLender(tx, config, request, id, input) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureEmployeeBankAccess, lockEmployeeIdentities, assertUniqueEmployeeLender,
|
||||
ensureEmployeeBankAccess, lockEmployeeIdentities, assertUniqueEmployeeLender, isEmployeeLenderUniqueViolation,
|
||||
previewEmployeeLender, linkEmployeeLender, refreshEmployeeLender,
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户