Files
ShiErFeng/docs/superpowers/plans/2026-09-20-pda-business-workflow-implementation.md
T

37 KiB
Raw Blame History

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 request from @/utils/request and 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 normalizeScanValue and isDuplicateScan from @/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 loading and 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.hasPermiAnd and $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, createIdempotencyKey and $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

测试读取页面源码并断言:

  1. 使用 PalletScanInput,扫描调用 scanInboundPallet(qrToken, { warehouseId })。
  2. 只接受 flowState === 'UNSTOCKED',按 palletId 去重。
  3. 草稿 payload 只含 { warehouseId, palletIds },编辑 payload 只含 { inboundId, warehouseId, palletIds }。
  4. 确认调用 confirmInbound(inboundId, { idempotencyKey }),失败分支不清空当前键。
  5. 编辑/删除/确认按钮分别包含 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, remainingPackages as 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, initialPackages and one operation-scoped idempotencyKey。

  • 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 UsageSuccessResponseModel fields usageId, remainingPackages, flowState for 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.js and two independent PalletScanInput instances。

  • Request side state: sourceSiteId, targetWarehouseId, requestScannedPallets, requestIdempotencyKey。

  • Confirm side state: returnId, expectedPalletIds, confirmScannedPallets, confirmIdempotencyKey。

  • Produces handleRequestScan, submitReturnRequest, loadReturnDetail, handleConfirmScan, submitReturnConfirm, sameIdSet。

  • Step 1: 写退回两阶段失败契约

断言测试覆盖:

  1. 申请扫码使用 { mode: 'request' },首个托盘确定 sourceSiteId,后续托盘必须属于同一现场。
  2. 申请 payload 为 sourceSiteId,targetWarehouseId,palletIds,idempotencyKey,申请键和确认键互不复用。
  3. 确认先读取详情,状态必须为 REQUESTED,扫描使用 { mode: 'confirm', returnId },扫码集合必须与详情托盘 ID 集合完全相同。
  4. 确认写请求前再次 getReturn(returnId),比较最新状态和 ID 集合后才调用 confirmReturn。
  5. 请求或确认扫码/提交的旧响应不能覆盖新选择;失败不清除对应幂等键。
  • 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 和后端回归;每个任务都以显式路径提交。