first commit

This commit is contained in:
Michelle Zan
2026-09-03 15:35:23 +08:00
commit 9b26161bcf
510 changed files with 29402 additions and 0 deletions
+222
View File
@@ -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()