feat(client): A3 客户与行业目录(两级行业、客户组、字段审批)

背景:A3 需把行业 / 客户 / 客户组 / 客户级字段立为资产根节点;用户确认两级行业树与混搭 UI(行业 B / 客户列表 A / 详情 A / 新建 B / 客户组 B / 字段 A)。

改动:
- 新增 ADR-002,行业改为两级树,客户挂末级行业;同步 PRD / 技术架构 / 开发计划。
- 新增 client 模块:industries / customers / customer-groups / customer-fields 接口、真实 PrismaCustomerDirectory、客户状态机与客户转交、客户组数据范围解析、跨客户 404。
- 新增迁移与幂等种子(电商/金融两级行业、2 客户、1 客户组、4 条字段)。
- 前端新增行业/客户/客户组/字段审批页面与路由、导航更名(客户管理 / 行业管理 / 客户组),复用现有 token。
- 补充 shared 契约单测、client 集成用例、A3 浏览器 e2e。

影响面:client 模块与 client 表、shared 契约、web 路由/导航、ScopeContext 数据范围、OpenAPI 与种子;A1 客户转交占位由 client 实现替换。
回滚:回滚本提交;数据库保留新增表或按报告执行 down SQL;CUSTOMER_DIRECTORY 可切回空集实现。
验证:format:check / lint / typecheck / test 全绿(shared 131 / api 90 / web 125,共 346);test:int 5 文件 39 条;A3 e2e 1/1;build 8/8;真实浏览器六页验证通过。
This commit is contained in:
Stev_Wang committed 2026-11-03 21:45:38 +08:00
1 parent 8626996bb1
commit 140dc386c1
54 files changed
+9018 -147

No files matched your search

