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

9.6 KiB
Raw Blame History

参考:移植经验与踩坑记录

全程参考此文档,避免重蹈覆辙。每完成一个模块,将新经验追加到本文档。


前端经验

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 后面

<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 字段内容
  • 但加密/非加密场景返回结构不一致,必须用兼容写法:
    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 依赖合同模块,暂未移植