build-field-xlsx-20260904.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. # -*- coding: utf-8 -*-
  2. """按原 xlsx 格式补全业务表字段类型与描述,输出到桌面与 docs。"""
  3. import json
  4. from openpyxl import Workbook, load_workbook
  5. from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
  6. from openpyxl.utils import get_column_letter
  7. from pathlib import Path
  8. ROOT = Path(r"c:\Users\Liumouren\Desktop\临时文件\元以科技\松江\松江农民退养补贴")
  9. SRC = ROOT / "docs" / "松江农民土地退补系统字段名称.xlsx"
  10. FRESH = ROOT / "docs" / "_dms_fields_fresh_20260904.json"
  11. OUT_DESKTOP = Path(r"c:\Users\Liumouren\Desktop") / "松江农民土地退补系统字段名称.xlsx"
  12. OUT_DOCS = ROOT / "docs" / "松江农民土地退补系统字段名称.xlsx"
  13. models = json.loads(FRESH.read_text(encoding="utf-8"))
  14. TYPE_CN = {
  15. "text": "文本",
  16. "double": "数值(小数)",
  17. "integer": "数值(整数)",
  18. "timestamp": "日期时间",
  19. "long": "长整型",
  20. "float": "数值(小数)",
  21. }
  22. FRONT_CN = {
  23. "content": "文本框",
  24. "float_num": "小数",
  25. "int_num": "整数",
  26. "date_time": "日期时间",
  27. "date": "日期",
  28. }
  29. def type_label(f):
  30. t = f.get("type") or ""
  31. ft = f.get("frontType") or ""
  32. base = TYPE_CN.get(t, t or "")
  33. if ft and ft in FRONT_CN:
  34. return f"{base}/{FRONT_CN[ft]}" if base else FRONT_CN[ft]
  35. return base
  36. index = []
  37. for key, m in models.items():
  38. for f in m["fields"]:
  39. index.append((key, f))
  40. def find_field(name_hints, prefer_keys=None):
  41. hints = [h for h in name_hints if h]
  42. prefer_keys = prefer_keys or []
  43. candidates = []
  44. for key, f in index:
  45. alias = (f.get("alias") or "").strip()
  46. name = (f.get("name") or "").strip()
  47. short = name[2:] if name.startswith("c_") else name
  48. for h in hints:
  49. h = str(h).strip()
  50. if not h:
  51. continue
  52. target = h if h.startswith("c_") else ("c_" + h)
  53. if alias == h or name == h or short == h or name == target:
  54. candidates.append((key, f, 0))
  55. elif h in alias or (alias and alias in h):
  56. candidates.append((key, f, 1))
  57. if prefer_keys:
  58. for pk in prefer_keys:
  59. for c in candidates:
  60. if c[0] == pk:
  61. return c
  62. return candidates[0] if candidates else None
  63. EXPLICIT = {
  64. "人员姓名": (["personnel"], ["姓名", "c_name"]),
  65. "人员身份证号码": (["personnel"], ["身份证号", "c_id_number"]),
  66. "社会保险登记号": (["personnel"], ["个人社会保险登记码", "c_social_insurance_code"]),
  67. "手机号": (["personnel"], ["手机号", "c_phone_number"]),
  68. "联系地址": (["personnel"], ["家庭住址", "c_address"]),
  69. "户籍性质": (["personnel"], ["户籍性质", "c_household_type"]),
  70. "街镇": (["personnel"], ["街镇ID", "c_town_id"]),
  71. "村居": (["personnel"], ["村居ID", "c_village_id"]),
  72. "村委代码": (["personnel"], ["村委代码", "c_village_code"]),
  73. "委托方案代码": (["personnel", "region"], ["委托方案代码", "c_entrust_plan_code"]),
  74. "委托方案名称": (["personnel"], ["委托方案名称", "c_entrust_plan_name"]),
  75. "收款人名称": (["personnel"], ["收款人名称", "c_payee_name"]),
  76. "银行卡号": (["personnel"], ["银行卡号", "c_bank_card_number"]),
  77. "发放银行": (["personnel", "batchStat", "batch"], ["开户行", "c_bank_name", "发放银行"]),
  78. "收款银行行号": (["personnel"], ["收款银行行号", "c_bank_branch_code"]),
  79. "退养资格类型": (["personnel"], ["退养资格类型", "c_retire_qualify_type"]),
  80. "居保状态": (["personnel"], ["居保养老状态", "c_insurance_status"]),
  81. "死亡状态": (["personnel"], ["死亡状态", "c_death_status"]),
  82. "死亡日期": (["personnel"], ["死亡日期", "c_death_date"]),
  83. "继承人姓名": (["personnel"], ["继承人姓名", "c_heir_name"]),
  84. "继承人与逝者关系": (["personnel"], ["继承人关系", "c_heir_relation"]),
  85. "继承人联系方式": (["personnel"], ["继承人电话", "c_heir_phone"]),
  86. "继承人身份证": (["personnel"], ["继承人身份证", "c_heir_id_number"]),
  87. "继承人银行卡": (["personnel"], ["继承人银行卡", "c_heir_bank_card"]),
  88. "丧葬费补差额": (["personnel"], ["丧葬费补差额", "c_funeral_diff_amount"]),
  89. "丧葬是否已发放": (["personnel", "payment"], ["c_remarks"]),
  90. "补贴金额": (["standard"], ["金额", "c_amount"]),
  91. "丧葬金额": (["standard"], ["丧葬金额", "c_funeral_amount"]),
  92. "生效时间": (["standard"], ["生效时间", "c_date_time"]),
  93. "失效时间": (["standard"], ["失效时间", "c_expire_time"]),
  94. "标准类型": (["standard"], ["标准类型", "c_standard_type"]),
  95. "是否当前有效": (["standard"], ["是否当前有效", "c_current_flag"]),
  96. "调整原因": (["standard"], ["调整原因", "c_adjust_reason"]),
  97. "享受起始年月": (["personnel"], ["享受起始年月", "代发起始年月", "c_enjoy_start_month"]),
  98. "代发截止年月": (["personnel"], ["代发截止年月", "c_enjoy_end_month"]),
  99. "补发开始年月": (["personnel"], ["补发开始年月", "c_supplement_start_month"]),
  100. "下次发放起始年月": (["personnel"], ["下次发放起始年月", "c_next_pay_start_month"]),
  101. "区级资金承担比例": (["region"], ["区级资金承担比例", "c_district_share_percent"]),
  102. "镇级资金承担比例": (["region"], ["镇级资金承担比例", "c_town_share_percent"]),
  103. "是否已出盘": (["batch", "payment"], ["是否已出盘", "出盘", "c_disk_out"]),
  104. "实发金额": (["payment"], ["实发金额", "c_actual_amount"]),
  105. "回盘是否成功": (["payment"], ["回盘是否成功", "c_return_success"]),
  106. "回盘结果": (["payment"], ["回盘结果", "c_return_result"]),
  107. "调标前月补": (["payment"], ["调标前月补", "c_old_monthly_amount"]),
  108. "调标后月补": (["payment"], ["调标后月补", "c_new_monthly_amount"]),
  109. "调标应发月数": (["payment"], ["调标应发月数", "c_adjust_months"]),
  110. "告警等级": (["alert"], ["告警等级", "c_level"]),
  111. "告警类型": (["alert"], ["告警类型", "c_type"]),
  112. "告警内容": (["alert"], ["告警内容", "c_content"]),
  113. "失败原因码": (["alert", "payment"], ["失败原因码", "c_fail_code"]),
  114. "处理状态": (["alert", "process"], ["处理状态", "c_status"]),
  115. "处理结果": (["alert", "process"], ["处理结果", "c_result"]),
  116. "发起人": (["alert", "process"], ["发起人", "c_initiator"]),
  117. "接收人": (["alert", "process"], ["接收人", "c_receiver"]),
  118. "流程id": (["process", "alert"], ["流程ID", "流程id", "c_process_id"]),
  119. "处理人": (["alert", "process"], ["处理人", "c_handler"]),
  120. "处理时间": (["alert", "process"], ["处理时间", "c_handle_time"]),
  121. "文件地址": (["alert", "batch"], ["文件地址", "c_file_url"]),
  122. "备注描述": (["alert", "process"], ["备注", "c_remarks"]),
  123. "关联批次ID": (["batchStat", "payment", "batch"], ["关联批次ID", "批次ID", "c_batch_id"]),
  124. "批次号": (["batch", "batchStat", "payment"], ["批次号", "c_batch_number"]),
  125. "汇总表编号": (["batchStat"], ["汇总表编号", "c_summary_no"]),
  126. "发放年月": (["batch", "batchStat", "payment"], ["发放年月", "c_pay_month"]),
  127. "镇id": (["batchStat", "batch", "personnel"], ["街镇ID", "镇ID", "c_town_id"]),
  128. "村id": (["batchStat", "batch", "personnel"], ["村居ID", "村ID", "c_village_id"]),
  129. "统计粒度": (["batchStat"], ["统计粒度", "c_stat_level"]),
  130. "应支付人数": (["batchStat", "batch"], ["应支付人数", "c_payable_count"]),
  131. "应支付总额": (["batchStat", "batch"], ["应支付总额", "c_payable_amount"]),
  132. "实支付人数": (["batchStat", "batch"], ["实支付人数", "c_paid_count"]),
  133. "实支付总额": (["batchStat", "batch"], ["实支付总额", "c_paid_amount"]),
  134. "支付未成功人数": (["batchStat", "batch"], ["支付未成功人数", "失败人数", "c_fail_count"]),
  135. "支付未成功总额": (["batchStat", "batch"], ["支付未成功总额", "c_fail_amount"]),
  136. "标准金额": (["batchStat", "payment"], ["标准金额", "月补金额", "c_standard_amount"]),
  137. "死亡金额": (["batchStat", "payment"], ["死亡金额", "丧葬金额", "c_funeral_amount"]),
  138. "补充金额": (["batchStat", "payment"], ["补充金额", "c_extra_amount"]),
  139. "补发金额": (["batchStat", "payment"], ["补发金额", "c_supplement_amount"]),
  140. "回盘日期": (["batch", "batchStat", "payment"], ["回盘日期", "c_return_date"]),
  141. "经办人": (["batch", "batchStat"], ["经办人", "c_operator"]),
  142. "复核人": (["batch", "batchStat"], ["复核人", "c_reviewer"]),
  143. "备注": (["batchStat", "batch", "personnel"], ["备注", "c_remarks"]),
  144. "人数": (["batch", "batchStat"], ["人数", "c_person_count"]),
  145. }
  146. CAT_DEFAULT = {
  147. "人员基本信息": ("村合作社填报", "随时更新"),
  148. "区划与委托": ("系统维护/村合作社填报", "按需更新"),
  149. "银行信息": ("村合作社填报", "随时更新"),
  150. "退养资格": ("镇经发填报", "随时更新"),
  151. "居保匹配": ("大数据匹配", "随时更新"),
  152. "死亡状态匹配": ("大数据匹配", "随时更新"),
  153. "死亡与继承": ("镇社区事务受理中心/系统计算", "随时更新"),
  154. "金额标准": ("政策文件/系统计算", "一年一次或按业务"),
  155. "告警": ("系统计算", "每月更新"),
  156. "批次统计": ("系统计算", "每月更新"),
  157. }
  158. TABLE_META = {
  159. "personnel": ("人员基本信息表", "村合作社/镇社区/系统", "随时更新"),
  160. "region": ("区划表", "系统维护", "按需更新"),
  161. "standard": ("金额标准表", "政策文件/镇经发", "一年一次或调标时"),
  162. "alert": ("告警表", "系统计算", "随时更新"),
  163. "process": ("流程/特殊业务表", "业务部门填报", "随时更新"),
  164. "batch": ("批次表", "系统计算", "每月更新"),
  165. "batchStat": ("批次统计表", "系统计算", "每月更新"),
  166. "payment": ("发放明细表", "系统计算/回盘导入", "每月更新"),
  167. }
  168. SPECIAL_NOTES = {
  169. "丧葬是否已发放": "当前多用人员备注 FUNERAL_PAID 标记,非独立字段",
  170. "生效时间": "对应标准表 c_date_time",
  171. "是否当前有效": "对应标准表 c_current_flag",
  172. "死亡状态": "取值:正常/已死亡;与业务态 c_biz_status=FUNERAL 并存",
  173. "街镇": "存业务ID,界面展示中文名",
  174. "村居": "存业务ID,界面展示中文名",
  175. "镇id": "存业务ID,界面展示中文名",
  176. "村id": "存业务ID,界面展示中文名",
  177. "发放银行": "人员表对应开户行 c_bank_name",
  178. }
  179. wb0 = load_workbook(SRC)
  180. ws0 = wb0.active
  181. orig = []
  182. for i, row in enumerate(ws0.iter_rows(values_only=True), 1):
  183. if i == 1:
  184. continue
  185. vals = list(row)
  186. while len(vals) < 7:
  187. vals.append(None)
  188. cat, name, typ, src, freq, note, must = vals[:7]
  189. orig.append(
  190. {
  191. "cat": cat,
  192. "name": name,
  193. "type": typ,
  194. "source": src,
  195. "freq": freq,
  196. "note": note,
  197. "must": must,
  198. }
  199. )
  200. cur_cat = None
  201. for r in orig:
  202. if r["cat"]:
  203. cur_cat = r["cat"]
  204. else:
  205. r["cat"] = cur_cat
  206. enriched = []
  207. used_fields = set()
  208. for r in orig:
  209. name = r["name"]
  210. prefer, hints = EXPLICIT.get(name, (None, [name]))
  211. hit = find_field(hints, prefer) if prefer is not None else None
  212. if hit is None:
  213. hit = find_field([name] + list(hints or []), prefer)
  214. note_parts = []
  215. if r["note"]:
  216. note_parts.append(str(r["note"]))
  217. typ = r["type"]
  218. must = r["must"]
  219. src = r["source"]
  220. freq = r["freq"]
  221. if hit:
  222. key, f, _ = hit
  223. used_fields.add((key, f["name"]))
  224. typ = type_label(f)
  225. if must is None and f.get("must"):
  226. must = "是"
  227. m = models[key]
  228. note_parts.append(f"DMS:{m['label']} {f['name']}")
  229. desc = f.get("describe") or ""
  230. if desc and desc not in ("格式为富文本", "number", "时间戳"):
  231. note_parts.append(str(desc))
  232. if name in SPECIAL_NOTES:
  233. note_parts.append(SPECIAL_NOTES[name])
  234. else:
  235. note_parts.append("未在DMS单字段精确匹配,请核对业务口径")
  236. if name in SPECIAL_NOTES:
  237. note_parts.append(SPECIAL_NOTES[name])
  238. if not src or not freq:
  239. ds, df = CAT_DEFAULT.get(r["cat"], ("业务填报", "按需更新"))
  240. src = src or ds
  241. freq = freq or df
  242. enriched.append(
  243. [
  244. r["cat"],
  245. name,
  246. typ,
  247. src,
  248. freq,
  249. ";".join([p for p in note_parts if p]),
  250. must or "",
  251. ]
  252. )
  253. rows_sheet1 = []
  254. prev = None
  255. for row in enriched:
  256. cat = row[0]
  257. show_cat = cat if cat != prev else None
  258. prev = cat
  259. rows_sheet1.append([show_cat, row[1], row[2], row[3], row[4], row[5], row[6]])
  260. # 人员表其余未出现字段
  261. extra_rows = []
  262. append_cat = "人员表补充字段"
  263. for f in models["personnel"]["fields"]:
  264. if ("personnel", f["name"]) in used_fields:
  265. continue
  266. used_fields.add(("personnel", f["name"]))
  267. note = f"DMS:人员基本信息表 {f['name']}"
  268. if f["name"] == "c_death_status":
  269. note += ";取值正常/已死亡;与 c_biz_status=FUNERAL 并存"
  270. extra_rows.append(
  271. [
  272. None,
  273. f.get("alias") or f["name"],
  274. type_label(f),
  275. "系统/业务填报",
  276. "随时更新",
  277. note,
  278. "是" if f.get("must") else "",
  279. ]
  280. )
  281. if extra_rows:
  282. extra_rows[0][0] = append_cat
  283. thin = Border(
  284. left=Side(style="thin", color="B0B0B0"),
  285. right=Side(style="thin", color="B0B0B0"),
  286. top=Side(style="thin", color="B0B0B0"),
  287. bottom=Side(style="thin", color="B0B0B0"),
  288. )
  289. header_fill = PatternFill("solid", fgColor="1F4E79")
  290. header_font = Font(color="FFFFFF", bold=True, name="微软雅黑", size=11)
  291. cat_fill = PatternFill("solid", fgColor="D6E3F0")
  292. body_font = Font(name="微软雅黑", size=10)
  293. wrap = Alignment(wrap_text=True, vertical="center")
  294. HEADERS7 = ["类别", "字段名", "字段类型", "数据来源", "更新频率", "备注", "是否必须满足"]
  295. HEADERS_FULL = HEADERS7 + ["字段编码", "业务表", "栏目ID", "模型ID"]
  296. def style_header(ws, headers):
  297. for col, h in enumerate(headers, 1):
  298. cell = ws.cell(1, col, h)
  299. cell.fill = header_fill
  300. cell.font = header_font
  301. cell.alignment = Alignment(horizontal="center", vertical="center")
  302. cell.border = thin
  303. ws.row_dimensions[1].height = 22
  304. ws.freeze_panes = "A2"
  305. ws.auto_filter.ref = f"A1:{get_column_letter(len(headers))}1"
  306. def autosize(ws, widths):
  307. for i, w in enumerate(widths, 1):
  308. ws.column_dimensions[get_column_letter(i)].width = w
  309. def write_rows(ws, rows, headers):
  310. style_header(ws, headers)
  311. for r_i, row in enumerate(rows, 2):
  312. for c_i, val in enumerate(row, 1):
  313. cell = ws.cell(r_i, c_i, "" if val is None else val)
  314. cell.font = body_font
  315. cell.alignment = wrap
  316. cell.border = thin
  317. if c_i == 1 and val:
  318. cell.fill = cat_fill
  319. cell.font = Font(name="微软雅黑", size=10, bold=True)
  320. wb = Workbook()
  321. ws1 = wb.active
  322. ws1.title = "业务字段清单"
  323. write_rows(ws1, rows_sheet1, HEADERS7)
  324. autosize(ws1, [16, 18, 16, 22, 12, 55, 12])
  325. if extra_rows:
  326. r0 = len(rows_sheet1) + 3
  327. tip = ws1.cell(r0 - 1, 1, "—— 以下为对照 DMS 后补充的人员表字段 ——")
  328. tip.font = Font(name="微软雅黑", bold=True, color="C00000")
  329. for i, row in enumerate(extra_rows):
  330. for c_i, val in enumerate(row, 1):
  331. cell = ws1.cell(r0 + i, c_i, "" if val is None else val)
  332. cell.font = body_font
  333. cell.alignment = wrap
  334. cell.border = thin
  335. if c_i == 1 and val:
  336. cell.fill = cat_fill
  337. cell.font = Font(name="微软雅黑", size=10, bold=True)
  338. ws2 = wb.create_sheet("业务表字段全量")
  339. full_rows = []
  340. for key in ["personnel", "region", "standard", "payment", "batch", "batchStat", "alert", "process"]:
  341. m = models[key]
  342. tname, src, freq = TABLE_META[key]
  343. first = True
  344. for f in m["fields"]:
  345. note = f"字段编码 {f['name']}"
  346. desc = f.get("describe") or ""
  347. if desc and desc not in ("格式为富文本", "number", "时间戳"):
  348. note += ";" + str(desc)
  349. if f["name"] == "c_death_status":
  350. note += ";取值正常/已死亡;与 c_biz_status=FUNERAL 并存"
  351. if f["name"] == "c_return_success":
  352. note += ";回盘失败以 false + 备注回盘失败|RETURN_FAIL 为准"
  353. full_rows.append(
  354. [
  355. tname if first else None,
  356. f.get("alias") or f["name"],
  357. type_label(f),
  358. src,
  359. freq,
  360. note,
  361. "是" if f.get("must") else "",
  362. f["name"],
  363. m["tableCode"],
  364. m["columnId"],
  365. m["modelId"],
  366. ]
  367. )
  368. first = False
  369. write_rows(ws2, full_rows, HEADERS_FULL)
  370. autosize(ws2, [18, 20, 16, 22, 14, 50, 12, 28, 18, 10, 10])
  371. ws3 = wb.create_sheet("业务表索引")
  372. style_header(ws3, ["业务表", "表编码", "栏目ID", "模型ID", "字段数", "说明"])
  373. idx_rows = [
  374. ("人员基本信息表", "sjnmtybt_ryjbxx", 1847, 1970, len(models["personnel"]["fields"]), "人员底数、资格、居保、死亡继承等"),
  375. ("区划表", "sjnmtybt_xzqh", 1842, 1965, len(models["region"]["fields"]), "区/镇/村及资金比例、委托方案"),
  376. ("金额标准表", "sjnmtybt_jebz", 1843, 1966, len(models["standard"]["fields"]), "月补/丧葬标准、调标"),
  377. ("发放明细表", "sjnmtybt_ffmx", 1849, 1974, len(models["payment"]["fields"]), "应发实发、回盘、调标差额明细"),
  378. ("批次表", "sjnmtybt_pcb", 1845, 1968, len(models["batch"]["fields"]), "全区/村月批、出盘回盘状态"),
  379. ("批次统计表", "sjnmtybt_pctj", 1846, 1969, len(models["batchStat"]["fields"]), "镇/村汇总统计"),
  380. ("告警表", "sjnmtybt_alart", 1841, 1964, len(models["alert"]["fields"]), "死亡/回盘失败等告警"),
  381. ("流程/特殊业务表", "sjnmtybt_lcb", 1844, 1967, len(models["process"]["fields"]), "特殊业务审批与流程"),
  382. ]
  383. for i, row in enumerate(idx_rows, 2):
  384. for c, v in enumerate(row, 1):
  385. cell = ws3.cell(i, c, v)
  386. cell.font = body_font
  387. cell.border = thin
  388. cell.alignment = wrap
  389. autosize(ws3, [18, 22, 10, 10, 10, 40])
  390. ws4 = wb.create_sheet("填写说明")
  391. notes = [
  392. "本文件在《松江农民土地退补系统字段名称》原表格式基础上补全:字段类型、备注(含DMS字段编码/表归属)、以及业务表全量字段。",
  393. "原表列:类别、字段名、字段类型、数据来源、更新频率、备注、是否必须满足。",
  394. "「业务字段清单」:保留原业务口径字段顺序并补类型/备注;文末追加人员表其余未列出字段。",
  395. "「业务表字段全量」:8 张核心业务表全部自定义字段(同原列格式,并增加字段编码/业务表/栏目ID/模型ID)。",
  396. "字段类型示例:文本/文本框、数值(小数)/小数、数值(整数)/整数、日期时间/日期时间。",
  397. "死亡状态 c_death_status 与业务态 c_biz_status=FUNERAL 并存;回盘失败以 c_return_success=false + 备注为准。",
  398. "生成日期:2026-09-04;数据来源:生产 DMS 模型 getModelById。",
  399. ]
  400. for i, t in enumerate(notes, 1):
  401. cell = ws4.cell(i, 1, t)
  402. cell.font = body_font
  403. cell.alignment = wrap
  404. ws4.column_dimensions["A"].width = 120
  405. wb.save(OUT_DESKTOP)
  406. wb.save(OUT_DOCS)
  407. print("saved", OUT_DESKTOP)
  408. print("saved", OUT_DOCS)
  409. print("sheet1_orig", len(rows_sheet1), "personnel_extra", len(extra_rows), "full", len(full_rows))