crm-code-audit.md 24 KB

CRM 移动端六入口代码审计

日期:2026-10-21。证据类型:静态代码、现有纯逻辑测试、从当前源码抽取函数的内存模拟。本报告不代表真实 UI、真实接口、审批写入或微信真机验收通过。

1. 执行范围与基线

  • 审计客户管理、产品管理、报价管理、报价审批、合同管理、合同审批及其直接表单/详情/选择器。
  • 提交基线:97cbba3b052e346089fe2793ec432f20b5a82021。开始时仅发现本次方案和 docs/testing/ 未跟踪文件;本子任务未改业务源码,未提交 Git,未写数据库,未操作浏览器,未运行 Playwright。
  • 已先读根与前端图谱报告的摘要、社区导航及 God Nodes。graphify-out/wiki/index.md、web-admin/graphify-out/wiki/index.md 均不存在。图谱提示客户/合同详情组件是高连接节点;图谱基线早于当前提交,因此具体结论均回到当前源码核对。
  • 本任务只新增审计文档,未运行代码图谱更新。
  • 规范优先级:当前 OpenSpec/UI 补遗 → 最新已确认迁移/样式设计 → 对应 Web 实现;历史交付文件只作为线索,不当成本次验证结果。

主要依据:

2. 能力矩阵

“实现可定位”仅表示当前源码有对应入口和处理,不表示已在真实 UI 或服务端验证成功。

入口 应保留能力 / Web 对照 当前实现可定位 明确删减或已确认边界 本轮判断
客户管理 /m/crm/customer 四视图、关键词/字段筛选/排序、分页;客户增改删、公海;详情资料/跟进/联系人/商机/合同/开票/日志。对照 crm/customer/index.vue、CustomerFormDialog.vue、CustomerDetailDialog.vue 四视图、远程 facet、分页请求序列保护;全量列表卡字段;增改与证件附件;批量删除/公海;主要联系人/默认开票、商机动作均有代码 导出排除;Web 无实际入口的转移/成交不新增;字段显隐已按 10-18 定稿移除;关联商机/合同/日志筛选仅已加载集合是 10-18 明确保留行为 输入重建、排序入口差距;其他业务操作待真实回归
产品管理 /m/crm/product 列表/详情/增改/单条上下架/删除/分类维护。对照 crm/product/index.vue、共享产品表单 最新列表固定 statusIn=on,分类/型号/价格筛选、扁平卡;详情锚点、分类全路径、品牌/别名/图片;删除创建人限制 列表只展示上架、无状态 Tab、成本价不展示均已确认;无导入导出、批量上下架、字段显隐;不把“下架后卡消失”报缺陷 输入重建;页面表单仍 Element,作为当前规范差距单列
报价管理 /m/crm/quote 全部/待审批/已审批、详情、增改、复制、删除、单条提交/作废、版本历史。对照 crm/quote/QuoteListPage.vue、QuoteFormPanel.vue、QuoteDetailDialog.vue 共享移动列表;页面表单 Vant 分支;详情全分区锚点;金额汇总/税费/别名/品牌;审批人有序多选及默认设置预填 不要求批量提交/审批、分享、转合同、生成报价单/PDF、导出、统计条、独立审批人设置菜单 输入重建;产品分页失败跳页;客户/商机选择上限与即时搜索;金额筛选回显
报价审批 /m/crm/quote/approval 待我审批/已审批;详情、当前步骤通过/驳回 列表查询用 pendingApprovalUserId/approvedByUserId;详情要求 status=submitted;复用 findPendingQuoteApproval;管理/审批动作分开 仅单条审批;审批页不改单 相关纯函数用例通过;真实角色/实际审批未验证
合同管理 /m/crm/contract 列表/详情/增改/复制/删除、提交/作废/恢复、扫描件;批量提交。对照 crm/contract/ContractListPage.vue、ContractFormDialog.vue、ContractDetailDialog.vue 草稿批量提交并提示跳过;详情按状态动作;附件;详情页面与共享表单 调价函管理、执行管理、续签、打印/PDF/Excel、桌面 facet/统计条/审批人设置菜单排除 输入重建;锚点高亮错误;详情税费/总计遗漏;金额筛选回显;表单保留桌面形态
合同审批 /m/crm/contract/approval 待我审批/已审批;单条/批量通过/驳回;部分失败反馈 列表 canApprove 过滤,驳回理由必填;批量审批 Promise.allSettled 统计成功/失败/跳过;详情有独立审批入口 审批页不改单;不能把报价不做批量的边界套到合同 详情审批按钮门禁与服务端不同;真实批量部分失败待验证

