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

20 KiB
Raw Blame 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
  • 根因:缓存 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
  • 根因:为支持打卡关联 CRM 拓展,biz_type 需新增 'crm' 值。但约束 work_checkin_biz_type_checkif not exists(conname=...) 判断,已经部署过旧版库里该约束已存在,升级后分支被跳过 → 约束值列表仍不含 crm。而本次改动的 WorkServiceImpl.saveCheckIn 已开放写入 biz_type='crm' → 存量库上写入 CRM 关联打卡/日报/跟进时触发 CHECK 违反,返回 500。
  • 影响CRM 拓展关联的外勤打卡、日报、跟进记录在所有已部署环境落库失败。
  • 修改建议:改用“先 DROP CONSTRAINT IF EXISTSADD 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 ExpansionServiceImplTest.java
  • 问题:校验由“姓名/电话/职位必填”收紧为“行内任一字段有值时,除生日外 wecomAdded、specialNote 也必填”。① 既有单测 buildContact() 只填 name/mobile/title必抛异常导致测试红② 任何未升级、不传这两个字段的调用方新增/编辑渠道会被拒。
  • 修改建议:同步补全 buildContact() 两个字段;确认所有渠道联系人调用方(Expansion.tsx 大改、crmQuickCreate/shared.tsx)已同步传参;若存在不可控旧客户端,后端可对这两个字段降级为可空并在展示层兜底。

M2. 渠道↔CRM 互移导致渠道联系人专有字段丢失(且回退路径必然报错)

  • 文件ExpansionServiceImpl.java
  • 问题
    • 渠道→CRMtoCrmContactRequests 只保留 name/mobile/title职务、生日、是否加企业微信、特别说明被丢弃且迁回渠道也无法恢复CRM 表无这些列);
    • CRM→渠道MoveCrmToChannelRequest.contacts 为空时后端回退用源 CRM 联系人,但转换后 wecomAdded/specialNote 恒为 null遇到 normalizeRequiredContacts(必填)必抛异常——前端即使留空想走“回退”也会失败。
  • 影响:互移一次后核心联系信息丢失;回退路径不可用。
  • 修改建议:若需保留,在 crm_crm_expansion_contact 补冗余列并双向迁移;否则在互移弹窗显著提示“生日/企微/特别说明将不被带入”,并在前端默认补默认值(如 wecomAdded="否")规避报错。

