# 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 实现;历史交付文件只作为线索,不当成本次验证结果。 主要依据: - [当前 H5 样式补遗](/Users/zlwu/work/00code/shanxia2.0/openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md:1825):输入即查、选择器即时模糊匹配、Vant 页面控件、详情锚点、详情字段/金额口径与 Web 一致。 - [客户迁移范围](/Users/zlwu/work/00code/shanxia2.0/docs/superpowers/specs/2026-10-17-mp-webview-crm-customer-design.md:9)、[产品迁移范围](/Users/zlwu/work/00code/shanxia2.0/docs/superpowers/specs/2026-10-15-mp-webview-crm-product-design.md:21)、[报价迁移范围](/Users/zlwu/work/00code/shanxia2.0/docs/superpowers/specs/2026-10-15-mp-webview-crm-quote-design.md:29)、[合同迁移范围](/Users/zlwu/work/00code/shanxia2.0/docs/superpowers/specs/2026-10-15-mp-webview-crm-contract-design.md:30)。 - [产品最新列表定稿](/Users/zlwu/work/00code/shanxia2.0/docs/superpowers/specs/2026-10-18-mp-crm-product-list-card-design.md:9)、[客户最新列表定稿](/Users/zlwu/work/00code/shanxia2.0/docs/superpowers/specs/2026-10-18-mp-crm-customer-list-card-design.md:7)、[客户详情子列表定稿](/Users/zlwu/work/00code/shanxia2.0/docs/superpowers/specs/2026-10-18-mp-crm-customer-detail-tabs-design.md:1)。 ## 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()` 仅选择当前已加载行一致,不将其误报为全库全选失败。见 [客户选择逻辑](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue:525)。合同列表处理批量时清空选择、刷新列表的代码已存在,但没有实际调用审批接口。 ## 3. 可静态确认的问题与复现路径 ### CRM-A01 · P2 · 合同正文滚到产品明细时,高亮被前面的甲乙方分区覆盖 - 证据:正文先甲乙方后产品,[正文甲乙方](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue:113)、[正文产品](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue:177);`detailTabs` 却先产品后甲乙方,[Tab 顺序](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue:533)。[滚动同步](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue:1077) 将最后一个 `top <= probe` 的 Tab 设为当前项,依赖数组与 DOM 同序。 - 原源码函数模拟:`probe=104`,`products.top=100`,`parties.top=-900`,其他后续区块未到达。实际 `activeTab=parties`,预期 `products`。 - 浏览器待复现:管理/审批入口打开任意合同详情,点“产品明细”,待 700ms 滚动锁结束,再在产品区上下微滚;核对高亮是否跳到“甲乙方”。不需要修改数据。 - 建议:Tab 顺序与正文同序,或按实际位置选择最接近探测线的分区;同时回归向上/向下滚动与点击跳转。 ### CRM-A02 · P2 · 报价产品选择器第二页失败后重试会跳到第三页 - [分页函数](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue:2280) 在请求前执行 `mobileProductPage += 1`;[错误处理](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue:2306) 没有回滚页码,也没有保存失败页。 - 原源码函数模拟:初始已加载 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` 互斥:[客户](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue:5)、[产品](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/product/index.vue:5)、[报价共用列表](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue:5)、[合同共用列表](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/MpContractList.vue:5)。真实 `MpBusinessHeader` 和更多筛选均在 `v-else` 内。 - 以客户为例,[fetchPage](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue:376) 重置先清 `rows` 再 `loading=true`,[300ms 防抖](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/index.vue:440) 每次输入触发;因此命中骨架分支,搜索 input 被销毁。产品、报价、合同同样在 reset 清集合后进入 loading。 - 静态结论:查询过程必然重建真实输入 DOM;焦点、软键盘、输入法组合与弹层内部滚动的实际影响交由主代理 CUA 记录。本子任务不将主代理观察冒充自己的浏览器验证。 - 浏览器待复现:任一列表在搜索框输入前半关键词,停顿超过 300ms,再继续输入且不重新点击;检查焦点/键盘和最终关键词。打开“更多”,输入文本或金额并停顿,检查弹层是否闪烁、输入中断或条件重置。 - 建议:头部与筛选保持挂载,骨架只替换列表区域,首次加载与后续查询分开处理。 ### CRM-A04 · P2 · 报价客户/商机选择只返回前 30 个匹配项,且输入后不即时查 - [客户选择 UI](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue:930)、[商机选择 UI](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue:1017) 只有 `@search`,没有输入监听或关键词 watcher,输入后要点键盘搜索才查询,清空也不自动重查。 - [客户请求](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue:2423) 固定 `page:1,pageSize:30`;[商机请求](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/quote/components/QuoteFormPanel.vue:2472) 固定 `pageNum:1,pageSize:30`。弹层不读取 total,也没有更多页入口。 - 准确边界:关键词传给服务器,能搜到首批以外的特定记录;问题是未输入精确名称时或一个模糊词匹配超过 30 条时,剩余匹配项无法浏览。并非“只能在本地 30 条里搜”。Web 的 [客户选择器](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/CustomerSelector.vue:86) 有分页。 - 浏览器待复现:新增报价打开客户选择;输入唯一客户关键词后等 500ms,核对是否仍显示旧列表;按搜索后再核对;再使用命中超过 30 条的模糊词,尝试访问第 31 条。商机同理。 ### CRM-A05 · P2 · 合同详情在前置已驳回时仍给后续审批人显示通过/驳回 - [pendingApproval](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue:615) 只在 pending 集合内取最小 step,没有检查合同状态、前置驳回或 `isCurrent`;[按钮生成](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue:644) 直接使用此结果。 - 原源码 computed 模拟:合同 `status=draft`;步骤 1 已 rejected;当前用户步骤 2 pending。结果 `canApprove=true`,动作 `['approve','reject']`。 - 服务端[驳回推进](/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_approval.py:339) 将合同置 draft,并保留后续 pending;[审批校验](/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_approval.py:261) 要求 `is_current=1` 和本轮前置均 approved。[按合同审批入口](/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_approval.py:393) 也查当前节点。 - **这是前端按钮门禁错误;没有证据显示后端允许越权审批。** 未执行任何审批写入。 - 浏览器待复现:用隔离记录建立两步审批,第一步驳回后,以第二步用户重新打开保留的 `/m/crm/contract/:id?from=approve` 详情;预期无通过/驳回。需第二个真实账号与隔离流程。 - 对照:报价详情先检查 `status=submitted`,并使用具备前置驳回判断的 `findPendingQuoteApproval`;相关纯逻辑测试本次通过。 ### CRM-A06 · P2 · 报价/合同更多筛选确定后,“全部金额”与实际金额条件不一致 - [报价 applyMoreFilters](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/quote/MpQuoteList.vue:598)、[合同 applyMoreFilters](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/MpContractList.vue:880) 总是令 `amountRange=''`,同时保留 moreDraft 的金额上下限。 - 原源码函数模拟:先选 `1-10w`,更多内上下限保持 `10000/100000`,直接确定;实际 `minAmount=10000,maxAmount=100000,amountRange=''`。空 range 对应快捷栏“全部金额”,但查询继续限制金额;“更多·已筛”虽会出现,快捷栏仍误导。 - 浏览器待复现:报价管理或合同管理选“1–10万”→更多→不改金额直接确定,检查快捷栏文案及结果集。审批入口复用同组件同样受影响。 - 建议:已知区间保留对应标签,自定义区间显示“自定义金额”,不要把生效金额条件标成全部。 ### CRM-A07 · P2 · 合同详情遗漏产品税费与 Web 的合计总金额 - Web [产品税费列](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractDetailDialog.vue:259) 展示 `taxAmount`;移动[产品明细](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue:177) 有税率、含税单价、含税总价,但没有税费行。API 可读字段 [taxAmount](/Users/zlwu/work/00code/shanxia2.0/backend/app/crud/crm/contract_product.py:46) 已存在。 - Web [合计总金额](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractDetailDialog.vue:1626) 明确为 `amount + freight + barrelDeposit`。移动[金额区](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/contract/detail.vue:87) 仅分别展示三项及 `amountWithTax`,没有该合计。`amountWithTax` 是独立接口字段,不能视为前述求和展示的等价替代。 - 只报已确认的字段遗漏;本轮没有据此断言已保存金额计算错误。执行数量、回款/开票、调价函等涉及原迁移删减边界的栏目未直接作为本条缺陷扩大范围。 - 浏览器待复现:同一非框架合同在 Web 查看和移动详情只读对照,选择含非零税费、运费、押金的数据,核对税费与总计可见性。 ### CRM-A08 · P2 · 客户迁移要求的排序没有操作入口 - [迁移清单](/Users/zlwu/work/00code/shanxia2.0/docs/superpowers/specs/2026-10-17-mp-webview-crm-customer-design.md:11) 包含排序,10-18 列表定稿仅明确删“显示字段”,未删排序。 - 当前移动列表与筛选组件没有排序选择;[syncFromProps](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerFilters.vue:350) 还显式删除 `sortField`/`sortOrder`;[buildQuery](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/CustomerFilters.vue:563) 不回传排序。 - 浏览器待复现:客户列表及更多筛选查找按创建时间/最后跟进等排序的入口,与 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](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/product-form-dialog/index.vue:21) 页面分支仍展示 `el-input`、`el-tree-select`;合同 [ContractFormDialog.vue](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue:1) 为全屏 `el-dialog` 并保留桌面选择器 [735 行](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractFormDialog.vue:735)。与当前 H5 补遗的 Vant/底部弹层方向有差距;旧迁移允许复用表单,不把这单独当成真实业务不可用。 3. **客户详情 Tab**:仍采用互斥 `van-tab` 内容,带 swipeable,[detail.vue](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/m/crm/customer/detail.vue:10);产品/报价/合同为同页锚点。客户 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/` 执行: ```sh 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](/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/mp/MpAttachmentGrid.vue:2) 使用 `class="mp-crm-att-grid"`。同测试还期待旧 `att-doc-thumb` 与 `att-add mp-wv-touch-target`,当前组件/SCSS 已有新结构。将其登记为 **测试契约陈旧(P3,CRM-T01)**;未改测试以制造通过,未据此宣称附件 UI 有缺陷。真实附件大小、删除命中区及长文件名仍待 CUA。 额外内存模拟使用 `typescript` AST 从当前 `.vue` 的 `