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

172 lines
20 KiB
Markdown
Raw Normal View History

# 代码改动审计报告
> 审计对象:仓库 `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 为低成本可改项;互移路径仍缺单元测试,建议优先补。