@@ -652,13 +652,13 @@ flowchart LR
#### 5.4.2 客户管理
**行业与客户是资产划分的两级目录**:行业 = 客户的业务属性(电商 / 金融等),客户 = 行业下的具体服务对象。信息模块以「行业 → 客户」上下级菜单呈现,是全部资产与生产任务的根节点。
**行业与客户是资产划分的两级目录**:行业本身为两级树(一级行业 = 业务属性,如电商 / 金融;二级行业 = 细分分类,如服饰鞋包 / 信贷),客户挂末级行业。信息模块以「行业树 → 客户」上下级菜单呈现,是全部资产与生产任务的根节点。
最小集:客户名称 / 简称(**简称必填**,用于全部自动命名)、所属行业(绑定一个行业包,可申请客户级覆盖)、服务状态(在服务 / 暂停 / 结束)、内部负责人与客户侧对接人、交付偏好(命名习惯、目录结构、常用规格、素材禁忌)、客户资产包、自定义字段(分两层,见下)。
- **自定义字段分两层**:基础信息字段由运营录入时自由追加键值对(如门店编号、内部产品代码),每客户 ≤ 10 个,**只用于检索与报表,永不进入生成**;需要进入生成或参与校核的字段走 5.4.9 审批(≤ 5 个)。
- **唯一性**:行业名称全局唯一;客户名称在同一行业内唯一。
- **不可跨行业移动**:客户必须归属一个行业,调整行业走「停用 + 新建」;删除行业前须校验其下无客户、无资产,否则只能停用。
- **唯一性**:一级行业名称全局唯一;二级行业名称在同一父级下唯一(不同一级行业下可重名);客户名称在同一末级行业下唯一。
- **不可跨行业移动**:客户必须归属一个末级行业,调整行业走「停用 + 新建」;删除行业前须校验其下无子行业、无客户、无资产,否则只能停用;有启用中的子行业时禁止停用。
- **新建客户时可套用行业资产包**:选行业 → 带出模板、场景、人物、提示词、卖点 → 替换为客户 Logo 与警示语。这是"新客户冷启动 ≤ 1 个工作日"的落地方式。
- **客户组**:把客户按行业 / 团队 / 服务等级分组;客户组是批量授权与数据范围的最小单位,避免逐个客户分配权限(见 5.4.3)。
- 客户暂停时只读,不能发起新生成;结束时全部只读,资产转"只读资产"。
@@ -785,9 +785,10 @@ flowchart LR
| 脚本方向 | 口播 / 剧情 / 测评 / 种草 / 情景…,可按行业收窄 |
| 情绪与风格 | 亲和 / 专业 / 激情 / 幽默 / 悬念 |
| 人群标签 | 年龄 / 性别 / 身份 / 消费力,可扩展 |
| 行业二级 | 电商 → 服饰鞋包 / 美妆个护…;金融 → 信贷 / 保险 / 理财 |
| 素材形态 | 展示 / 口播 / 剧情 / 图文动效 / 信息图 |
> 注:「行业二级」不再作为字典项,改由 `industries` 两级行业树承载(ADR-002);其余字典口径不变。
**变更规则**:字典变更**只影响新数据,历史数据保留快照**;否则改一次字典,历史统计全部失真。
**通用枚举模型**:所有字典共用一张枚举表(类型 + 编码 + 名称 + 父编码 + 所属行业 + 排序 + 启用),一张表同时承载行业二级树与受控词表;编码不可改、名称可改;按行业作用域继承——查询取「本行业 + 父行业 + 通用」三层合并,行业包只覆盖差异项。
@@ -948,7 +949,7 @@ flowchart LR
| R-17 | 超级管理员保护 | 超管角色不可删改停用;授予超管须二次确认并审计;建议 ≤ 2 人 |
| R-18 | 职责互斥 | 生成与审核、行业包编辑与发布、字段申请与审批不可同属一个角色(超级管理员除外) |
| R-19 | 视频方向与脚本方向边界 | 脚本方向决定"说什么"(钩子与结构),视频方向决定"怎么呈现"(口播 / 剧情 / 产品展示);二者并列、不合并 |
| R-20 | 行业 / 客户目录 | 行业是客户的业务属性,两级目录为全部资产的归属根节点;客户不可跨行业移动,行业名称全局唯一、客户名称同一行业内唯一 |
| R-20 | 行业 / 客户目录 | 行业本身为两级树(一级行业 → 二级行业),客户挂末级行业;行业与客户构成资产归属的两级目录;客户不可跨行业移动,一级行业名称全局唯一、二级行业名称在同一父级下唯一、客户名称在同一末级行业下唯一 |
| R-21 | 脚本编号唯一 | 过审入库时按 `客户编码-YYMMDD+序号` 原子分配;草稿不占号;编号一经分配不重用、不可改 |
| R-22 | 客户审核链接安全 | 免登录链接须签名 + 有效期 + 限范围(只读 / 可改稿);链接可随时作废;改稿保留原稿快照与操作人 |
| R-23 | 提示词与模型配置版本化 | 保存即快照、可回滚;任务创建时记录所用版本号,事后可复现「当时为什么生成成这样」 |
+126
View File
@@ -0,0 +1,126 @@
# ADR-002 行业目录:由单层改为两级树,客户挂末级行业
| 项 | 内容 |
| --- | --- |
| 状态 | 已接受(2026-11-03,用户确认 A3 口径) |
| 决策人 | 产品 / 技术负责人(用户) |
| 记录人 | 工程 Lead |
| 关联 | 开发计划 V0.1 §6.4 A3、§7.4、§12.1 M0 DoD、附录 C;PRD V1.6 §5.4.2 / §5.4.5、R-20;技术架构 V0.4 §5.3 / §5.4.1 / §6.3.2;ADR-001 |
| 影响范围 | `client` 模块、`industry` 模块(行业包绑定)、`catalog` / `script` / `media` / `asset` 的行业维度、数据范围与统计报表、A4 字典口径、开发计划与 PRD/架构文档 |
## 背景
PRD R-20 与技术架构 §5.3 均把 `industries` 描述为**单层行业目录**(「行业 = 客户的业务属性」,名称全局唯一),「行业二级(电商 → 服饰鞋包 / 美妆个护)」在 PRD §5.4.5 中作为 A4 字典项处理;开发计划 §6.4 A3-BE-01 也按单层 `industries` 描述。
A3 开卡前用户确认:行业目录需要**两级树**(一级行业 → 二级行业),客户归属到末级行业。该口径与既有文档冲突,且影响客户归属、行业包匹配、数据范围、统计与 A4 字典边界,属于 AGENTS §0.4 要求先提 ADR 的决策。
## 决策
### D1 行业为两级树,最多两级
`industries` 表增加:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `parent_id` | ulid FK → industries,可空 | 一级行业为空;二级行业指向一级行业 |
| `level` | smallint | 1 / 2;由服务端按 `parent_id` 推导并校验 |
| `sort_order` | int | 同级排序,默认 10 |
| `status` | varchar | 启用 / 停用 |
| `remark` | varchar(200) | 备注 |
约束:
- 仅允许两级;二级行业下不可再建子行业(应用层校验 + 数据库可选触发器兜底)。
- 一级行业 `parent_id IS NULL AND level = 1`;二级行业 `parent_id NOT NULL AND level = 2`。
- 客户只挂末级行业,详见 D2。
### D2 客户挂末级行业,行业包按祖先回退
- 客户 `industry_id` 指向**末级行业**:有子行业的一级行业不可被客户选择;一级行业无子行业时可直接选择。
- 客户创建后不可跨行业移动;调整行业走「停用 + 新建」(PRD R-20 原口径不变)。
- 行业包匹配按「精确行业 → 一级行业 → 默认」回退(B0 落地);行业顾问/行业指南同样按「二级 → 一级 → default → 内置准则」匹配(PRD §5.4.7 / §7.x)。
- 行业级资产包、行业级字段覆盖等按「客户所属末级行业 → 一级行业」继承。
### D3 行业名称唯一性:一级全局唯一 + 二级同父唯一
- 一级行业名称全局唯一。
- 二级行业名称在同一 `parent_id` 下唯一;不同一级行业下允许重名(如「其他」)。
- 客户名称仍在同一末级行业下唯一(`(industry_id, name)` 唯一,原口径不变)。
### D4 删除与停用:只停用 + 引用保护
- 删除:无子行业、无客户、无资产时才可删除;否则只能停用。
- 停用:有启用中的子行业时禁止停用,提示先处理子级;有客户时允许停用,停用后新建客户不可选该行业,已有客户数据不受影响。
- 停用一级行业不级联停用子级;需逐级处理,避免一次操作影响面过大。
### D5 权限复用现有 30 个权限点
A3 行业管理不新增权限点:
- 查看:`customer:read`;写(新建 / 编辑 / 停用 / 删除):`customer:manage`。
- 开发计划附录 C 写的 `industry_pack:view` 在 A2 定稿的 30 个权限点中不存在,按本 ADR 回写为 `customer:read`;`industry:manage` 仍表示行业包编辑(B0)。
### D6 「行业二级」从 A4 字典中移除
PRD §5.4.5 的「行业二级」字典项改由 `industries` 两级树承载;A4 字典保留其他受控词表。A4-BE-01 的「三层合并(本行业 + 父行业 + 通用)」中的「行业作用域」按两级树的祖先链解释。
## 备选方案
| 方案 | 结论 | 原因 |
| --- | --- | --- |
| 保持单层行业(PRD/架构原文) | 未采用 | 用户明确要求两级;单层无法表达「电商 → 服饰鞋包」的行业分类 |
| 单层落地 + 预留 `parent_id` | 未采用 | 本期 UI 与客户选择就要两级联动,预留字段会造成前后端口径分叉 |
| 行业二级放 A4 字典 | 未采用 | 字典项没有行业归属/状态/引用保护,且客户 `industry_id` 无法稳定指向二级;A3 就需要行业树维护 |
| **两级树 + 客户挂末级(本 ADR)** | 采用 | 与用户口径一致;行业包按祖先回退,保留单层时期的继承语义 |
## 后果
### 正面
- 行业分类可表达一级/二级,客户归属更精确;行业包可在任一级配置并向下继承。
- 行业二级不再依赖 A4 字典,A3 即可交付完整行业树维护。
- 权限点不新增,复用 A2 定稿目录,治理成本低。
### 负面 / 代价
- 与 PRD R-20、技术架构 `industries`、开发计划 A3/A4/M0 DoD 的原文冲突,必须同步修订,否则文档与实现不一致。
- 客户归属校验从「行业存在」升级为「行业存在且为末级」,A2 `CustomerDirectory` 与数据范围解析需按末级行业返回客户。
- 行业统计、报表、行业包匹配需处理祖先链,查询复杂度上升。
- 历史数据若已按单层行业写入(当前本地库无行业数据),需 `parent_id` 置空并标记为一级行业;A3 首次建表,不涉及存量迁移。
### 迁移 / 兼容策略
A3 首次创建 `industries` 表即按两级结构落地,采用 expand → migrate → contract:
1. 建表含 `parent_id` / `level` / `sort_order` / `status` / `remark`;
2. 迁移脚本对存量行业(如有)设 `parent_id = NULL, level = 1`;
3. 应用层先写 `level`,后续若需严格约束再补触发器 / 检查约束;
4. 回滚:删除新增列即可回到单层语义(客户 `industry_id` 仍指向原行业)。
## 待办 / 回写清单
| 文件 | 章节 | 需要改成什么 |
| --- | --- | --- |
| `docs/PRD/灵犀-信息流广告素材一站式生产平台PRD.md` | R-20 | 明确行业两级树、客户挂末级、一级全局唯一 / 二级同父唯一 |
| 同上 | §5.4.2 | 客户所属行业改为末级行业;补充两级树与停用/删除规则 |
| 同上 | §5.4.5 | 「行业二级」从字典表移除,注明由 `industries` 两级树承载 |
| 同上 | §5.4.7 / §7.x 行业指南 | 明确「二级 → 一级 → default」回退 |
| `docs/技术架构/灵犀_信息流素材生产平台_技术架构_V0.1.md` | §5.3 表清单 | `industries` 增加 `parent_id` / `level`,约束改为「最多两级、一级全局唯一、二级同父唯一」 |
| 同上 | §5.4.1 `customers` | `industry_id` 指向末级行业;补充「客户不可挂有子行业的一级行业」 |
| 同上 | §6.3.2 | 行业接口权限改为 `customer:read` / `customer:manage`;补充两级树与删除/停用校验 |
| `docs/开发计划/灵犀_信息流素材生产平台_开发计划_V0.1.md` | §6.4 A3-BE-01 | 改为两级行业树;删除/停用规则按 ADR-002 D4 |
| 同上 | §7.4 | 页面口径补充「一级/二级树、客户挂末级」 |
| 同上 | §12.1 M0 DoD | 「行业两级」改为「行业两级树(一级 → 二级),客户挂末级」 |
| 同上 | 附录 C | `/system/industries` 权限改为 `customer:read` / `customer:manage` |
| `docs/开发计划/灵犀_A2角色权限与数据范围_实施报告_V0.1.md` | §5 遗留 1 | 注明 A3 接入真实客户目录时按末级行业返回客户 |
| `README.md` / `AGENTS.md` | 文档索引 / 事实基线 | 视需要补充 ADR-002 与行业口径变更 |
> 文档同步按 AGENTS §0.4「先改文档,再改代码」执行;本 ADR 先于 A3 后端实现落地。
## 关联
- [ADR-001 权限点目录与六类数据范围](ADR-001-权限点目录与六类数据范围.md)
- 开发计划 V0.1 §6.4 A3、§7.4、§12.1
- PRD V1.6 R-20、§5.4.2、§5.4.5
- 技术架构 V0.4 §5.3、§5.4.1、§6.3.2
- 前端原型:`design/a3-ui-v1/`(三套候选,待用户选择)
+1
View File
@@ -6,6 +6,7 @@
| 编号 | 决策 | 状态 | 日期 | 关联 |
| --- | --- | --- | --- | --- |
| [ADR-001](ADR-001-权限点目录与六类数据范围.md) | 角色权限模型:权限点目录保留 `read/write` 只增不改、特殊权限五项 6 编码、SoD 三对互斥、六类数据范围全链路 + `CustomerDirectory` 端口、超管隐式全集、A2 审计先结构化日志 | 已接受 | 2026-11-03 | A2 / A3 / DP-14 |
| [ADR-002](ADR-002-两级行业目录.md) | 行业目录由单层改为两级树(一级 → 二级),客户挂末级行业,一级全局唯一 / 二级同父唯一,只停用 + 引用保护,「行业二级」从 A4 字典移除 | 已接受 | 2026-11-03 | A3 / A4 / B0 |
## 新增流程
@@ -0,0 +1,158 @@
# 灵犀 A3 客户与行业目录 · 实施报告 V0.1
| 项 | 内容 |
| --- | --- |
| 实施日期 | 2026-11-03 |
| 批次 | A3(客户与行业目录) |
| 状态 | 已完成,门禁通过;未推送 |
| 依据 | 开发计划 V0.1 §6.4 / §7.4 / §12.1 / 附录 C;PRD V1.6 §5.4.2 / §5.4.9、R-07 / R-20;技术架构 V0.4 §5.3 / §5.4.1 / §6.3.2;ADR-002 |
| 决策记录 | [ADR-002 两级行业目录](../adr/ADR-002-两级行业目录.md) |
| 视觉基线 | A0「方向 A · 云白蓝」;A3 用户选定混搭:**行业管理 B / 客户列表 A / 客户详情 A / 新建客户 B / 客户组 B / 字段审批 A** |
---
## 1 范围与完成度
| 任务 | 内容 | 状态 |
| --- | --- | --- |
| A3-BE-01 | 两级行业树(一级 → 二级)、名称唯一、停用/删除引用保护 | ✅ |
| A3-BE-02 | 客户档案、简称必填、同末级行业唯一、状态机(在服务 / 暂停 / 结束) | ✅ |
| A3-BE-03 | 客户组与成员,客户组作为数据范围最小单位 | ✅ |
| A3-BE-04 | 客户级字段申请 / 审批,进入生成 ≤ 5,超限 40001 | ✅ |
| A3-BE-05 | 客户上下文与归属校验,跨客户 404;真实 `CustomerDirectory` | ✅ |
| A3-FE-01 | 行业管理(B:左两级树 + 右详情) | ✅ |
| A3-FE-02 | 客户列表 / 详情(A)、四步新建(B) | ✅ |
| A3-FE-03 | 客户组(B:左列表 + 右成员) | ✅ |
| A3-FE-04 | 字段申请与审批(A:全宽表格 + 审批弹窗) | ✅ |
| A3-QA-01 | 唯一性 / 状态 / 范围 / 字段上限用例 + 浏览器 e2e | ✅ |
> 用户确认口径:两级行业树、客户挂末级行业、一级全局唯一 + 二级同父唯一、只停用 + 引用保护、字段双层、资产包留入口、客户转交纳入 A3、复用现有 30 个权限点。
---
## 2 交付物
### 2.1 文档与决策
- 新增 [ADR-002 两级行业目录](../adr/ADR-002-两级行业目录.md) 并登记 ADR 索引;含 PRD / 技术架构 / 开发计划回写清单。
- 同步 PRD R-20 / §5.4.2 / §5.4.5、技术架构 §5.3 / §5.4.1 / §6.3.2、开发计划 §6.4 / §7.4 / §12.1 / 附录 C。
- 前端原型:`design/a3-ui-v1/`(三套候选 + 独立评审 + 用户选定混搭);实现截图:`design/a3-ui-v1/impl/`。
### 2.2 共享契约(`packages/shared`)
- `schemas/client.ts`:行业两级树 / 引用、客户档案与状态、客户组、客户级字段、转交、列表 query;写接口 `.strict()`。
- 上限常量 `CUSTOMER_BASE_FIELD_LIMIT = 10`、`CUSTOMER_GEN_FIELD_LIMIT = 5`(A4 迁入 `system_configs`,R-26)。
- `schemas/client.spec.ts`:行业 / 客户 / 客户组 / 字段契约覆盖。
### 2.3 数据库与种子
- 迁移 `prisma/migrations/20261103130048_a3_client_industry_customer/`(expand):
- `industries`:`parent_id` / `level` / `sort_order` / `status` / `remark`;部分唯一索引(一级全局唯一、二级同父唯一);两级 CHECK 与非自引用 CHECK;
- `customers`:`industry_id`、`short_name`、状态、负责人、对接人、偏好 JSONB、基础字段 JSONB、软删除;
- `customer_groups` / `customer_group_members`;
- `customer_fields`:进入生成与审批状态、申请人 / 审批人、驳回原因。
- `prisma/seed.ts`:幂等写入 电商(服饰鞋包 / 美妆个护)、金融(信贷)、2 个客户、1 个客户组、4 条客户字段(1 条待审批)。
- `prisma/SEED.md` 待同步(见遗留)。
### 2.4 后端(`apps/api/src/modules/client`)
| 接口 | 说明 |
| --- | --- |
| `GET/POST /industries`、`GET/PATCH/DELETE /industries/{id}`、`PATCH /industries/{id}/status` | 两级树、引用、停用/删除保护 |
| `GET/POST /customers`、`GET/PATCH /customers/{id}`、`PATCH /customers/{id}/status` | 列表(ScopeContext 客户维度)、详情(跨客户 404)、状态机 |
| `POST /customers/{id}/transfer`、`PUT /customers/{id}/groups`、`POST /customers/{id}/apply-industry-pack` | 转交、客户组、B0 空态 |
| `GET/POST /customer-groups`、`GET/PATCH/DELETE /customer-groups/{id}`、`POST /customer-groups/{id}/members` | 客户组与成员幂等 |
| `GET /customer-fields`、`POST /customer-fields/{id}/approve`、`GET/POST /customers/{id}/fields` | 审批队列、通过/驳回(驳回原因必填) |
| `POST /users/{id}/transfer-customers` | 保留 A1 路径,由 client 模块落地批量转交 |
- 真实 `PrismaCustomerDirectory` 替换 `EmptyCustomerDirectory`;六类数据范围解析接入客户 / 客户组 / 部门 / 创建人。
- 客户上下文:所有客户读写携带 `ScopeContext`;跨客户统一 404;客户组删除前校验 `data_scopes` 引用。
- 审计:行业 / 客户 / 客户组 / 字段 / 转交均带 `@Audit()` 结构化日志(A5 落库)。
- 测试:`client.int.spec.ts` 6 条(行业唯一/停用停用、客户唯一/跨客户/状态机、客户组范围、字段审批/超限、转交、状态只读)。
### 2.5 前端(`apps/web`)
| 页面 | 路由 | 选定布局 |
| --- | --- | --- |
| 行业管理 | `/system/industries` | B:左两级树 + 右详情(白底卡片) |
| 客户列表 | `/system/customers` | A:全宽表格 + 筛选条 |
| 客户详情 | `/system/customers/:id` | A:左锚点 + 中列 + 右侧状态 |
| 新建客户 | `/system/customers/new` | B:左侧步骤 + 右侧表单,四步可回退 |
| 客户组 | `/system/customer-groups` | B:左列表 + 右成员 |
| 客户字段审批 | `/system/customer-fields/approvals` | A:全宽表格 + 通过/驳回弹窗 |
- 侧边栏更名:「客户」→「客户管理」、「行业树」→「行业管理」;新增「客户组」;「客户字段」指向审批页,权限 `customer_field:approve`。
- 样式:`styles/client.css` 复用 `--lx-*` token;未新增依赖、未内联 SVG(图标走 `@lingxi/ui/icons`)。
- API:`api/customers.ts` + `hooks/queries/use-client.ts`,全部用 shared Zod schema 解析。
---
## 3 验证结果
| 门禁 | 命令 | 结果 |
| --- | --- | --- |
| 格式 | `pnpm format:check` | ✅ |
| Lint | `pnpm lint` | ✅ 0 warning |
| 类型 | `pnpm typecheck` | ✅ 10/10 tasks |
| 单测 | `pnpm test` | ✅ shared 131 / api 90 / web 125,共 346 |
| 集成 | `pnpm test:int` | ✅ 5 文件 / 39 条(含 A3 client 6 条) |
| 浏览器 e2e | `A3_E2E=1 pnpm exec playwright test e2e/a3-client.spec.ts` | ✅ 1 passed(11.6s) |
| 构建 | `pnpm build` | ✅ 8/8 tasks |
| OpenAPI | `pnpm openapi:export` | ✅ 新增 industries / customers / customer-groups / customer-fields 路径 |
浏览器验证(真实 API + PG + Chrome):登录 → 行业管理树/详情 → 客户列表 → 客户详情 → 新建客户 → 客户组 → 字段审批六页均正常渲染;控制台仅 favicon 404。截图:`design/a3-ui-v1/impl/`。
---
## 4 关键口径与实现说明
1. **两级行业**:`level` 由 `parent_id` 推导;数据库 CHECK 保证「parent 为空 ⇔ level=1」,服务层禁止二级下再建子级;名称唯一通过部分唯一索引兜底。
2. **客户挂末级**:`assertLeafIndustry` 校验;客户更新不接收 `industryId`,跨行业移动走停用 + 新建。
3. **状态机**:在服务 ⇄ 暂停;在服务 / 暂停 → 结束;结束不可逆。暂停 / 结束客户更新、字段申请、转交均返回 40903。
4. **字段双层**:基础字段在 `customers.custom_fields` JSONB(≤10,免审批);`customer_fields` 只承载结构化 / 进入生成字段,进入生成需审批,按「占用槽位」计数(含待审批、不含已驳回)。
5. **数据范围**:`CustomerDirectory` 真实实现;客户列表按 `customerIds` 过滤;单条跨客户读统一 404;客户组数据范围用例已验证。
6. **客户转交**:`/customers/{id}/transfer` 需 `customer:transfer`;`/users/{id}/transfer-customers` 保留 A1 路径,允许 `customer:transfer` 或 `user:manage`。
7. **资产包**:`POST /customers/{id}/apply-industry-pack` 当前返回 42201「B0 批次提供」,前端显示空态,不伪造数据。
---
## 5 已知差异与遗留
| 编号 | 项 | 说明 / 建议 |
| --- | --- | --- |
| 1 | 行业资产包套用 | 依赖 B0/B1,当前接口返回明确业务提示 + 前端空态;B0 落地后接通。 |
| 2 | 审计落库 | A3 变更走 `@Audit()` 结构化 Pino 日志;审计表、检索、导出按 A5。 |
| 3 | 字段上限配置 | 共享常量 `10 / 5`;A4 `system_configs` 落地后迁入,前端上限读配置接口(R-26)。 |
| 4 | 客户级字段类型 | 仅白名单 文本 / 数字 / 日期 / 单选;选项在原型中简化,完整枚举来源随 A4 字典。 |
| 5 | 行业资产数 | `assetCount` 固定 0;B1 资产表落地后接入。 |
| 6 | 客户列表分页 | 前端一次拉 100 条(A3 规模);B2 后按大数据量补游标分页 UI。 |
| 7 | 客户详情面包屑 | 由导航派生,末级显示「客户管理」而非客户名;与 A2 角色编辑器同源问题,后续统一。 |
| 8 | 状态截图 | 原型阶段 A/B/C 状态图已覆盖;实现截图以主态 + e2e 为准,暂停/结束状态由 API int + e2e 覆盖。 |
| 9 | `prisma/SEED.md` | 需补 A3 行业 / 客户 / 客户组 / 字段种子说明(本次未改)。 |
| 10 | `AppModule` 的 worker | `pnpm dev` 下 worker 因 `REDIS_URL` 缺失退出,API/Web 正常;A0 遗留,非 A3 范围。 |
---
## 6 回滚方式
1. 代码:回滚本批 commit(client 模块、前端页面、shared schema、路由 / 导航、迁移文件、seed、ADR 与文档)。
2. 数据库:A3 为新增表,回滚可保留表(不影响 A1/A2);如需彻底回滚,执行 down SQL 删除 `customer_fields → customer_group_members → customer_groups → customers → industries`(先备份种子数据)。
3. 配置:`CUSTOMER_DIRECTORY` 改回 `EmptyCustomerDirectory` 即恢复 A2 空客户集行为(需同时回滚 client 模块 provider)。
4. 前端:回滚路由 / 导航与 `pages/system/{industries,customers,customer-groups,customer-fields}`,恢复「客户 / 行业树」旧菜单。
---
## 7 本地运行
```bash
pnpm check:dev
pnpm db:init
pnpm dev
# 浏览器 http://127.0.0.1:5173 → 管理中心 / 行业管理、客户管理、客户组、客户字段
```
A3 e2e(需先起 `pnpm dev`):
```bash
A3_E2E=1 pnpm exec playwright test e2e/a3-client.spec.ts --reporter=list
```
@@ -10,7 +10,7 @@
| 文档名称 | 灵犀 · 信息流素材生产平台 项目开发计划 |
| 版本 | V0.1(初稿,待评审) |
| 日期 | 2026-11-02 |
| 状态 | **执行中**:A0 工程骨架与契约基线已完成(2026-11-02);A1 身份认证与用户管理已完成(2026-11-03);A2 角色权限与数据范围已完成(2026-11-03);下一步 A3 |
| 状态 | **执行中**:A0 工程骨架与契约基线已完成(2026-11-02);A1 身份认证与用户管理已完成(2026-11-03);A2 角色权限与数据范围已完成(2026-11-03);A3 客户与行业目录已完成(2026-11-03);下一步 A4 |
| 需求基线 | PRD V1.6([灵犀-信息流广告素材一站式生产平台PRD.md](../PRD/灵犀-信息流广告素材一站式生产平台PRD.md)) |
| 技术基线 | 技术架构 V0.4([灵犀_信息流素材生产平台_技术架构_V0.1.md](../技术架构/灵犀_信息流素材生产平台_技术架构_V0.1.md))+ `AGENTS.md` V1.0 + `docs/工程规范/` V1.0 |
| 覆盖范围 | **M0 任务级;M1–M4 迭代级 + 任务组级;M5 预研边界(不排期)** |
@@ -381,7 +381,7 @@ flowchart LR
| **A0** | ✅ 已完成 | 工程骨架与契约基线 | — | 6 | 4–5 日 | W0 | lint / typecheck / test / CI 全绿;三依赖可起;api / web / worker 可启动;OpenAPI 可导出 |
| **A1** | ✅ 已完成 | 身份认证与用户管理 | A0 | 6 | 4–5 日 | W1 | 登录 / 刷新 / 登出;用户生命周期;路由权限;权限三态用例 |
| **A2** | ✅ 已完成 | 角色权限与数据范围 | A1 | 5.5 | 4–5 日 | W2 | 角色可配;SoD 命中拒绝;特殊权限默认关闭;范围过滤生效 |
| **A3** | 未开始 | 客户与行业目录 | A1 / A2 | 7 | 4–5 日 | W2 | 行业 / 客户 / 客户组可用;唯一性;状态只读;字段审批 |
| **A3** | ✅ 已完成 | 客户与行业目录 | A1 / A2 | 7 | 4–5 日 | W2 | 行业 / 客户 / 客户组可用;唯一性;状态只读;字段审批 |
| **A4** | 未开始 | 数据字典与系统配置 | A3 | 4 | 3–4 日 | W3 | 三层合并;编码不可改;阈值同源;配置版本 + 审计 |
| **A5** | 未开始 | 审计与日志 | A1 / A4 | 4 | 3–4 日 | W2–W3 | 关键操作全留痕;不可篡改;导出自身审计 |
| **A6** | 未开始 | 任务中心与队列骨架 | A0 / A1 | 6 | 4–5 日 | W2–W3 | 提交 → SSE → 完成 / 失败;幂等;取消 / 重试 |
@@ -576,7 +576,7 @@ PRD §7 把 **M-27(素材元素级拆解)** 与「自动字幕 → TTS 配
| 编号 | 任务 | 端 | 内容与产出 | 依赖 | 验收要点 | EPD |
| --- | --- | --- | --- | --- | --- | --- |
| A3-BE-01 | 行业目录 | BE | `industries`;名称全局唯一;删除前校验无客户 / 无资产,否则只能停用;行业不可跨行业移动校验 | A2-BE-04 | 重名 40902;有客户时删除被拒;停用后新建客户不可选 | 0.5 |
| A3-BE-01 | 行业目录(两级) | BE | `industries`;一级 → 二级最多两级;一级名称全局唯一、二级同父唯一;客户只挂末级行业;删除前校验无子行业 / 客户 / 资产,否则只能停用;有启用子行业时禁止停用 | A2-BE-04 | 重名 40902;有子行业 / 客户时删除被拒;有启用子行业停用被拒;停用后新建客户不可选 | 0.5 |
| A3-BE-02 | 客户档案与状态机 | BE | `customers`;简称必填、`(industry_id, name)` 唯一;服务状态(在服务 / 暂停 / 结束);负责人 / 对接人;交付偏好;套用行业资产包入口 | A3-BE-01 | 简称缺失 42201;暂停只读、不能发起新任务;结束全只读 | 1.0 |
| A3-BE-03 | 客户组 | BE | `customer_groups` + `customer_group_members`;客户组作为授权与数据范围最小单位 | A3-BE-02 | 成员增删幂等;数据范围引用客户组后可见集合正确 | 0.5 |
| A3-BE-04 | 客户级字段申请与审批 | BE | `customer_fields`;申请(定义 + 用途 + 是否进入生成)→ 审批 → 生效;每客户 ≤ 5 个进入生成字段、基础字段 ≤ 10 | A3-BE-02 | 超限 40001(字段校验失败);未审批字段不进入生成;审批与变更进审计 | 1.0 |
@@ -589,6 +589,8 @@ PRD §7 把 **M-27(素材元素级拆解)** 与「自动字幕 → TTS 配
**本批门禁**:可建行业 → 客户 → 客户组;唯一性与状态机生效;客户字段审批可用;所有查询带客户维度。
> **A3 完成情况(2026-11-03)**:9/9 项 WBS 通过。两级行业树(ADR-002)、客户档案与状态机、客户组、客户级字段双层与审批、客户转交、真实 `CustomerDirectory`、客户上下文与跨客户 404 全部落地;前端按用户选定混搭实现(行业 B / 客户列表 A / 客户详情 A / 新建 B / 客户组 B / 字段 A),侧栏更名「客户管理」「行业管理」并新增「客户组」。门禁:`format:check / lint / typecheck / test` 全绿(shared 131 / api 90 / web 125,共 346),`test:int` 39 项(5 文件,含 A3 client 6 条),A3 浏览器 e2e 1/1(11.6s),`build` 8/8;实现记录、截图与遗留见 [A3 实施报告](./灵犀_A3客户与行业目录_实施报告_V0.1.md)。已知遗留:B0 行业资产包套用、A5 审计落库、A4 字段上限配置、客户详情面包屑末级名称。
### 6.5 A4 数据字典与系统配置
**目标**:字典与配置成为唯一来源,杜绝前后端硬编码。**对应**:P-06、P-13(部分)、PRD §5.4.5;`R-10` / `R-01` / `R-26`;架构 §5.3 / §5.7。
@@ -952,7 +954,7 @@ PRD §7 把 **M-27(素材元素级拆解)** 与「自动字幕 → TTS 配
| --- | --- |
| 页面 | `/industries`、`/customers`、`/customers/new`(分步)、`/customers/:id`、`/customer-groups`、`/customer-fields/approvals` |
| 接口 | `GET/POST/PATCH /industries`、`GET/POST/PATCH /customers`、`/customer-groups`、`/customers/{id}/fields`、`/customer-fields/{id}/approve`、`POST /customers/{id}/apply-industry-pack` |
| 关键规则 | 行业名称全局唯一;客户名称同行业内唯一;**简称必填**(用于全部自动命名);客户必须归属一个行业、不可跨行业移动;暂停只读、结束只读归档(`R-07`);基础字段 ≤ 10 且永不进入生成,进入生成字段 ≤ 5 且必须审批 |
| 关键规则 | 行业两级树;一级名称全局唯一、二级同父唯一;客户名称同末级行业唯一;**简称必填**(用于全部自动命名);客户必须归属末级行业、不可跨行业移动;暂停只读、结束只读归档(`R-07`);基础字段 ≤ 10 且永不进入生成,进入生成字段 ≤ 5 且必须审批 |
| 交互要点 | 新建客户为分步表单(行业 → 档案 → 交付偏好 → 资产包),可回退;状态为暂停 / 结束时顶部只读横幅;客户组作为授权单位,批量授权入口清晰 |
| 验收 | ① 重名返回 40902;② 简称缺失 42201;③ 暂停客户不能提交新任务;④ 客户组授权后范围正确;⑤ 字段超限 40001、未审批字段不进入生成 |
@@ -1232,7 +1234,7 @@ infra/:compose、Nginx、监控、备份脚本占位
- [ ] 超管首登强制改密;用户 / 角色 / 部门可用;
- [ ] 自定义角色 + 权限点 + 六类数据范围 + 特殊权限可用;
- [ ] SoD 三对规则生效且有拒绝用例;
- [ ] 行业两级、客户 / 客户组 / 客户端字段审批可用;
- [ ] 行业两级树(一级 → 二级,客户挂末级)、客户 / 客户组 / 客户端字段审批可用;
- [ ] 通用字典三层合并、编码不可改、历史快照可用;
- [ ] 审计七类关键动作留痕、只读、导出留痕;
- [ ] AI 四层配置 + Mock 全链路 + 密钥不回显;
@@ -1379,7 +1381,7 @@ infra/:compose、Nginx、监控、备份脚本占位
| `/system/users`、`/system/users/new`、`/system/users/:id` | 用户与部门 | `user:view` / `user:manage` | A1 |
| `/system/departments` | 部门管理(两级组织树、停用/启用) | `user:view` / `user:manage` | A1 |
| `/system/roles`、`/system/roles/:id` | 角色管理 | `role:view` / `role:manage` | A2 |
| `/system/industries` | 行业管理 | `industry_pack:view` / `customer:manage` | A3 |
| `/system/industries` | 行业管理 | `customer:read` / `customer:manage` | A3 |
| `/system/customers`、`/system/customers/new`、`/system/customers/:id` | 客户管理 | `customer:view` / `customer:manage` | A3 |
| `/system/customer-groups` | 客户组 | `customer_group:manage` | A3 |
| `/system/customer-fields/approvals` | 字段审批 | `customer_field:approve` | A3 |
@@ -830,7 +830,7 @@ erDiagram
| 组织权限 | `role_permissions` | 角色 → 权限点 | 复合主键 |
| 组织权限 | `user_roles` | 用户 → 角色(可多) | 复合主键 |
| 组织权限 | `data_scopes` | 角色数据范围 | `scope_type` + 目标 ID |
| 客户与行业 | `industries` | 行业目录(业务属性) | 名称全局唯一 |
| 客户与行业 | `industries` | 行业目录(两级树:一级 → 二级) | 一级名称全局唯一;二级同一父级下唯一;最多两级 |
| 客户与行业 | `customers` | 客户 | `(industry_id, name)` 唯一;`short_name` 必填 |
| 客户与行业 | `customer_groups` | 客户组 | 授权与范围最小单位 |
| 客户与行业 | `customer_group_members` | 客户组 ↔ 客户 | 复合主键 |
@@ -900,7 +900,7 @@ erDiagram
| **字段** | **类型** | **说明** |
| --- | --- | --- |
| `id` | ulid PK | — |
| `industry_id` | ulid FK → industries | 不可为空;跨行业移动禁止(应用层 + 触发器可选) |
| `industry_id` | ulid FK → industries | 不可为空;必须指向**末级行业**(有子行业的一级行业不可选);跨行业移动禁止(应用层 + 触发器可选) |
| `name` | text | 同行业内唯一 |
| `short_name` | text | **必填**,用于自动命名,需满足通用字符规则 |
| `status` | enum | 在服务 / 暂停 / 结束 |
@@ -1102,7 +1102,7 @@ sequenceDiagram
| **方法** | **路径** | **权限点** | **说明** |
| --- | --- | --- | --- |
| GET/POST/PATCH | `/industries`、`/industries/{id}` | `industry_pack:view` / `customer:manage` | 行业目录;删除前校验 |
| GET/POST/PATCH | `/industries`、`/industries/{id}` | `customer:read` / `customer:manage` | 两级行业树;删除前校验无子行业 / 客户 / 资产;有启用子行业禁止停用 |
| GET/POST/PATCH | `/customers`、`/customers/{id}` | `customer:view` / `customer:manage` | 客户;`short_name` 必填 |
| GET/POST | `/customer-groups`、`/customer-groups/{id}/members` | `customer_group:manage` | 客户组与成员 |
| POST | `/customers/{id}/apply-industry-pack` | `customer:manage` | 套用行业资产包(冷启动) |