20 KiB
20 KiB
代码改动审计报告
审计对象:仓库
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_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 ;ExpansionServiceImplTest.java
- 问题:校验由“姓名/电话/职位必填”收紧为“行内任一字段有值时,除生日外 wecomAdded、specialNote 也必填”。① 既有单测
buildContact()只填 name/mobile/title,必抛异常导致测试红;② 任何未升级、不传这两个字段的调用方新增/编辑渠道会被拒。 - 修改建议:同步补全
buildContact()两个字段;确认所有渠道联系人调用方(Expansion.tsx大改、crmQuickCreate/shared.tsx)已同步传参;若存在不可控旧客户端,后端可对这两个字段降级为可空并在展示层兜底。
M2. 渠道↔CRM 互移导致渠道联系人专有字段丢失(且回退路径必然报错)
- 文件:ExpansionServiceImpl.java
- 问题:
- 渠道→CRM:
toCrmContactRequests只保留 name/mobile/title,职务、生日、是否加企业微信、特别说明被丢弃,且迁回渠道也无法恢复(CRM 表无这些列); - CRM→渠道:
MoveCrmToChannelRequest.contacts为空时后端回退用源 CRM 联系人,但转换后 wecomAdded/specialNote 恒为 null,遇到normalizeRequiredContacts(必填)必抛异常——前端即使留空想走“回退”也会失败。
- 渠道→CRM:
- 影响:互移一次后核心联系信息丢失;回退路径不可用。
- 修改建议:若需保留,在
crm_crm_expansion_contact补冗余列并双向迁移;否则在互移弹窗显著提示“生日/企微/特别说明将不被带入”,并在前端默认补默认值(如 wecomAdded="否")规避报错。
M3. 未签单 tab 导出遗漏“禁用/非标准阶段码”商机(违反约束7)
- 文件:Opportunities.tsx
- 问题:把“列表可见商机的真实 stageCode 合并进默认导出阶段集合”的逻辑只写在
archived(已签单)分支;未签单(active)tab 的导出默认stageCodes仅字典启用项,若存在非 archived、非丢单但携带禁用/非标准阶段(如 S7A/S7B、S4A、OMS 同步值)的商机,会“列表可见但导出遗漏”,正违反既定约束7。 - 修改建议:把该合并逻辑抽成通用函数,对
active与archived都套用。
M4. 日报联动跟进时间由“提交时刻”改为固定 09:00
- 文件:WorkServiceImpl.java
- 问题:
syncReportFollowUps的跟进时间followUpTime = reportDate.atTime(9:00),之前是取日报提交时间/当前时间。日报在 09:00 后提交时,跟进/提醒时间被回拨到当天早晨 09:00(可能已过期、排序提前)。 - 修改建议:若确为需求(统一营业开始时间)建议用
max(提交时刻, 09:00)或下一工作日 09:00;否则恢复提交时间语义。需产品确认是误改还是有意。
M5. 注册资金(registeredCapital)被设为新增必填(行为变更)
- 文件:Expansion.tsx 、
crmQuickCreate/shared.tsx - 问题:对无该字段历史的旧渠道,编辑保存会被强制补填;快速创建/迁移同样强制。
- 修改建议:若是新需求可保留;若为兼容旧数据,建议仅新记录必填、存量允许空。
M6. OMS 推送阶段改为经 crm_oms_dict_mapping 映射 + base-url 默认值变更
- 文件:OmsClient.java 、application.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.java、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) |
大商机量导出全量拉取+附加跟进,慢/占内存 | 确认导出列是否需跟进字段,必要时分批 |
五、建议处理顺序
第一优先级(影响线上数据正确性/可用性,建议本次发布前必须处理):
- 商机列表缓存陈旧数据(H1)
- 存量库打卡
crmCHECK 约束(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(多选数组)在前端 APIencodeExpansionMultiValue拼串后提交,后端为 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 迁移后,渠道的覆盖省/市被级联删除且不转入 CRM(CRM 无覆盖字段) | 属设计取舍但无提示,容易误判。建议在弹窗提示覆盖地市将不迁移 |
| 低 | 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 比较改 String;Expansion.tsx onChange 不再Number())。 - 建议:M-M1/M-M2/M-M3 为产品级取舍,上线前需与需求方对齐;M-M5/M-M6 为低成本可改项;互移路径仍缺单元测试,建议优先补。