feat/m3-increments #1

Merged
liuwenxin merged 100 commits from feat/m3-increments into main 2026-10-19 04:59:57 -04:00
Showing only changes of commit 4552f2e703 - Show all commits

View File

@ -0,0 +1,106 @@
# M2 实施计划:基础数据管理(②)+ 业务数据管理(③)
## 背景
- 需求 28 项裁决已落定(2026-10-12),M1(系统设置与参数体系)已完成并推送 `feat/system-params`。按剩余开发计划,M2 = ②基础数据 + ③业务数据 **合并交付**,一次表结构评审定稿 T+ 预留列与报表字段清单(即本计划 A 节)。
- 目标:三类基础数据(人员/产品/客户)+ 四类业务数据(业务/发货/回款/评级)全流程可维护、可留痕,为 M3 绩效核算供数。
- 设计调研已完成并抽查核对(`models/base.py` 命名约定、`formula.py` 纯函数、`test_models.py` 白名单、迁移现状 V3 均与设计一致)。
## 开工前处置
1. 当前 `feat/system-params` 工作区有未提交内容(2 个已删除 docs 文件、若干未跟踪的测试/文档)——先提交或与用户确认处置。
2. 从 `feat/system-params` 切出 `feat/base-business-data`(M2 外键依赖 M1 的 `business_roles`/参数体系)。
## A. 表结构(V4 基础数据 6 表 + V5 业务数据 7 表)
公共约定(照 `models/base.py` 现状):表名小写蛇形复数;`NAMING_CONVENTION` 自动产出 `uk_/idx_/fk_`;`mysql_table_args("中文表注释")`;可编辑表继承 `TimestampMixin`;**append-only 留痕表不继承**(仅手写 `create_time`,`login_logs` 先例)。金额 `DECIMAL(14,2)`、单价 `(14,4)`,schema 入出参一律 `str`,服务层 `Decimal`/`Fraction`。
### T+ 预留四列(覆盖 6 张主表:personnel/products/customers/businesses/shipments/receipts)
| 列 | 类型 | 说明 |
| --- | --- | --- |
| `source_no` | VARCHAR(64) NULL | T+ 原始单据号,普通索引(幂等去重锚点;唯一键待 IT 定键后小迁移升级) |
| `t_plus_id` | VARCHAR(64) NULL | T+ 内部标识 |
| `sync_batch_no` | VARCHAR(32) NULL | 同步批次号(M4 写入) |
| `synced_at` | DATETIME NULL | 最近同步时间 |
全部可空、不设唯一键(对照表 #26 键语义未定)。`personnel_plans` 仅 `source_no` 单列;明细行表不加(随主表同步)。**「同步不覆盖人工字段」写入表注释**:`business_items.guided_price`、各归属列、`personnel.business_role_id`/`salary_standard`、`customers.customer_type`、`businesses.status`。
### V4 六表
1. **`personnel`** 人员表:`personnel_name`、`business_role_id`(FK→business_roles RESTRICT,同步不覆盖)、`manager_id`(FK→personnel 自引用,NULL=经理本人/未分配,支撑「本团队业务员」)、`phone`、`hire_date`、`is_probation`(试用期照算不发放,裁决 #17)、`salary_standard DECIMAL(12,2)`、`status`(1在职 0离职)、T+4 列、`remark`。不与 `users` 关联(一期营销人员不登录)。
2. **`personnel_regions`** 产品辐射区域:`personnel_id` FK、`region_name`、`remark`(1:N 子表,随人员维护)。
3. **`personnel_plans`** 市场拓展计划:`personnel_id` FK、`plan_period`、`plan_content`、`target_desc`、`source_no`。
4. **`products`** 产品表:`product_name`、`spec_model NOT NULL DEFAULT ''`(空串参与唯一键,对齐 MySQL 组合索引 NULL 不去重先例)、`unit`、`stock_quantity`(T+ 语义待确认,原样存储)、`sale_price`、`product_category`(`municipal_water`/`rail_transit`,**与 M1 `product.threshold` 参数维度同一值域常量 `PRODUCT_CATEGORIES`**,阈值由 M1 参数承担不建列)、`status`、T+4 列、`remark`。`uk_products_name_spec(product_name, spec_model)`。
5. **`customers`** 客户表:`company_name`、`owner_name`(客户负责人**人名字符串**,非系统人员引用)、`contact_person`、`contact_phone`、`address`、`industry`、`customer_type`(`personal` 个人户 / `manager_personal` 经理个人户 / `enterprise` 企业户,计提规则匹配锚点,同步不覆盖)、`owner_personnel_id` FK(归属,变更不回溯,裁决 #19)、T+4 列、`remark`。`uk_customers_company_owner(company_name, owner_name)`。
6. **`ownership_change_logs`** 归属变更留痕(**append-only**):`owner_type`(1客户 2业务)、`customer_id`/`business_id` 可空 FK、`old/new_owner_personnel_id`、`old/new_assistant_personnel_id`(冗余固化防人员删除后不可读)、`change_reason`、`operator_id`(FK→users SET NULL)、`operator_name`、`create_time`。独立表而非 JSON:可分页/join 人员/导出,与 M1 `param_values` 同一哲学。
### V5 七表
7. **`businesses`** 业务表:`business_no`(自动生成 `YW{YYYYMMDD}-{4位序号}`,即报表「合同编号」,uk)、`customer_id` FK、`business_date`、`status`(1执行中 2已完成 3已终止,终态不可变更)、`owner_personnel_id`(可空=待指定,M3 核算前置校验非空)、`assistant_personnel_id`(一期核算不使用,裁决 #15 剔除协助 25%,仅记录事实)、`is_special`/`special_note`、**五个汇总冗余列** `total_sale_amount`/`total_cost_amount`/`total_ship_amount`/`total_received_amount`/`total_freight_amount`(服务层事务内单点重算,禁手工直改;运费不计毛利润口径,裁决 #2)、T+4 列、`remark`。
8. **`business_items`** 业务产品明细:`business_id` FK、`product_id` 可空 FK(可空=自由录入,配合「数据提取」)、`product_name`/`spec_model`/`unit` 冗余固化(防产品改名影响历史)、`product_category`(**行级折算锚点**,混合业务按产品逐条算,裁决 #14)、`quantity`、`sale_price`、`sale_amount`(服务层按 数量×单价 固化)、`cost_amount`(可空,来源待 T+ 确认)、`guided_price`(人工录入,同步不覆盖)、`remark`。**不建** `profit_amount`/`profit_rate`/`converted_profit` 落库列——派生值落库会在成本调整/参数改版后漂移,M2 以只读毛利润视图出数,M3 台账才是固化点。
9. **`business_status_logs`** 状态流转留痕(**append-only**):`business_id` FK、`old_status`、`new_status`、`change_reason`、`operator_id`/`operator_name`、`create_time`。
10. **`shipments`** 发货单:`shipment_no`(`SH{YYYYMMDD}-{序号}`,uk)、`business_id` FK、`ship_date`(**回款天数基准日,发货日首日=第 1 天**,裁决 #1)、`freight_amount`(仅记录展示)、`total_amount`(明细汇总冗余)、T+4 列、`remark`。
11. **`shipment_items`** 发货明细:`shipment_id` FK、`product_id` 可空、`product_name`/`spec_model`/`unit` 冗余、`quantity`、`sale_price`、`amount`。
12. **`receipts`** 回款表:`receipt_batch_no`(`HK{YYYYMMDD}-{序号}`,uk)、`business_id` FK、`shipment_id` **可空** FK(**手动指定发货批次**,裁决 #18;须与 business_id 同属一个业务,服务层强校验;指定后回款天数方可计算)、`receipt_date`(M3 按此日匹配参数版本)、`amount`(≥0,一期禁负数)、`is_barter`(以物抵顶标记,M3 按 80% 折算,M2 仅标记)、`settlement_status`(0未核算 1已核算,**M3 防重锚点**,预置避免 M3 反向迁移)、T+4 列、`remark`。
13. **`customer_rating_grades`** 评级等级配置:`grade_name` uk、`min_score`/`max_score DECIMAL(5,1)`、`sort_order`(判定=按序首个 min_score ≤ 总分,避免区间边界歧义)、`remark`。种子 4 条(迁移 + init-dev 双写):A 优秀 90-100、B 良好 75-89、C 合格 60-74、D 待改进 0-59。评分项固定 5 项 × 20 分为前端常量(#25:仅打分工具,不影响绩效金额)。
### 编号生成规则(`core/constants.py` 集中前缀常量)
`{前缀}{YYYYMMDD}-{当日4位序号}`,序号按自然日重置。服务层 `SELECT MAX(no) ... FOR UPDATE` 后 +1;撞 uk(1062)兜底重试一次后仍失败返回 400(沿用 M1 code-review 修复过的并发兜底模式)。
### 客户分类 × M1 参数对应(M3 消费契约)
| customer_type | 核算规则 | M1 参数 |
| --- | --- | --- |
| `personal` | `10‰ − 天数×4/360‰`,361 天起 4‰ | `salesperson.rate.*`/`days.boundary` |
| `manager_personal` | 归经理本人,12.5%+5% 结构 | `manager.share.*` |
| `enterprise` | 固定计提(2‰/3‰ 按日期) | `enterprise.rate`(与产品类别交叉) |
## B. 接口与前端
- 新增 `app/api/v1/base/`(`/api/v1/base`,人员/产品/客户)与 `app/api/v1/business/`(`/api/v1/business`,业务/发货/回款/评级),与 `system/*` 平级。约 30 个端点。
- **权限**:全部登录即可(`get_current_user`,不套 `AdminUser`)——M2 是业务操作(需规 2.5.1「普通用户」),M1 的 `AdminUser` 仅用于 system/*。写操作全部 `audit_service.audit()` 留痕。
- 关键端点:人员 CRUD+区域/计划全量替换(**clear 后立即 flush**,`replace_roles` 先例坑);产品 CRUD+`POST /products/extract`(业务明细未入库产品按名称+规格批量建产品,幂等);客户归属变更(事务内更新+插留痕);业务 CRUD(明细行数组、汇总重算 `_recalc_totals`、状态机 1→2/3 终态 400、毛利润视图 `GET /businesses/{id}/gross-profit?asOf=`);发货/回款 CRUD(回款跨业务指定批次 400、已核算禁改禁删);评级等级 CRUD(区间不重叠校验)。
- 关键校验:删除保护三类(被引用→400 引导离职/停用/改终止);金额 ≥0;`quantity>0`;销售额 0 防除零(`formula.profit_rate` 已内置)。
- 前端 7 页(复用 `usePagedList`/`useConfirmAction`/`AppPagination`/`StatusTag`/`--ys-*` token,不新增基础组件):`/base/personnel`(编辑抽屉内子表编辑区域/计划)、`/base/products`(类别下拉展示当前阈值、提取产品按钮)、`/base/customers`(归属变更二次确认文案含「不回溯历史」)、`/business/businesses`(明细行编辑器、详情抽屉含状态时间线/毛利润卡片)、`/business/shipments`、`/business/receipts`(选业务级联加载该业务发货单,可清空)、`/business/rating`(5 项评分实时出等级+映射维护抽屉,标注「仅打分工具」)。菜单新增「基础数据」「业务数据」两组,均不设 `requiresAdmin`。api 封装 7 个文件(蛇形字段直出,同 `businessRoles.ts` 模式)。金额展示字符串直出,禁 float 格式化。
## C. 任务分解(T1–T15)
依赖链:`T1 → (T2∥T3∥T4) → T5 → T6 → T7 → T8 → (T9∥T10) → T11 → (T12∥T13∥T14) → T15`。每任务「实现 → 测试/冒烟 → commit(`feat(scope): 中文说明`)」。
| # | 任务 | 要点 |
| --- | --- | --- |
| T1 | V4 迁移 + 基础数据模型 | `models/personnel.py`/`product.py`/`customer.py`;**注册进 `models/__init__.py`(漏注册会生成删表迁移)**;`test_models.py` EXPECTED_TABLES +6;autogenerate → 人工核对 → 导出 `db/migration/V4__*.sql`;同步 `init-dev.sql`(含 alembic_version 标记);`db/queries/基础数据.sql` 留档 |
| T2 | 人员管理后端 | schemas/repositories/services/api 四层;岗位存在且启用、manager 非自己且在职、删除保护→提示改离职、区域/计划全量替换 clear 后立即 flush |
| T3 | 产品管理后端 | 类别校验复用 `PRODUCT_CATEGORIES`;1062→400;删除保护;`extract` 幂等 |
| T4 | 客户管理后端 | 公司+负责人唯一;归属变更(更新+留痕同事务);留痕分页查询 |
| T5 | 基础数据集成测试(统一编写点) | `test_personnel.py`/`test_products.py`/`test_customers.py`:唯一冲突、删除保护、留痕内容、未登录 401、审计双记录、非法枚举;更新 `backend/tests/README.md` |
| T6 | V5 迁移 + 业务数据模型 | `models/business.py`/`shipment.py`/`receipt.py`/`rating.py`;EXPECTED_TABLES +7;等级种子 4 条双写;`db/migration/V5__*.sql` + `init-dev.sql` + `db/queries/业务数据.sql` |
| T7 | 业务管理后端 | 编号生成;明细全量替换+五项汇总重算;状态机终态 400;归属留痕;毛利润视图(`resolve_snapshot` + `formula.profit_rate`/`converted_profit`,asOf 参数) |
| T8 | 发货管理后端 | 金额/运费汇总回写业务冗余列;终态业务禁新发货;被回款指定禁删 |
| T9 | 回款管理后端 | 批次号;**shipment_id 同业务强校验**;已核算禁改禁删;合计回写;DTO 出 `paymentDays`(`formula.payment_days`) |
| T10 | 评级等级配置后端 | 区间不重叠/0≤min≤max≤100 校验;名称唯一 |
| T11 | 业务数据集成测试(统一编写点) | 编号格式与序号递增、终态拦截、三处汇总冗余一致性、跨业务 400、已核算禁改、毛利润视图(27 天回款天数/防除零/取参)、审计、401;**全量 pytest 回归** |
| T12 | 前端基础数据三页 | 人员/产品/客户;归属变更确认文案含「不回溯历史」;`pnpm type-check && pnpm build` |
| T13 | 前端业务/发货/回款三页 | 明细行编辑器、详情抽屉、回款级联指定批次、金额字符串直出 |
| T14 | 前端评级计算器页 | 5 项评分→实时等级;映射维护抽屉;「仅打分工具」标注 |
| T15 | e2e 与文档收尾 | `e2e/base_data.py`/`business_data.py` 全链路(建数据→业务→发货→回款→回款天数→毛利润→评级);全量回归(后端基线 406+ 新增全绿、前端构建、e2e);三份 README 更新 |
## 验证方式
- T1/T6:`uv run pytest tests/integration/test_schema_sync.py tests/unit/test_models.py -q` 全绿(双轨零漂移是硬关口)。
- T5/T11:模块集成用例统一编写并执行;T11 后 `uv run pytest -q` 全量回归。
- T12–T14:`pnpm type-check && pnpm build`。
- T15:后端全量 pytest + 前端构建 + 两个 e2e 脚本全过;`ai-aux/AI_CONTEXT.md` 历史任务摘要由主会话收尾时更新。
## 风险与开放问题(默认处理,不阻塞开工)
1. 毛利润成本来源待 T+:`cost_amount` 可空+人工可录先行;视图中成本缺失的行标注「无法计算」。
2. 人员条目 T+ 边界:一期手工 CRUD 完整可用,同步以 `source_no` 幂等、不覆盖人工字段。
3. 拓展计划粒度:自由条目容器(期间+内容+指标说明)。
4. 业务编号前缀:一期代码常量 YW/SH/HK,如需配置走 M1 参数体系。
5. 终态回退:一期不可变更(400);如需重开再加 2→1 规则。
6. 负数回款/红字:一期禁负金额;冲销需求出现时 M3 前补设计。
7. 回款未指定批次:允许保存,列表提供筛选,M3 核算前置校验非空。
8. T+ 幂等键:`source_no` 普通索引先行,IT 定键后小迁移升级。