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()
|