计划:实施 PDA 业务操作功能
This commit is contained in:
@@ -0,0 +1,721 @@
|
|||||||
|
# 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 方法和关键参数名称;同时为工具写可执行单元测试:
|
||||||
|
|
||||||
|
```js
|
||||||
|
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`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --test tests/business-contract.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:因五个 API 模块、扫码工具和测试脚本尚未存在而失败;此时不修改后端或依赖。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现最小 API 封装和测试脚本**
|
||||||
|
|
||||||
|
`package.json` 只增加以下脚本,不增加依赖:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"test:business": "node --test tests/business-contract.test.mjs"
|
||||||
|
```
|
||||||
|
|
||||||
|
每个 API 文件只做路径和参数映射。例如入库模块必须保持如下接口形状:
|
||||||
|
|
||||||
|
```js
|
||||||
|
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 与工具契约通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test:business
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:本任务新增的 API 路径、HTTP 方法、参数位置、扫码规范化、重复窗口和 UUID 测试通过;页面相关测试在后续任务加入。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 只提交本任务文件**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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` 时只读/禁用。提交逻辑必须保持以下顺序:
|
||||||
|
|
||||||
|
```js
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test:business
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:组件的 HID 回车、手工提交、空值、重复值和请求锁静态契约通过。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 提交组件**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 集合:
|
||||||
|
|
||||||
|
```js
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test:business
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:五个路由、权限入口、登录安全静态契约通过。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交页面壳层改动**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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: 实现草稿写入和确认重试**
|
||||||
|
|
||||||
|
```js
|
||||||
|
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: 运行入库契约和构建前静态检查**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test:business
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:入库页面的字段、状态、权限、幂等键和服务端刷新契约通过。
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交入库页面**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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: 实现草稿和确认**
|
||||||
|
|
||||||
|
```js
|
||||||
|
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: 运行测试并提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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: 实现幂等提交和成功刷新**
|
||||||
|
|
||||||
|
```js
|
||||||
|
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: 运行测试并提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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: 实现使用提交和结果刷新**
|
||||||
|
|
||||||
|
```js
|
||||||
|
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: 运行测试并提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`,长度和成员都必须一致:
|
||||||
|
|
||||||
|
```js
|
||||||
|
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: 实现确认前复核和幂等提交**
|
||||||
|
|
||||||
|
```js
|
||||||
|
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: 运行测试并提交**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test:business
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:所有 Node 20 测试通过且零失败。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 运行 H5 构建,不运行 APK 构建**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run build:h5
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:uni-app H5 构建成功。禁止运行 `pnpm run build:app`、`pnpm run dev:app`、APK 签名、安装或发布命令;构建产物不加入提交。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 运行已有后端业务回归**
|
||||||
|
|
||||||
|
工作目录:`shierfeng-fastapi-backend`,使用当前已配置 Python 环境,不安装依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 与敏感信息检查**
|
||||||
|
|
||||||
|
仓库根目录执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 和后端回归;每个任务都以显式路径提交。
|
||||||
Reference in New Issue
Block a user