批量补充:客户“全选已加载”文案与 toggleAll() 仅选择当前已加载行一致,不将其误报为全库全选失败。见 客户选择逻辑。合同列表处理批量时清空选择、刷新列表的代码已存在,但没有实际调用审批接口。

3. 可静态确认的问题与复现路径

CRM-A01 · P2 · 合同正文滚到产品明细时,高亮被前面的甲乙方分区覆盖

  • 证据:正文先甲乙方后产品,正文甲乙方、正文产品;detailTabs 却先产品后甲乙方,Tab 顺序。滚动同步 将最后一个 top <= probe 的 Tab 设为当前项,依赖数组与 DOM 同序。
  • 原源码函数模拟:probe=104,products.top=100,parties.top=-900,其他后续区块未到达。实际 activeTab=parties,预期 products。
  • 浏览器待复现:管理/审批入口打开任意合同详情,点“产品明细”,待 700ms 滚动锁结束,再在产品区上下微滚;核对高亮是否跳到“甲乙方”。不需要修改数据。
  • 建议:Tab 顺序与正文同序,或按实际位置选择最接近探测线的分区;同时回归向上/向下滚动与点击跳转。

CRM-A02 · P2 · 报价产品选择器第二页失败后重试会跳到第三页

  • 分页函数 在请求前执行 mobileProductPage += 1;错误处理 没有回滚页码,也没有保存失败页。
  • 原源码函数模拟:初始已加载 p1–p20,第二页请求失败后页码仍为 2;再次加载实际请求第 3 页,调用序列 [2,3],结果为 p1–p20 + p41–p60,缺 p21–p40。
  • 浏览器待复现:新增/编辑报价,选择主体后打开产品选择器;产品命中数超过 40,在第二页制造一次网络失败,再触底。应重试第 2 页,不能跳过。该网络边界测试需要受控环境;本子任务未操作网络或浏览器。
  • 选择器本身确实是服务端关键词查询、每页 20 条,并传 matchProductNo:false、matchModel:true;不能误写成只过滤本地首屏。

CRM-A03 · P2 · 六入口每次重搜均卸载真实搜索框及更多筛选内容

  • 六入口由四个实现覆盖,其外层都用完整骨架与业务区域 v-if/v-else 互斥:客户、产品、报价共用列表、合同共用列表。真实 MpBusinessHeader 和更多筛选均在 v-else 内。
  • 以客户为例,fetchPage 重置先清 rows 再 loading=true,300ms 防抖 每次输入触发;因此命中骨架分支,搜索 input 被销毁。产品、报价、合同同样在 reset 清集合后进入 loading。
  • 静态结论:查询过程必然重建真实输入 DOM;焦点、软键盘、输入法组合与弹层内部滚动的实际影响交由主代理 CUA 记录。本子任务不将主代理观察冒充自己的浏览器验证。
  • 浏览器待复现:任一列表在搜索框输入前半关键词,停顿超过 300ms,再继续输入且不重新点击;检查焦点/键盘和最终关键词。打开“更多”,输入文本或金额并停顿,检查弹层是否闪烁、输入中断或条件重置。
  • 建议:头部与筛选保持挂载,骨架只替换列表区域,首次加载与后续查询分开处理。

