Files
twoJob-demo/scripts/gen_devdoc.py
T
2026-09-03 15:35:23 +08:00

223 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成《2号人事部·功能仿写开发需求文档》(Markdown)。
数据来源: SQLite features(172功能页释义/开发提示) + downloads(41个真实导出Excel, 提取表头做字段字典)
+ screenshots(整页界面参考)。
输出: docs/开发需求文档.md
"""
import os, sqlite3, collections, re, datetime
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DB = os.path.join(BASE, "db", "research.db")
DL_ROOT = os.path.join(BASE, "downloads")
OUT = os.path.join(BASE, "docs", "开发需求文档.md")
EXCEL_EXT = (".xlsx", ".xls")
def db():
c = sqlite3.connect(DB)
c.row_factory = sqlite3.Row
return c
def load_tree():
c = db()
rows = [dict(r) for r in c.execute(
"SELECT node_key,parent_key,level,name,full_path,url,description,understanding,"
"screenshot_path,keep,sort_order FROM features ORDER BY sort_order,id")]
c.close()
by = collections.defaultdict(list)
for r in rows:
by[r["parent_key"] or ""].append(r)
return by
def excel_headers(path, max_scan=6):
"""读取 Excel(.xlsx 用 openpyxl / .xls 用 xlrd), 返回识别出的表头字段列表
(取前若干行中非空单元格最多的一行)。"""
try:
if path.lower().endswith(".xls"):
import xlrd, warnings
warnings.filterwarnings("ignore")
wb = xlrd.open_workbook(path)
ws = wb.sheet_by_index(0)
best, best_cnt = [], 0
for r in range(min(max_scan, ws.nrows)):
vals = [str(ws.cell_value(r, cc)).strip() for cc in range(ws.ncols)
if str(ws.cell_value(r, cc)).strip()]
if len(vals) > best_cnt:
best, best_cnt = vals, len(vals)
return [] if best_cnt == 1 else best
import openpyxl
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
ws = wb.active
best, best_cnt = [], 0
for row in ws.iter_rows(min_row=1, max_row=max_scan):
vals = [str(c.value).strip() for c in row if c.value is not None and str(c.value).strip()]
if len(vals) > best_cnt:
best, best_cnt = vals, len(vals)
wb.close()
# 去掉过长(可能是合并标题)的单格行
if best_cnt == 1:
return []
return best
except Exception as e:
return []
def scan_downloads():
"""返回 {module_name: [(dir, filename, headers[])]}。module_name 已去数字前缀。"""
out = collections.defaultdict(list)
files = []
for root, _, fs in os.walk(DL_ROOT):
for f in sorted(fs):
if f.lower().endswith(EXCEL_EXT):
files.append((root, f))
for root, f in files:
rel = os.path.relpath(root, DL_ROOT)
segs = [] if rel == "." else rel.split(os.sep)
seg0 = segs[0] if segs else ""
module = "任务中心(异步导出)" if seg0.startswith("_任务") else re.sub(r"^\d+_", "", seg0)
headers = excel_headers(os.path.join(root, f))
out[module].append((rel.replace(os.sep, "/"), f, headers))
return out
def slug(s):
return re.sub(r"[^\w\u4e00-\u9fa5]+", "-", s or "").strip("-")
def load_approval_templates():
"""读取审批模板库清单(若表不存在返回空)。"""
try:
c = db()
rows = [dict(r) for r in c.execute(
"SELECT name,category,provider,description FROM approval_templates ORDER BY sort_order,id")]
c.close()
return rows
except Exception:
return []
def main():
by = load_tree()
dl = scan_downloads()
total_files = sum(len(v) for v in dl.values())
modules = by.get("", [])
leaves_count = 0
def count_leaves(pk):
n = 0
for r in by.get(pk, []):
if by.get(r["node_key"]):
n += count_leaves(r["node_key"])
else:
n += 1
return n
L = []
w = L.append
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
w("# 2号人事部 · 功能仿写开发需求文档\n")
w("> **数据来源**:https://2haohr.com 全站功能调研\n>")
w("> **调研规模**:%d 个一级模块 / %d 个功能页\n>" % (len(modules), sum(count_leaves(m["node_key"]) for m in modules)))
w("> **配套资产**:整页界面截图 `screenshots/`|真实导出数据样本 `downloads/`(%d 个 Excel)|交互式审核+样本下载界面 `http://localhost:8777`\n>" % total_files)
w("> **生成时间**:%s\n" % now)
w("\n本文档用于**仿写开发**:每个功能页含【功能释义】【开发提示】【界面参考截图】,"
"并在模块末尾附**真实导出数据的字段清单**(直接来自线上导出的 Excel 表头),可据此设计数据库表结构与列表字段。\n")
# 概述表
w("\n## 一、模块总览\n")
w("| # | 模块 | 功能页数 | 抓取的数据样本 |")
w("|---|------|:---:|:---:|")
for i, m in enumerate(modules, 1):
nm = m["name"]
cnt = count_leaves(m["node_key"])
samp = len(dl.get(nm, []))
w("| %d | %s | %d | %s |" % (i, nm, cnt, (str(samp) + " 个" if samp else "—")))
# 明细
w("\n## 二、模块功能明细\n")
for mi, m in enumerate(modules, 1):
w("\n### %d. %s\n" % (mi, m["name"]))
def walk(pk, depth):
for r in by.get(pk, []):
children = by.get(r["node_key"])
if children:
if r["level"] >= 1:
w("\n#### ▸ %s\n" % r["name"])
walk(r["node_key"], depth + 1)
else:
nonlocal leaves_count
leaves_count += 1
keep = "" if r["keep"] in (None, 1) else " ~~(审核标记删除)~~"
w("\n**%s**%s" % (r["name"], keep))
if r["url"]:
w("- `URL` %s" % r["url"])
if r["description"]:
w("- **功能释义**:%s" % r["description"])
if r["understanding"] and "待补充细化" not in r["understanding"]:
w("- **开发提示**:%s" % r["understanding"])
if r["screenshot_path"]:
w("- **界面参考**:`%s`" % r["screenshot_path"])
w("")
walk(m["node_key"], 0)
# 模块导出样本字段
samples = dl.get(m["name"], [])
if samples:
w("\n> **📎 本模块真实导出数据样本(%d 个)与字段:**\n" % len(samples))
for rel, fn, headers in samples:
name = os.path.splitext(fn)[0]
if headers:
fields = "、".join(headers[:40])
more = " …" if len(headers) > 40 else ""
w("> - **%s**(%d 字段):%s%s" % (name, len(headers), fields, more))
else:
w("> - **%s**(字段未能解析,见文件)" % name)
w("")
# 附: 任务中心异步导出
if dl.get("任务中心(异步导出)"):
w("\n## 三、异步导出样本(任务中心生成)\n")
w("> 以下报表在页面点击导出后由服务端异步生成,经“任务中心”收割:\n")
for rel, fn, headers in dl["任务中心(异步导出)"]:
name = os.path.splitext(fn)[0]
fields = (":" + "、".join(headers[:40])) if headers else ""
w("> - **%s**%s" % (name, fields))
w("")
# 附: 审批模板库清单
tpls = load_approval_templates()
if tpls:
w("\n## 四、审批模板库清单(预置审批流程)\n")
w("> 来自 `审批 / 常规 / 审批模板库`(%d 个预置流程模板)。这些是**审批流程模板**"
"(点“使用”实例化为审批表单),非数据表,无导出文件;仿写时可据此设计审批流程配置。\n" % len(tpls))
bycat = collections.OrderedDict()
for t in tpls:
bycat.setdefault(t["category"] or "其他", []).append(t)
for cat, items in bycat.items():
w("\n#### ▸ %s(%d)\n" % (cat, len(items)))
w("| 模板 | 提供方 | 说明 |")
w("|------|------|------|")
for t in items:
w("| %s | %s | %s |" % (t["name"], t["provider"] or "—", t["description"] or ""))
w("")
w("\n---\n")
w("*本文档由调研数据自动汇编。功能释义/开发提示可在 `http://localhost:8777` 审核界面逐条修订后重新生成;"
"字段清单来自线上真实导出 Excel 的表头,为一手数据。*\n")
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w", encoding="utf-8") as f:
f.write("\n".join(L))
print("written:", OUT)
print("modules:", len(modules), "leaves:", leaves_count, "sample files:", total_files)
if __name__ == "__main__":
main()