223 lines
8.9 KiB
Python
223 lines
8.9 KiB
Python
#!/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()
|