feat(corporate-bank-account-payment-order): 基于原模板生成建行制单

这个提交包含在:
2026-08-03 01:18:10 +08:00
父节点 91564d6774
当前提交 23c3768c46
共修改 19 个文件,包含 713 行新增和 152 行删除
@@ -1,42 +1,59 @@
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const { encryptStringOrThrow } = require("../src/services/crypto");
const { generatePaymentOrderXls } = require("../src/services/paymentOrderXls");
const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "payment-order-xls-"));
const outputPath = path.join(temporaryDirectory, "synthetic-order.xls");
try {
const fieldEncryptionKey = Buffer.alloc(32, 7).toString("base64");
const config = { fieldEncryptionKey };
const encrypt = (value) => encryptStringOrThrow(config, value);
const generated = generatePaymentOrderXls(config, {
batch_no: "PB-20260802-001",
payment_account_snapshot_json: { bankInstitutionCode: "CCB", accountName: "合成测试公司", accountNumberEncrypted: encrypt("6217000000000012345") },
items: [{ id: "1", entity_code: "BX-20260802-0001", amount: "1586.00", snapshot_json: { payee: { name: "合成员工", bankName: "中国建设银行", bankInstitutionCode: "CCB", bankAccountEncrypted: encrypt("6222000000000056789") } } }],
});
fs.writeFileSync(outputPath, generated.buffer);
const check = spawnSync("python3", ["-c", [
"import sys,xlrd",
"book=xlrd.open_workbook(sys.argv[1])",
"assert book.sheet_names()==['Sheet1','Sheet2','Sheet3']",
"assert [(s.nrows,s.ncols) for s in book.sheets()]==[(2,15),(0,0),(0,0)]",
"sheet=book.sheet_by_index(0)",
"assert sheet.cell_type(1,1)==xlrd.XL_CELL_TEXT and sheet.cell_value(1,1)=='6217000000000012345'",
"assert sheet.cell_type(1,3)==xlrd.XL_CELL_TEXT and sheet.cell_value(1,3)=='01'",
"assert sheet.cell_type(1,4)==xlrd.XL_CELL_TEXT and sheet.cell_value(1,4)=='6222000000000056789'",
"assert sheet.cell_type(1,8)==xlrd.XL_CELL_TEXT",
"assert sheet.cell_type(1,9)==xlrd.XL_CELL_NUMBER and sheet.cell_value(1,9)==1586.0",
"assert sheet.cell_value(1,10)=='报销款' and sheet.cell_value(1,5)=='合成员工'",
].join(";"), outputPath], { encoding: "utf8" });
if (check.status !== 0) {
process.stderr.write(check.stderr || check.stdout || "xlrd roundtrip failed\n");
process.exitCode = 1;
} else {
process.stdout.write("[payment-order-xls][PASS] xlrd BIFF8 roundtrip verified\n");
}
} finally {
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
const fieldEncryptionKey = Buffer.alloc(32, 7).toString("base64");
const config = { fieldEncryptionKey };
const encrypt = (value) => encryptStringOrThrow(config, value);
const syntheticAccounts = [
"6222000000000012345",
"6222000000000023456",
"6222000000000034567",
"6222000000000045678",
];
const generated = generatePaymentOrderXls(config, {
batch_no: "PB-20260802-001",
payment_account_snapshot_json: {
bankInstitutionCode: "CCB",
accountName: "合成测试公司",
bankName: "中国建设银行",
accountNumberEncrypted: encrypt("6217000000000012345"),
},
items: syntheticAccounts.map((account, index) => ({
id: String(index + 1),
entity_code: `BX-20260802-${String(index + 1).padStart(4, "0")}`,
amount: `${index + 1}.20`,
snapshot_json: {
payee: {
name: `合成员工${index + 1}`,
bankName: "中国建设银行",
bankInstitutionCode: "CCB",
bankAccountEncrypted: encrypt(account),
},
},
})),
});
const pythonBin = process.env.PAYMENT_ORDER_PYTHON_BIN ?? "python3";
const verifier = spawnSync(pythonBin, [path.resolve(__dirname, "verify-payment-order-xls.py")], {
input: generated.buffer,
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
timeout: 15_000,
env: {
PATH: path.isAbsolute(pythonBin) ? path.dirname(pythonBin) : "/usr/local/bin:/usr/bin:/bin",
LANG: "C.UTF-8",
PYTHONDONTWRITEBYTECODE: "1",
PYTHONNOUSERSITE: "1",
},
});
if (verifier.status !== 0 || verifier.error) {
process.stderr.write(verifier.stderr || "[payment-order-xls][FAIL] verifier unavailable\n");
process.exitCode = 1;
} else {
process.stdout.write(verifier.stdout);
}
+124
查看文件
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Independently verify generated BIFF8 data and visual template contracts."""
from __future__ import annotations
from pathlib import Path
import sys
try:
import xlrd
except ImportError:
sys.stderr.write("[payment-order-xls][FAIL] verifier dependency unavailable\n")
raise SystemExit(1)
TEMPLATE_PATH = Path(__file__).resolve().parents[1] / "src" / "assets" / "payment-order" / "ccb-payment-order-template.xls"
EXPECTED_HEADERS = [
"*序号",
"*付款方客户账号",
"*付款方账户名称",
"*收款方行别代码\n(01-本行 02-国内他行)",
"*收款方客户账号",
"*收款方账户名称",
"收款方开户行名称",
"收款方联行号",
"客户方流水号",
"*金额",
"*用途",
"备注",
"是否短信通知收款人(0-不通知,1-通知,默认为0-不通知。)",
"收款人手机号码",
"短信通知附加信息",
]
def visual_signature(book: xlrd.book.Book, sheet: xlrd.sheet.Sheet, row: int, column: int) -> tuple:
xf = book.xf_list[sheet.cell_xf_index(row, column)]
font = book.font_list[xf.font_index]
alignment = xf.alignment
background = xf.background
border = xf.border
return (
font.name,
font.height,
font.bold,
font.italic,
font.underline_type,
font.colour_index,
alignment.hor_align,
alignment.vert_align,
alignment.text_wrapped,
alignment.rotation,
alignment.shrink_to_fit,
background.fill_pattern,
background.pattern_colour_index,
background.background_colour_index,
border.left_line_style,
border.right_line_style,
border.top_line_style,
border.bottom_line_style,
border.left_colour_index,
border.right_colour_index,
border.top_colour_index,
border.bottom_colour_index,
)
def main() -> int:
stage = "read"
try:
generated_bytes = sys.stdin.buffer.read(16 * 1024 * 1024 + 1)
assert len(generated_bytes) <= 16 * 1024 * 1024
template = xlrd.open_workbook(str(TEMPLATE_PATH), formatting_info=True)
generated = xlrd.open_workbook(file_contents=generated_bytes, formatting_info=True)
stage = "structure"
assert generated.sheet_names() == ["Sheet1", "Sheet2", "Sheet3"]
assert [(sheet.nrows, sheet.ncols) for sheet in generated.sheets()] == [(5, 15), (0, 0), (0, 0)]
source_sheet = template.sheet_by_index(0)
sheet = generated.sheet_by_index(0)
stage = "headers"
assert sheet.row_values(0) == EXPECTED_HEADERS
stage = "column_widths"
stage = "explicit_column_widths"
assert {column: info.width for column, info in sheet.colinfo_map.items()} == {
column: info.width for column, info in source_sheet.colinfo_map.items()
}
stage = "header_height"
assert sheet.rowinfo_map[0].height == source_sheet.rowinfo_map[0].height
for row in range(1, 5):
stage = "data_row_height"
assert sheet.rowinfo_map[row].height == source_sheet.rowinfo_map[1].height
for column in range(15):
stage = "visual_style"
assert visual_signature(generated, sheet, row, column) == visual_signature(template, source_sheet, 1, column)
stage = "cell_types"
for row in range(1, 5):
for column in (0, 1, 2, 3, 4, 5, 6, 8, 10, 11, 12):
stage = f"text_cell_{column}"
assert sheet.cell_type(row, column) == xlrd.XL_CELL_TEXT
for column in (7, 13, 14):
stage = f"empty_cell_{column}"
assert sheet.cell_type(row, column) in (xlrd.XL_CELL_EMPTY, xlrd.XL_CELL_BLANK)
assert sheet.cell_value(1, 1) == "6217000000000012345"
assert sheet.cell_value(1, 3) == "01"
assert sheet.cell_value(1, 4) == "6222000000000012345"
assert sheet.cell_value(1, 5) == "合成员工1"
assert sheet.cell_value(1, 10) == "报销款"
assert sheet.cell_value(1, 12) == "0"
for row in range(1, 5):
assert sheet.cell_type(row, 9) == xlrd.XL_CELL_NUMBER
stage = "amount_format"
assert generated.format_map[generated.xf_list[sheet.cell_xf_index(row, 9)].format_key].format_str == "0.00"
sys.stdout.write("[payment-order-xls][PASS] original BIFF8 template data and visual contracts verified\n")
return 0
except Exception:
sys.stderr.write(f"[payment-order-xls][FAIL] BIFF8 template contract mismatch: {stage}\n")
return 1
if __name__ == "__main__":
raise SystemExit(main())