# 参考:移植经验与踩坑记录 > 全程参考此文档,避免重蹈覆辙。每完成一个模块,将新经验追加到本文档。 --- ## 前端经验 ### 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`) - 状态字段用 `` 渲染 ### 4. 表单字段注释放在 label 后面 ```vue ``` ### 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` 依赖合同模块,暂未移植