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.

193 lines
14 KiB

1 week ago
# 商机模块(opportunity-module)接口验收报告(重写版)
> **本文替代旧版 `verify-report.WRONG.bak.md`。** 旧版核心结论大面积错误,重写说明见文末「附录:为何重写」。
>
> - **验收方式**:原型驱动 + 黑盒实测。后端 `localhost:8080`(远程 DB `8.129.84.155`),调试用户 `userId=739564171091247104`。
> - **实测脚本**:`e2e_test.py`(29 条主链)+ `supplement_test.py`(49 条补测)+ 手工 curl(状态机 9 步完整流转、批量流转成功路径)。
> - **路由发现**:一次性读取 4 个商机 Controller + 5 个规则/字典/偏好 Controller 的 `@*Mapping`(仅用于发现待测路由,验收结论一律以 HTTP 实测为准)。
> - **判定图例**:✅ 存在且业务可用|⚠ 存在但有契约差异/边界问题|❌ 真实缺失或真实 bug。
---
## 一、总体结论
**后端接口整体已实现,可用率极高。全量实测未发现任何真正的 404(路由缺失)。** 报告范围内 85 个后端路由全部存在并正确响应。
唯一的**致命缺陷**是新建商机接口必然 500。其余问题均为「前端对接需注意的契约差异」,非缺失。
| 维度 | 结论 |
|------|------|
| 路由缺失(真 404) | **0 个** |
| 致命 bug | **1 个**:`POST /api/opportunity` 新建必 500 |
| 契约差异(需前端适配) | 6 类(分页字段、参数名、路径风格、字典编码、视图路径、region 参数) |
| 状态机 9 条流转 | **全部实测通过**(单条 + 批量成功路径均验证) |
---
## 二、致命缺陷(❌ 必须修)
### D-01 ❌ 新建商机 `POST /api/opportunity` 必然 500
- **实测**:`POST /api/opportunity`,body `opportunityName=xxx&oppSource=opp_source_02``HTTP 500 code:50001 系统内部错误`。多次、多参数组合稳定复现。
- **后端日志根因**(`logs/crm-app.log`):
```
DataIntegrityViolationException: Field 'current_stage_id' doesn't have a default value
INSERT INTO opportunity (... 无 current_stage_id ...) VALUES (...)
at OpportunityIntakeImpl.open(OpportunityIntakeImpl.java:73)
at OpportunityCreateServiceImpl.createOpportunity(OpportunityCreateServiceImpl.java:79)
```
- **定性**:`OpportunityIntakeImpl.open()` 组装 INSERT 时未给 `current_stage_id` 赋值,而 DB 该列 `NOT NULL` 且无默认值。
- **影响**:新建商机 100% 失败,是商机业务链的入口——**最高优先级**。
- **修复方向**:入库前按「适用范围命中的阶段模板」解析出初始阶段并写入 `current_stage_id`;若允许无阶段初始态,则给该列加默认值 / 允许 NULL。
- **注**:旧报告 W-02 把字段名写成 `bid_form`(错误),但「新建必 500」的现象判断是对的。实际肇因列是 `current_stage_id`
---
## 三、状态机族(旧报告全判 ❌,实测全部 ✅)
用**已存在的推进中商机**(`748098957468499968` 等)跑通完整流转,避开 D-01 的新建 500。
| 流转 | 接口 | 单条实测 | 批量接口 | 批量实测 |
|------|------|---------|---------|---------|
| 领取 | `POST /api/opportunity/claim` | ✅ 200 code:0 | `POST /claim-batch` | ✅ 成功路径 code:0 |
| 分配 | `POST /api/opportunity/assign` | ✅ 200 code:0 | `POST /assign-batch` | ✅ 成功路径 code:0 |
| 抛公海 | `POST /api/opportunity/release-pool` | ✅ 200 code:0 | — | — |
| 暂缓 | `POST /api/opportunity/pause` | ✅ 200 code:0 | — | — |
| 恢复 | `POST /api/opportunity/resume` | ✅ 200 code:0 | — | — |
| 关闭 | `POST /api/opportunity/close` | ✅ 200 code:0 | `POST /close-batch` | ✅ code:0 |
| 重开 | `POST /api/opportunity/reopen` | ✅ 200 code:0 | — | — |
| 移交 | `POST /api/opportunity/handover` | ✅ 200 code:0 | — | — |
- **完整链路实测**:`pause → resume → handover → release-pool → claim → release-pool → assign → close → reopen`,9 步全 200 code:0。
- **状态守卫正确**:对源状态不匹配的流转(如「推进中」再 claim)返 `code:66003 当前状态不允许此操作`——是正确的状态机守卫,非缺失。
- **路径风格**:后端为 **flat 风格**(`/api/opportunity/claim`,id 走 body),**不是** RESTful `/api/opportunity/{id}/claim`。旧报告只测了 RESTful 路径得到 404 → 误判「状态机全缺」。
> ⚠ 契约差异:状态机全部走 `POST + form body`(`id`、`pauseReason`、`poolReason`、`closeReason`、`reopenReason`、`newOwnerUserId`/`expectedOwnerUserId`、`userId`、`ids`),前端不能用 RESTful 路径拼 id。
---
## 四、列表族(旧报告部分判 ❌,实测全部 ✅)
| 页面 | 接口 | 实测 | 说明 |
|------|------|------|------|
| 销售机会列表(3 视图) | `POST /api/opportunity/page`(`viewType` 区分) | ✅ 200 code:0 | 分页字段 `content`/`total`(见 C-01) |
| 商机公海列表 | `POST /api/opportunity/page?viewType=pool` | ✅ 200 code:0 | 同上 |
| 看板-阶段汇总 | `POST /api/opportunity/board/stage-summary` | ✅ 200 code:0 | 旧报告判 M-02 ❌,误判 |
| 看板-单阶段卡片 | `POST /api/opportunity/board/cards` | ✅ 200 code:0 | 旧报告判 M-03 ❌,误判 |
| 最近访问-上报 | `POST /api/opportunity/view-touch` | ✅ 200 code:0 | 后端以 `view-touch` 承担「访问上报」,非 spec 的 `recent-visits/touch`(旧报告 M-01 误判为缺失) |
| 可转商机线索 | `GET /api/opportunity/convertible-leads` | ✅ 200 code:0 | 新建页关联线索来源 |
> ⚠ C-01 分页字段:后端为 **Spring Data / MyBatis-Plus 风格** `data.content` + `data.total`(另有 `size/current/pages/empty`),**不是** `data.records`。旧报告 W-01 判断正确。
---
## 五、详情族(旧报告全判 ❌,实测全部 ✅)
后端为 flat 风格,id/oppId 走 query 或 body,**非** RESTful `/{id}/xxx`
| 功能 | 接口 | 实测 |
|------|------|------|
| 商机详情 | `GET /api/opportunity/detail?id=` | ✅ 200 code:0,返回完整字段 |
| 编辑商机 | `POST /api/opportunity/edit` | ✅ 200 code:0 |
| 关联客户列表 | `GET /api/opportunity/customer/list?oppId=` | ✅ 200 code:0 |
| 方案卡列表 | `GET /api/opportunity/scheme-card/list?oppId=` | ✅ 200 code:0 |
| 阶段进度 | `GET /api/opportunity/stage/progress?oppId=` | ✅ 200 code:0 |
| 现场勘察分页 | `POST /api/opportunity/site-survey/page` | ✅ 200 code:0(`content`/`total`) |
| 相关附件列表 | `GET /api/opportunity/attachment/list?oppId=` | ✅ 200 code:0 |
| 团队成员列表 | `GET /api/opportunity/team/list?oppId=` | ✅ 200 code:0 |
| 操作日志分页 | `POST /api/opportunity/oplog/page` | ✅ 200 code:0(`content`/`total`) |
| 关注 / 取消关注 | `POST /api/opportunity/focus`、`/unfocus` | ✅ 200 code:0 |
> ⚠ C-02 参数名:详情/子表全部用 **`oppId`**(非 spec 的 `opportunityId`)、详情主体用 **`id`**。旧报告 W-03/04/05 判断正确。
### 5.1 详情子操作(补测)
| 功能 | 接口 | 实测 | 说明 |
|------|------|------|------|
| 写跟进 | `POST /api/opportunity/follow/add` | ✅ 200 code:0 | 写入后 `last_valid_follow_time` 正确刷新(有效跟进定义生效) |
| 跟进分页 | `POST /api/opportunity/follow/page` | ✅ 200 code:0 | `content`/`total` |
| 删除跟进 | `POST /api/opportunity/follow/delete` | ✅ 200 code:0 | 删除写 `ROW_DELETE` 操作日志(联动正确) |
| 新增勘察 | `POST /api/opportunity/site-survey/add` | ⚠ 需 `engineerUserId` 等必填 | 路由存在,缺参返 `40001 缺少必填参数:engineerUserId`(正确校验) |
| 删除勘察 | `POST /api/opportunity/site-survey/delete` | ✅ 路由存在 | |
| 附件新增 | `POST /api/opportunity/attachment/add` | ✅ 路由存在 | **10 个上限校验正确**:超限返 `60001 跟进附件最多 10 个` |
| 附件删除 | `POST /api/opportunity/attachment/delete` | ✅ 200 code:0 | |
| 方案卡保存 | `POST /api/opportunity/scheme-card/save` | ✅ 路由存在 | 业务守卫正确:无客户归属返 `66007 关联客户不属于本商机` |
| 方案卡提交 | `POST /api/opportunity/scheme-card/submit` | ✅ 路由存在 | |
| 方案卡详情 | `GET /api/opportunity/scheme-card/detail?id=` | ✅ 路由存在 | 不存在 id 返 `66008 方案卡不存在`(正确) |
| 可选模板 | `GET /api/opportunity/scheme-card/selectable-templates?oppId=` | ✅ 200 code:0 | |
---
## 六、规则族(A7-3-2,旧报告"路径变更✅",实测全部 ✅)
后端统一 `/api/rule/opp-*`,方法统一 **POST**(分页/操作)+ **GET**(详情/版本),**非** spec RESTful `/{id}/versions` 等。旧报告 M-04/05/06 把 `/versions` 判为缺失,实测**均存在**。
| 子域 | 分页 | 详情 | 版本 | 复制 | 发布 | 停用 | 删除 |
|------|------|------|------|------|------|------|------|
| 阶段模板 `opp-stage-template` | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ⚠ |
| 方案卡模板 `opp-scheme-template` | ✅ | ✅ | ✅ | ✅ | ⚠ | ✅ | ⚠ |
| 公海规则 `opp-pool-rule` | ✅ | ✅⚠ | ✅⚠ | ⚠ | ⚠ | ⚠ | ⚠ |
- **分页**:`POST /api/rule/opp-{stage-template|scheme-template|pool-rule}/page` — ✅ 200 code:0,`content`/`total`。
- **详情/版本**:`GET .../detail?id=`、`GET .../versions?id=` — ✅(阶段、方案卡实测 code:0)。
- **复制**:`POST .../copy?id=` — ✅(阶段、方案卡 code:0)。
- **停用**:`POST .../disable?id=` — ✅(阶段、方案卡 code:0)。
-**发布 `publish` / 删除 `delete` / 存草稿 `save-draft`**:路由均存在,实测返业务校验错(非缺失):
- `publish``模板名称/规则名称不能为空`(需带完整模板体)
- `delete``仅草稿版本可删除`(对已发布版本的正确守卫)
- `save-draft``适用范围参数非法`(需合法 `applyScope`
-**公海规则** `detail/versions/copy/disable/delete` 补测未带 `id``40001 缺少必填参数:id`——路由存在,测试用例未准备 pool-rule 种子 id,非缺失。
- 方案卡模板另有 `GET .../field-defs?templateId=` — ✅ 200 code:0。
> 旧报告在规则族的「✅路径变更」结论**是对的**(因为它这一段确实用了 flat 路径判定)。
---
## 七、字典 / 视图 / 地区族(补测,均 ✅)
| 功能 | 接口 | 实测 | 说明 |
|------|------|------|------|
| 字典分组分页 | `POST /api/dict/group/page` | ✅ 200 code:0 | |
| 字典分组启用列表 | `GET /api/dict/group/enabled-list` | ✅ 200 code:0 | |
| 字典项分页 | `POST /api/dict/item/page` | ✅ 200 code:0 | |
| 字典项启用列表 | `GET /api/dict/item/enabled-list?groupCode=` | ✅ 200 code:0 | 商机各枚举取值来源 |
| 列偏好读取/保存 | `GET /api/preference/get`、`POST /api/preference/save` | ✅ 200 code:0 | 自定义列 |
| 保存视图列表 | `GET /api/preference/view/list?scopeKey=` | ✅ 200 code:0 | |
| 保存视图-保存 | `POST /api/preference/view/save` | ⚠ 需 JSON body | form 提交返 `40001 请求格式不支持,请使用 JSON` |
| 保存视图-删除/置默认 | `POST /api/preference/view/delete`、`/set-default` | ✅ 路由存在 | 需 `scopeKey` 参数 |
| 地区列表 | `GET /api/rule/region/list` | ✅ 200 code:0 | |
| 地区分级 | `GET /api/rule/region/level?level=` | ⚠ 需 `level` 参数 | 缺参返 `40001 缺少必填参数:level`(正确校验) |
> ⚠ C-03 字典编码:商机字典 groupCode 为 **`opp_stage` / `opp_status` / `opp_pause_reason` / `opp_close_reason`**(`opp_` 前缀),**非** spec 里写的 `opportunity_stage` 等。前端取字典需用实际编码。
> ⚠ C-04 视图保存 `POST /api/preference/view/save` 必须 `Content-Type: application/json`,不接受 form。
---
## 八、契约差异清单(前端对接必读,非 bug)
| 编号 | 差异 | spec 预期 | 实际 |
|------|------|-----------|------|
| C-01 | 分页字段 | `data.records` | `data.content` + `data.total`(+size/current/pages/empty) |
| C-02 | 详情/子表参数名 | `opportunityId` | `oppId`(详情主体 `id`) |
| C-03 | 商机字典 groupCode | `opportunity_stage` … | `opp_stage` / `opp_status` / `opp_pause_reason` / `opp_close_reason` |
| C-04 | 视图保存请求体 | form | 必须 JSON |
| C-05 | 状态机/规则族路径风格 | RESTful `/{id}/xxx` | flat:id 走 body/query,`/api/opportunity/claim`、`/api/rule/opp-*` |
| C-06 | 地区分级 | 无参 | 必传 `level` |
---
## 九、范围外(未测/未报,遵循锁定范围)
一键拉群 / 推送方案(A3-1-1-2-5 + A7-3-2-3)/ 督办 / 转项目(A3-1-1-2-10 + ConvertToProjectCmd)/ 工作计划联动(A3-1-1-1-7)/ 项目查重规则(A7-3-2-4)。
---
## 附录:为何重写(旧报告 `verify-report.WRONG.bak.md` 的问题)
1. **根本错误——测错了路径**:旧报告按 `spec-endpoints.md` 推断的 **RESTful 路径**(`GET /{id}`、`POST /{id}/claim`、`GET /{id}/versions` …)去打,后端实际是 **flat 风格**,于是几乎每条都得到 404,被误判为「接口未实现」。共约 **39 条 ❌ 属误判**(状态机 13、详情 5、跟进 3、勘察 2、附件 4、团队 2、日志 1、协作 3、看板 3、规则 versions 3 等)。
2. **未参考同目录实测产物**:`e2e_test.py` / `03-list.html` 就在同一目录、用的是正确的 flat 路径,但旧报告完全没引用。
3. **W-02 字段名错误**:新建 500 的肇因列是 `current_stage_id` 而非 `bid_form`(现象判断对,归因错)。
4. **e2e_test.py 自身的"37/37 OK"也不可信**:其状态机段依赖新建返回的 id,而新建必 500 → id 为 None → 8 条状态机被 **SKIP**(重跑实为 OK=28 / FAIL=1 / SKIP=8)。本报告改用**已存在商机**实测状态机,才拿到真实的全通过证据。
**本报告的证据基础**:`e2e_test.py`(重跑)+ `supplement_test.py`(49 条补测)+ 手工 curl 完整状态机链 + 后端日志根因定位 + Controller 路由清单交叉核对。