24 KiB
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 样式补遗:输入即查、选择器即时模糊匹配、Vant 页面控件、详情锚点、详情字段/金额口径与 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 验证项,未宣称视觉失败或通过:
- 短字段两列密度:客户资料/表单
CustomerFields、产品/报价/合同详情多为逐行 KV;合同表单页面样式明确grid-template-columns:1fr。日期/数量/税率/状态等可作为两列优化候选,但不得把 Web 弹窗“两列”要求机械套成全部移动字段必须两列。需在 375/414 宽长值、空值、错误提示下验证可读性。 - 产品/合同表单还保留 Element 控件:产品共享表单 index.vue 页面分支仍展示
el-input、el-tree-select;合同 ContractFormDialog.vue 为全屏el-dialog并保留桌面选择器 735 行。与当前 H5 补遗的 Vant/底部弹层方向有差距;旧迁移允许复用表单,不把这单独当成真实业务不可用。 - 客户详情 Tab:仍采用互斥
van-tab内容,带 swipeable,detail.vue;产品/报价/合同为同页锚点。客户 10-18 专项设计明确按资料/跟进/联系人等业务 Tab 改造,而通用补遗要求详情锚点;这是需结合最新产品意图确认的规范差异,不将业务 Tab 的存在直接判为缺陷。 - 返回恢复:列表查询多放组件内 ref,详情返回常
router.replace(listPath);需核验条件、展开项和滚动位置是否合理恢复。未检查 router 缓存全链路前不下丢状态定论。 - 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/服务端验证的优先序
- 六入口断续输入搜索及“更多”连续填条件,确认 A03 实际焦点/弹层影响。
- 同一合同详情的产品锚点与 Web 税费/总金额对照(A01/A07),只读可完成。
- 报价客户/商机即时搜索与超过 30 条匹配、报价/合同金额筛选回显(A04/A06),只读可完成。
- 隔离环境下产品选择分页失败重试(A02),禁止通过真实库批量造数据替代前置条件确认。
- 双用户隔离合同审批记录验证 A05;再验证合同批量混合状态、成功/失败/跳过计数和状态刷新。
- 375/414 宽产品/合同新增页、选择器、详情短字段密度、附件、底栏;微信软键盘/文件选择/桥接另行真机验证。
结论边界:本子任务完成代码对照和上述非浏览器验证;尚未覆盖真实权限隔离、真实保存/上传/审批/删除、微信原生行为、实际像素布局及全字段往返保存。不能据纯函数通过或静态可定位,宣称六入口验收通过。