#!/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()