first commit
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user