CRM-A04 · P2 · 报价客户/商机选择只返回前 30 个匹配项,且输入后不即时查

  • 客户选择 UI、商机选择 UI 只有 @search,没有输入监听或关键词 watcher,输入后要点键盘搜索才查询,清空也不自动重查。
  • 客户请求 固定 page:1,pageSize:30;商机请求 固定 pageNum:1,pageSize:30。弹层不读取 total,也没有更多页入口。
  • 准确边界:关键词传给服务器,能搜到首批以外的特定记录;问题是未输入精确名称时或一个模糊词匹配超过 30 条时,剩余匹配项无法浏览。并非“只能在本地 30 条里搜”。Web 的 客户选择器 有分页。
  • 浏览器待复现:新增报价打开客户选择;输入唯一客户关键词后等 500ms,核对是否仍显示旧列表;按搜索后再核对;再使用命中超过 30 条的模糊词,尝试访问第 31 条。商机同理。

CRM-A05 · P2 · 合同详情在前置已驳回时仍给后续审批人显示通过/驳回

  • pendingApproval 只在 pending 集合内取最小 step,没有检查合同状态、前置驳回或 isCurrent;按钮生成 直接使用此结果。
  • 原源码 computed 模拟:合同 status=draft;步骤 1 已 rejected;当前用户步骤 2 pending。结果 canApprove=true,动作 ['approve','reject']。
  • 服务端驳回推进 将合同置 draft,并保留后续 pending;审批校验 要求 is_current=1 和本轮前置均 approved。按合同审批入口 也查当前节点。
  • 这是前端按钮门禁错误;没有证据显示后端允许越权审批。 未执行任何审批写入。
  • 浏览器待复现:用隔离记录建立两步审批,第一步驳回后,以第二步用户重新打开保留的 /m/crm/contract/:id?from=approve 详情;预期无通过/驳回。需第二个真实账号与隔离流程。
  • 对照:报价详情先检查 status=submitted,并使用具备前置驳回判断的 findPendingQuoteApproval;相关纯逻辑测试本次通过。

CRM-A06 · P2 · 报价/合同更多筛选确定后,“全部金额”与实际金额条件不一致

  • 报价 applyMoreFilters、合同 applyMoreFilters 总是令 amountRange='',同时保留 moreDraft 的金额上下限。
  • 原源码函数模拟:先选 1-10w,更多内上下限保持 10000/100000,直接确定;实际 minAmount=10000,maxAmount=100000,amountRange=''。空 range 对应快捷栏“全部金额”,但查询继续限制金额;“更多·已筛”虽会出现,快捷栏仍误导。
  • 浏览器待复现:报价管理或合同管理选“1–10万”→更多→不改金额直接确定,检查快捷栏文案及结果集。审批入口复用同组件同样受影响。
  • 建议:已知区间保留对应标签,自定义区间显示“自定义金额”,不要把生效金额条件标成全部。

CRM-A07 · P2 · 合同详情遗漏产品税费与 Web 的合计总金额

  • Web 产品税费列 展示 taxAmount;移动产品明细 有税率、含税单价、含税总价,但没有税费行。API 可读字段 taxAmount 已存在。
  • Web 合计总金额 明确为 amount + freight + barrelDeposit。移动金额区 仅分别展示三项及 amountWithTax,没有该合计。amountWithTax 是独立接口字段,不能视为前述求和展示的等价替代。
  • 只报已确认的字段遗漏;本轮没有据此断言已保存金额计算错误。执行数量、回款/开票、调价函等涉及原迁移删减边界的栏目未直接作为本条缺陷扩大范围。
  • 浏览器待复现:同一非框架合同在 Web 查看和移动详情只读对照,选择含非零税费、运费、押金的数据,核对税费与总计可见性。

