feat(corporate-bank-account-payment-order): 基于原模板生成建行制单
这个提交包含在:
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fill the fixed blank CCB BIFF8 template without exposing row data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
try:
|
||||
import xlrd
|
||||
from xlutils.filter import process, XLRDReader, XLWTWriter
|
||||
except ImportError:
|
||||
sys.stderr.write("[payment-order-template][ERROR] runtime_dependency_unavailable\n")
|
||||
raise SystemExit(70)
|
||||
|
||||
|
||||
TEMPLATE_SHA256 = "72e83ab6b7cd71263ca00196746de896e02a02390bab5c896dabd76421e40c0e"
|
||||
TEMPLATE_PATH = Path(__file__).resolve().parents[1] / "assets" / "payment-order" / "ccb-payment-order-template.xls"
|
||||
EXPECTED_SHEETS = ["Sheet1", "Sheet2", "Sheet3"]
|
||||
COLUMN_COUNT = 15
|
||||
TEMPLATE_DATA_ROW = 1
|
||||
MAX_INPUT_BYTES = 2 * 1024 * 1024
|
||||
MAX_ROWS = 5000
|
||||
|
||||
|
||||
class TemplateFillError(Exception):
|
||||
def __init__(self, code: str):
|
||||
super().__init__(code)
|
||||
self.code = code
|
||||
|
||||
|
||||
def read_payload() -> dict:
|
||||
payload_bytes = sys.stdin.buffer.read(MAX_INPUT_BYTES + 1)
|
||||
if len(payload_bytes) > MAX_INPUT_BYTES:
|
||||
raise TemplateFillError("input_too_large")
|
||||
try:
|
||||
payload = json.loads(payload_bytes.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
raise TemplateFillError("input_invalid") from None
|
||||
if not isinstance(payload, dict) or set(payload) != {"rows"}:
|
||||
raise TemplateFillError("input_invalid")
|
||||
rows = payload.get("rows")
|
||||
if not isinstance(rows, list) or not rows or len(rows) > MAX_ROWS:
|
||||
raise TemplateFillError("rows_invalid")
|
||||
for row in rows:
|
||||
if not isinstance(row, list) or len(row) != COLUMN_COUNT:
|
||||
raise TemplateFillError("row_invalid")
|
||||
for column, value in enumerate(row):
|
||||
if column == 9:
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value <= 0:
|
||||
raise TemplateFillError("amount_invalid")
|
||||
elif not isinstance(value, str):
|
||||
raise TemplateFillError("text_cell_invalid")
|
||||
return payload
|
||||
|
||||
|
||||
def read_template() -> tuple[bytes, xlrd.book.Book]:
|
||||
try:
|
||||
template_bytes = TEMPLATE_PATH.read_bytes()
|
||||
except OSError:
|
||||
raise TemplateFillError("template_unavailable") from None
|
||||
if hashlib.sha256(template_bytes).hexdigest() != TEMPLATE_SHA256:
|
||||
raise TemplateFillError("template_hash_mismatch")
|
||||
try:
|
||||
book = xlrd.open_workbook(file_contents=template_bytes, formatting_info=True)
|
||||
except Exception:
|
||||
raise TemplateFillError("template_invalid") from None
|
||||
if book.sheet_names() != EXPECTED_SHEETS:
|
||||
raise TemplateFillError("template_structure_mismatch")
|
||||
sheet = book.sheet_by_index(0)
|
||||
if sheet.nrows < 4 or sheet.ncols != COLUMN_COUNT:
|
||||
raise TemplateFillError("template_structure_mismatch")
|
||||
return template_bytes, book
|
||||
|
||||
|
||||
def fill_workbook(rows: list[list[object]], source_book: xlrd.book.Book) -> bytes:
|
||||
writer = XLWTWriter()
|
||||
try:
|
||||
process(XLRDReader(source_book, "ccb-payment-order-template.xls"), writer)
|
||||
target_book = writer.output[0][1]
|
||||
source_sheet = source_book.sheet_by_index(0)
|
||||
target_sheet = target_book.get_sheet(0)
|
||||
target_sheet._cell_overwrite_ok = True
|
||||
|
||||
source_row_info = source_sheet.rowinfo_map.get(TEMPLATE_DATA_ROW)
|
||||
source_row_height = source_row_info.height if source_row_info else 735
|
||||
source_row_height_mismatch = source_row_info.height_mismatch if source_row_info else False
|
||||
|
||||
for row_offset, values in enumerate(rows):
|
||||
target_row_index = row_offset + 1
|
||||
target_row = target_sheet.row(target_row_index)
|
||||
target_row.height = source_row_height
|
||||
target_row.height_mismatch = source_row_height_mismatch
|
||||
for column, value in enumerate(values):
|
||||
style_index = source_sheet.cell_xf_index(TEMPLATE_DATA_ROW, column)
|
||||
style = writer.style_list[style_index]
|
||||
if column == 9:
|
||||
style = copy.deepcopy(style)
|
||||
style.num_format_str = "0.00"
|
||||
target_sheet.write(target_row_index, column, value, style)
|
||||
|
||||
output = io.BytesIO()
|
||||
target_book.save(output)
|
||||
return output.getvalue()
|
||||
except TemplateFillError:
|
||||
raise
|
||||
except Exception:
|
||||
raise TemplateFillError("fill_failed") from None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
payload = read_payload()
|
||||
_, source_book = read_template()
|
||||
output = fill_workbook(payload["rows"], source_book)
|
||||
if not output.startswith(bytes.fromhex("d0cf11e0a1b11ae1")):
|
||||
raise TemplateFillError("output_invalid")
|
||||
sys.stdout.buffer.write(output)
|
||||
return 0
|
||||
except TemplateFillError as error:
|
||||
sys.stderr.write(f"[payment-order-template][ERROR] {error.code}\n")
|
||||
return 70
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
在新工单中引用
屏蔽一个用户