From a748b406180f149978f95fad540f136e3a1d03a5 Mon Sep 17 00:00:00 2001 From: linli2004 Date: Tue, 16 Jun 2026 14:44:20 +0800 Subject: [PATCH 1/3] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20finance=20Demo?= =?UTF-8?q?=20=E4=B8=8E=E7=94=A8=E6=88=B7=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demo(examples/finance/): - index.html 演示完整 5 步流程(录入→标准化→分类→审核→统计) - seed.py 生成种子数据 - README / RUN_DEMO / PLAN 文档 用户文档: - docs/user-guide/:财务管理 + 人力资源 + 资产 + 业务线用户手册 - docs/dev/finance-integration-plan.md:集成策略与分期计划 - docs/add/hr-email-import.md:招聘邮箱导入架构设计 - STATUS.md:项目状态总览 --- STATUS.md | 26 ++ docs/add/hr-email-import.md | 217 +++++++++++++ docs/dev/finance-integration-plan.md | 157 +++++++++ docs/user-guide/asset.md | 95 ++++++ docs/user-guide/business.md | 1 + docs/user-guide/finance.md | 108 +++++++ docs/user-guide/human.md | 17 + docs/user-guide/index.md | 6 + examples/finance/PLAN.md | 180 +++++++++++ examples/finance/README.md | 24 ++ examples/finance/RUN_DEMO.md | 61 ++++ examples/finance/demo.db | Bin 0 -> 45056 bytes examples/finance/index.html | 459 +++++++++++++++++++++++++++ examples/finance/seed.py | 225 +++++++++++++ 14 files changed, 1576 insertions(+) create mode 100644 STATUS.md create mode 100644 docs/add/hr-email-import.md create mode 100644 docs/dev/finance-integration-plan.md create mode 100644 docs/user-guide/asset.md create mode 100644 docs/user-guide/business.md create mode 100644 docs/user-guide/finance.md create mode 100644 docs/user-guide/human.md create mode 100644 docs/user-guide/index.md create mode 100644 examples/finance/PLAN.md create mode 100644 examples/finance/README.md create mode 100644 examples/finance/RUN_DEMO.md create mode 100644 examples/finance/demo.db create mode 100644 examples/finance/index.html create mode 100644 examples/finance/seed.py diff --git a/STATUS.md b/STATUS.md new file mode 100644 index 0000000..fdcf3f7 --- /dev/null +++ b/STATUS.md @@ -0,0 +1,26 @@ +# STATUS + +## Recent commits + +- `1eea498` myst.yml 仅保留 user-guide 目录,移除其他 toc 条目 +- `59c6893` 添加资产职能用户手册(基于 clig.dev 评审完善) +- `09c50f0` 移除已废弃的产品文档技能(BRD/DRD/PRD) +- `c3ac35e` 添加 Apache 2.0 协议 +- `45fe194` record architecture decisions belong to human principle + +## Branches + +- `main` — 当前活跃分支 +- `gh-pages` — GitHub Pages 部署分支 + +## Submodules + +无子模块。 + +## docs/ 状态 + +**myst.yml 当前 toc 仅包含:** +- `user-guide/asset.md` + +**遗留目录(文件仍在磁盘,但已从 myst.yml 移除):** +- `brd/`, `prd/`, `drd/`, `ixd/`, `add/`, `dev/`, `ops/` diff --git a/docs/add/hr-email-import.md b/docs/add/hr-email-import.md new file mode 100644 index 0000000..4c9ef35 --- /dev/null +++ b/docs/add/hr-email-import.md @@ -0,0 +1,217 @@ +# 招聘邮箱导入程序设计 + +## 问题 + +人力资源团队使用招聘专用邮箱(如 `zhaopin@quanttide.com`)接收简历投递、面试安排、录用沟通等邮件。目前这些邮件散落在邮箱中,没有结构化的候选人数据管理。 + +`docs/user-guide/human.md` 中有占位命令 `qtadmin human xxxxx`,描述为"使用 lark-cli 获取招聘邮箱数据并提交到服务端",但无实现。 + +## 设计目标 + +- 将招聘邮件从邮箱中导入为结构化候选人数据 +- 自动分类邮件类型(简历投递 / 面试邀请 / 录用通知 / 拒信) +- 提取候选人关键信息(姓名、岗位、联系方式) +- 支持增量导入和持续监控 +- 与 provider API 对接持久化数据 + +## 整体架构 + +``` +┌──────────────┐ subprocess ┌──────────────────┐ HTTP ┌──────────────┐ +│ lark-cli │ ◄──────────────► │ qtadmin human │ ────────► │ Provider │ +│ (mail API) │ │ import-email │ │ (FastAPI) │ +│ │ │ │ │ │ +│ +triage │ ──邮件列表────── │ 1. fetch │ │ POST /hr/ │ +│ +message │ ──邮件详情────── │ 2. classify │ │ candidates │ +│ attachments │ ──附件下载────── │ 3. extract │ │ POST /hr/ │ +│ │ │ 4. submit │ │ emails │ +└──────────────┘ └──────────────────┘ └──────┬───────┘ + │ + ▼ + ┌──────────────┐ + │ PostgreSQL │ + │ (via SQLite │ + │ for dev) │ + └──────────────┘ +``` + +### 分层职责 + +| 层 | 职责 | 技术 | +|---|---|---| +| **Connector** | 通过 lark-cli 访问招聘邮箱 | CLI subprocess 调用 `lark-cli mail` | +| **Pipeline** | 获取 → 分类 → 提取 → 提交 | Typer 命令编排 | +| **Provider** | 持久化候选人数据 | FastAPI + SQLAlchemy | +| **Storage** | 数据存储 | PostgreSQL(生产)/ SQLite(开发) | + +## CLI 设计 + +### 命令树 + +``` +qtadmin human +├── connect 测试邮箱连接 +├── import-email 全量导入(fetch + classify + extract + submit) +│ ├── --mailbox 指定邮箱地址(默认配置文件中的招聘邮箱) +│ ├── --days 导入近 N 天的邮件(默认 30) +│ ├── --limit 最大导入数 +│ ├── --dry-run 预览模式,不提交 +│ ├── --since 指定起始日期 +│ └── --watch 持续监控模式 +├── emails list 查看已导入的邮件 +├── candidates list 查看已提取的候选人 +└── classify 手动分类单封邮件 +``` + +### import-email 流程 + +``` +import-email + │ + ├── 1. fetch ── lark-cli mail +triage → 获取未处理的邮件列表 + │ └── 过滤:排除已导入的(按 message_id 去重) + │ + ├── 2. read ── lark-cli mail +message → 逐封读取详情 + │ └── 含正文、发件人、收件人、主题、附件元数据 + │ + ├── 3. classify ── 规则分类 + │ ├── resume 简历投递 — 含附件简历 + │ ├── interview 面试邀请 — 主题含"面试"/"interview" + │ ├── offer 录用通知 — 主题含"offer"/"录用" + │ ├── rejection 拒信 — 主题含"感谢"/"unfortunately" + │ └── other 其他 + │ + ├── 4. extract ── 从邮件中提取结构化信息 + │ ├── candidateName 候选人姓名(从正文/签名/附件推断) + │ ├── position 应聘岗位(从主题/正文提取) + │ ├── email 发件人邮箱 + │ ├── phone 联系方式(从正文正则匹配) + │ ├── attachments 附件列表(简历文件) + │ └── summary 邮件摘要 + │ + ├── 5. download ── lark-cli mail attachments → 下载附件简历 + │ + ├── 6. submit ── POST 到 provider API + │ ├── POST /hr/emails 保存邮件记录 + │ └── POST /hr/candidates 保存候选人(如已分类为 resume) + │ + └── 7. report ── 打印导入结果汇总 +``` + +### 设计原则(来自 clig.dev) + +- **默认存草稿,确认后发送**:`--dry-run` 预览变更,不加则询问确认 +- **输出示例**:每次运行打印汇总表 +- **退出码**:成功 0,部分失败 1,完全失败 2 +- **标准 flag 名**:`--dry-run`, `--limit`, `--since` 等 + +## Provider API 设计 + +### 数据模型 + +```python +# Candidate +candidate: + id: UUID + name: str # 候选人姓名 + email: str # 发件人邮箱 + phone: str? # 联系方式 + position: str? # 应聘岗位 + source: str # 来源渠道 ("email") + source_email_id: UUID # 关联邮件 + resume_file_url: str? # 简历文件地址 + status: str # new / contacted / interviewed / offered / hired / rejected + created_at: datetime + updated_at: datetime + +# RecruitmentEmail +email: + id: UUID + message_id: str # lark-cli message_id(去重依据) + mailbox: str # 邮箱地址 + subject: str + sender_name: str + sender_email: str + received_at: datetime + category: str # resume / interview / offer / rejection / other + body_text: str? # 纯文本正文 + has_attachments: bool + attachment_metadata: json? # 附件列表 [{name, size, type}] + is_imported: bool + imported_at: datetime? + +# ImportLog +import_log: + id: UUID + run_at: datetime + total_emails: int + imported_count: int + skipped_count: int + failed_count: int + errors: json? +``` + +### 端点 + +```python +POST /api/v1/hr/emails # 批量提交导入的邮件 +POST /api/v1/hr/candidates # 创建候选人(从简历邮件提取) +GET /api/v1/hr/candidates # 候选人列表(支持筛选) +GET /api/v1/hr/candidates/:id # 候选人详情 +PATCH /api/v1/hr/candidates/:id # 更新候选人状态 +GET /api/v1/hr/import-logs # 导入历史 +``` + +## 数据分类规则 + +邮件分类使用关键词规则,初始版本无需 ML: + +| 类别 | 判定条件 | 优先级 | +|---|---|---| +| **resume** | 有附件(.pdf/.doc/.docx)且主题/正文含"简历"/"应聘"/"求职"/"application" | 最高 | +| **offer** | 主题含"offer"/"录用"/"入职通知" | 高 | +| **interview** | 主题含"面试"/"interview"/"邀约" | 高 | +| **rejection** | 主题含"感谢投递"/"unfortunately"/"不合适" | 中 | +| **other** | 默认 | 最低 | + +## 分阶段实施 + +### Phase 1 — CLI 获取+本地保存 + +- 实现 `qtadmin human import-email --dry-run`,将邮件数据保存到本地 JSON 文件 +- 不依赖 provider,不依赖数据库 +- 可手动审核分类结果 + +### Phase 2 — Provider API + 数据库 + +- 在 provider 中添加 SQLAlchemy + SQLite +- 实现数据模型和 CRUD 端点 +- CLI 加上 `--submit` 模式,对接 provider API + +### Phase 3 — 简历解析 + +- 集成简历解析(python-resume-parser 或类似库) +- 从 PDF/DOCX 中提取结构化简历信息 +- 与候选人数据合并 + +### Phase 4 — 持续监控 + +- 实现 `--watch` 模式,使用 lark-cli mail +watch 实时监听新邮件 +- 新邮件到达自动触发导入 +- 可选:发送飞书 IM 通知给 HR + +## 设计取舍 + +| 取舍 | 选择 | 代价 | +|---|---|---| +| CLI 调用 lark-cli vs 直接使用 Lark OAPI SDK | CLI subprocess 调用 lark-cli | 多一层进程开销,依赖本地安装 lark-cli | +| 规则分类 vs ML 分类 | 初始用规则,预留 ML 接口 | 泛化能力有限,需持续维护规则 | +| JSON 文件中间态 vs 直写数据库 | Phase 1 先落文件,Phase 2 再落库 | Phase 1→2 需做数据迁移 | +| SQLite vs PostgreSQL | 开发用 SQLite,生产用 PostgreSQL(SQLAlchemy 抽象) | 需注意方言差异 | + +## 不解决的问题 + +- **简历解析的准确性**:Phase 3 评估后决定是否引入 ML 模型 +- **多邮箱聚合**:当前只支持单招聘邮箱,多邮箱需后续扩展 +- **候选人去重**:同一候选人多次投递的去重策略需后续定义 +- **与现有 HR 系统对接**:不替换现有 HR 系统,数据由 HR 团队确认后手动导出 diff --git a/docs/dev/finance-integration-plan.md b/docs/dev/finance-integration-plan.md new file mode 100644 index 0000000..6503029 --- /dev/null +++ b/docs/dev/finance-integration-plan.md @@ -0,0 +1,157 @@ +# Finance Integration Plan + +## 背景 + +`quanttide-finance-toolkit` 已经在 `qtadmin` 内形成镜像落点: + +- `quanttide-finance-toolkit/packages/fastapi` 对应 `qtadmin/packages/finance/fastapi` +- `quanttide-finance-toolkit/packages/dart` 对应 `qtadmin/packages/finance/dart` +- `quanttide-finance-toolkit/packages/flutter` 对应 `qtadmin/packages/finance/flutter` +- `quanttide-finance-toolkit/demo` 对应 `qtadmin/examples/finance` + +当前问题不是“如何把 toolkit 再搬一次”,而是: + +1. 哪一侧是唯一开发主线 +2. finance 如何进入 `qtadmin` 的 Studio 领域结构 +3. 后端、共享 DTO、Studio UI 三层如何分责 + +## 现状判断 + +### 已确认事实 + +- `qtadmin/packages/finance` 与 `quanttide-finance-toolkit/packages` 的核心代码基本一致。 +- 差异主要集中在脚本包装、文档位置和仓库外围文件,不在核心业务实现。 +- `qtadmin` 主仓已包含 finance demo 与后端测试。 +- `src/studio/packages/` 目前没有 finance 领域包,说明 finance 尚未真正进入 Studio 架构。 + +### 风险 + +- 若继续双仓双写,finance 会持续分叉。 +- 若直接把 `packages/finance/flutter` 当作 Studio 模块使用,会把演示壳和正式客户端耦合在一起。 +- 若过早并入 `src/provider`,会在权限、组织、租户模型尚未稳定时放大维护成本。 + +## 目标 + +### 目标一:唯一事实来源 + +将 `qtadmin/packages/finance` 设为 finance 的唯一开发主线。 + +### 目标二:分层清晰 + +finance 在 `qtadmin` 内分为三层: + +- `packages/finance/fastapi`:独立后端能力 +- `packages/finance/dart`:共享 DTO / domain model +- `packages/finance/flutter`:Flutter API adapter +- `src/studio/packages/qtadmin-finance`:Studio 专属页面、状态管理、领域视图 + +### 目标三:先接主流程,再补外围 + +Studio 集成优先覆盖: + +1. 录入 +2. 分类审核 +3. 统计看板 + +凭证层不是第一阶段重点。 + +## 目标架构 + +```text +qtadmin/ +├── packages/ +│ └── finance/ +│ ├── fastapi/ # finance backend +│ ├── dart/ # shared DTO and models +│ └── flutter/ # API client and Flutter adapter +├── examples/ +│ └── finance/ # demo and manual verification +└── src/ + └── studio/ + └── packages/ + └── qtadmin-finance/ + ├── lib/ + │ ├── finance.dart + │ └── src/ + │ ├── config/ + │ ├── screens/ + │ └── views/ + └── test/ +``` + +## 决策 + +### 1. 主线归属 + +- 日常开发只改 `qtadmin/packages/finance` +- `quanttide-finance-toolkit` 作为过渡仓或发布镜像,不再承担双向手工同步 + +### 2. Studio 集成方式 + +- 新建 `src/studio/packages/qtadmin-finance` +- 该包依赖 `packages/finance/flutter` 暴露的 API client +- 该包只承载 Studio 语境下的页面、路由目标、状态管理、交互视图 + +### 3. 后端接入方式 + +- 短期:`packages/finance/fastapi` 独立运行,Studio 通过 base URL 调用 +- 中期:待权限、组织、租户边界稳定后,再评估是否挂入统一 provider + +### 4. demo 定位 + +- `examples/finance` 保留为联调与产品验证环境 +- 不作为 Studio 正式实现的源码来源 + +## 分期实施 + +### P0:治理收口 + +- 明确 `qtadmin/packages/finance` 为唯一主线 +- 在 toolkit 仓补迁移说明 +- 停止双向手改 + +### P1:包边界固定 + +- 明确 `packages/finance/dart` 只做共享 DTO / model +- 明确 `packages/finance/flutter` 只做 adapter,不继续堆页面壳 +- 新建 `qtadmin-finance` 占位包 + +### P2:Studio 最小接入 + +- 在 Studio 中增加 finance 路由入口 +- 建立录入、审核、统计三个页面骨架 +- 将现有 finance API client 接入 Studio 层状态管理 +- 将 finance API base URL 提升到 Studio 应用配置,避免路由层硬编码 + +### P3:组织与权限 + +- 补组织维度筛选 +- 补角色权限 +- 补统一鉴权注入 + +### P4:仓库归档 + +- 评估 `quanttide-finance-toolkit` 是归档、镜像发布,还是作为外部只读仓保留 + +## 第一阶段工作项 + +### 必做 + +- 新增 `qtadmin-finance` 包骨架 +- 为 finance Studio 集成建立文档约束 +- 保持 finance demo、adapter、backend 三层可独立演进 +- 通过 `QTADMIN_FINANCE_API_BASE_URL` 管理 Studio 联调地址 + +### 暂不做 + +- 不立即改 `src/studio/lib/router.dart` +- 不立即将 finance 并入 `src/provider` +- 不立即改动凭证层设计 + +## 验收标准 + +- finance 只存在一套日常维护源码主线 +- `src/studio/packages/qtadmin-finance` 成为后续 Studio 集成落点 +- `packages/finance/flutter` 与 Studio UI 边界清晰 +- 后续新增 finance 功能不再需要在两个仓库间复制 +- Studio finance 不再依赖硬编码 `localhost` API 地址 diff --git a/docs/user-guide/asset.md b/docs/user-guide/asset.md new file mode 100644 index 0000000..238a4ec --- /dev/null +++ b/docs/user-guide/asset.md @@ -0,0 +1,95 @@ +# 数字资产职能 + +通过 `qtadmin asset` 命令管理数字资产。 + +无参数时显示简要帮助;`qtadmin asset --help` 或 `qtadmin asset -h` 列出所有子命令及用法。 + +## 安装 + +```bash +pip install qtadmin-cli +# 或从源码安装 +pip install -e src/cli +``` + +--- + +## 命令 + +### `qtadmin asset backup` (stable) — 日志归档 + +将 `docs/journal/` 下的过期日志移到 `docs/archive/journal/`,自动提交并推送子模块。建议每周运行一次。 + +```bash +qtadmin asset backup # 归档 3 天前的日志(默认) +qtadmin asset backup --days 7 # 归档 7 天前的日志 +qtadmin asset backup --dry-run # 预览模式,不实际移动 +qtadmin asset backup --yes # 跳过确认直接执行 +qtadmin asset backup -y # 同上,短格式 +``` + +默认会询问确认;使用 `--yes` / `-y` 跳过交互,`--dry-run` 预览变更。 + +执行输出: + +``` +$ qtadmin asset backup -y +项目根目录:/home/user/project +扫描到 38 个日志文件 +开始归档... +已移动:docs/journal/default/2026-05-28.md -> docs/archive/journal/default/2026-05-28.md +提交子模块变更... +已推送:docs/journal +归档完成! +``` + +常见错误: +- 子模块存在未提交变更时,`backup` 会先尝试提交,失败则提示用户手动处理 +- 网络断开导致 push 失败时,命令输出推送错误信息,本地 commit 仍然保留 + +### `qtadmin asset audit` (stable) — 资产审计 + +审计 Git 仓库是否符合标准资产体系规范。建议发布前运行。 + +```bash +qtadmin asset audit # 审计当前目录 +qtadmin asset audit /path/to/repo # 审计指定仓库 +qtadmin asset audit --verbose # 显示所有通过项目 +``` + +审计通过时退出码为 0,未通过时退出码为 1。 + +审计项: +- 必需文件:README.md、CONTRIBUTING.md、AGENTS.md、CHANGELOG.md、.gitignore +- 上述文件的内容规范 +- 子模块状态(未推送的提交会被标记) +- 提交信息是否符合 Conventional Commits +- CHANGELOG 与 pyproject.toml 版本一致性 + +执行输出: + +``` +$ qtadmin asset audit +✅ 所有审计项通过 + +$ qtadmin asset audit --verbose +✅ 必需文件:README.md — 通过 +✅ 必需文件:CONTRIBUTING.md — 通过 +… +✅ 提交规范符合度 — 3/3 符合 (100%) +✅ 版本发布规范一致性 — 通过 +``` + +--- + +## 限制 + +- `asset refresh` 已移除(功能已迁移至其他工具) +- `asset apply` 规划中,尚未实现 + +## 说明 + +- 两个命令均经过单元测试和集成测试覆盖,可在 v0.0.1 生产使用 +- 更多用法参见 `qtadmin asset backup --help`、`qtadmin asset audit --help` +- 详细文档见 `src/cli/docs/user/asset_backup.md` +- 在线文档:[https://github.com/quanttide/quanttide-tech](https://github.com/quanttide/quanttide-tech) diff --git a/docs/user-guide/business.md b/docs/user-guide/business.md new file mode 100644 index 0000000..dbff196 --- /dev/null +++ b/docs/user-guide/business.md @@ -0,0 +1 @@ +# 商务拓展职能 diff --git a/docs/user-guide/finance.md b/docs/user-guide/finance.md new file mode 100644 index 0000000..5dbbd20 --- /dev/null +++ b/docs/user-guide/finance.md @@ -0,0 +1,108 @@ +# 财务管理 + +量潮财务管理模块提供财务记录的标准化录入、分类和统计分析能力。核心流程: + +``` +录入 → 标准化确认 → 分类审核 → 统计看板 +``` + +## 使用方式 + +### Studio 工作台 + +在量潮管理后台侧边栏点击"财务管理"进入 Finance 工作区。 + +工作区分为四个区域: + +**统计概览** — 顶部三张卡片显示当前范围内的汇总数据: +- Records:标准化记录总数 +- Amount:金额汇总 +- Classified:已确认分类的记录数 + +**手工录入** — 左侧"Manual Entry"面板: +1. 填写 Raw Text(原始描述,如"打车到机场,188 元") +2. 填写 Business Date(格式 `YYYY-MM-DD`) +3. 填写 Amount Cents(金额,单位为分,如 `18800` 表示 ¥188.00) +4. 选择 Record Type(expense / income / transfer / reimbursement / other) +5. 选择 Direction(outflow 支出 / inflow 收入) +6. 填写 Department 和 Person +7. 填写 Description +8. 点击"提交录入" + +录入成功后系统同时创建 SourceRecord 和 NormalizedRecord。 + +**审核队列** — 右侧"Review Queue"面板: +- 每条记录显示描述、日期、金额、部门、当前分类 +- 点击"编辑"可修改记录字段 +- 点击"审核"打开分类审核对话框,选择分类并确认/驳回 +- 勾选多条记录后点击"批量确认"可一次审核多条 + +**趋势与分布** — 下方面板: +- "Department Breakdown":按部门分组的金额与记录数排行 +- "Monthly Trend":月度金额与记录数趋势 + +### 命令行 + +```bash +# 启动后端服务 +cd packages/finance/fastapi +uvicorn fastapi_quanttide_finance.app:app --reload + +# 运行 Dart 测试 +cd packages/finance/dart +dart test + +# 运行 FastAPI 测试 +cd packages/finance/fastapi +python -m pytest +``` + +### 演示 Demo + +```bash +cd examples/finance +python seed.py # 生成种子数据 +# 打开 index.html 体验完整流程 +``` + +Demo 覆盖五步产品流程:录入 → 标准化确认 → 自动分类 → 批量审核 → 统计看板。 + +## 核心概念 + +| 术语 | 说明 | +|---|---| +| SourceRecord | 原始记录,保留导入时的原始证据和文本 | +| NormalizedRecord | 标准化记录,抽取后的结构化事实字段 | +| ClassificationResult | 分类结果,作为叠加维度不写入标准化记录 | +| RecordLink | 关联表,连接 SourceRecord 与 NormalizedRecord | +| amount_cents | 金额,单位为分(如 ¥188.00 = 18800) | +| direction | 资金方向,outflow(支出)或 inflow(收入) | + +## API + +后端提供 REST API(默认 `http://localhost:8000`): + +| 端点 | 方法 | 说明 | +|---|---|---| +| `/source-records` | GET/POST | 原始记录列表/创建 | +| `/source-records/{id}` | GET | 原始记录详情 | +| `/source-records/{id}/normalize` | POST | 执行标准化 | +| `/normalized-records` | GET/POST | 标准化记录列表/创建 | +| `/normalized-records/{id}` | GET/PATCH | 标准化记录详情/更新 | +| `/normalized-records/{id}/classifications` | GET/POST | 分类结果列表/创建 | +| `/classifications/{id}` | PATCH | 审核分类(accepted/rejected) | +| `/statistics/summary` | GET | 统计汇总 | +| `/statistics/breakdown` | GET | 分组统计 | +| `/statistics/trend` | GET | 趋势统计 | +| `/statistics/drilldown` | GET | 明细查询 | + +## Studio 集成配置 + +API base URL 通过 `QTADMIN_FINANCE_API_BASE_URL` 环境变量或 `FinanceModuleConfig` 注入,避免硬编码。 + +## 限制 + +- 金额以分为单位,字段约束 `amount_cents >= 0`,方向通过 `outflow`/`inflow` 表示 +- `raw_text` 超过 65535 字符会被拒绝 +- `description` 超过 1000 字符自动截断 +- 分类审核通过(`accepted`)后才纳入统计口径 diff --git a/docs/user-guide/human.md b/docs/user-guide/human.md new file mode 100644 index 0000000..a5c26f7 --- /dev/null +++ b/docs/user-guide/human.md @@ -0,0 +1,17 @@ +# 人力资源职能 + +## 使用方式 + +### 导入招聘邮箱 + +```bash +qtadmin human xxxxx +``` + +命令行工具使用`lark-cli`获取招聘邮箱数据并提交到服务端。 + +### 查看招聘进度 + +(工作台操作) + +可以xxxx看xxxx。 diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md new file mode 100644 index 0000000..d3b9170 --- /dev/null +++ b/docs/user-guide/index.md @@ -0,0 +1,6 @@ +# 用户指南 + +量潮管理后台分为业务和职能两类,以创始人视角统一管理公司各项事务。 + +业务:量潮数据、量潮课堂、量潮咨询、量潮云等。 +职能:人力资源、财务管理、商务拓展、数字资产等。 diff --git a/examples/finance/PLAN.md b/examples/finance/PLAN.md new file mode 100644 index 0000000..2f0777e --- /dev/null +++ b/examples/finance/PLAN.md @@ -0,0 +1,180 @@ +# Demo 计划:全流程财务演示(产品版) + +## 目标 + +制作一个**单页 Web 应用**覆盖 M1–M4 全链路,让财务部同事体验真实工作流: +**录入结构化单据 → 确认标准化 → 系统自动预分类 → 批量审核确认 → 统计看效果**。 + +--- + +## 与现有 demo/index.html 的关系 + +**新建文件**,不与现有 M4 统计页共用。文件名为 `demo/index.html`(覆盖原统计页),或另起 `demo/full.html`。 + +--- + +## 支持范围(硬性约束) + +| 维度 | 约束 | 原因 | +|------|------|------| +| 可录入的 source_type | 仅 `manual` | `csv_row` 是批量导入场景,demo 演示单笔录入 | +| 分类 taxonomy | 固定 `expense_type` | 后端只有一个 | +| 分类 category | `办公用品`/`差旅`/`采购`/`工资`/`其他` | 来自 `services/classification.py` | +| 币种 | 默认 `CNY`,不做多币种 | 无业务需求 | +| 金额聚合 | 始终返回数值 | `currency=*` 不暴露 | + +**不包含**:`csv_row`/`image`/`chat`/`bank_tx`/`form`/`api`;`source_channel`。 + +--- + +## 主流程 + +``` +① 录入单据 ② 确认标准化 ③ 系统自动预分类 ④ 批量审核 ⑤ 看统计 + (编辑 → 确认) + 人工调分类 (一键提交一类) +``` + +--- + +## 功能模块 + +### 模块 1:结构化单据录入(M1 修正) + +用户填的是业务字段,不是 `raw_text`。 + +**表单**: + +| 字段 | 控件 | 说明 | +|------|------|------| +| 金额 (元) | `` | 必填,单位元 | +| 方向 | `` | 默认当天 | +| 描述 | `` | 必填 | +| 部门 | `` | 选填 | + +**提交后**:前端组装为 `{ source_type: "manual", raw_text: "..." }` 调 `POST /source-records`。同时直接写入 `NormalizedRecord`(跳过标准化步骤,见下方说明)。 + +**为什么跳过 POST /source-records → POST .../normalize 两步?** +当前 `manual` Normalizer 没有结构化解析逻辑,它只是原样把 `raw_text` 拷贝到 `description`,`record_type=other`,`normalization_status=draft`。既然 demo 录入时已经填好了结构化字段,直接在录入时写入 `NormalizedRecord` 更高效。提交逻辑变为: + +``` +POST /source-records(存原始证据) ++ POST /normalized-records(同时写入标准化记录,字段来自表单) +``` + +--- + +### 模块 2:确认标准化(M2 修正) + +实际是"预览 → 编辑 → 确认"。 + +录入后立即展示一条**可编辑的标准化记录预览**: + +| 字段 | 来源 | +|------|------| +| 金额 / 方向 / 日期 | 来自录入表单 | +| 部门 / 人员 | 来自录入表单 | +| 描述 | 可编辑 | +| record_type | 自动设为 `expense`,可改 | + +**按钮**: + +| 操作 | 说明 | +|------|------| +| 确认 | 标准化记录写入 `normalization_status=normalized` | +| 修改 | 编辑后重新提交 | +| 放弃 | 删除该条记录(不回写) | + +--- + +### 模块 3:自动分类 + 批量审核(M3 修正) + +**核心变化**:系统先自动预分类,用户只需确认。 + +#### 3a. 自动预分类(前端规则引擎) + +根据 `NormalizedRecord.description` 的关键词匹配,预填 `category`: + +| 关键词规则 | 分类 | +|-----------|------| +| 含"机票""高铁""住宿""差旅""出差""酒店""交通" | 差旅 | +| 含"A4""墨盒""文具""打印""办公桌""办公椅""纸" | 办公用品 | +| 含"采购""购买""设备""服务器""电脑""软件" | 采购 | +| 含"工资""奖金""薪" | 工资 | +| 未匹配任何规则 | 其他 | + +规则写在 JS 中(`function autoClassify(description)`),不入库。**演示目的,非生产逻辑**。 + +#### 3b. 分类展示分组 + +标准化记录按 `category` 分组展示: + +``` +┌─ 差旅(3 条待审核)─────────────────────┐ +│ [x] 张伟 - 北京出差机票 - ¥2,300 │ +│ [x] 李强 - 上海高铁票 - ¥650 │ +│ [x] 王芳 - 杭州住宿 - ¥480 │ +│ [确认以上 3 条为差旅] [批量修改为...] │ +├────────────────────────────────────────┤ +│ ...其他类别... │ +└────────────────────────────────────────┘ +``` + +**交互步骤**: + +1. 每条记录旁显示系统预判的分类标签(绿色:已匹配 / 灰色:待确认) +2. 用户扫一眼,如果整体分类正确 → 点"确认以上 N 条为 XX" → 批量调 `POST .../classifications` → `review_status=accepted` +3. 如果某条分错了 → 在该条旁下拉框手动改分类 +4. 改完后调单条确认 + +**批量确认 API 调用**:逐条 POST(后端无批量端点,demo 前端循环调 `POST /normalized-records/{id}/classifications`)。 + +#### 3c. 已审核分类状态变更 + +确认后该条移到"已审核"区,统计看板 `classified_count` 立即增加。 + +--- + +### 模块 4:统计看板(M4 — 复用) + +同之前设计,但去掉过滤栏、去掉分类过滤器,自动刷新。 + +--- + +## API 调用清单 + +``` +# 1. 录入原始证据 +POST /source-records + { "source_type": "manual", "raw_text": "研发部张伟购A4纸一箱" } + +# 2. 同时写入标准化记录(录入时已结构化) +POST /normalized-records + { "record_type": "expense", "business_date": "2026-09-01", + "amount_cents": 12000, "direction": "outflow", + "department": "研发部", "person": "张伟", + "description": "购买A4纸一箱", + "normalization_status": "normalized" } + +# 3. 批量确认分类 +for each id in [3, 7, 12]: + POST /normalized-records/{id}/classifications + { "taxonomy": "expense_type", "category": "办公用品", + "classifier_kind": "rule", "confidence": 0.85 } + +# 4. 查看统计 +GET /statistics/summary +GET /statistics/breakdown?dimension=department +GET /statistics/trend?granularity=month +GET /statistics/drilldown?limit=15 +``` + +--- + +## 注意事项 + +1. **自动分类是演示辅助,非生产逻辑**。JS 关键词匹配只是为了 demo 中"系统先分、人再确认"的交互能跑通。生产环境应走后端规则引擎或 AI。 +2. **录入时写 NormalizedRecord** 跳过了 Normalizer 流程。这是因为 demo 场景下用户输入已经是结构化数据。生产环境仍应通过 Normalizer 做抽取。 +3. **批量确认无后端批量端点**,前端循环调单条 API。如果后续需要,可以加 `POST /classifications/batch`。 +4. **中文映射表**同之前版本。 diff --git a/examples/finance/README.md b/examples/finance/README.md new file mode 100644 index 0000000..54aa625 --- /dev/null +++ b/examples/finance/README.md @@ -0,0 +1,24 @@ +# 量潮财务工具 — 全流程演示 + +覆盖 M1–M4 全链路的 5 步产品流程体验。 + +## 快速启动 + +参见 [`RUN_DEMO.md`](RUN_DEMO.md)。 + +## 演示流程 + +``` +① 录入单据 → ② 确认标准化 → ③ 系统自动分类 → ④ 批量审核 → ⑤ 统计看板 +``` + +## 目录说明 + +| 文件 | 用途 | +|------|------| +| `index.html` | 演示界面(浏览器打开) | +| `seed.py` | 初始化演示数据 | +| `run_demo.sh` | macOS/Linux 一键启动 | +| `run_demo.bat` | Windows 一键启动 | +| `RUN_DEMO.md` | 启动指南 | +| `PLAN.md` | 产品设计文档 | diff --git a/examples/finance/RUN_DEMO.md b/examples/finance/RUN_DEMO.md new file mode 100644 index 0000000..46d6340 --- /dev/null +++ b/examples/finance/RUN_DEMO.md @@ -0,0 +1,61 @@ +# 量潮财务工具 — 部署与启动指南(Windows) + +## 环境要求 + +- Python 3.12+ +- 现代浏览器(Chrome / Edge / Firefox) + +--- + +## 步骤 + +### 1. 创建虚拟环境并安装依赖 + +```powershell +cd packages\fastapi +python -m venv .venv +.venv\Scripts\Activate.ps1 +pip install -e ".[dev]" +``` + +### 2. 初始化演示数据 + +```powershell +cd demo +python seed.py --reset +``` + +预期输出:创建约 57 条 SourceRecords + NormalizedRecords + 分类数据。 + +### 3. 启动后端服务 + +```powershell +cd packages\fastapi +.venv\Scripts\Activate.ps1 +$env:DEMO_DB="..\..\demo\demo.db"; uvicorn fastapi_quanttide_finance.app:app --reload +``` + +访问 `http://localhost:8000/health` 验证,返回 `{"status":"ok"}` 即正常。 + +### 4. 打开演示页面 + +在文件管理器中双击 `demo\index.html` 用浏览器打开。 + +--- + +## 演示流程 + +``` +录入单据 → 确认标准化 → 系统自动分类 → 批量审核 → 统计看板 +``` + +--- + +## 常见问题 + +| 问题 | 解决 | +|------|------| +| 端口被占用 | `uvicorn` 加 `--port 8001`,并修改 `index.html` 中 `API` 变量 | +| 数据为空 | 重新执行 `python seed.py --reset` | +| 页面无数据 | 确认 `$env:DEMO_DB` 指向了正确的 `demo.db` 路径 | +| 虚拟环境未激活 | `.venv\Scripts\Activate.ps1`(PowerShell)或 `.venv\Scripts\activate.bat`(CMD) | diff --git a/examples/finance/demo.db b/examples/finance/demo.db new file mode 100644 index 0000000000000000000000000000000000000000..0304a08c3400ad3d982a9684764b94f5de584a70 GIT binary patch literal 45056 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 literal 0 HcmV?d00001 diff --git a/examples/finance/index.html b/examples/finance/index.html new file mode 100644 index 0000000..b272f39 --- /dev/null +++ b/examples/finance/index.html @@ -0,0 +1,459 @@ + + + + + +量潮财务工具 — 全流程演示 + + + + +
+

