日期:2026-10-21。审计版本:97cbba3b0 工作区。范围:/m/pl/plan、/m/pl/plan-cert 及其直接表单、选择器、附件和共享业务逻辑。本报告是八页测试任务的子报告,不替代主报告的真实 UI 证据。
结论:存在已由纯逻辑复现的搜索竞态与失败分页漏页;运输表单移动化和详情字段对齐仍有缺口。危化证核心查询、状态门禁、上传载荷及后端状态保护的 22 项既有逻辑/入口检查通过,但不能据此判定业务验收通过。 本子任务没有浏览器操作、接口登录、业务 API 请求、数据库连接/写入或对象存储上传,没有创建业务数据。
| 对象 | 只读核查结论 | 配置来源 |
|---|---|---|
| 当前本机后端 | 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 请求。真实新增、安排、齐备、上传等写闭环应继续标为待验证。
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 只作为线索;本次没有把其历史模拟浏览器通过结果计入当前通过数。“代码具备”表示有实现,不等于真实服务、微信或权限验收通过。
| 能力 | 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/下载待真机 |
| 危化证导出 | 唯一明确删除的业务能力 | 无导出 | 明确删减,不报缺陷 |
| 选择器/布局 | 数据与过滤方式 | 查全/交互结论 |
|---|---|---|
| 装/送客户下拉 | 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 密度建议,尚无实测滚动距离 |
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/)。index.vue:261–287 已用 requestVersion,此问题不能套报到危化证主列表。loadMore 在请求前递增页码,失败不回退;位置依次为车辆:163、承运商:121、地址:120、合同:178、产品:266。plan/index.vue:439–445 同样提前加页,:527–550 无回退,列为同机制静态候选,未在本次单独模拟滚动观察器。nextPage,成功后才赋值 page(:264、:283),有错误/重试状态。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触达目标冲突,需检查最终命中区而非只看可见字高。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-admin/src/views/pl/plan/index.vue:939 打印菜单和:988–990 三类单据组件。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 查看面板有合同组件;是否所有关联查看都应保留按最新全量对齐范围复核。plan-cert/CertDetail.vue:471–489 只有点击 scrollIntoView、activeTab=base、resize监听;没有 scroll/IntersectionObserver 更新活动分区。plan/detail.vue:659–696 已实现滚动高亮及点击滚动锁。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。是否因其他校验挡住保存、哪些字段可被覆盖,仍需隔离测试,不能报告已发生数据丢失。components/business/attachment-upload-field/index.vue:1072–1079 的 .preview-toolbar--mp .el-button 显式 height/min-height:28px !important,适用于 /m/** 预览旋转、缩放等按钮。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 函数没有路由调用。不重定义为此次移动迁移新回归,不实施越权请求,也不声称真实权限已通过。stopProductMeta 优先精确 planProductId,无此ID时才按 productId 兜底;后者存在同产品多行时的歧义,需要先证明数据允许丢失 planProductId 才能定缺陷。| 检查 | 方法及安全边界 | 结果 |
|---|---|---|
| 危化证查询/状态/载荷/日期/元数据/合同补全 | 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缓存写入:
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
从 web-admin/ 执行。仅读取SFC、使用本地TypeScript转译及Node VM,不加载整个应用或网络API;断言目标是“复现当前缺陷”,后续修复后这些旧行为断言应失败,不能作为产品正确性的验收测试。
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(/<script setup[^>]*>([\s\S]*?)<\/script>/)[1]
const ast = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
return names.map(name => {
const fn = ast.statements.find(n => ts.isFunctionDeclaration(n) && n.name.text === name)
assert(fn)
return ts.transpile(fn.getText(ast), { target: ts.ScriptTarget.ES2022 })
}).join('\n')
}
const targets = [
['MpVehicleLibrarySelectSheet', 'vehicleApi', 'page'],
['MpCarrierLibrarySelectSheet', 'carrierApi', 'page'],
['MpTransportAddressSelectSheet', 'transportAddressApi', 'page'],
['MpContractSelectSheet', 'listContract', 'pageNum'],
['MpProductSelectSheet', 'fetchProductList', 'pageNum']
]
;(async () => {
for (const [name, api, pageKey] of targets) {
let calls = []
const ctx = {
rows: { value: [] }, loading: { value: false }, total: { value: 0 },
keyword: { value: 'old' }, page: { value: 1 }, pageNum: { value: 1 }, pageSize: 20,
activeTab: { value: 'carrier' }, hasMore: { value: true }, props: {},
isRestrictedMode: { value: false }, ElMessage: { error() {} }, pickBxPageResult: r => r
}
const request = q => new Promise((resolve, reject) => calls.push({ q, resolve, reject }))
ctx[api] = api.endsWith('Api') ? { list: request } : request
vm.createContext(ctx)
vm.runInContext(extract(`src/components/mp/${name}.vue`, ['fetchList', 'loadMore']), ctx)
const old = ctx.fetchList(true)
ctx.keyword.value = 'new'
const fresh = ctx.fetchList(true)
calls[1].resolve({ list: [{ id: 'new' }], rows: [{ id: 'new' }], total: 1 })
await fresh
calls[0].resolve({ list: [{ id: 'old' }], rows: [{ id: 'old' }], total: 1 })
await old
assert.equal(ctx.rows.value[0].id, 'old')
calls = []
ctx[pageKey].value = 1
ctx.loading.value = false
ctx.rows.value = [{ id: 'page1' }]
const pending = [], original = ctx.fetchList
ctx.fetchList = (...args) => {
const p = original(...args)
p.catch(() => {})
pending.push(p)
return p
}
ctx.loadMore()
calls[0].reject(new Error('simulated network error'))
await pending[0].catch(() => {})
ctx.loadMore()
calls[1].resolve({ list: [{ id: 'page3' }], rows: [{ id: 'page3' }], total: 60 })
await pending[1]
assert.equal(calls.map(c => c.q.page ?? c.q.pageNum).join(','), '2,3')
console.log(`${name}: old-response overwrite and failed-page skip reproduced`)
}
})().catch(error => { console.error(error); process.exit(1) })
本报告未覆盖浏览器真实布局、软键盘、微信WebView票据、iOS/Android原生选文件/相册、PDF内核、剪贴板、真机下载、实际数据权限或跨租户隔离。没有截图证据。未更改业务代码、SQL、配置、测试脚本;只创建本审计文档,因此不运行图谱更新、不提交Git。