实现企业银行账户与建行制单(corporate-bank-account-payment-order)

这个提交包含在:
2026-08-02 23:36:40 +08:00
父节点 f946d64539
当前提交 5261d2849b
共修改 46 个文件,包含 2751 行新增和 73 行删除
+197
查看文件
@@ -0,0 +1,197 @@
const XLSX = require("xlsx");
const { decryptStringOrThrow } = require("./crypto");
const HEADERS = Object.freeze([
"*序号",
"*付款方客户账号",
"*付款方账户名称",
"*收款方行别代码\n(01-本行 02-国内他行)",
"*收款方客户账号",
"*收款方账户名称",
"收款方开户行名称",
"收款方联行号",
"客户方流水号",
"*金额",
"*用途",
"备注",
"是否短信通知收款人(0-不通知,1-通知,默认为0-不通知。)",
"收款人手机号码",
"短信通知附加信息",
]);
class PaymentOrderXlsError extends Error {
constructor(code, details = {}) {
super(code);
this.name = "PaymentOrderXlsError";
this.code = code;
this.details = details;
}
}
function buildCustomerSerial(batchNo, rowNumber) {
const serial = `${String(batchNo).trim()}-${String(rowNumber).padStart(3, "0")}`;
if (serial.length > 32) {
throw new PaymentOrderXlsError("customer_serial_too_long");
}
if (!/^[A-Z0-9-]+$/.test(serial)) {
throw new PaymentOrderXlsError("customer_serial_invalid_characters");
}
return serial;
}
function requiredText(value, code) {
const normalized = typeof value === "string" ? value.trim() : "";
if (!normalized) throw new PaymentOrderXlsError(code);
return normalized;
}
function decryptRequired(config, value, code) {
try {
return requiredText(value ? decryptStringOrThrow(config, value) : "", code);
} catch (error) {
if (error instanceof PaymentOrderXlsError) throw error;
throw new PaymentOrderXlsError(code);
}
}
function stringCell(value = "") {
return { t: "s", v: String(value) };
}
function buildPaymentOrderRows(config, batch) {
const blockers = [];
const addBlocker = (code, entityCode) => {
blockers.push({ code, ...(entityCode ? { entityCode: String(entityCode) } : {}) });
};
const payer = batch?.payment_account_snapshot_json;
if (!payer || typeof payer !== "object" || Array.isArray(payer)) {
addBlocker("payment_account_snapshot_missing");
}
if (payer && !payer.bankInstitutionCode) {
addBlocker("payer_bank_institution_missing");
} else if (payer && payer.bankInstitutionCode !== "CCB") {
addBlocker("payer_bank_not_supported");
}
let payerAccount = "";
let payerName = "";
if (payer) {
try {
payerAccount = decryptRequired(config, payer.accountNumberEncrypted, "payer_account_unavailable");
} catch (error) {
addBlocker(error.code ?? "payer_account_unavailable");
}
try {
payerName = requiredText(payer.accountName, "payer_name_missing");
} catch (error) {
addBlocker(error.code ?? "payer_name_missing");
}
}
const items = [...(batch.items ?? [])].sort((left, right) =>
String(left.entity_code).localeCompare(String(right.entity_code), "zh-CN")
|| String(left.id).localeCompare(String(right.id)),
);
if (!items.length) addBlocker("items_missing");
const serials = new Set();
let totalAmount = 0;
const preparedRows = items.map((item, index) => {
const payee = item.snapshot_json?.payee;
const entityCodeHint = typeof item.entity_code === "string" && item.entity_code.trim()
? item.entity_code.trim()
: undefined;
if (!payee?.bankInstitutionCode) {
addBlocker("payee_bank_institution_missing", entityCodeHint);
} else if (payee.bankInstitutionCode !== "CCB") {
addBlocker("payee_bank_not_supported", entityCodeHint);
}
let payeeAccount = "";
let payeeName = "";
let entityCode = "";
try {
payeeAccount = decryptRequired(config, payee?.bankAccountEncrypted, "payee_account_unavailable");
} catch (error) {
addBlocker(error.code ?? "payee_account_unavailable", entityCodeHint);
}
try {
payeeName = requiredText(payee?.name, "payee_name_missing");
} catch (error) {
addBlocker(error.code ?? "payee_name_missing", entityCodeHint);
}
try {
entityCode = requiredText(item.entity_code, "entity_code_missing");
} catch (error) {
addBlocker(error.code ?? "entity_code_missing");
}
const amount = Number(item.amount);
if (!Number.isFinite(amount) || amount <= 0) {
addBlocker("amount_invalid", entityCodeHint);
}
let serial = "";
try {
serial = buildCustomerSerial(batch.batch_no, index + 1);
if (serials.has(serial)) addBlocker("customer_serial_duplicate", entityCodeHint);
serials.add(serial);
} catch (error) {
addBlocker(error.code ?? "customer_serial_invalid", entityCodeHint);
}
if (Number.isFinite(amount) && amount > 0) totalAmount += amount;
return { payee, payeeAccount, payeeName, entityCode, amount, serial };
});
if (blockers.length) {
throw new PaymentOrderXlsError(blockers[0].code, { blockers });
}
const rows = preparedRows.map(({ payee, payeeAccount, payeeName, entityCode, amount, serial }, index) => {
return [
stringCell(index + 1),
stringCell(payerAccount),
stringCell(payerName),
stringCell("01"),
stringCell(payeeAccount),
stringCell(payeeName),
stringCell(payee.bankName ?? "中国建设银行"),
stringCell(""),
stringCell(serial),
{ t: "n", v: Number(amount.toFixed(2)), z: "0.00" },
stringCell("报销款"),
stringCell(entityCode),
stringCell("0"),
stringCell(""),
stringCell(""),
];
});
return { rows, totalAmount: totalAmount.toFixed(2), serials: [...serials] };
}
function buildSheet(rows) {
const sheet = {};
const allRows = [HEADERS.map(stringCell), ...rows];
for (let row = 0; row < allRows.length; row += 1) {
for (let column = 0; column < HEADERS.length; column += 1) {
sheet[XLSX.utils.encode_cell({ r: row, c: column })] = allRows[row][column] ?? stringCell("");
}
}
sheet["!ref"] = XLSX.utils.encode_range({ r: 0, c: 0 }, { r: allRows.length - 1, c: HEADERS.length - 1 });
return sheet;
}
function generatePaymentOrderXls(config, batch) {
const { rows, totalAmount, serials } = buildPaymentOrderRows(config, batch);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, buildSheet(rows), "Sheet1");
XLSX.utils.book_append_sheet(workbook, {}, "Sheet2");
XLSX.utils.book_append_sheet(workbook, {}, "Sheet3");
const buffer = XLSX.write(workbook, { bookType: "biff8", type: "buffer" });
return { buffer, rowCount: rows.length, totalAmount, serials };
}
module.exports = {
HEADERS,
PaymentOrderXlsError,
buildCustomerSerial,
buildPaymentOrderRows,
generatePaymentOrderXls,
};
+1 -1
查看文件
@@ -86,7 +86,7 @@ const NAVIGATION_REGISTRY = Object.freeze({
organization: { key: "organization", label: "组织架构", icon: "account_tree", href: "/organization", anyOf: ["system.users", "report.dashboard.view"], featureFlagKey: FEATURE_FLAG_KEYS.employeeLifecycle },
contractsRegistry: { key: "contractsRegistry", label: "合同", icon: "description", href: "/contracts-registry", anyOf: ["project.manage", "project.view_all", "system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.projectLinking },
workflowDefinitions: { key: "workflowDefinitions", label: "流程定义", icon: "schema", href: "/system/workflow-definitions", anyOf: ["system.settings"], featureFlagKey: FEATURE_FLAG_KEYS.workflowDefinitionAdmin },
systemSettings: { key: "systemSettings", label: "管理", icon: "settings", href: "/system/settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage", "system.audit.view"] },
systemSettings: { key: "systemSettings", label: "管理", icon: "settings", href: "/system/settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage", "system.audit.view", "payment.account.read", "payment.account.manage"] },
featureFlags: { key: "featureFlags", label: "功能管理", icon: "toggle_on", href: "/system/feature-flags", anyOf: ["system.settings"] },
auditLogs: { key: "auditLogs", label: "审计日志", icon: "manage_search", href: "/audit-logs", anyOf: ["system.audit.view"] },