Files
shanxia2.0/docs/crm-migration/ref-lessons-learned.md
T
2026-06-01 08:21:21 +08:00

185 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 参考:移植经验与踩坑记录
> 全程参考此文档,避免重蹈覆辙。每完成一个模块,将新经验追加到本文档。
---
## 前端经验
### 1. 附件上传必须遵循门户标准
- 替换为 `fetchUploadAttachment`
- 上传触发样式遵循「附件上传卡片化布局」(140×96px)
- 参考 `openspec/.../ui-conventions/spec.md` §「附件上传卡片化布局」
### 2. 弹窗统一用 el-dialog,禁止 el-drawer
- `el-drawer` → `el-dialog`
- `size="80%"` → `width="960px"`
- 移除 `direction="rtl"`
- 添加 `backdrop-filter: none`、`class="dialog-form-layout"`
### 3. 详情查看弹窗按 OpenSpec 标准样式
- 使用 `SystemViewDialog` 模式(参考 `ProductViewDialog.vue`)
- 状态字段用 `<el-tag>` 渲染
### 4. 表单字段注释放在 label 后面
```vue
<template #label>排序号<span class="form-label-tip">数值越小越靠前</span></template>
```
### 5. Element Plus 组件不要手动 import
- shanxia2.0 使用自动导入,删除所有手动 import
### 6. 视图切换保留原 CRM 组件
- `el-radio-group` + `el-radio-button` 是 Element Plus 原生组件,保留不变
### 13. API 响应解包用兼容写法
- `request.post` 内部已解包 `res.data.data`,返回值是 API 的 `data` 字段内容
- 但加密/非加密场景返回结构不一致,**必须**用兼容写法:
```js
const data = (res as any)?.data || res || {}
list.value = data.list || data.rows || []
```
- **禁止**直接写 `res.data?.list`(多套了一层 `.data`,结果为 `undefined`)
- 参考已有页面的写法:`product/index.vue`、`customer/index.vue`
### 14. 字典类型名必须匹配 API 实际返回值
- 同一业务概念可能存在多个字典(如行业:`crm_industry` 中文值 vs `crm_customer_industry` code 值)
- 必须**先看 API 实际返回的 dict_value 是什么格式**(中文?code?),再选择对应的 `dict_type`
- API 返回 `industry: "it_internet"` → 用 `crm_customer_industry`
- API 返回 `industry: "石油化工"` → 用 `crm_industry`
- 在模板和 `dictStore.getLabel()` / `dictStore.getTagType()` 中使用匹配的字典名
### 15. 跨模块组件直接引用,不要重新实现
- 已有模块的通用组件(如 `CustomerDetailDialog`、`ClaimDialog`)应直接 import 使用
- 公海池查看详情 → 引用 `../CustomerDetailDialog.vue`,不要写占位 `ElMessage.info('功能开发中')`
- 新模块的"查看"按钮:先搜索是否已有同类型的详情弹窗组件
### 16. 下拉选择器加载全量用户时注意 pageSize 上限
- 后端用户列表等通用 API 有 `pageSize` 上限(`le=100`)
- 下拉选择器不能传 `pageSize: 1000`,会触发 422 错误
- 传 `pageSize: 100`,如果用户超过 100 人需要支持搜索过滤(`filterable`)
## 后端经验
### 7. 编号自动生成
- CRUD 层实现,查询当日最大编号 +1,跨日重置,租户隔离
### 8. 唯一性校验排除软删除
- 校验查询加 `del_flag=0`
### 9. 关联统计字段同步更新
- 新增/删除关联记录时同步更新主表统计字段
### 10. 先理解 Java 再写 Python
- 先找 Java Controller → Service → Mapper 完整调用链
- 用中文写业务逻辑说明
- 注意:事务范围、级联更新、校验顺序、异常处理
- **特别注意 Java 代码中的注释**,原 CRM 的注释往往包含关键业务规则(如"不清空 ownerUserName,保留原负责人姓名")
### 17. 状态变更时必须保留关联展示字段
- 客户放入公海池(`status → pool`)时,`owner_user_id` 清空但 `owner_user_name` **不能清空**
- 原 CRM 明确注释:"保留原负责人姓名用于公海池列表显示"
- **原则**:ID 字段可以清空(关联已断开),但冗余的姓名/展示字段必须保留(用于历史记录展示)
- 移植前必须逐行对比原 CRM 的 `set` 语句,确认哪些字段清空、哪些保留
### 18. 历史数据修复必须配套 SQL
- 代码逻辑修复后,已被错误逻辑破坏的历史数据需要 SQL 回填
- 回填 SQL 写在增量脚本末尾,用 `INNER JOIN sys_user` 等关联查出名字回填
- 示例:`UPDATE crm_customer c INNER JOIN sys_user u ON c.into_pool_by = u.id SET c.owner_user_name = u.nick_name WHERE c.status = 'pool' AND c.owner_user_name IS NULL`
## 数据库经验
### 11. DDL 与 ORM 字段顺序一致
- DDL 和 Model 保持相同排列顺序
### 12. SQL 增量脚本必须保存
- 每次建表/ALTER TABLE 时输出增量 SQL 到 `docs/sql/`
---
## 线索管理模块经验(2026-05-31 新增)
### 19. 转化逻辑必须在事务中执行
- 线索→客户转化涉及两张表(crm_clue + crm_customer),必须用事务
- `db.add(customer)` → `db.flush()` → 更新线索 → `db.commit()`
- CrmCustomer.clue_id 回写线索 ID,建立反向关联
- 转化前必须校验:已转化/无效线索不可重复转化
### 20. 跟进记录级联更新线索 4 个字段
- 新增跟进后必须更新线索:`last_time=now, follow_count+1, next_time=跟进.nextTime, status='follow'`
- 删除跟进**不回退** followCount(原 CRM 设计决策)
- 修改跟进时如果 nextTime 变更,需同步更新线索 nextTime
### 21. 前端适配先拷贝再逐行修改,不要直接新写
- 先将原 CRM 的 Vue 文件原样拷贝到 shanxia2.0
- 然后按差异对照表逐行阅读理解后修改
- 好处:保留原 CRM 完整的表单字段/验证规则/交互逻辑,不会遗漏
### 22. el-drawer→el-dialog 布局需调整
- 抽屉是侧边全高,弹窗是居中固定宽度
- 抽屉内左右分栏布局(左信息+右 Tab)需调整为弹窗内垂直布局
- detailDrawer.vue 从 ~900 行抽屉改弹窗后仍保持完整功能
### 23. 多视图 Tab 列表页共用同一个查询函数
- 5 个视图(我的/全部/待跟进/今日新增/已转化)条件不同但基础表相同
- 后端用一个 list_clues 函数 + view_mode 参数切换 WHERE 条件
- 前端根据当前 Tab 调用不同的 API 函数(fetchMyClueList / fetchToFollowClueList 等)
### 24. 附件上传用 AttachmentUploadField 组件
- 跟进记录弹窗原 CRM 有 ~300 行自定义上传样式代码
- 替换为 shanxia2.0 已有的 `AttachmentUploadField` 组件后缩减到 ~190 行
- 参考 `customer/FollowFormDialog.vue` 的附件实现
### 25. 操作日志 Tab 暂不迁移
- 原 CRM 详情页有"操作日志" Tab,调用 `/monitor/operlog` 接口
- shanxia2.0 可能没有此接口,注释掉 Tab 并加 TODO
- 后续根据 shanxia2.0 审计日志能力决定是否实现
### 26. 线索模块 — 快速搜索勿用多字段 AND
- 列表快捷搜索应传单一 `keyword`,后端 OR 匹配 `clueName/mobile/company`
- 勿把同一关键词同时写入 `clueName`、`company`、`mobile`(会变成 AND,几乎查不到)
### 27. 线索模块 — 批量操作要处理 failItems
- 删除/分配/无效等批量接口可能部分失败(如已转化线索)
- 前端须解析 `successCount` / `failItems`,勿一律 `ElMessage.success`
### 28. 线索按钮权限用 menuCodes 而非 v-auth
- 门户 `v-auth` 依赖路由 `authList`,静态路由未配置时无效
- 线索页使用 `useCluePerm` + `/web/auth/menu-codes` 映射 `crm:clue:*`
### 29. Excel 导出必须集中做字典 code→label(对齐 Java ExcelDictConvert)
- Java:`CrmClueVo` 等 `*Vo` 字段上的 `@ExcelDictFormat(dictType=...)` + `ExcelDictConvert`
- 门户:**禁止**在 `crm.py` 各 export 路由里直接 `r.get("source")` 写原始 code
- 在 `backend/app/utils/crm_export_columns.py` 定义 `ExportColumn(..., dict_type="crm_clue_source")`
- 路由统一调用 `export_rows_to_xlsx_response(db, tenant_no=..., columns=..., rows=...)`
- 工具类:`backend/app/utils/excel_export.py`(预加载字典、写 xlsx、StreamingResponse)
- 已接入模块:线索、客户、产品、联系人、客户跟进、商机
### 30. 导出依赖 openpyxl,venv 须显式安装
- `requirements.txt` 已声明 `openpyxl>=3.1.0`,但 venv 可能未装 → 导出报 `No module named 'openpyxl'`
- 修复:`python3 -m pip install openpyxl --target=backend/venv/lib/python3.9/site-packages` 或 `./backend/start.sh --install`
- 若 venv 从其他机器拷贝导致 pip shebang 失效,用 `python3 -m pip` 安装
### 31. 前端 Blob 导出须适配 http 拦截器
- `responseType: 'blob'` 时,响应是二进制 xlsx,**没有** JSON `code` 字段
- 旧逻辑在拦截器里校验 `code === 200` 会导致导出永远失败
- 已在 `web-admin/src/utils/http/index.ts` 集中处理:二进制直返;JSON 错误体(如 500)仍解析报错
- `request()` 对 blob 返回 `res.data`,页面直接用 `Blob` 下载,勿取 `.data.data`
- 导出 API 建议加 `showErrorMessage: false`,避免与页面 `ElMessage.error` 重复
### 32. CRM 查看详情统一 SystemViewDialog(商机/线索已改)
- 原 CRM `el-drawer` 宽抽屉 → 门户 `SystemViewDialog`(hero + KV + 可选右侧时间线)
- 打开详情前 `await dictStore.loadAllDict()`,字段用 `dictStore.getLabel` / `tagType`
- 子接口 `pageSize` 遵守后端上限(如跟进列表 `pageSize: 100`,勿传 500)
### 33. 负责人/用户下拉字段名对齐 fetchUserOptions
- 门户 `fetchUserOptions()` 返回 `{ label, value }[]`
- 模板勿写 `item.nickName` / `item.userId`(原 CRM 字段名)
- 新增表单默认可设 `ownerUserId = 当前用户 id`
### 34. 商机模块补充
- DDL/菜单:`docs/sql/incremental_crm_opportunity.sql`
- 字典兜底:`web-admin/src/utils/crm-opportunity-labels.ts`(类型等字典缺失时)
- 看板/漏斗/统计路由在 `sqxp.ts`,菜单未单独配置时可复用 `crm:opportunity:list`
- `winWithContract` 依赖合同模块,暂未移植