37 KiB
PDA 托盘业务操作功能 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 在不改变 FastAPI 业务契约的前提下,为 shierfeng-fastapi-app 增加可在 H5/开发环境验证的托盘入库、装填、发货、现场使用和退回操作。
Architecture: PDA 复用现有 request、JWT、传输加密和后端业务接口;API 封装按业务模块拆分,扫码输入和幂等键生成作为无业务状态的共享工具。五个页面各自维护表单、扫码会话、请求代数和服务端刷新状态,后端继续作为权限、库存、状态机、产线绑定和退回集合校验的唯一权威。
Tech Stack: uni-app Vue 3、Pinia、Tailwind CSS、现有 src/utils/request.js、Node 20 node:test、pnpm;不增加依赖。
Spec: docs/superpowers/specs/2026-09-20-pda-business-workflow-design.md
Global Constraints
- 不打包、签名、安装或发布 APK;只运行 PDA 的 H5/开发构建。
- 不调用摄像头扫码,不新增原生扫码 SDK;扫码输入必须兼容 HID 键盘逐字符输入后回车提交,并保留手工输入。
- 不修改现有 FastAPI 业务接口的请求/响应契约,不在 PDA 前端计算最终库存或托盘状态。
- 基础资料维护和六类报表继续使用 Web 后台,不复制到 PDA。
- 不把管理员密码、Token、Cookie 或其他凭据写入源码、测试固定文本或日志;登录表单不得预填密码。
- 不修改
.env.dev、.env.prod、前端环境文件、用户已有 APP 改动、后端日志和无关缓存。 - 不安装、升级或删除依赖;使用仓库已有
node_modules、Node 20 和 pnpm 配置。 - 所有业务写请求必须使用后端要求的 camelCase 字段和幂等键;网络失败后直接重试复用原键,改变业务输入后才生成新键。
- 页面入口只按当前用户权限显示;页面内按钮权限与后端 401/403 校验同时保留,前端隐藏不能替代后端权限。
- 每个提交只包含本任务明确列出的文件;暂存和提交使用显式路径,禁止
git add -A、git clean、重置或覆盖用户已有改动。 - 每个任务完成后使用中文提交说明,并用
git commit --only保护工作区中既有暂存内容。
文件结构与责任边界
新建文件
shierfeng-fastapi-app/src/api/business/inbound.js:首次入库列表、选项、扫码、详情、草稿增改删和确认请求。shierfeng-fastapi-app/src/api/business/filling.js:装填列表、菌种选项、扫码、详情和装填提交请求。shierfeng-fastapi-app/src/api/business/shipment.js:发货列表、仓库/现场选项、扫码、详情、草稿增改删和确认请求。shierfeng-fastapi-app/src/api/business/usage.js:使用列表、产线选项、扫码、详情和使用提交请求。shierfeng-fastapi-app/src/api/business/return.js:退回列表、仓库选项、扫码、申请、详情和确认请求。shierfeng-fastapi-app/src/utils/palletScan.js:扫码值规范化、短时重复判断和 Web Crypto UUID v4 幂等键生成;不保存业务状态。shierfeng-fastapi-app/src/components/business/PalletScanInput.vue:HID/手工输入、回车提交、空值校验、短时重复提示和输入焦点管理。shierfeng-fastapi-app/src/pages/business/inbound/index.vue:入库草稿编辑、扫码、删除和确认。shierfeng-fastapi-app/src/pages/business/filling/index.vue:单托盘装填。shierfeng-fastapi-app/src/pages/business/shipment/index.vue:发货草稿编辑、扫码、删除和确认。shierfeng-fastapi-app/src/pages/business/usage/index.vue:现场使用、产线选择和服务端结果展示。shierfeng-fastapi-app/src/pages/business/return/index.vue:退回申请和仓库确认两个独立操作区。shierfeng-fastapi-app/tests/business-contract.test.mjs:Node 20 静态 API/页面/字段/安全契约与扫码工具测试。
修改文件
shierfeng-fastapi-app/package.json:增加不依赖新包的test:business脚本。shierfeng-fastapi-app/src/pages.json:注册五个业务页面路由。shierfeng-fastapi-app/src/pages/login.vue:移除旧默认密码预填值,保留现有认证流程。shierfeng-fastapi-app/src/pages/work/index.vue:增加按权限过滤的五类 PDA 业务入口。
明确不修改
shierfeng-fastapi-backend/module_business/**:后端接口已存在,本计划只消费其当前契约。shierfeng-fastapi-frontend/**:Web 端作为接口和交互参考,不复制改动。shierfeng-fastapi-app/src/utils/request.js、传输加密、Pinia 用户会话和环境配置:除非测试证明共享层存在直接阻塞,否则保持现状。
已核实的后端接口与字段
| 模块 | 查询/选项/扫码 | 写请求 | 必须保持的写入字段 |
|---|---|---|---|
| 入库 | GET /business/inbound/list、/warehouse-options、/scan/{qrToken}?warehouseId=、/{inboundId} |
POST /business/inbound、PUT /business/inbound、DELETE /business/inbound/{ids}、PUT /business/inbound/{id}/confirm |
草稿:warehouseId,palletIds;确认:idempotencyKey |
| 装填 | GET /business/filling/list、/strain-options、/scan/{qrToken}、/{fillingId} |
POST /business/filling |
qrToken,warehouseId,strainId,batchNo,packageSpec,initialPackages,idempotencyKey |
| 发货 | GET /business/shipment/list、/warehouse-options、/site-options、/scan/{qrToken}?sourceWarehouseId=&targetSiteId=、/{shipmentId} |
POST /business/shipment、PUT /business/shipment、DELETE /business/shipment/{ids}、PUT /business/shipment/{id}/confirm |
草稿:sourceWarehouseId,targetSiteId,palletIds;确认:idempotencyKey |
| 使用 | GET /business/usage/list、/line-options?siteId=、/scan/{qrToken}、/{usageId} |
POST /business/usage |
qrToken,lineId,usedPackages,idempotencyKey |
| 退回 | GET /business/return/list、/warehouse-options、/scan/{qrToken}?mode=&returnId=、/{returnId} |
POST /business/return/request、PUT /business/return/{id}/confirm |
申请:sourceSiteId,targetWarehouseId,palletIds,idempotencyKey;确认:scannedPalletIds,idempotencyKey |
后端扫码响应统一使用 data.palletId/palletCode/flowState/warehouseId/siteId/currentLineId/strainId/strainCode/strainName/batchNo/packageSpec/capacityPackages/remainingPackages/allowedActions;列表响应使用 rows 和 total;详情使用 data。装填和使用写请求没有单独确认接口,成功后以列表、扫码结果或使用响应为准。
Task 1: 用测试锁定 API 封装、扫码工具和幂等键
Files:
- Create:
shierfeng-fastapi-app/tests/business-contract.test.mjs - Create:
shierfeng-fastapi-app/src/api/business/inbound.js - Create:
shierfeng-fastapi-app/src/api/business/filling.js - Create:
shierfeng-fastapi-app/src/api/business/shipment.js - Create:
shierfeng-fastapi-app/src/api/business/usage.js - Create:
shierfeng-fastapi-app/src/api/business/return.js - Create:
shierfeng-fastapi-app/src/utils/palletScan.js - Modify:
shierfeng-fastapi-app/package.json
Interfaces:
- Produces API exports
listInbound,getInboundWarehouseOptions,scanInboundPallet,getInbound,addInbound,updateInbound,delInbound,confirmInbound;其他四个模块使用已核实接口表中的同义导出名。 - Produces
normalizeScanValue(value),isDuplicateScan(value,lastValue,scannedAt,lastScannedAt,duplicateWindowMs)和createIdempotencyKey(cryptoProvider)。 - All API wrappers import
requestfrom@/utils/requestand return its promise without改写响应结构。
各模块的完整导出名固定如下,页面不得自行拼接 URL:
-
inbound.js:listInbound(query)、getInboundWarehouseOptions()、scanInboundPallet(qrToken,query)、getInbound(inboundId)、addInbound(data)、updateInbound(data)、delInbound(inboundIds)、confirmInbound(inboundId,data)。 -
filling.js:listFilling(query)、getFillingStrainOptions()、scanFillingPallet(qrToken)、getFilling(fillingId)、addFilling(data)。 -
shipment.js:listShipment(query)、getShipmentWarehouseOptions()、getShipmentSiteOptions()、scanShipmentPallet(qrToken,query)、getShipment(shipmentId)、addShipment(data)、updateShipment(data)、delShipment(shipmentIds)、confirmShipment(shipmentId,data)。 -
usage.js:listUsage(query)、getUsageLineOptions(siteId)、scanUsagePallet(qrToken)、getUsage(usageId)、addUsage(data)。 -
return.js:listReturn(query)、getReturnWarehouseOptions()、scanReturnPallet(qrToken,query)、requestReturn(data)、getReturn(returnId)、confirmReturn(returnId,data)。 -
Step 1: 先写 API 与工具的失败测试
在测试文件中用 readFile 读取五个 API 文件,用正则逐个断言 URL、HTTP 方法和关键参数名称;同时为工具写可执行单元测试:
test('扫码值只去除首尾空白并拒绝空值', async () => {
const { normalizeScanValue } = await import('../src/utils/palletScan.js')
assert.equal(normalizeScanValue(' QR-001\n'), 'QR-001')
assert.throws(() => normalizeScanValue(' \n\t '), /二维码不能为空/)
})
test('幂等键使用 randomUUID 或安全随机字节,不使用时间和伪随机数', async () => {
const { createIdempotencyKey } = await import('../src/utils/palletScan.js')
const expected = '12345678-1234-4123-8123-123456789abc'
assert.equal(createIdempotencyKey({ randomUUID: () => expected }), expected)
assert.equal(
createIdempotencyKey({
getRandomValues(bytes) {
bytes.fill(0)
return bytes
}
}),
'00000000-0000-4000-8000-000000000000'
)
})
test('同一值只在重复时间窗口内被识别', async () => {
const { isDuplicateScan } = await import('../src/utils/palletScan.js')
assert.equal(isDuplicateScan('QR-001', 'QR-001', 1200, 1000, 500), true)
assert.equal(isDuplicateScan('QR-001', 'QR-002', 1200, 1000, 500), false)
assert.equal(isDuplicateScan('QR-001', 'QR-001', 1801, 1000, 500), false)
})
API 契约测试至少覆盖以下请求:入库和发货的列表、选项、扫码、详情、草稿增改删、确认;装填和使用的列表、选项、扫码、详情、提交;退回的列表、选项、扫码、申请、详情、确认。测试还要断言确认/写请求的函数名和 data/params 位置,防止把 query 字段误放入 body。
- Step 2: 运行失败测试确认缺口
工作目录:shierfeng-fastapi-app
node --test tests/business-contract.test.mjs
预期:因五个 API 模块、扫码工具和测试脚本尚未存在而失败;此时不修改后端或依赖。
- Step 3: 实现最小 API 封装和测试脚本
package.json 只增加以下脚本,不增加依赖:
"test:business": "node --test tests/business-contract.test.mjs"
每个 API 文件只做路径和参数映射。例如入库模块必须保持如下接口形状:
import request from '@/utils/request'
export function listInbound(query) {
return request({ url: '/business/inbound/list', method: 'get', params: query })
}
export function scanInboundPallet(qrToken, query) {
return request({ url: '/business/inbound/scan/' + qrToken, method: 'get', params: query })
}
export function addInbound(data) {
return request({ url: '/business/inbound', method: 'post', data })
}
export function confirmInbound(inboundId, data) {
return request({
url: '/business/inbound/' + inboundId + '/confirm',
method: 'put',
data,
headers: { repeatSubmit: false }
})
}
其他函数按接口表逐一实现,不增加前端字段别名。createIdempotencyKey 优先使用 crypto.randomUUID(),否则用 crypto.getRandomValues(new Uint8Array(16)) 按 UUID v4 设置版本和变体位;缺少安全随机源时抛出明确错误,不使用 Date.now()、Math.random() 或账号信息。
- Step 4: 运行测试确认 API 与工具契约通过
pnpm test:business
预期:本任务新增的 API 路径、HTTP 方法、参数位置、扫码规范化、重复窗口和 UUID 测试通过;页面相关测试在后续任务加入。
- Step 5: 只提交本任务文件
git add -- \
shierfeng-fastapi-app/package.json \
shierfeng-fastapi-app/tests/business-contract.test.mjs \
shierfeng-fastapi-app/src/api/business/inbound.js \
shierfeng-fastapi-app/src/api/business/filling.js \
shierfeng-fastapi-app/src/api/business/shipment.js \
shierfeng-fastapi-app/src/api/business/usage.js \
shierfeng-fastapi-app/src/api/business/return.js \
shierfeng-fastapi-app/src/utils/palletScan.js
git commit --only -m '新增:PDA 业务 API 与扫码幂等工具' -- \
shierfeng-fastapi-app/package.json \
shierfeng-fastapi-app/tests/business-contract.test.mjs \
shierfeng-fastapi-app/src/api/business/inbound.js \
shierfeng-fastapi-app/src/api/business/filling.js \
shierfeng-fastapi-app/src/api/business/shipment.js \
shierfeng-fastapi-app/src/api/business/usage.js \
shierfeng-fastapi-app/src/api/business/return.js \
shierfeng-fastapi-app/src/utils/palletScan.js
Task 2: 先测试再实现 HID 扫码输入组件
Files:
- Create:
shierfeng-fastapi-app/src/components/business/PalletScanInput.vue - Modify:
shierfeng-fastapi-app/tests/business-contract.test.mjs
Interfaces:
-
Consumes
normalizeScanValueandisDuplicateScanfrom@/utils/palletScan. -
Props:
disabled:Boolean=false、loading:Boolean=false、duplicateWindowMs:Number=500。 -
Emits:
scan(normalizedValue)、invalid(message)、duplicate(normalizedValue)。 -
Exposes no business state; parent controls request lock through
loadingand owns accepted pallet rows. -
Step 1: 写组件静态契约测试并确认 RED
在 business-contract.test.mjs 读取组件源码,断言存在 @confirm="handleSubmit"、normalizeScanValue、isDuplicateScan、三个 emit 名称、:disabled="disabled || loading" 或等价锁定逻辑,并断言没有 uni.scanCode、摄像头 API 或状态机字段判断。执行 pnpm test:business,预期组件测试失败。
- Step 2: 实现组件输入状态机
组件使用 uni-app 原生 <input>,同时绑定 @confirm(HID 回车和手工确认)与一个可点击的“提交扫描”按钮;输入框在 disabled 或 loading 时只读/禁用。提交逻辑必须保持以下顺序:
function handleSubmit() {
if (props.disabled || props.loading) return
let value
try {
value = normalizeScanValue(scanValue.value)
} catch (error) {
errorMessage.value = error.message
emit('invalid', error.message)
focusInput(true)
return
}
const scannedAt = Date.now()
if (isDuplicateScan(value, lastScanValue, scannedAt, lastScanAt, props.duplicateWindowMs)) {
scanValue.value = ''
emit('duplicate', value)
focusInput()
return
}
lastScanValue = value
lastScanAt = scannedAt
scanValue.value = ''
emit('scan', value)
focusInput()
}
Date.now()只用于比较短时重复输入,不得用于生成业务幂等键。组件自动聚焦、扫描完成后清空输入并重新聚焦;页面切换、请求进行中和权限不足时由父页面传入 disabled/loading。
- Step 3: 运行组件契约和工具测试确认 GREEN
pnpm test:business
预期:组件的 HID 回车、手工提交、空值、重复值和请求锁静态契约通过。
- Step 4: 提交组件
git add -- shierfeng-fastapi-app/src/components/business/PalletScanInput.vue shierfeng-fastapi-app/tests/business-contract.test.mjs
git commit --only -m '新增:PDA HID 扫码输入组件' -- \
shierfeng-fastapi-app/src/components/business/PalletScanInput.vue \
shierfeng-fastapi-app/tests/business-contract.test.mjs
Task 3: 注册路由、移除密码预填并增加权限入口
Files:
- Modify:
shierfeng-fastapi-app/src/pages.json - Modify:
shierfeng-fastapi-app/src/pages/login.vue - Modify:
shierfeng-fastapi-app/src/pages/work/index.vue - Modify:
shierfeng-fastapi-app/tests/business-contract.test.mjs
Interfaces:
-
Produces routes
pages/business/inbound/index、pages/business/filling/index、pages/business/shipment/index、pages/business/usage/index、pages/business/return/index。 -
Uses existing
$auth.hasPermiOr/$auth.hasPermiAndand$tab.navigateTo; does not change Pinia or permission storage. -
Step 1: 写路由、权限和凭据安全测试并确认 RED
测试必须断言五个页面路径和标题存在,工作台包含五类业务权限字符串,且 src/pages/login.vue 不再包含 admin123 或其他密码字面量。测试也要断言入口跳转路径与页面路径一致。
- Step 2: 注册五个页面并实现入口过滤
在 pages.json 按工作台之后加入五个页面,使用中文标题“仓库入库”“托盘装填”“发货出库”“现场使用”“退回仓库”。工作台增加“现场业务”区块,入口权限使用以下 OR 集合:
const businessEntries = [
{ path: '/pages/business/inbound/index', permissions: ['business:inbound:list', 'business:inbound:query', 'business:inbound:add', 'business:inbound:edit', 'business:inbound:remove', 'business:inbound:confirm'] },
{ path: '/pages/business/filling/index', permissions: ['business:filling:list', 'business:filling:query', 'business:filling:add'] },
{ path: '/pages/business/shipment/index', permissions: ['business:shipment:list', 'business:shipment:query', 'business:shipment:add', 'business:shipment:edit', 'business:shipment:remove', 'business:shipment:confirm'] },
{ path: '/pages/business/usage/index', permissions: ['business:usage:list', 'business:usage:query', 'business:usage:add'] },
{ path: '/pages/business/return/index', permissions: ['business:return:list', 'business:return:query', 'business:return:request', 'business:return:confirm'] }
]
模板只渲染 proxy.$auth.hasPermiOr(entry.permissions) 为真的入口;点击前再次检查权限并调用 $tab.navigateTo(entry.path)。页面没有任何授权时不显示业务区块。保留现有非本任务系统入口,不扩展其他模块。
- Step 3: 清除登录页密码默认值
将 loginForm 的 username 和 password 初始值改为空字符串,验证码和登录 API 流程保持不变;开发联调账号只在人工输入时使用,不写入源码、测试或日志。
- Step 4: 运行测试确认 GREEN
pnpm test:business
预期:五个路由、权限入口、登录安全静态契约通过。
- Step 5: 提交页面壳层改动
git add -- \
shierfeng-fastapi-app/src/pages.json \
shierfeng-fastapi-app/src/pages/login.vue \
shierfeng-fastapi-app/src/pages/work/index.vue \
shierfeng-fastapi-app/tests/business-contract.test.mjs
git commit --only -m '新增:PDA 业务页面路由与权限入口' -- \
shierfeng-fastapi-app/src/pages.json \
shierfeng-fastapi-app/src/pages/login.vue \
shierfeng-fastapi-app/src/pages/work/index.vue \
shierfeng-fastapi-app/tests/business-contract.test.mjs
Task 4: 实现首次入库 PDA 页面
Files:
- Create:
shierfeng-fastapi-app/src/pages/business/inbound/index.vue - Modify:
shierfeng-fastapi-app/tests/business-contract.test.mjs
Interfaces:
-
Consumes all exports from
src/api/business/inbound.js,PalletScanInput,createIdempotencyKeyand$auth. -
Produces page methods
handleInboundScan,submitInboundDraft,saveInboundUpdate,confirmInboundDraft,removeScannedPallet,resetInboundOperation。 -
Page state stores only scanned display rows, draft form values, request/loading/error state and confirm idempotency key; it never stores a client-calculated inventory state。
-
Step 1: 写入库页面契约测试并确认 RED
测试读取页面源码并断言:
- 使用
PalletScanInput,扫描调用scanInboundPallet(qrToken, { warehouseId })。 - 只接受
flowState === 'UNSTOCKED',按palletId去重。 - 草稿 payload 只含
{ warehouseId, palletIds },编辑 payload 只含{ inboundId, warehouseId, palletIds }。 - 确认调用
confirmInbound(inboundId, { idempotencyKey }),失败分支不清空当前键。 - 编辑/删除/确认按钮分别包含
business:inbound:edit/remove/confirm,入口操作需要同时具备query与对应写权限。
- Step 2: 实现仓库选项、草稿扫码和编辑状态
页面加载授权仓库选项和第一页列表;新增时选择仓库后允许连续扫描。每次扫码携带当前仓库 ID,接受服务端返回的 palletId/palletCode/flowState,重复托盘只提示不追加。切换仓库清空当前扫描集合并使旧扫码响应失效。
编辑时先调用 getInbound(inboundId),只有详情仍为 DRAFT 才填充仓库和明细;详情未返回前禁止扫码、保存和删除。列表刷新/页面 onShow 以服务端 rows 为准,不从本地草稿推导状态。
- Step 3: 实现草稿写入和确认重试
function submitInboundDraft() {
return addInbound({
warehouseId: form.value.warehouseId,
palletIds: scannedPallets.value.map((item) => item.palletId)
})
}
function confirmInboundDraft(row) {
const idempotencyKey = ensureConfirmIdempotencyKey(row.inboundId)
confirmLoading.value = true
confirmInbound(row.inboundId, { idempotencyKey })
.then(() => {
clearConfirmIdempotency()
loadInboundList()
})
.catch(() => {
confirmError.value = '入库确认失败;再次确认将复用本次幂等键'
})
.finally(() => {
confirmLoading.value = false
})
}
后端新增草稿响应不保证返回单据 ID,因此保存后只关闭编辑区并重新读取列表,由用户从服务端返回的 DRAFT 行选择确认,不按时间或本地数组猜测刚创建的单据。确认成功后清除键;网络超时、401 以外的可重试失败保留键,401 交由现有 request 会话处理。
- Step 4: 运行入库契约和构建前静态检查
pnpm test:business
预期:入库页面的字段、状态、权限、幂等键和服务端刷新契约通过。
- Step 5: 提交入库页面
git add -- shierfeng-fastapi-app/src/pages/business/inbound/index.vue shierfeng-fastapi-app/tests/business-contract.test.mjs
git commit --only -m '新增:PDA 首次入库操作页' -- \
shierfeng-fastapi-app/src/pages/business/inbound/index.vue \
shierfeng-fastapi-app/tests/business-contract.test.mjs
Task 5: 实现整托发货 PDA 页面
Files:
- Create:
shierfeng-fastapi-app/src/pages/business/shipment/index.vue - Modify:
shierfeng-fastapi-app/tests/business-contract.test.mjs
Interfaces:
-
Consumes
src/api/business/shipment.js、PalletScanInput和createIdempotencyKey。 -
Produces
handleShipmentScan,submitShipmentDraft,saveShipmentUpdate,confirmShipmentDraft,removeScannedPallet。 -
Scanned rows expose
palletCode,strainName/strainCode,batchNo,remainingPackagesas read-only display values。 -
Step 1: 写发货页面失败契约
断言页面加载仓库和现场选项,扫描调用 scanShipmentPallet(qrToken, { sourceWarehouseId, targetSiteId }),只接受 flowState === 'FILLED_IN_WAREHOUSE' && remainingPackages > 0,草稿/确认字段与接口表一致,并保留确认失败时的幂等键。
- Step 2: 实现来源仓库、目标现场和集合扫码
来源仓库或目标现场改变时清空托盘集合并使旧请求失效。扫描期间锁定输入和提交按钮;同一 palletId 不重复追加。列表、详情和选项失败显示页面内可重试错误,不吞掉后端业务消息。
- Step 3: 实现草稿和确认
function submitShipmentDraft() {
return addShipment({
sourceWarehouseId: form.value.sourceWarehouseId,
targetSiteId: form.value.targetSiteId,
palletIds: scannedPallets.value.map((item) => item.palletId)
})
}
function confirmShipmentDraft(row) {
const idempotencyKey = ensureConfirmIdempotencyKey(row.shipmentId)
return confirmShipment(row.shipmentId, { idempotencyKey })
}
保存后重新读取列表;确认前要求 DRAFT,成功后重新加载列表和页面状态。错误重试只复用同一确认键,改变单据或托盘集合时清除并重新生成键;不在前端扣减 remainingPackages。
- Step 4: 运行测试并提交
pnpm test:business
git add -- shierfeng-fastapi-app/src/pages/business/shipment/index.vue shierfeng-fastapi-app/tests/business-contract.test.mjs
git commit --only -m '新增:PDA 整托发货操作页' -- \
shierfeng-fastapi-app/src/pages/business/shipment/index.vue \
shierfeng-fastapi-app/tests/business-contract.test.mjs
预期:测试通过后才执行提交;提交只包含发货页面和契约测试增量。
Task 6: 实现单托盘装填 PDA 页面
Files:
- Create:
shierfeng-fastapi-app/src/pages/business/filling/index.vue - Modify:
shierfeng-fastapi-app/tests/business-contract.test.mjs
Interfaces:
-
Consumes
listFilling、getFillingStrainOptions、scanFillingPallet、addFilling和PalletScanInput。 -
Produces form fields
qrToken,warehouseId,strainId,batchNo,packageSpec,initialPackagesand one operation-scopedidempotencyKey。 -
Step 1: 写装填页面失败契约
断言页面只接受 flowState === 'EMPTY_IN_WAREHOUSE',扫码成功后展示仓库和容量,提交 payload 包含且只包含后端 FillingCreateModel 字段,包数校验为严格正整数,成功/失败状态不伪造库存。
- Step 2: 实现扫码和表单
扫码后保存服务端返回的 qrToken、warehouseId、palletCode、capacityPackages,加载菌种选项。菌种、批次、规格和包数在请求期间锁定;包数的前端校验只做正整数和非空检查,容量和托盘状态由后端最终判定。
- Step 3: 实现幂等提交和成功刷新
function submitFilling() {
const idempotencyKey = ensureFillingIdempotencyKey()
return addFilling({
qrToken: form.value.qrToken,
warehouseId: form.value.warehouseId,
strainId: form.value.strainId,
batchNo: form.value.batchNo.trim(),
packageSpec: form.value.packageSpec.trim(),
initialPackages: form.value.initialPackages,
idempotencyKey
})
}
提交成功后清空表单并刷新装填列表,再以扫码查询或列表结果显示服务端状态;刷新失败不能覆盖已经成功的写入提示。提交失败保留键;修改托盘、菌种、批次、规格或包数后清除键,下一次输入生成新键。
- Step 4: 运行测试并提交
pnpm test:business
git add -- shierfeng-fastapi-app/src/pages/business/filling/index.vue shierfeng-fastapi-app/tests/business-contract.test.mjs
git commit --only -m '新增:PDA 托盘装填操作页' -- \
shierfeng-fastapi-app/src/pages/business/filling/index.vue \
shierfeng-fastapi-app/tests/business-contract.test.mjs
Task 7: 实现客户现场使用 PDA 页面
Files:
- Create:
shierfeng-fastapi-app/src/pages/business/usage/index.vue - Modify:
shierfeng-fastapi-app/tests/business-contract.test.mjs
Interfaces:
-
Consumes
listUsage、getUsageLineOptions(siteId)、scanUsagePallet、addUsage和PalletScanInput。 -
Produces
handleUsageScan,loadLineOptions,submitUsage,resetUsageOperation。 -
Uses
UsageSuccessResponseModelfieldsusageId,remainingPackages,flowStatefor result display only。 -
Step 1: 写现场使用失败契约
断言页面只接受 AT_SITE_UNUSED 或 AT_SITE_PARTIAL 且 remainingPackages > 0;没有 currentLineId 时按扫码响应的 siteId 获取产线,有绑定时锁定该产线;提交使用 qrToken,lineId,usedPackages,idempotencyKey,不在本地推算新状态。
- Step 2: 实现扫码、产线锁定和正整数校验
每次新扫码先使旧产线请求失效、清空旧结果和幂等键。扫码成功后保存托盘、菌种、批次、余量和现场信息;首次使用显示可选产线,已有 currentLineId 时只能使用该产线。使用包数必须是严格正整数,超过当前展示余量时给出提示,但即使通过前端检查也仍由服务端校验。
- Step 3: 实现使用提交和结果刷新
function submitUsage() {
const idempotencyKey = ensureUsageIdempotencyKey()
return addUsage({
qrToken: form.value.qrToken,
lineId: form.value.lineId,
usedPackages: form.value.usedPackages,
idempotencyKey
}).then((response) => {
usageResult.value = response.data
clearUsageIdempotencyKey()
return loadUsageList()
})
}
网络失败时保留键并显示“再次提交将复用本次幂等键”;成功只采用 response.data.remainingPackages 和 response.data.flowState,然后清理表单或允许重新扫码。列表和扫码响应晚于新会话时必须被请求代数丢弃。
- Step 4: 运行测试并提交
pnpm test:business
git add -- shierfeng-fastapi-app/src/pages/business/usage/index.vue shierfeng-fastapi-app/tests/business-contract.test.mjs
git commit --only -m '新增:PDA 客户现场使用操作页' -- \
shierfeng-fastapi-app/src/pages/business/usage/index.vue \
shierfeng-fastapi-app/tests/business-contract.test.mjs
Task 8: 实现托盘退回申请与仓库确认
Files:
- Create:
shierfeng-fastapi-app/src/pages/business/return/index.vue - Modify:
shierfeng-fastapi-app/tests/business-contract.test.mjs
Interfaces:
-
Consumes all exports from
src/api/business/return.jsand two independentPalletScanInputinstances。 -
Request side state:
sourceSiteId,targetWarehouseId,requestScannedPallets,requestIdempotencyKey。 -
Confirm side state:
returnId,expectedPalletIds,confirmScannedPallets,confirmIdempotencyKey。 -
Produces
handleRequestScan,submitReturnRequest,loadReturnDetail,handleConfirmScan,submitReturnConfirm,sameIdSet。 -
Step 1: 写退回两阶段失败契约
断言测试覆盖:
- 申请扫码使用
{ mode: 'request' },首个托盘确定sourceSiteId,后续托盘必须属于同一现场。 - 申请 payload 为
sourceSiteId,targetWarehouseId,palletIds,idempotencyKey,申请键和确认键互不复用。 - 确认先读取详情,状态必须为
REQUESTED,扫描使用{ mode: 'confirm', returnId },扫码集合必须与详情托盘 ID 集合完全相同。 - 确认写请求前再次
getReturn(returnId),比较最新状态和 ID 集合后才调用confirmReturn。 - 请求或确认扫码/提交的旧响应不能覆盖新选择;失败不清除对应幂等键。
- Step 2: 实现申请区
加载可访问目标仓库和退回列表。申请扫码成功后保存 palletId,palletCode,siteId,remainingPackages,同一托盘不重复,目标仓库或会话变化时清空集合和申请键。requestReturn 成功后清除申请键、清空申请区并刷新列表;不根据响应消息猜测新 returnId,确认入口从服务端返回的 REQUESTED 列表选择单据。
- Step 3: 实现确认区的详情快照与整集合扫码
选择退回单后调用 getReturn,保存详情中的 items[].palletId 作为预期集合。确认扫码必须命中预期集合且不能重复;切换退回单使确认扫码代数、详情代数和确认键同时失效。实现集合比较时使用去重后的 Set,长度和成员都必须一致:
function sameIdSet(left, right) {
if (left.length !== right.length) return false
const leftSet = new Set(left)
const rightSet = new Set(right)
return leftSet.size === left.length &&
rightSet.size === right.length &&
right.every((id) => leftSet.has(id))
}
- Step 4: 实现确认前复核和幂等提交
async function submitReturnConfirm() {
const returnId = confirmForm.value.returnId
const scannedPalletIds = confirmScannedPallets.value.map((item) => item.palletId)
if (!returnId || !sameIdSet(expectedPalletIds.value, scannedPalletIds)) {
confirmError.value = '必须逐个扫描并完整匹配退回单全部托盘'
return
}
const latest = (await getReturn(returnId)).data
if (latest.status !== 'REQUESTED' ||
!sameIdSet(latest.items.map((item) => item.palletId), scannedPalletIds)) {
confirmError.value = '退回单状态或托盘明细已变化,请重新选择并扫码'
return
}
const idempotencyKey = ensureConfirmIdempotencyKey(returnId)
await confirmReturn(returnId, { scannedPalletIds, idempotencyKey })
}
确认成功后清除确认键、刷新退回列表和详情;网络失败保留键。申请确认两个阶段的输入锁、错误状态和重试键必须分开,不能用一个全局 loading 或一个幂等键混用。
- Step 5: 运行测试并提交
pnpm test:business
git add -- shierfeng-fastapi-app/src/pages/business/return/index.vue shierfeng-fastapi-app/tests/business-contract.test.mjs
git commit --only -m '新增:PDA 托盘退回两阶段操作页' -- \
shierfeng-fastapi-app/src/pages/business/return/index.vue \
shierfeng-fastapi-app/tests/business-contract.test.mjs
Task 9: 完成全量契约验证、H5 构建和交付检查
Files:
- Modify:
shierfeng-fastapi-app/tests/business-contract.test.mjs(仅在发现测试覆盖缺口时补充) - Preserve: 所有后端、Web、环境、日志、缓存和用户已有 APP 文件
Interfaces:
-
Consumes all five PDA pages, five API modules, shared scanner and existing backend contract。
-
Produces a verified H5/development build;does not produce an APK。
-
Step 1: 完成静态契约覆盖清单
测试必须逐项覆盖:
-
五个 API 文件的路径、方法、参数位置和导出函数。
-
五个页面的路由存在、页面调用对应 API、camelCase payload 字段正确。
-
五类状态条件:
UNSTOCKED、EMPTY_IN_WAREHOUSE、FILLED_IN_WAREHOUSE、AT_SITE_UNUSED/AT_SITE_PARTIAL、退回REQUESTED。 -
HID 回车、空值、重复扫码、扫描请求锁和手工输入。
-
入库/发货确认、装填、使用、退回申请/确认的幂等键生成、保留和成功清理时机。
-
页面权限标识和登录页无密码预填。
-
不存在摄像头扫码调用、APK 构建脚本调用、管理员密码或 Token 字面量。
-
Step 2: 运行 PDA 契约测试
工作目录:shierfeng-fastapi-app
pnpm test:business
预期:所有 Node 20 测试通过且零失败。
- Step 3: 运行 H5 构建,不运行 APK 构建
pnpm run build:h5
预期:uni-app H5 构建成功。禁止运行 pnpm run build:app、pnpm run dev:app、APK 签名、安装或发布命令;构建产物不加入提交。
- Step 4: 运行已有后端业务回归
工作目录:shierfeng-fastapi-backend,使用当前已配置 Python 环境,不安装依赖:
PYTHONDONTWRITEBYTECODE=1 python -m pytest tests/module_business -q
ruff format --check module_business tests/module_business
ruff check module_business tests/module_business
预期:已有后端业务测试、格式检查和静态检查通过;若本地 Python 环境无法执行,记录实际错误,不伪造通过结果。
- Step 5: 做最终 diff 与敏感信息检查
仓库根目录执行:
git diff --check HEAD~9..HEAD
rg -n --fixed-strings 'admin123' shierfeng-fastapi-app/src shierfeng-fastapi-app/tests
rg -n 'Bearer[[:space:]]+[A-Za-z0-9._-]+' shierfeng-fastapi-app/src shierfeng-fastapi-app/tests
git status --short
敏感信息检查应无命中;git status 中只允许出现本计划任务的 PDA 文件和用户原有改动,不能出现由构建生成且准备提交的 APK、环境文件、日志或缓存。若提交数量因任务合并而不同,将 HEAD~9..HEAD 改为本次 PDA 首个提交到当前 HEAD 的实际范围,不扩大检查目标。
- Step 6: 按人工验收清单交付
在受控本地环境手工验证:管理员和受限普通用户登录;未授权入口不显示且直接接口返回 403;HID 扫码回车、重复扫码、空值和网络重试;库存余量、托盘状态、产线绑定、退回冻结与事件追溯均来自服务端;页面刷新和断网重试不重复扣减库存。管理员密码只在人工输入,不记录到终端输出、源码或日志。
计划自检
- 设计文档的范围、五类流程、认证安全、扫码输入、状态/幂等、验证、部署边界分别由 Tasks 1–9 覆盖。
- 所有跨任务接口在“已核实的后端接口与字段”和各任务的
Interfaces中定义,页面调用名称与 API 导出名称保持一致。 - 没有新增依赖、后端契约改动、摄像头 SDK、APK 构建或凭据写入步骤。
- 实现前先写失败契约,随后运行最小测试,再执行 H5 和后端回归;每个任务都以显式路径提交。