unis_crm/代码改动审计报告.md

172 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 代码改动审计报告
> 审计对象:仓库 `unis_crm` 当前工作区所有未提交改动(含新增文件)
> 审计方式:对后端/前端各模块 diff 逐一审查 + 关键高危点代码实读复核
> 生成时间2026-09-03
## 一、概述
本次改动范围很大,覆盖五大模块:
| 模块 | 主要风险 |
|------|---------|
| 拓展模块(后端) | 渠道联系人必填收紧导致旧调用/回归+测试红;互移字段丢弃;并发唯一性 |
| 拓展模块(前端) | 编辑旧渠道静默丢联系人;互移字段丢失;迁移校验阻断 |
| 商机模块 | **列表缓存陈旧数据(高危数据错误)**;未签单导出遗漏禁用/非标准阶段 |
| 工作/日报/打卡 | **存量库 CHECK 约束未含 `crm` 导致写入失败(高危部署问题)**日报联动时间改为固定09:00 |
| OMS/登录鉴权 | OMS回调硬编码弱token、核心逻辑未落地Dashboard懒加载改动 |
**审计结论一句话**
- 大部分功能需求6行固定联系人、sys_is 字典企微、互移入口、阶段筛选动态获取、鉴权回归)**实现正确**
- 但有 **2 个高危问题**(商机列表缓存陈旧数据、存量库打卡`crm`约束)会直接影响线上功能,**强烈建议先修**
-**4 个中危回归点**(渠道联系人必填收紧、互移字段丢失、日报联动时间、未签单导出遗漏)涉及数据一致性,建议按产品确认后修改;
- 其余为低危/防御性项,可暂缓。
---
## 二、高危问题(建议优先处理)
### H1. 商机列表 · Tab 内切换筛选后显示陈旧数据(数据正确性回归)
- **文件**[Opportunities.tsx](file:///Users/kangwenjing/Downloads/crm/unis_crm/frontend/src/pages/Opportunities.tsx#L1676-1697)
- **根因**:缓存 key 是 `archiveTab:keyword:filter:stageOptions.length`(按筛选粒度),但缓存**槽**却只有一个 `tabItemsCacheRef.current[archiveTab]`(按 tab 粒度)。同一 tab 内先选“S2”再切回“全部”时`loadedTabKeysRef` 已在上次记录“全部”这个 key于是命中缓存分支直接回显**上一次 S2 筛选**的结果,而非重新请求“全部”。
- **影响**:同一 tab 内切换筛选/关键字返回与筛选不匹配的商机列表,且不刷新不恢复,属于数据展示错误。
- **修改建议**:缓存值按 `loadKey` 粒度存储(`Map<loadKey, items>`),命中已加载 key 时用对应 key 的数据,而不是当前槽位的值;或将 key 写入槽位时校验与当前 `loadKey` 一致,不一致则强制重新请求。
### H2. 工作打卡 · 存量库 CHECK 约束未含 `crm` 类型 → 写入直接报错(部署级)
- **文件**[WorkCheckInSchemaInitializer.java](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/common/WorkCheckInSchemaInitializer.java#L40-54)
- **根因**:为支持打卡关联 CRM 拓展,`biz_type` 需新增 `'crm'` 值。但约束 `work_checkin_biz_type_check``if not exists(conname=...)` 判断,**已经部署过旧版库**里该约束已存在,升级后分支被跳过 → 约束值列表仍不含 `crm`。而本次改动的 `WorkServiceImpl.saveCheckIn` 已开放写入 `biz_type='crm'` → 存量库上写入 CRM 关联打卡/日报/跟进时触发 CHECK 违反,返回 500。
- **影响**CRM 拓展关联的外勤打卡、日报、跟进记录在所有已部署环境落库失败。
- **修改建议**:改用“先 `DROP CONSTRAINT IF EXISTS``ADD CONSTRAINT`”的幂等方式重建约束Java initializer 与 `sql/init_full_pg17.sql` 两处同步);上线前在目标库执行一次 `ALTER TABLE ... DROP CONSTRAINT IF EXISTS work_checkin_biz_type_check; ALTER TABLE ... ADD CONSTRAINT ... check(biz_type is null or biz_type in ('sales','channel','opportunity','crm'));`
---
## 三、中危问题(数据一致性/行为变更,需产品确认后修改)
### M1. 渠道联系人必填收紧 → 旧调用方/单测被破坏(回归面)
- **文件**[ExpansionServiceImpl.java](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java#L906-943) [ExpansionServiceImplTest.java](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/test/java/com/unis/crm/service/impl/ExpansionServiceImplTest.java#L253-259)
- **问题**:校验由“姓名/电话/职位必填”收紧为“行内任一字段有值时,**除生日外 wecomAdded、specialNote 也必填**”。① 既有单测 `buildContact()` 只填 name/mobile/title必抛异常导致测试红② 任何未升级、不传这两个字段的调用方新增/编辑渠道会被拒。
- **修改建议**:同步补全 `buildContact()` 两个字段;确认所有渠道联系人调用方(`Expansion.tsx` 大改、`crmQuickCreate/shared.tsx`)已同步传参;若存在不可控旧客户端,后端可对这两个字段降级为可空并在展示层兜底。
### M2. 渠道↔CRM 互移导致渠道联系人专有字段丢失(且回退路径必然报错)
- **文件**[ExpansionServiceImpl.java](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java#L1250-1284)
- **问题**
- 渠道→CRM`toCrmContactRequests` 只保留 name/mobile/title**职务、生日、是否加企业微信、特别说明被丢弃**且迁回渠道也无法恢复CRM 表无这些列);
- CRM→渠道`MoveCrmToChannelRequest.contacts` 为空时后端回退用源 CRM 联系人,但转换后 wecomAdded/specialNote 恒为 null遇到 `normalizeRequiredContacts`(必填)必抛异常——前端即使留空想走“回退”也会失败。
- **影响**:互移一次后核心联系信息丢失;回退路径不可用。
- **修改建议**:若需保留,在 `crm_crm_expansion_contact` 补冗余列并双向迁移;否则在互移弹窗**显著提示**“生日/企微/特别说明将不被带入”,并在前端默认补默认值(如 wecomAdded="否")规避报错。
### M3. 未签单 tab 导出遗漏“禁用/非标准阶段码”商机违反约束7
- **文件**[Opportunities.tsx](file:///Users/kangwenjing/Downloads/crm/unis_crm/frontend/src/pages/Opportunities.tsx#L2252-2279)
- **问题**:把“列表可见商机的真实 stageCode 合并进默认导出阶段集合”的逻辑**只写在 `archived`(已签单)分支**未签单activetab 的导出默认 `stageCodes` 仅字典启用项,若存在非 archived、非丢单但携带禁用/非标准阶段(如 S7A/S7B、S4A、OMS 同步值的商机会“列表可见但导出遗漏”正违反既定约束7。
- **修改建议**:把该合并逻辑抽成通用函数,对 `active``archived` 都套用。
### M4. 日报联动跟进时间由“提交时刻”改为固定 09:00
- **文件**[WorkServiceImpl.java](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/WorkServiceImpl.java#L2157-2159)
- **问题**`syncReportFollowUps` 的跟进时间 `followUpTime = reportDate.atTime(9:00)`,之前是取日报提交时间/当前时间。日报在 09:00 后提交时,跟进/提醒时间被回拨到当天早晨 09:00可能已过期、排序提前
- **修改建议**:若确为需求(统一营业开始时间)建议用 `max(提交时刻, 09:00)` 或下一工作日 09:00否则恢复提交时间语义。**需产品确认是误改还是有意**。
### M5. 注册资金registeredCapital被设为新增必填行为变更
- **文件**[Expansion.tsx](file:///Users/kangwenjing/Downloads/crm/unis_crm/frontend/src/pages/Expansion.tsx#L983-985) 、`crmQuickCreate/shared.tsx`
- **问题**:对无该字段历史的旧渠道,编辑保存会被强制补填;快速创建/迁移同样强制。
- **修改建议**:若是新需求可保留;若为兼容旧数据,建议仅新记录必填、存量允许空。
### M6. OMS 推送阶段改为经 `crm_oms_dict_mapping` 映射 + base-url 默认值变更
- **文件**[OmsClient.java](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/OmsClient.java#L115-117) 、[application.yml](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/resources/application.yml)
- **问题**`projectStage` 由透传改为查映射表 `mapStageToOms`;若映射表迁移未入目标库会抛异常中断推送;`oms.base-url` 默认从 `192.168.4.78` 改为 `192.168.2.158`
- **修改建议**确认映射表迁移为发布前置OMS 地址改为环境配置而非提交内网 IP 默认值;映射查询失败降级为透传。
### M7. 渠道联系人字段丢失/校验边界(前端)——展开
- **文件**[crmQuickCreate/shared.tsx](file:///Users/kangwenjing/Downloads/crm/unis_crm/frontend/src/features/crmQuickCreate/shared.tsx#L64-146)
- **问题**
- `isEmptyChannelContact` 把“仅填生日”的行判为非空,再经 `isCompleteChannelContact`(要求 name/mobile/title/wecom/specialNote 全有)判为不完整 → **用户只给某行补个生日会被整体拦截提交**,与“生日选填、空行可留空”文案矛盾;
- 编辑旧渠道(>6 条/重复职责的历史数据)回填固定 6 行时,多余的被静默丢弃。
- **修改建议**:判空排除 birthday仅生日视为空行对重复职责/超 6 条给明确提示而非静默删除。
### M8. ICU 关联Dashboard 首页卡片改懒加载(向后兼容破坏)+ 潜在无限请求
- **文件**[DashboardAnalyticsConfigService.java](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/DashboardAnalyticsConfigService.java)、[Dashboard.tsx](file:///Users/kangwenjing/Downloads/crm/unis_crm/frontend/src/pages/Dashboard.tsx)
- **问题**`getDashboardPanel` 由“真实计算卡片值”改为返回空 dataLoaded=false需前端走新接口 `/api/dashboard/analytics-cards/data` 补齐;本仓库前端已适配,但其他消费者/缓存会拿到空值。懒加载若某 key 服务端过滤未返回,会反复触发请求且失败态无提示。
- **修改建议**:确认无外部消费者则保留;请求侧记录已请求 key 去重,失败给卡片级错误态与重试。
---
## 四、低危/防御性问题(可暂缓)
| # | 位置 | 问题 | 建议 |
|---|------|------|------|
| L1 | `OmsCallbackController.java` L25,51,63-71 | token 硬编码弱值、回调报文不打码、核心阶段回写仍是 TODO 却返回“成功” | token 配置化、脱敏日志、先落地幂等写入再上线 |
| L2 | `ExpansionMapper.xml` `insertChannelCoverage/insertSalesRegion` | 整批拼单条 INSERT数据极多时超限 | 分批或设上限 |
| L3 | `crm_crm_expansion.end_user` 无唯一约束 | 应用层 count 校验有并发窗口 | 如“最终用户唯一”是诉求,加部分唯一索引 |
| L4 | 渠道→CRM 互移 `clearOpportunityChannelExpansion``ExpansionServiceImpl.java` L448 | 商机与渠道关联清空且不指向新 CRM不可追踪 | 如需追踪,新增来源映射列/记入商机备注 |
| L5 | 渠道→CRM 互移未迁移覆盖地市coverage | 覆盖区域信息迁移后丢失 | 与产品确认处置 |
| L6 | `OpportunityMapper.xml` L288-317 阶段值 | 历史遗留 `o.stage='lost'` 不满足 `not in('L')`,丢单列表/导出遗漏旧数据(改动前已存在) | SQL 层合并 legacy 值 `in ('L','lost')` |
| L7 | `OpportunityServiceImpl.java` L64-65,944-974 | 集成反写强制 S5 在状态计算后执行可能导致 status/stage 不一致;`WON_STAGE_CODES` 未被使用 | 先定 stage 再算 status移除/使用死代码 |
| L8 | `Work.tsx` L1926,2269,4919 | 打卡/日报对象选择器默认类型由 `sales` 改为 `opportunity` | 若非有意,恢复 `"sales"` |
| L9 | `SearchOrInputSelect.tsx` L115-122 | 选错后无“清除/取消”能力 | 补“不选择”项valueId 置 0 |
| L10 | `SearchableSelect.tsx` vs `SearchOrInputSelect` 选中比较 | 严格相等 vs `Number()` 宽松比较(当前同为 number暂不影响 | 统一比较方式 |
| L11 | `Opportunities.tsx` L1674-1725 | 切 tab 时 reset 与 load 时序错位,短暂脏列表/闪烁 | 用标志位跳过 stale 请求 |
| L12 | 导出 `getOpportunityOverview(..., limit=null, includeDetails=true)` | 大商机量导出全量拉取+附加跟进,慢/占内存 | 确认导出列是否需跟进字段,必要时分批 |
---
## 五、建议处理顺序
**第一优先级(影响线上数据正确性/可用性,建议本次发布前必须处理):**
1. 商机列表缓存陈旧数据H1
2. 存量库打卡 `crm` CHECK 约束H2
**第二优先级(数据一致性/明确回归,建议尽快确认修复):**
3. 渠道联系人必填收紧导致的单测红 + 旧调用回归M1
4. 互移字段丢失 + 回退路径报错M2/M7
5. 未签单导出遗漏禁用/非标准阶段M3
6. 日报联动时间回拨M4先确认是否有意
7. 注册资金必填M5确认是否新需求
**第三优先级(防御性/待产品决策):**
8. OMS 回调安全 + 阶段回写落地L1/M6
9. Dashboard 懒加载兼容性M8
10. 其余低危项L2L12
---
## 六、已核验无问题项(避免过度修改)
- 商机阶段筛选动态获取字典、`IN/NOT IN` 绑定参数、切 tab 重置筛选 → 正确;
- 渠道联系人固定 6 行、工作职责只读、`sys_is` 字典取值 → 正确;
- 互移事务边界、`FOR UPDATE` 防重复迁移、NO ACTION 外键清理顺序 → 正确;
- 鉴权auth.ts、路由App.tsx 懒加载)、登录回归 → 未受影响;
- ECharts 模块化注册齐全、Dashboard 单卡失败不拖垮、异常处理器只将未命中资源由 500 转 404 → 无回归。
> 本报告基于对工作区未提交改动的静态审查。互移moveChannelToCrm / moveCrmToChannel路径目前**无单元测试覆盖**,如决定保留互移功能,建议补充测试后再上线。
---
## 附《移至CRM拓展 / 移至渠道拓展》专项验证结果
> 复核范围两迁移动后端实现、mapper SQL、实时库表结构、前端两个移动弹窗表单初始化 / 校验 / 联系人编辑器 / 提交 API 编码)。
> 依据实时库确认:渠道联系人表 `crm_channel_expansion_contact` 含 `duty/birthday/wecom_added/special_note`CRM 联系人表 `crm_crm_expansion_contact` **仅** name/mobile/title。
### 已验证无问题的部分
- 事务边界:两个移动方法均 `@Transactional`,任一失败整体回滚。
- 并发防重:`selectChannelExpansionForMove` / `selectCrmExpansionForMove``FOR UPDATE` 行锁。
- 引用迁移完整:`updateFollowUpBiz` / `updateCheckinBiz` / `updateReportMessageBiz` 双向把跟进/打卡/日报改挂到新记录biz_type + biz_id 一起迁移。
- 覆盖地市:`crm_channel_expansion_coverage.channel_id` 为 `ON DELETE CASCADE`,删渠道自动清理,无孤儿行。
- 前台出错色 setterchannel→CRM 目标用 `invalidMoveCrmContactRows`、CRM→channel 用 `invalidMoveChannelContactRows`,命名反直觉但**未写反**,正确。
- `extensionType`(多选数组)在前端 API `encodeExpansionMultiValue` 拼串后提交,后端为 String正常`channelAttribute` / `coverageProvince` 等同理。
- 移动 API 携带 `coverageItems`(来自 `...rest`),后端 `replaceChannelCoverage` 正常写入。
### 已确认的 Bug / 需落地的字段诉求(按影响排序)
| 级别 | 编号 | 位置 | 问题 | 影响 / 建议 |
|------|------|------|------|------------|
| 高 | M-M1 | `selectChannelContactsForMove`([ExpansionMapper.xml](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/resources/mapper/expansion/ExpansionMapper.xml#L1370)) + `toCrmContactRequests`([ExpansionServiceImpl.java](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java#L1250)) | 渠道→CRM 迁移时,渠道联系人的 `duty/birthday/wecom_added/special_note`(是否加企微/特别说明/生日等)被丢弃。列表查询 `selectChannelContacts`:988 明明会查出这些字段(dto 有),迁移查询却只取 name/mobile/title | 迁移后渠道联系人的拓展字段**不可逆丢失**,且 CRM 联系人表无对应列可存。需产品确认:要么给 crm 联系人表加列并迁移带上字段,要么迁移前提示用户这些数据将不保留 |
| 中 | M-M2 | `clearOpportunityChannelExpansion`([ExpansionMapper.xml](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/resources/mapper/expansion/ExpansionMapper.xml#L1358)) | 渠道→CRM 迁移把 `crm_opportunity.channel_expansion_id` 置空;机会表无 CRM 关联列,商机不能挂到新 CRM | 原渠道下的商机迁移后失去渠道关联business 记录仍在但被游离)。需产品确认是否要用销售/其它维度重新关联 |
| 中 | M-M3 | `moveCrmToChannel` 仅接 endUser/industryAttr/purchaseDate/officeName→province其余 CRM 独有字段不迁移;前端 `handleOpenMove` CRM 分支完全重置为空白 | CRM→渠道迁移业务数据几乎全丢失软件点数/扩容机会/进货商/新华三/在线情况/过保时间等),且弹窗无任何预填,等于重填一份 | 结构性(渠道表无这些列)但无任何提示。建议弹窗明示「部分字段无法迁移需重新填写」,避免误以为迁移完整 |
| 中 | M-M4 | `deleteChannelExpansion` 上级联删覆盖CRM→渠道 覆盖写入正常 | 渠道→CRM 迁移后,渠道的覆盖省/市被级联删除且不转入 CRMCRM 无覆盖字段) | 属设计取舍但无提示,容易误判。建议在弹窗提示覆盖地市将不迁移 |
| 低 | M-M5 | `clearCrmSupplierRefs`([ExpansionMapper.xml](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/resources/mapper/expansion/ExpansionMapper.xml#L1364)) 只置 `supplier_id=null`,不清 `supplier_name` | 其它 CRM 把被迁渠道当进货商时,迁移后 supplier_id 空但 supplier_name 残留 | 详情显示名称但跳转/关联失效。建议连 name 一起清或回退 JOIN |
| 低 | M-M6 | `resolveChannelProvince`([ExpansionServiceImpl.java](file:///Users/kangwenjing/Downloads/crm/unis_crm/backend/src/main/java/com/unis/crm/service/impl/ExpansionServiceImpl.java#L1301)) 反查失败时返回 office_name 原文 | CRM→渠道 若 office_name 非字典码province 会存成「代表处名」而非省份,与 city(cnarea) 不一致 | 违反「省市区严格对应」的既定约束,建议失败时回退到字典 label 或允许前端补选省份 |
| 低 | M-M7 | `handleOpenMove`:3479 渠道→CRM | 把渠道「成立时间」自动作为「采购时间」预填、并自动 +3 年当过保时间 | 用户不手改时采购/过保可能不符实际。建议采购时间不强预填,或仅作可删的占位 |
| 低 | M-M8 | `moveCrmToChannel` 弹窗不暴露 stage/landedFlag | 迁移后渠道 stage 恒为 initial_contact、landedFlag 恒 false | 无法体现 CRM 侧真实状态;属字段语义取舍,需产品确认 |
### 与上一轮已修复问题的关系
- 大雪花 id 经前端 `Number(id)` 精度丢失导致 「移至 CRM 拓展」FK 违反(`fk_crm_crm_expansion_h3c_contact`),以及下拉「全部被勾选」——两者同源,本轮已确认修复([SearchOrInputSelect.tsx](file:///Users/kangwenjing/Downloads/crm/unis_crm/frontend/src/components/SearchOrInputSelect.tsx) 比较改 String[Expansion.tsx](file:///Users/kangwenjing/Downloads/crm/unis_crm/frontend/src/pages/Expansion.tsx) onChange 不再 `Number()`)。
- 建议M-M1/M-M2/M-M3 为产品级取舍上线前需与需求方对齐M-M5/M-M6 为低成本可改项;互移路径仍缺单元测试,建议优先补。