# 代码改动审计报告 > 审计对象:仓库 `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`),命中已加载 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`(已签单)分支**;未签单(active)tab 的导出默认 `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. 其余低危项(L2–L12) --- ## 六、已核验无问题项(避免过度修改) - 商机阶段筛选动态获取字典、`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`,删渠道自动清理,无孤儿行。 - 前台出错色 setter:channel→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 迁移后,渠道的覆盖省/市被级联删除且不转入 CRM(CRM 无覆盖字段) | 属设计取舍但无提示,容易误判。建议在弹窗提示覆盖地市将不迁移 | | 低 | 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 为低成本可改项;互移路径仍缺单元测试,建议优先补。