量潮财务全流程演示

+

① 录入单据 → ② 确认标准化 → ③ 系统自动分类 → ④ 批量审核 → ⑤ 统计看板

+ + + + +
+

① 录入单据 M1

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+ + +
+

② 确认标准化 M2

+
录入单据后,此处显示标准化预览。
+
+ + +
+

③ 系统预分类 + 批量审核 M3

+
标准化确认后,系统自动分类并在此展示。
+
+ + +
+

④ 统计看板 M4

+
+
+

部门分布

+

月度趋势

+
+
+
+ + + + diff --git a/examples/finance/seed.py b/examples/finance/seed.py new file mode 100644 index 0000000..b7c8794 --- /dev/null +++ b/examples/finance/seed.py @@ -0,0 +1,225 @@ +"""M4 Demo — 填充演示数据到独立 demo 数据库。 + +不会触碰主开发库的 quanttide_finance.db。 +通过 --reset 确认后清空 demo 数据重新生成。 +""" + +import argparse +import sys +from datetime import date +from pathlib import Path +from random import choice, randint, seed as random_seed + +from sqlalchemy import create_engine, event +from sqlalchemy.engine import Engine +from sqlalchemy.orm import sessionmaker + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "packages/fastapi/src")) +from fastapi_quanttide_finance.models.source_record import SourceRecord +from fastapi_quanttide_finance.models.normalized_record import NormalizedRecord +from fastapi_quanttide_finance.models.record_link import RecordLink +from fastapi_quanttide_finance.models.classification_result import ClassificationResult +from fastapi_quanttide_finance.database import Base + + +@event.listens_for(Engine, "connect") +def _set_sqlite_pragma(dbapi_connection, connection_record): + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + + +DEMO_DIR = Path(__file__).resolve().parent +DEMO_DB_PATH = DEMO_DIR / "demo.db" +DB_URL = f"sqlite:///{DEMO_DB_PATH}" + +# ------- 演示数据 ------- + +DEPARTMENTS = ["研发部", "市场部", "行政部", "财务部", "销售部", "采购部"] +PEOPLE = { + "研发部": ["张伟", "李强", "王芳", "刘洋"], + "市场部": ["陈静", "赵敏", "周杰"], + "行政部": ["吴婷", "郑浩"], + "财务部": ["孙丽", "黄伟", "林涛"], + "销售部": ["马超", "朱红", "何亮", "徐飞"], + "采购部": ["胡明", "郭雪"], +} +COUNTERPARTIES = [ + "京东企业购", "携程商旅", "滴滴企业版", "中国石油", "中国移动", + "顺丰速运", "联想集团", "用友网络", "华为技术", "阿里云", +] +RECORD_DESCRIPTIONS = [ + "办公用品采购", "差旅报销", "项目外包服务费", "设备维修费", + "培训费用", "交通补贴", "通讯费", "快递费", + "软件订阅费", "招待费", "房租水电", "保洁服务", +] +AMOUNT_RANGES = { # (min, max) 单位分 + "expense": (5000, 500000), + "income": (10000, 2000000), + "transfer": (50000, 1000000), + "reimbursement": (10000, 200000), + "other": (1000, 100000), +} +CLASSIFICATION_CATEGORIES = ["办公用品", "差旅", "采购", "工资", "其他"] +CATEGORY_WEIGHT = {"办公用品": .3, "差旅": .25, "采购": .2, "工资": .15, "其他": .1} + + +def weighted_choice(options, weights): + r = randint(1, 100) + cumulative = 0 + for opt, w in zip(options, weights): + cumulative += w * 100 + if r <= cumulative: + return opt + return options[-1] + + +def main(): + parser = argparse.ArgumentParser(description="填充 demo 数据库") + parser.add_argument("--reset", action="store_true", help="确认清空 demo 数据后重新生成") + args = parser.parse_args() + + random_seed(42) + + engine = create_engine(DB_URL, echo=False) + SessionLocal = sessionmaker(bind=engine) + + if args.reset and DEMO_DB_PATH.exists(): + DEMO_DB_PATH.unlink() + print(f"Removed existing demo DB: {DEMO_DB_PATH}") + + if DEMO_DB_PATH.exists(): + print(f"Demo DB already exists: {DEMO_DB_PATH}") + print("Use --reset to regenerate.") + sys.exit(0) + + # 建表 + Base.metadata.create_all(engine) + print(f"Created demo DB: {DEMO_DB_PATH}") + + session = SessionLocal() + + # ---- 生成原始记录 ---- + all_srs = [] + sr_id = 0 + for month in [6, 7, 8]: + for dept in DEPARTMENTS: + num = randint(2, 4) + for _ in range(num): + sr_id += 1 + day = randint(1, 28) + person = choice(PEOPLE[dept]) + sr = SourceRecord( + source_type="manual", + raw_text=f"{dept}{person}提交的{choice(RECORD_DESCRIPTIONS)}", + ingestion_status="normalized", + ) + session.add(sr) + session.flush() + all_srs.append(sr) + + session.commit() + print(f"Created {len(all_srs)} SourceRecords") + + # ---- 生成标准化记录 ---- + all_nrs = [] + for i, sr in enumerate(all_srs): + dept = DEPARTMENTS[i % len(DEPARTMENTS)] + person = choice(PEOPLE[dept]) + month = 6 + (i // (len(DEPARTMENTS) * 3)) # 大致分配到 6-8 月 + day = 1 + (i % 28) + record_type = choice(["expense", "expense", "expense", "reimbursement", "other"]) + direction = "outflow" if record_type != "income" else choice(["outflow", "inflow"]) + amt_range = AMOUNT_RANGES.get(record_type, (10000, 100000)) + amount_cents = randint(*amt_range) + + nr = NormalizedRecord( + primary_source_id=sr.id, + record_type=record_type, + business_date=date(2026, month, day), + amount_cents=amount_cents, + currency="CNY", + direction=direction, + department=dept, + person=person, + counterparty=choice(COUNTERPARTIES), + description=choice(RECORD_DESCRIPTIONS), + normalization_status="normalized", + ) + session.add(nr) + session.flush() + all_nrs.append(nr) + + # 建立 RecordLink + rl = RecordLink( + source_record_id=sr.id, + normalized_record_id=nr.id, + relation_type="primary", + ) + session.add(rl) + + session.commit() + print(f"Created {len(all_nrs)} NormalizedRecords + {len(all_nrs)} RecordLinks") + + # ---- 生成分类 ---- + total_classifications = 0 + accepted_total = 0 + for nr in all_nrs: + # 约 85% 的记录有分类(剩余的作为"未分类"展示在统计中) + if randint(1, 100) > 85: + continue + cat = weighted_choice(CLASSIFICATION_CATEGORIES, [ + CATEGORY_WEIGHT[c] for c in CLASSIFICATION_CATEGORIES + ]) + is_accepted = randint(1, 100) <= 75 # 75% 已审核 + + cr = ClassificationResult( + normalized_record_id=nr.id, + taxonomy="expense_type", + category=cat, + classifier_kind="manual", + confidence=0.95 if is_accepted else 0.70, + review_status="accepted" if is_accepted else "candidate", + is_active=True, + ) + session.add(cr) + total_classifications += 1 + if is_accepted: + accepted_total += 1 + + session.commit() + print(f"Created {total_classifications} classifications ({accepted_total} accepted, {total_classifications - accepted_total} candidates)") + + # ---- 验证 ---- + total = session.query(NormalizedRecord).count() + sum_amount = session.query(__import__("sqlalchemy").func.sum(NormalizedRecord.amount_cents)).scalar() or 0 + classified = ( + session.query(NormalizedRecord) + .filter( + NormalizedRecord.id.in_( + session.query(ClassificationResult.normalized_record_id).filter( + ClassificationResult.review_status == "accepted", + ClassificationResult.is_active == True, + ) + ) + ) + .count() + ) + + print(f"\n=== Demo Data Summary ===") + print(f"Total records: {total}") + print(f"Sum amount_cents: {sum_amount:,} (¥{sum_amount/100:,.2f})") + print(f"Records with accepted classification: {classified}") + print(f"Classification rate: {classified/total*100:.0f}%") + print(f"\nDemo DB: {DEMO_DB_PATH}") + print("Ready! Start uvicorn and open the demo.") + print() + print("启动后端时需指定 demo 数据库:") + print(" DEMO_DB=1 uvicorn fastapi_quanttide_finance.app:app --reload") + print("或手动修改 database.py 中的 DATABASE_URL 指向 demo/demo.db") + + session.close() + + +if __name__ == "__main__": + main() From 614d06227e28bdcac83fd9120ea21a05e47edef5 Mon Sep 17 00:00:00 2001 From: linli2004 Date: Tue, 16 Jun 2026 15:10:12 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E5=8F=96=E6=B6=88=E8=B7=9F=E8=B8=AA?= =?UTF-8?q?=20demo.db=EF=BC=8C=E5=8A=A0=E5=85=A5=20*.db=20=E5=BF=BD?= =?UTF-8?q?=E7=95=A5=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + examples/finance/demo.db | Bin 45056 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 examples/finance/demo.db diff --git a/.gitignore b/.gitignore index be0fe38..ad0fe5e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ Thumbs.db .terraform/ terraform/terraform.tfstate terraform/terraform.tfstate.backup +*.db diff --git a/examples/finance/demo.db b/examples/finance/demo.db deleted file mode 100644 index 0304a08c3400ad3d982a9684764b94f5de584a70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45056 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 From a10d990fa65d3489cd772159d9ea8ccd90576613 Mon Sep 17 00:00:00 2001 From: linli2004 Date: Tue, 16 Jun 2026 19:59:09 +0800 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20CHANGELOG=20?= =?UTF-8?q?=E2=80=94=20v0.2.0=20finance=20=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92078b7..660ad71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/). +## [0.2.0] - 2026-06-16 + +### Added + +- **finance 模块** (`packages/finance/`) — 财务记录标准化与分析全链路 + - **Dart 包** (`packages/finance/dart/`):DTO 定义(SourceRecord、NormalizedRecord、ClassificationResult)、Journal/JournalEntry 凭证模型、freezed 序列化与 JSON 序列化、46 个单元测试 + - **FastAPI 后端** (`packages/finance/fastapi/`):SQLAlchemy ORM 模型、Alembic 迁移、Pydantic 请求/响应 schema、CRUD REST 路由、Normalizer 服务、统计接口(Summary/Breakdown/Trend/Drilldown)、121 个测试 +- `.github/workflows/dart-check.yml`:Dart CI 工作流(push/PR 自动执行 analyze + test) +- `.github/workflows/dart-publish.yml`:Dart CD 工作流(tag 匹配发布到 pub.dev) +- `docs/user-guide/finance.md`:finance 用户指南(Studio 操作 + CLI 使用 + 核心概念) +- `examples/finance/`:finance 演示(seed.py 数据填充 + HTML 看板) +- `STATUS.md`:项目全局状态文档 + +### Changed + +- `.gitignore`:全面重构——新增 Dart 构建产物(.dart_tool/)、Python 缓存(__pycache__/、*.pyc、.pytest_cache)、数据库文件(*.db)、IDE 配置、Terraform 等忽略规则 +- `docs/myst.yml`:同步更新目录结构 + +### Docs + +- `docs/dev/finance-integration-plan.md`:finance 集成计划(任务分解与依赖关系) +- `docs/user-guide/asset.md`、`business.md`、`human.md`、`index.md`:用户文档新增及更新 +- `docs/add/hr-email-import.md`:HR 邮件导入方案 + ## [0.1.0] - 2026-05-09 ### Studio