CRM-A08 · P2 · 客户迁移要求的排序没有操作入口

  • 迁移清单 包含排序,10-18 列表定稿仅明确删“显示字段”,未删排序。
  • 当前移动列表与筛选组件没有排序选择;syncFromProps 还显式删除 sortField/sortOrder;buildQuery 不回传排序。
  • 浏览器待复现:客户列表及更多筛选查找按创建时间/最后跟进等排序的入口,与 Web 客户页排序对照。如有后续用户明确删减排序的记录,应按该记录降为不适用;当前可见规范中未发现该删减。

4. 选择器与布局边界检查

检查点 静态结果 后续验证
客户列表编号/名称等 facet 服务端 fetchCustomerColumnFacetValues;每次带搜索词,limit=500;保留选中值,具备请求版本保护 不能因 limit=500 就说只搜本地首批;需用第 501 个值的唯一关键词验证服务器结果与回显
产品分类/型号 分类取树,列表展示全路径;型号选项 limit=500;更多内可手输型号 型号快捷列表超过 500 的覆盖依赖更多输入,需核对用户可发现性
报价产品 服务端即时搜索、滚动分页、主体约束及名称/拼音/型号匹配参数存在 A02;已选项跨检索/分页保持、不同主体危化限制需真实回归
报价客户/商机 服务端搜索但固定 30,无即时输入事件 A04
报价联系人/负责人/主体/审批人 联系人由客户拉取;负责人/主体 picker 列表;审批人多选有序,未见人员搜索入口 大量人员/联系人下可用性待测,不因缺少搜索直接推断数据丢失
合同表单选择器 页面 variant 继续挂 ProductSelector/CustomerSelector/ContactSelector/QuoteSelector;桌面选择器具备服务端分页 不报“只检索首 20/50”;但移动弹层宽度、键盘与滚动须查
客户关联商机修改 fetchCustomerOptions({keyword,limit:100}),点击“搜索客户”才查 大量匹配项与即时搜索体验候选;不是仅本地过滤
客户详情关联合同/商机/日志 查询分页、关键词/快捷筛选作用已加载集合 10-18 专项设计明确允许,未报为新缺陷;界面若未说明“已加载”可做文案改善

布局待 CUA 验证项,未宣称视觉失败或通过:

  1. 短字段两列密度:客户资料/表单 CustomerFields、产品/报价/合同详情多为逐行 KV;合同表单页面样式明确 grid-template-columns:1fr。日期/数量/税率/状态等可作为两列优化候选,但不得把 Web 弹窗“两列”要求机械套成全部移动字段必须两列。需在 375/414 宽长值、空值、错误提示下验证可读性。
  2. 产品/合同表单还保留 Element 控件:产品共享表单 index.vue 页面分支仍展示 el-input、el-tree-select;合同 ContractFormDialog.vue 为全屏 el-dialog 并保留桌面选择器 735 行。与当前 H5 补遗的 Vant/底部弹层方向有差距;旧迁移允许复用表单,不把这单独当成真实业务不可用。
  3. 客户详情 Tab:仍采用互斥 van-tab 内容,带 swipeable,detail.vue;产品/报价/合同为同页锚点。客户 10-18 专项设计明确按资料/跟进/联系人等业务 Tab 改造,而通用补遗要求详情锚点;这是需结合最新产品意图确认的规范差异,不将业务 Tab 的存在直接判为缺陷。
  4. 返回恢复:列表查询多放组件内 ref,详情返回常 router.replace(listPath);需核验条件、展开项和滚动位置是否合理恢复。未检查 router 缓存全链路前不下丢状态定论。
  5. 403、加载失败与空结果:客户/产品有页内重试;报价列表非 403 catch 仅 return,可能落为空状态,实际错误提示还受全局 request 拦截器影响;需浏览器异常样本辨别,未按静态吞错直接认定完全无提示。

5. 执行的测试与证据

执行前已读脚本和目标模块导入,确认只做纯函数/源码断言:不启动浏览器、不发真实业务请求。test-mp-crm-{customer,product,quote,contract}.mjs 及 *-dom.mjs 含 Playwright,本轮未执行。

在 web-admin/ 执行:

