# 运输移动端只读代码审计 日期:2026-10-21。审计版本:`97cbba3b0` 工作区。范围:`/m/pl/plan`、`/m/pl/plan-cert` 及其直接表单、选择器、附件和共享业务逻辑。本报告是八页测试任务的子报告,不替代主报告的真实 UI 证据。 **结论:存在已由纯逻辑复现的搜索竞态与失败分页漏页;运输表单移动化和详情字段对齐仍有缺口。危化证核心查询、状态门禁、上传载荷及后端状态保护的 22 项既有逻辑/入口检查通过,但不能据此判定业务验收通过。** 本子任务没有浏览器操作、接口登录、业务 API 请求、数据库连接/写入或对象存储上传,没有创建业务数据。 ## 1. 环境与审计依据 ### 1.1 环境隔离结论 | 对象 | 只读核查结论 | 配置来源 | | --- | --- | --- | | 当前本机后端 | 8000 端口存在 Python 监听进程,工作目录为本仓库 `backend`,命令指向 `app.main:app`;进程环境未观察到 `ENV_FILE`、`DATABASE_URL`、`MINIO_ENDPOINT`、`MINIO_BUCKET` 显式覆盖 | `lsof` + 对该进程 `ps eww` 的内存内白名单提取,未输出完整命令或环境 | | 默认业务数据库 | 非回环公网端点,属于远程资源,**不是本地隔离数据库**;与生产文件的数据库配置不同 | `backend/.env:19`;`backend/app/core/config.py:204–206` 默认读取此文件 | | 默认 MinIO | 非回环公网端点,**不是本地隔离对象存储**;端点和 bucket 与生产文件不同 | `backend/.env:39`;同一配置加载机制 | | 生产文件 | 数据库、MinIO 同样为远程端点;不能因默认文件不同就推断默认环境是本次专用、无人共享的测试环境 | `backend/.env.production:19`、`:39`;生产启动脚本 `backend/start-prod.sh:19` | 没有输出密码、token、完整连接串或存储密钥。上述证据支持“当前默认配置指向远程资源、未证明专用隔离”,不证明该远程资源只有本测试账号使用。没有发起数据库探测查询或 MinIO 请求。真实新增、安排、齐备、上传等写闭环应继续标为待验证。 ### 1.2 规范优先级与图谱 - 已阅读根 `AGENTS.md`;没有发现目标目录下覆盖它的 AGENTS 文件。 - 已阅读根、前端、后端 `graphify-out/GRAPH_REPORT.md` 的社区/核心节点;三个 `wiki/index.md` 路径均不存在。前端图谱核心节点包括 `@/api/pl`、`pl-plan-form-dialog`;根图谱包括 `get_tenant_no`、`DbUserContext`。图谱构建提交早于本次工作区,最终定位按当前源码核实。 - 移动视觉以 `docs/miniprogram-ui-spec-sanqiang.md` 第 12 节、尤其 12.7/12.8 及对应 OpenSpec 补遗为准;`/m/**` 使用三墙色板,不能按桌面 Fresh Mint 颜色规则误报。 - 运输范围:`docs/superpowers/specs/2026-09-14-mp-webview-transport-plan-design.md` 的 D2/D9,及 `2026-09-15-mp-webview-pl-plan-mobile-redesign-design.md` 的 D3–D13。 - 危化证范围:`docs/superpowers/specs/2026-10-17-mp-webview-pl-plan-cert-design.md`;其旧 expense-card 视觉被最新扁平卡规范取代,业务范围仍有效。 - 历史记录 `2026-10-17-mp-pl-plan-cert-delivery.md` 只作为线索;本次没有把其历史模拟浏览器通过结果计入当前通过数。 ## 2. Web / 移动能力矩阵 “代码具备”表示有实现,不等于真实服务、微信或权限验收通过。 | 能力 | Web 基准 / 移动应保留 | 当前移动代码 | 判定与证据 | | --- | --- | --- | --- | | 运输列表搜索、状态、日期 | 保留 `PL_PLAN_LIST_TABS`、日期区间、关键词 | 有,300ms 输入查询、滚动分页 | 代码具备;竞态已逻辑复现,见 T01 | | 运输列表复杂列筛选/导出/列设置 | 09-15 D13 明确不含列设置、导出、强制改状态 | 未做桌面全部列筛选 | 不把明确删减报成缺陷;现有更多筛选仍应真实生效 | | 新增、草稿、编辑、安排/调整 | 同一业务表单和状态规则 | `form.vue:5` 复用 `PlPlanFormDialog variant=page`;同一 `handleSubmit` | 核心入口具备;移动交互与加载失败状态见 T03/T07 | | 创建成功但安排失败 | 保留已创建 ID,不能重复创建 | `pl-plan-form-dialog/index.vue:2732–2744` 发送 `{planId, arranged:false}` 并退出创建态 | 静态保护存在;真实部分成功闭环待测 | | 复制、等通知、取消等通知、作废 | 保留;归档/作废等状态约束同 Web | 使用 `pl-plan-action-guards.ts` 和 `mp-pl-plan-detail-actions.ts` | 代码具备;未实测多角色及真实状态变化 | | 三方人车快速创建 | 明确保留 | `plan/paste.vue` 全屏包装;表单另有快捷入口 | 入口具备;嵌套快捷入口仍是桌面组件,见 T03 | | 打印预览、存图 | 09-14 D2/D9、09-15 D12 明确保留;不要求手机系统打印机 | 当前无 `print.vue`、无 print 路由、操作类型中无 print | 静态范围缺口 T04;本次未找到后续明确取消打印的依据 | | 运输详情基础、运输、多送货段 | 应与 Web 查看字段一致 | 有独立详情和多段切换 | 仅部分对齐,见 T05 | | 运费 | 编辑明确不在本期;**只读字段仍应一致** | 详情只显示有无运费、金额、备注 | 运费编辑不报缺陷;只读字段缺漏报 T05 | | 详情锚点 | 长页分区定位;当前测试计划还要求滚动高亮同步 | 运输详情有 scroll 监听;危化证只有点击定位 | 危化证滚动高亮静态缺口 T06;运输表单没有锚点导航,见 T03 | | 危化证五 Tab、默认未上传 | 必须含危化产品;未上传需附件缺口且未齐备;默认排除已取消 | `model.buildQuery`、默认状态及 Tab 冲突优先级 | 8 项 model 测试中的相关断言通过 | | 危化证完整筛选、动态候选、排序 | 复用 Web 列筛定义;仅去掉导出 | `CertFilters.vue`、Web `columnFilterDefs.ts`,服务端候选接口 | 代码具备;实际结果集合、候选覆盖与 UI 待测 | | 危化证批量齐备/取消 | 重取最新状态,跳过不适用项,报告成功/失败/跳过 | `index.vue:395–443` 分别计数;逐个 GET 后筛选再提交 | 静态具备;不同角色和混合状态的真实批量闭环待测 | | 上传/齐备前端权限 | `pl:plan-cert:upload` / `pl:plan-cert:ready`,权限失败关闭入口 | `index.vue:446–460` 默认 false,失败保留 false;详情同样检查 | 静态具备;服务端既有权限缺口单列,不称已修复 | | 已取消/已归档不可修改证书 | Web/服务端统一限制 | `model.canEdit`;后端 `update_plan_product_certs` 已拒绝 | 4 项后端状态/保存替身测试通过 | | 危化证保存 | 按计划产品 ID 提交三个 URL;允许先齐备后补附件 | `CertUpload.vue:99–140` 重取详情、上传中保护、保存失败保留内容;`model.certPayload` 白名单 | 逻辑/静态具备;空槽位不阻止保存是明确规则,不是遗漏必填 | | 分段与证书关联 | 证书挂计划产品,段产品通过 `planProductId` 引用;不同合同同产品要区分 | `CertDetail.vue:445–465` 共享段显示映射;`CertStop.vue:110–130` 元数据/反馈按 ID;上传按 `p.id` | 有正确 ID 关联路径;多段同产品不同证书的真实保存回显待测,未证实串证 | | 附件预览/下载/移除 | 图片、PDF 等既有 WebView 支持格式;72×72 宫格 | `CertAttachments` 复用 `AttachmentUploadField`,后者按 `/m` 路由切宫格 | 静态具备;图片旋转/缩放工具触达见 T08;微信 PDF/下载待真机 | | 危化证导出 | 唯一明确删除的业务能力 | 无导出 | 明确删减,不报缺陷 | ## 3. 选择器与两列布局清单 | 选择器/布局 | 数据与过滤方式 | 查全/交互结论 | | --- | --- | --- | | 装/送客户下拉 | `pl-plan-load-section/index.vue:331–359`,初始 `limit:20`,关键词服务端 `limit:50`;送货侧同类实现 | 不是只过滤首批内存;超过 50 条同关键词结果无下拉分页。可进一步缩小关键词,或进入完整客户选择器,但后者当前是桌面弹窗 | | 完整客户选择 | `CustomerSelector.vue:2` 桌面 `el-dialog width=1500px` + 表格;`:277–302` 服务端分页20/关键词 | 有全量分页能力,移动主交互形态不符合规范;父表单没有移动分支 | | 司机/押运员 | `usePlRemoteOptions.ts:155–185` 服务端姓名/手机号 `q`,第一页最多100;父表单按启用状态及人员类型过滤,车辆未选时禁用 | 不是首批本地过滤;无下一页 UI。应使用不在初始100条内的唯一姓名/手机验证检索;普通/跨归属账号待测 | | 车辆/承运商库 | 专用 Sheet,服务端 `q`、每页20;车辆分三方/山峡 | 查询覆盖服务端;存在竞态、分页漏页 T01/T02;仍用自制 `MpBottomSheet`,未统一 Vant | | 装/卸地址库 | `MpTransportAddressSelectSheet.vue:91` 服务端 `q`、`status:'1'`、每页20 | 过滤名称/地址/联系人有查询入口;失败漏页与竞态已复现 | | 合同 | `MpContractSelectSheet.vue:147` 客户限定、排除 invalid、每页20;默认 approved/executing 可选 | 有服务端搜索与分页;竞态/漏页已复现;客户切换后候选归属待 UI 验证 | | 内部客户产品 | `MpProductSelectSheet.vue:229` 完整目录服务端检索,`matchModel:true`、`matchProductNo:false`、`status:'on'`、每页20 | 可查名称/拼音/型号(接口实现仍需真实结果验证);编号明确不参与该查询,placeholder 也没承诺编号;竞态/漏页已复现 | | 非内部客户合同产品行 | `pl-plan-form-dialog/index.vue:910–937` 汇总全部合同明细,打开桌面 `el-dialog`/`el-table` | 不是普通产品 Sheet;没有关键词控件,长合同明细定位困难,是当前可达交互缺口 | | 产品 Sheet 的受限模式 | `MpProductSelectSheet.vue:198–225` 全部允许 ID 逐条读取,再本地匹配名称/型号 | “拼音首字母”placeholder 与该函数不一致,纯逻辑已复现;但当前运输表单有合同走专用行选择器、无合同时允许 ID 为空,**未证明非空受限模式在本路径可达,作为共享组件测试债务,不计新增可达业务缺陷** | | 运输表单短字段两列 | `pl-plan-form-dialog/index.vue:2774–2810` 创建/计划日期、公司/类型、时间/联系方式;`:2851` 司机/押运员两列 | 已实现,不能笼统报“所有字段单列”。375/414 下长手机号、司机名、日期是否截断尚未测 | | 两类详情的短字段 | 运输详情逐行 KV;危化证基础逐个 `adv-field`,段产品逐行 `cert-kv` | 可将单位/包装、计划量/实绩量、状态/类型等成对布局;地址、备注、合同长标识保留通栏。这是 P3 密度建议,尚无实测滚动距离 | ## 4. 已复现逻辑缺陷与静态缺口 ### T01 — P2:旧请求覆盖最新搜索/筛选结果 - 证据等级:**实际源码函数的纯逻辑复现**;未做浏览器复现。 - 运输列表 `web-admin/src/views/m/pl/plan/index.vue:527–550` 没有请求序号/取消/结果条件比对;旧请求后返回会覆盖 `plans` 和 `total`。 - 同类函数:`MpVehicleLibrarySelectSheet.vue:127`、`MpCarrierLibrarySelectSheet.vue:93`、`MpTransportAddressSelectSheet.vue:91`、`MpContractSelectSheet.vue:147`、`MpProductSelectSheet.vue:229`(完整路径均位于 `web-admin/src/components/mp/`)。 - 受控步骤:依次发 old、new 搜索;先返回 new,再返回 old;最终关键词是 new,列表却为 old。6个目标均复现。车辆切换 Tab 也依赖同一无保护函数。 - UI 复现:在隔离网络替身/可控弱网下输入 A,待请求发出后改 B,让 A 晚返回;核对标题输入与候选集合。仅快速输入而未控制返回顺序不能保证触发。 - 对照:危化证列表 `index.vue:261–287` 已用 `requestVersion`,此问题不能套报到危化证主列表。 ### T02 — P2:选择器加载更多失败后跳过失败页 - 证据等级:**实际源码函数的纯逻辑复现**。 - 五个 Sheet 的 `loadMore` 在请求前递增页码,失败不回退;位置依次为车辆`:163`、承运商`:121`、地址`:120`、合同`:178`、产品`:266`。 - 受控步骤:保留第1页;加载第2页返回错误;再次加载更多;实际请求第3页,漏第2页。5个目标均复现。 - 运输主列表 `plan/index.vue:439–445` 同样提前加页,`:527–550` 无回退,列为同机制静态候选,未在本次单独模拟滚动观察器。 - UI 复现:候选大于40条,失败仅注入第2页,再点加载更多/继续滚动;对照第21–40条是否遗漏,确认错误之后是否有可见重试入口。 - 对照:危化证主列表先计算 `nextPage`,成功后才赋值 `page`(`:264`、`:283`),有错误/重试状态。 ### T03 — P2:运输表单仍混用桌面弹窗和过小控件,缺少分区锚点 - 证据等级:**静态确定实现差异**;溢出、触达和键盘影响待 UI 测量。 - `plan/form.vue:5–16` 使用 page 包装,但共享表单 `pl-plan-form-dialog/index.vue:24–174` 仍为 Element Plus 表单、日期和司机/押运下拉;`:363` 客户选择器无移动分支;`:411` 合同产品行仍为宽桌面表格弹窗;`:405` 三方快速建档未传 page variant。 - 客户选择器自身 `CustomerSelector.vue:2–5` 指定1500px宽;不能仅凭此断言最终 DOM 宽度,需 CUA 确认全局样式是否收敛。 - 表单只有 `sec-basic/sec-transport/sec-load/sec-remark` section ID,没有分区导航/scrollIntoView;不满足09-15确认的“长页+分区锚点”。 - `pl-plan-form-dialog/index.vue:2930–2947` 把输入最小高设34px、按钮28px、小按钮26px;与44px触达目标冲突,需检查最终命中区而非只看可见字高。 - UI 复现:375/414打开新增/编辑→客户放大选择→合同产品行→司机→三方快速建档;记录是否可单手检索、是否横滚、底栏/关闭被遮挡、软键盘后能否确认;不要保存共享业务记录。 ### T04 — P2:确认范围内的打印预览入口缺失 - 证据等级:**静态范围差异**。 - 当前 `web-admin/src/router/routes/staticRoutes.ts:145–179` 只有列表、新增、paste、编辑、安排、详情;`views/m/pl/plan/` 无 print 文件。 - `web-admin/src/utils/mp-pl-plan-detail-actions.ts:1–10` 无 print 类型;列表更多 `plan/index.vue:464–477` 和详情均无打印入口。 - Web 保留 `web-admin/src/views/pl/plan/index.vue:939` 打印菜单和`:988–990` 三类单据组件。 - 09-14/09-15范围明确保留预览与存图;未找到后续明确取消依据。若存在未入库的后续产品决策,应把此条改为范围变更,不能仅按当前实现视作已删减。 - UI 复现:同一可打印计划,在 Web 查看打印选项;移动列表更多/详情更多均查找。无需实际打印或写数据。 ### T05 — P2:运输详情缺少应保留的实际数量、运费和回传信息 - 证据等级:**静态字段差异**;真实记录对照待测。 - `plan/detail.vue:133–194` 装/送产品只显示单位、包装、计划量及送货方式,没有入/出库实际量;Web `pl-plan-view-tabs/index.vue:117–124` 使用 `show-stop-actual-qty`。 - 移动 `plan/detail.vue:206–212` 只有3个运费字段,Web `pl-plan-view-tabs/index.vue:1544–1585` 还含公里数、计算方式、运输量、单价、是否开票。 - 移动 `plan/detail.vue:471–482` 反馈只收集现场备注;Web `pl-plan-view-tabs/index.vue:132–141` 按段展示司机/现场/地磅反馈,能触达对应附件。移动当前无对应完整反馈/附件面板。 - 移动合同只是拼接文本(`:426–434`),Web 查看面板有合同组件;是否所有关联查看都应保留按最新全量对齐范围复核。 - UI 复现:只读选择已安排且含实绩、计重运费、司机/现场附件的计划;同账号 Web/移动对照,不用“暂无数据”的样本判断字段完整性。 ### T06 — P3:危化证详情手动滚动后 Tab 高亮不随当前分区变化 - 证据等级:**静态逻辑缺口**,未称浏览器已复现。 - `plan-cert/CertDetail.vue:471–489` 只有点击 `scrollIntoView`、`activeTab=base`、resize监听;没有 scroll/IntersectionObserver 更新活动分区。 - 对照 `plan/detail.vue:659–696` 已实现滚动高亮及点击滚动锁。 - UI 复现:打开危化证详情,先不点Tab,手动滚至证书与附件区;检查是否仍高亮基础信息。再点击Tab检查固定头偏移、末节可达性和返回位置。 ### T07 — P1候选:编辑详情读取失败后仍展示空表单和提交入口 - 证据等级:**解锁行为已纯逻辑复现;未做任何保存,数据影响尚未证实**。 - `pl-plan-form-dialog/index.vue:2225–2231` 先 resetForm 再 GET 现有计划;GET失败会抛出。`:2196–2209` 无错误态,finally 将 detailLoading 设false。 - 模板`:16`、`:317` 仅按 detailLoading 显示表单/底栏;`showSaveButton` 在`:770–773` 主要看运输状态,未检查详情是否加载成功。 - 受控调用真实 `handleOpened`,令读取失败,确认最终 detailLoading=false。是否因其他校验挡住保存、哪些字段可被覆盖,仍需隔离测试,不能报告已发生数据丢失。 - UI 复现:用网络替身使“编辑现有计划”的GET失败;检查是否展示错误态与重试,或变为空表单并保留保存/安排按钮。仅在完全隔离替身中点击提交检查调用,禁止向远程共享记录保存。 ### T08 — P2:附件图片预览工具的移动命中区小于规范 - 证据等级:**静态尺寸证据**;实际触控区域待 UI 测量。 - `components/business/attachment-upload-field/index.vue:1072–1079` 的 `.preview-toolbar--mp .el-button` 显式 `height/min-height:28px !important`,适用于 `/m/**` 预览旋转、缩放等按钮。 - 上传宫格自身有移动分支与下载/删除入口,不能把整个附件组件误报成140×96桌面卡。缺口集中在图片预览工具条。 - UI 复现:危化证详情打开现有图片,375/414下测旋转/放大/缩小/重置命中区及是否与关闭按钮重叠;PDF需另测加载失败、下载与微信内核支持。 ## 5. 已有明确延期事项与未证实问题 - `docs/superpowers/specs/2026-10-17-mp-pl-plan-cert-existing-issues.md` 第3项记录:证书上传/齐备后端未接对应按钮权限函数,并注明当时用户要求忽略。本次复核 `backend/app/api/pl/plan.py:2934`、`:2966` 仍主要检查登录、租户和数据范围;`pl_plan_cert_perm.py:18/22` 函数没有路由调用。不重定义为此次移动迁移新回归,不实施越权请求,也不声称真实权限已通过。 - 已取消计划禁止证书写入的历史问题已修复,当前后端替身测试通过;不重复报旧缺陷。 - 证书按计划产品归属,不能机械要求每个送货段各存一份证书。本次未发现确定的串证代码证据;必须用“同一 CRM 产品、不同计划产品ID/包装/合同、两段不同附件”样本核对选择、保存载荷、重进以及 Web 回显。 - `stopProductMeta` 优先精确 `planProductId`,无此ID时才按 `productId` 兜底;后者存在同产品多行时的歧义,需要先证明数据允许丢失 planProductId 才能定缺陷。 - 暂存校验比正式创建宽松、人工齐备允许后补附件、无手机系统打印机、无运费编辑、无导出,均有设计依据,不作为缺陷。 - 产品 Sheet 受限模式拼音不匹配是共享组件逻辑问题,但本审计未确认非空受限模式在运输表单的实际可达路径;不与当前合同产品行无搜索混为一项。 ## 6. 本次执行结果 | 检查 | 方法及安全边界 | 结果 | | --- | --- | --- | | 危化证查询/状态/载荷/日期/元数据/合同补全 | `pnpm exec tsx --test src/views/m/pl/plan-cert/model.test.ts`,纯函数和传入假lookup | 8/8通过 | | 小程序菜单、H5独立路由、菜单SQL约束 | `node --test scripts/mp-pl-plan-cert-entry.test.mjs`,只读文件/AST,不执行SQL | 3/3通过 | | 安排运输产品必填 | `pnpm exec tsx --test src/utils/pl-plan-stop-product-arrange-required.test.ts`,纯函数 | 4/4通过 | | 后端证书状态、筛选SQL构造、菜单徽标 | 下方网络禁用命令;MagicMock DB、SQL编译,不连数据库 | 7/7通过;9条已有Pydantic/SQLAlchemy弃用warning | | 五个 Sheet 的搜索竞态、失败页重试 | 从当前SFC AST提取真实fetchList/loadMore,注入受控Promise;无Vue挂载、无浏览器、无API模块导入 | 10/10探针复现错误行为(不是业务通过) | | 运输主列表旧结果覆盖 | 提取真实fetchPage,倒序完成Promise | 1/1探针复现 | | 编辑加载失败解锁 | 提取真实handleOpened,读取函数抛错 | 1/1探针复现;提交副作用未测 | | 产品受限模式拼音 | 提取真实matchesQuickKeyword | 1/1探针确认名称/型号匹配但拼音不匹配;可达性限制见上 | 既有检查共 **22项通过**,额外 **13个探针**记录缺陷/候选的实际函数行为;两者分别统计。没有执行任何Playwright浏览器脚本;`test-mp-pl-plan-cert.mjs` 及历史镜像函数脚本未作为本次真实UI或实际函数覆盖使用。 后端命令(从 `backend/` 执行),显式阻断任何遗漏的真实网络及Engine连接,关闭字节码和pytest缓存写入: ```bash PYTHONDONTWRITEBYTECODE=1 venv/bin/python -B - <<'PY' import socket def blocked(*args, **kwargs): raise RuntimeError('Audit guard: real network disabled') socket.create_connection = blocked socket.socket.connect = blocked socket.socket.connect_ex = blocked from sqlalchemy.engine import Engine Engine.connect = blocked import pytest raise SystemExit(pytest.main([ 'tests/test_pl_plan_product_certs.py', 'tests/test_pl_plan_cert_list_filters.py', 'tests/test_pl_menu_badge_plan_cert.py', '-q', '-p', 'no:cacheprovider' ])) PY ``` ### 6.1 五个选择器的可重复源码探针 从 `web-admin/` 执行。仅读取SFC、使用本地TypeScript转译及Node VM,不加载整个应用或网络API;断言目标是“复现当前缺陷”,后续修复后这些旧行为断言应失败,不能作为产品正确性的验收测试。 ```js const fs = require('node:fs'), vm = require('node:vm') const ts = require('typescript'), assert = require('node:assert/strict') function extract(file, names) { const source = fs.readFileSync(file, 'utf8').match(/