M3. 未签单 tab 导出遗漏“禁用/非标准阶段码”商机违反约束7

  • 文件Opportunities.tsx
  • 问题:把“列表可见商机的真实 stageCode 合并进默认导出阶段集合”的逻辑只写在 archived(已签单)分支未签单activetab 的导出默认 stageCodes 仅字典启用项,若存在非 archived、非丢单但携带禁用/非标准阶段(如 S7A/S7B、S4A、OMS 同步值的商机会“列表可见但导出遗漏”正违反既定约束7。
  • 修改建议:把该合并逻辑抽成通用函数,对 activearchived 都套用。

M4. 日报联动跟进时间由“提交时刻”改为固定 09:00

  • 文件WorkServiceImpl.java
  • 问题syncReportFollowUps 的跟进时间 followUpTime = reportDate.atTime(9:00),之前是取日报提交时间/当前时间。日报在 09:00 后提交时,跟进/提醒时间被回拨到当天早晨 09:00可能已过期、排序提前
  • 修改建议:若确为需求(统一营业开始时间)建议用 max(提交时刻, 09:00) 或下一工作日 09:00否则恢复提交时间语义。需产品确认是误改还是有意

M5. 注册资金registeredCapital被设为新增必填行为变更

  • 文件Expansion.tsxcrmQuickCreate/shared.tsx
  • 问题:对无该字段历史的旧渠道,编辑保存会被强制补填;快速创建/迁移同样强制。
  • 修改建议:若是新需求可保留;若为兼容旧数据,建议仅新记录必填、存量允许空。

M6. OMS 推送阶段改为经 crm_oms_dict_mapping 映射 + base-url 默认值变更

  • 文件OmsClient.javaapplication.yml
  • 问题projectStage 由透传改为查映射表 mapStageToOms;若映射表迁移未入目标库会抛异常中断推送;oms.base-url 默认从 192.168.4.78 改为 192.168.2.158
  • 修改建议确认映射表迁移为发布前置OMS 地址改为环境配置而非提交内网 IP 默认值;映射查询失败降级为透传。

M7. 渠道联系人字段丢失/校验边界(前端)——展开

  • 文件crmQuickCreate/shared.tsx
  • 问题
    • isEmptyChannelContact 把“仅填生日”的行判为非空,再经 isCompleteChannelContact(要求 name/mobile/title/wecom/specialNote 全有)判为不完整 → 用户只给某行补个生日会被整体拦截提交,与“生日选填、空行可留空”文案矛盾;
    • 编辑旧渠道(>6 条/重复职责的历史数据)回填固定 6 行时,多余的被静默丢弃。
  • 修改建议:判空排除 birthday仅生日视为空行对重复职责/超 6 条给明确提示而非静默删除。

M8. ICU 关联Dashboard 首页卡片改懒加载(向后兼容破坏)+ 潜在无限请求

  • 文件DashboardAnalyticsConfigService.javaDashboard.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 互移 clearOpportunityChannelExpansionExpansionServiceImpl.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_contactduty/birthday/wecom_added/special_noteCRM 联系人表 crm_crm_expansion_contact name/mobile/title。

已验证无问题的部分

  • 事务边界:两个移动方法均 @Transactional,任一失败整体回滚。
  • 并发防重:selectChannelExpansionForMove / selectCrmExpansionForMoveFOR UPDATE 行锁。
  • 引用迁移完整:updateFollowUpBiz / updateCheckinBiz / updateReportMessageBiz 双向把跟进/打卡/日报改挂到新记录biz_type + biz_id 一起迁移。
  • 覆盖地市:crm_channel_expansion_coverage.channel_idON DELETE CASCADE,删渠道自动清理,无孤儿行。
  • 前台出错色 setterchannel→CRM 目标用 invalidMoveCrmContactRows、CRM→channel 用 invalidMoveChannelContactRows,命名反直觉但未写反,正确。
  • extensionType(多选数组)在前端 API encodeExpansionMultiValue 拼串后提交,后端为 String正常channelAttribute / coverageProvince 等同理。
  • 移动 API 携带 coverageItems(来自 ...rest),后端 replaceChannelCoverage 正常写入。

已确认的 Bug / 需落地的字段诉求(按影响排序)

级别 编号 位置 问题 影响 / 建议
M-M1 selectChannelContactsForMove(ExpansionMapper.xml) + toCrmContactRequests(ExpansionServiceImpl.java) 渠道→CRM 迁移时,渠道联系人的 duty/birthday/wecom_added/special_note(是否加企微/特别说明/生日等)被丢弃。列表查询 selectChannelContacts:988 明明会查出这些字段(dto 有),迁移查询却只取 name/mobile/title 迁移后渠道联系人的拓展字段不可逆丢失,且 CRM 联系人表无对应列可存。需产品确认:要么给 crm 联系人表加列并迁移带上字段,要么迁移前提示用户这些数据将不保留
M-M2 clearOpportunityChannelExpansion(ExpansionMapper.xml) 渠道→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) 只置 supplier_id=null,不清 supplier_name 其它 CRM 把被迁渠道当进货商时,迁移后 supplier_id 空但 supplier_name 残留 详情显示名称但跳转/关联失效。建议连 name 一起清或回退 JOIN
M-M6 resolveChannelProvince(ExpansionServiceImpl.java) 反查失败时返回 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 比较改 StringExpansion.tsx onChange 不再 Number())。
  • 建议M-M1/M-M2/M-M3 为产品级取舍上线前需与需求方对齐M-M5/M-M6 为低成本可改项;互移路径仍缺单元测试,建议优先补。