node --import tsx --test src/views/m/crm/customer/model.test.ts src/utils/mp-crm-product-card.test.ts src/utils/mp-crm-product-category-tree.test.ts src/utils/mp-crm-quote-card.test.ts src/utils/mp-crm-contract-card.test.ts src/utils/mp-crm-confirm.test.ts src/utils/mp-crm-product-confirm.test.ts scripts/mp-crm-primitives.test.mjs

Node v24.15.0;退出码 1;结果 52 tests / 51 pass / 1 fail。

测试组 本次结果 能说明什么
客户 model 9 项通过 payload 精确 ID、时间、固定卡片字段、响应归一、开票失败后不重复创建客户的替身逻辑
产品卡片/分类树 11 项通过 固定只查上架、字段格式化、分类展开纯逻辑
报价卡片/查询/当前审批人 14 项通过 查询参数、卡片映射、当前最早步骤及前置驳回保护
合同卡片/查询 9 项通过 查询参数/金额格式化;不覆盖详情 pendingApproval 或批量真实接口
共用确认/产品确认别名 5 项通过 配置、危险 HTML 转义
primitives 源码契约 3 通过、1 失败 失败是旧附件类名断言,不能证明像素尺寸或真实点击命中

失败详情:scripts/mp-crm-primitives.test.mjs:83 断言 /class="att-grid\b/,当前 MpAttachmentGrid.vue 使用 class="mp-crm-att-grid"。同测试还期待旧 att-doc-thumb 与 att-add mp-wv-touch-target,当前组件/SCSS 已有新结构。将其登记为 测试契约陈旧(P3,CRM-T01);未改测试以制造通过,未据此宣称附件 UI 有缺陷。真实附件大小、删除命中区及长文件名仍待 CUA。

额外内存模拟使用 typescript AST 从当前 .vue 的 <script> 抽取真实函数/变量声明后转译;仅注入 ref/computed、矩形位置和假的 API 返回,无浏览器或业务请求:

CRM-A01 expected="products", actual="parties"
  positions: base=-1500, amount=-1200, parties=-900, products=100,
             approval=900, attachments=1300; probe=104
CRM-A02 calls=[2,3], pageAfterFailure=2
  loaded IDs: p1..p20 + p41..p60; missing p21..p40
CRM-A05 status="draft", canApprove=true, actions=["approve","reject"]
  input: step1 rejected; current user step2 pending
CRM-A06 quote: minAmount="10000", maxAmount="100000", amountRange=""
CRM-A06 contract: minAmount="10000", maxAmount="100000", amountRange=""

第一轮抽取脚本在已输出 A01 后因报价 <script> 带 name 属性而解析失败;改为读取任意 <script ...> 标签后,A02 单独执行退出码 0,A05/A06 执行退出码 0。该失败属于临时审计 harness,不属于产品缺陷,未改业务源码。

6. 交给真实 UI/服务端验证的优先序

  1. 六入口断续输入搜索及“更多”连续填条件,确认 A03 实际焦点/弹层影响。
  2. 同一合同详情的产品锚点与 Web 税费/总金额对照(A01/A07),只读可完成。
  3. 报价客户/商机即时搜索与超过 30 条匹配、报价/合同金额筛选回显(A04/A06),只读可完成。
  4. 隔离环境下产品选择分页失败重试(A02),禁止通过真实库批量造数据替代前置条件确认。
  5. 双用户隔离合同审批记录验证 A05;再验证合同批量混合状态、成功/失败/跳过计数和状态刷新。
  6. 375/414 宽产品/合同新增页、选择器、详情短字段密度、附件、底栏;微信软键盘/文件选择/桥接另行真机验证。

结论边界:本子任务完成代码对照和上述非浏览器验证;尚未覆盖真实权限隔离、真实保存/上传/审批/删除、微信原生行为、实际像素布局及全字段往返保存。不能据纯函数通过或静态可定位,宣称六入口验收通过。