You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

80 lines
7.1 KiB

7 days ago
# 04: P1 修复 B — 补齐 6 个 404 写端点(D-01~06:关联客户 ×3 + 团队成员 ×3)
`Type: task`
`Status: resolved`
`Blocked by: 02`
## 任务
原型有按钮、实体已存在、只缺 Controller/Service 写端点的 6 个接口:
| # | 缺陷 | 端点(建议,flat 风格) | 原型依据 |
|---|---|---|---|
| D-01 | 关联客户-添加 | `POST /api/opportunity/customer/add` | a3-1-1-1-2-4 添加关联客户弹窗 + 详情「添加关联客户」按钮 |
| D-02 | 关联客户-搜索 | `POST /api/opportunity/customer/search`(分页搜客户池候选) | 同弹窗内搜索框 |
| D-03 | 关联客户-设主要意向 | `POST /api/opportunity/customer/set-primary` | 同弹窗/列表切主交互 |
| D-04 | 团队成员-添加 | `POST /api/opportunity/team/add` | a3-1-1-1-8「+ 添加成员」 |
| D-05 | 团队成员-移除 | `POST /api/opportunity/team/delete` | 同页「移除」 |
| D-06 | 团队成员-编辑 | `POST /api/opportunity/team/update` | 同页「编辑」 |
## 实现要点(不是裸 CRUD,域规则在 CONTEXT.md)
**关联客户**(`crm-opportunity/CONTEXT.md` 规则):
- 一商机多客户;**有且仅一个主要意向客户**(add 时若该商机已无主要意向且新客户设为主要则直接生效;已有主要时切主走 set-primary)。
- set-primary = 三步事务:旧主要置 0 → 新主要置 1 → 商机主表 primaryCustomerNameSnapshot 回写。
- **无删除动作**(原型无删除按钮,别加 delete 端点)。
- search 是「候选客户池」搜索(从 crm 客户域搜,过滤已关联的),返回分页。
**团队成员**(规则):
- 行级授权白名单:add 把用户加白名单(projectRole/permission 按 DTO);delete 走软删(deleteKey 约定——看 OpportunityTeam 实体既有字段怎么设计的,与既有 delete_key 复用键约定一致)。
- **商机负责人必须在团队里**:delete 时若目标是当前负责人要拦截(错误码风格对齐既有 64xxx/66xxx 家族)。
- 移交商机(handover)时 team 表同步维护的逻辑已存在,别动。
**通用**:
- Controller IO 遵 ADR-0017(Param DTO 进、Result 出);权限注解对齐既有写端点(看 page/claim 等怎么标的)。
- 输入校验失败返回 400 类业务码(不是 500)。
- Bruno 集合 `E:\code\crm-api-docs` 里这 6 个端点此前不存在,票 10 会补文档;本票只管实现 + 单测。
## 验收
- 6 端点实测全通:add → list 回显 → update 改角色 → delete 软删 → set-primary 切主(旧主让位 + 快照回写)→ search 分页过滤已关联。
- 负路径:重复添加同一客户拦截;删除负责人拦截;无主要意向时 add 直设主要。
- `mvn compile` + 新增单测绿;`e2e-core.py` 相关段回归无回归。
## Answer
(20260829 完成)6 端点全部落地,验收 5/5 达成。
### 实现总览
- **端点**(`OpportunitySubController` 追加,flat 路径、隐式表单绑定,operatorId 取 SecurityUtils):
- `POST /customer/add`(D-01):必填校验(66001)→ 重复预检(66011)→ isPrimary=1 时降旧主 → insert(deleteKey=0)→ 主表 primaryCustomerId/primaryCustomerNameSnapshot 回写。无 delete 端点(原型无删除按钮)。
- `POST /customer/search`(D-02):候选池按拍板「快照池降级」(20260829)——客户模块 A4 未建,`OpportunityCustomerMapper.searchCandidatePool` 取 opportunity_customer 存活行按 customer_id 去重(MAX(customer_name_snapshot),兼容 ONLY_FULL_GROUP_BY),keyword 模糊 + NOT IN 排除当前商机已关联 + 分页;A4 建成切真源,签名不变。
- `POST /customer/set-primary`(D-03):2 参 @RequestParam;目标须已关联(66001)、已是主要幂等返回;三步事务:旧主降 0 → 新主置 1 → 主表快照回写,FIELD_CHANGE 日志。
- `POST /team/add`(D-04):批量 userIds 去重保序;全重整体拒 66012「该成员已存在」、部分重复跳过仅插新;username 快照服务端取 crm_auth_user(未知用户 66001);permission 不传默认 READ_ONLY(v29 页面无权限编辑入口,后端语义按 grill 拍板保留)。
- `POST /team/update`(D-06):支持直接换人(v29 A3-1-1-1-8;目标已在队 66012,快照服务端刷新,无需先加后删);**负责人行三重锁 66013**(换人/改项目角色/permission 禁改,仅 duty 可改——「请走移交 E9」)。
- `POST /team/delete`(D-05):软删 `set(DeleteKey, id).set(Deleted, 1)`(复用键约定,同成员可重新加入);负责人行拒 66013。
- **新增错误码**:66011 客户重复关联 / 66012 团队成员已存在 / 66013 负责人行锁定(OpportunityConstants,66xxx 家族)。
- **新增 duty 列**:OpportunityTeam+duty varchar(200)(v29 团队页列 5「职责」,非必填文本),ddl-auto: update 自动建列。
- **操作日志**:ROW_ADD/ROW_DELETE/FIELD_CHANGE + OplogLogType.ROW_CHANGE,opSource=MANUAL,bizRef 截 50 字。
- **ADR-0017**:4 DTO + 1 Param(extends BaseDTO/BaseParam,toEntity 写在 DTO);无主键语义出参 OpportunityCustomerCandidateDTO 不继承 BaseDTO;无权限注解(sys_menu 无 opportunity 权限点,动态拦截器口径对齐既有子域端点)。
### 验收对照
| 验收项 | 结果 |
|---|---|
| 6 端点实测全通(add→list 回显→update→delete→set-primary→search) | `t04-verify.py` 27/27 全绿(客户域 10 + 团队域 16 + CLEANUP) |
| 负路径:重复添加拦截 | 66011(客户)/ 66012(成员:全重拒 + 换人目标已在队) |
| 负路径:负责人行拦截 | 66013 三重(删/换人/改角色),仅 duty 可改 ✅ |
| 负路径:无主要时 add 直设主要 | C4:旧主 5002→0、新主 5003→1、主表快照=(5003,客户丙) |
| mvn compile | EXIT=0 |
| 单测 | 新增 OpportunitySubServiceImplTest 18/18;crm-opportunity 全模块 183/183(顺带清掉票 03 遗留 4 处失败——938aed9 把「无模板」改 fail-fast 未同步测试,测试侧对齐) |
| e2e-core 相关段回归 | F11/F12 11/11 ✅ 缺陷 0——6 端点探测全从「404 无端点」翻绿;报告另存 `*-t04-regression.*`,原始报告已恢复 |
### 工程记录
- **BeanUtils 按名复制坑**:DTO `oppId` vs 实体 `opportunityId` 名字不同,copyProperties 静默不复制 → INSERT 缺 opportunity_id 报 50001。修法:toEntity() 内显式 `entity.setOpportunityId(getOppId())`(团队域没踩到因手动 set)。BeanUtils 型 toEntity 均需逐一核对字段名。
- **Maven 增量构建静默跳过**:package 日志 BUILD SUCCESS 但 jar:jar 因「输入未变」跳过重建,fat jar 落后源码(api-docs 无新端点报 40401)。判别法:解剖 fat jar 的 BOOT-INF/lib 内嵌模块 jar 查字节码字符串常量(`setOpportunityId` 在 = 修复在内)。
- **沙箱拦截杀进程**:taskkill/Stop-Process 在沙箱内静默失效(输出通道死、进程存活);Bash 带 `required_permissions: all` 出沙箱后 taskkill /F /T 直接成功。
- **脚本沉淀**:`.scratch/opportunity-bugfix/t04-verify.py`(C/T/CLEANUP 三段,Sess 模式复用 t03)、`t04-jarcheck2.py`(jar 内容判别)。