docs: add implementation plan for mobile-h5 dibang backend adaptation

Break the approved design into tasks covering login, db HTTP client, select-only master data, and public vehicle plates for fill pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-03 22:21:55 +08:00
1 parent e1170aa855
commit 26192676a2
1 file changed
+494
@@ -0,0 +1,494 @@
# mobile-h5 地磅移动端适配一体化后端 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:** 将 `mobile-h5` 对接一体化 `backend`(门户 RSA 登录 + `db_*` 业务 API + CRM/PL 主数据只读选择),去掉基础资料 CRUD 与手输自动建档。
**Architecture:** H5 保留 Vant 页面;登录/CRM 走门户 `{code,data}` 信封;地磅业务走直接 JSON(对齐 `web-admin/src/utils/http/db.ts`)。主数据只选 CRM 客户、CRM 产品、`/api/transport/vehicle`(self+carrier)。现场页继续用出入库 id;免登场景补车辆只读公共接口。
**Tech Stack:** Vue 3 / Vant 4 / Vite / Axios / Pinia;`jsencrypt`;FastAPI(仅缺口补丁)
**Spec:** `docs/superpowers/specs/2026-08-03-mobile-h5-dibang-adapt-design.md`
---
## File map
| 文件 | 职责 |
|------|------|
| `mobile-h5/vite.config.ts` / `.env*` | 代理到 backend;清理 master-data 斜杠特例 |
| `mobile-h5/src/utils/loginRsa.ts` | RSA 登录体(对齐 web-admin) |
| `mobile-h5/src/utils/http/index.ts` | 门户信封解包 + Bearer;401 处理 |
| `mobile-h5/src/utils/http/db.ts` | 地磅直出 JSON 客户端(尾斜杠、Bearer) |
| `mobile-h5/src/api/authApi.ts` | login / refresh |
| `mobile-h5/src/api/{plans,inventory,weighing,dashboard}Api.ts` | id→string、`productId`、走 db 客户端 |
| `mobile-h5/src/api/crmOptionsApi.ts` | 客户/产品 list |
| `mobile-h5/src/api/vehicleOptionsApi.ts` | 运输车辆 list + 公共车牌 options |
| `mobile-h5/src/components/DbCustomerPicker.vue` 等 | 只选 Picker |
| `mobile-h5/src/views/**` | 表单接选择器;删手输建档 |
| `mobile-h5/src/router/index.ts` | 删 master-data / user 路由 |
| `backend/app/api/db/public_inventory.py`(或新文件) | 免登车牌 options |
| `backend/app/main.py` | 挂载公共路由(若新文件) |
---
### Task 1: 环境与代理
**Files:**
- Modify: `mobile-h5/vite.config.ts`
- Modify: `mobile-h5/.env.example`
- Modify: `mobile-h5/.env`(本地,勿提交密钥)
- Modify: `mobile-h5/package.json`(增加 `jsencrypt`)
- [ ] **Step 1: 安装 jsencrypt**
```bash
cd mobile-h5 && pnpm add jsencrypt
# 若无 pnpm:npm install jsencrypt
```
- [ ] **Step 2: 更新 `.env.example`**
```env
# 开发代理目标(Vite 用)
VITE_API_BASE_URL=http://127.0.0.1:8000
# 生产 API 前缀
VITE_API_URL=/api
```
- [ ] **Step 3: 简化 `vite.config.ts` 代理**
保留 `/api` → `VITE_API_BASE_URL`,**不要** rewrite 去掉 `/api`。
列表尾斜杠逻辑保留 `plans|inventory|weighing`,**删除** `master-data` / `companies|cargos|vehicles` 特例。
```ts
const listResources = ['plans', 'inventory', 'weighing']
// 删除 pathParts.length === 3 && master-data 整块
```
- [ ] **Step 4: 验证代理**
```bash
cd mobile-h5 && pnpm dev
# 另开终端,backend 已启动时:
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5174/api/db/health
```
Expected: `200`(或经代理可达的健康检查;若无 `/db/health` 则 `curl` 登录公钥接口):
```bash
curl -s http://127.0.0.1:5174/api/web/auth/login-rsa-public-key | head -c 200
```
Expected: JSON 含 `publicKeyPem` 或门户信封。
- [ ] **Step 5: Commit**
```bash
git add mobile-h5/package.json mobile-h5/pnpm-lock.yaml mobile-h5/vite.config.ts mobile-h5/.env.example
git commit -m "chore(mobile-h5): point Vite proxy at unified backend and add jsencrypt"
```
---
### Task 2: 登录 RSA + 双 HTTP 客户端
**Files:**
- Create: `mobile-h5/src/utils/loginRsa.ts`
- Create: `mobile-h5/src/utils/http/db.ts`
- Create: `mobile-h5/src/api/authApi.ts`
- Modify: `mobile-h5/src/utils/http/index.ts`
- Modify: `mobile-h5/src/views/login/index.vue`
- Modify: `mobile-h5/src/api/usersApi.ts`(删除或停用旧 `/auth/login`)
- Modify: `mobile-h5/src/store/modules/user.ts`(如需)
- [ ] **Step 1: 新增 `loginRsa.ts`**
对齐 `web-admin/src/utils/loginRsa.ts`:拉取 `/web/auth/login-rsa-public-key`,JSEncrypt 加密 `{ username, password, ts }`,返回 `{ encryptedLogin }`。
公钥请求用**未加密、未强制 Bearer** 的 axios 或现有 client(注意门户信封:若返回 `{code,data}` 则取 `data.publicKeyPem`)。
- [ ] **Step 2: 新增 `authApi.ts`**
```ts
import api from '@/utils/http' // 门户信封客户端
import { buildWebEncryptedLoginBody } from '@/utils/loginRsa'
export async function fetchLogin(username: string, password: string) {
const body = await buildWebEncryptedLoginBody(username, password)
// POST /web/auth/login → data: { token, refreshToken, expiresIn, userInfo }
return api.post<{
token: string
refreshToken: string
expiresIn: number
userInfo: {
id: string
username: string
realName?: string
role?: string
}
}>({ url: '/web/auth/login', data: body })
}
```
保证 `api.post` 对门户响应解包到 `data`(`code===200||code===0`)。
- [ ] **Step 3: 改造 `utils/http/index.ts`(门户客户端)**
- `baseURL`: 开发 `/api`,生产 `VITE_API_URL || '/api'`
- 成功:若有 `code` 字段则解包 `data`;否则原样返回
- 401:logout → `#/login`(hash 路由用 `router` 或 `/#/login`,勿用无 hash 的 `/login`)
- 错误 toast 优先 `message` / `detail`
- [ ] **Step 4: 新增 `utils/http/db.ts`(地磅客户端)**
参考 `web-admin/src/utils/http/db.ts`:
```ts
const DB_COLLECTION_ROOT = /^\/(plans|inventory|weighing)$/
function normalizeDbUrl(url?: string) {
if (!url || url.endsWith('/')) return url
return DB_COLLECTION_ROOT.test(url) ? `${url}/` : url
}
```
- 响应直接 `res.data`(无 code 信封)
- 请求附加 `Bearer ${accessToken}`
- export `get/post/patch/del` 供业务 API 使用
- [ ] **Step 5: 改登录页**
`login/index.vue` 调用 `fetchLogin`:
```ts
const data = await fetchLogin(formData.username, formData.password)
userStore.setLoginStatus(true)
userStore.setToken(data.token, data.refreshToken)
userStore.setUserInfo({
id: data.userInfo.id,
username: data.userInfo.username,
name: data.userInfo.realName || data.userInfo.username,
role: data.userInfo.role
})
router.push('/dashboard')
```
- [ ] **Step 6: 手工验证登录**
```bash
# backend + mobile-h5 dev 已启动
# 浏览器打开 http://127.0.0.1:5174/#/login
# 用门户账号(user_no)登录,应进入仪表盘
```
- [ ] **Step 7: Commit**
```bash
git add mobile-h5/src/utils/loginRsa.ts mobile-h5/src/utils/http/ mobile-h5/src/api/authApi.ts \
mobile-h5/src/views/login/index.vue mobile-h5/src/api/usersApi.ts mobile-h5/src/store/modules/user.ts
git commit -m "feat(mobile-h5): adapt portal RSA login and split db HTTP client"
```
---
### Task 3: 业务 API 类型与路径对齐
**Files:**
- Modify: `mobile-h5/src/api/plansApi.ts`
- Modify: `mobile-h5/src/api/inventoryApi.ts`
- Modify: `mobile-h5/src/api/weighingApi.ts`
- Modify: `mobile-h5/src/api/dashboardApi.ts`
对照权威类型:`web-admin/src/api/db/{plans,inventory,weighing}.ts`。
- [ ] **Step 1: 将全部业务 id 改为 `string`**
示例(plans):
```ts
export interface Plan {
id: string
planNo?: string
// ...
productId?: string
product: string
purchasePlanId?: never // 无此字段
lastWeighingSlipId?: string
fillLinkInventoryId?: string
// ...
}
export interface PlanCreate {
// ...
productId: string
plannedQuantity: number
// 不再只传 product 名称创建
}
```
`list/get/create/update/delete/getRelated/getThirdPartyPage` 的 `id` 参数一律 `string`。
`inventory` / `weighing` 同理:`purchasePlanId`、`inboundOutboundId` → `string`;Create 含 `productId: string`。
- [ ] **Step 2: 全部改用 db 客户端**
```ts
import db from '@/utils/http/db'
// list:
return db.get({ url: '/plans', params })
// 或 url: '/plans/' — db 客户端会 normalize
```
`dashboardApi` 同样走 db 客户端:`/dashboard/inbound/by-cargo` 等。
- [ ] **Step 3: 冒烟请求(需已登录 token)**
浏览器 Network 或临时脚本:登录后 `GET /api/plans/` 应 200 且元素 `id` 为字符串。
- [ ] **Step 4: Commit**
```bash
git add mobile-h5/src/api/plansApi.ts mobile-h5/src/api/inventoryApi.ts \
mobile-h5/src/api/weighingApi.ts mobile-h5/src/api/dashboardApi.ts
git commit -m "feat(mobile-h5): align db API types to string ids and productId"
```
---
### Task 4: 主数据 Options API + Picker 组件
**Files:**
- Create: `mobile-h5/src/api/crmOptionsApi.ts`
- Create: `mobile-h5/src/api/vehicleOptionsApi.ts`
- Create: `mobile-h5/src/components/pickers/DbCustomerPicker.vue`
- Create: `mobile-h5/src/components/pickers/DbProductPicker.vue`
- Create: `mobile-h5/src/components/pickers/DbVehiclePicker.vue`
- Delete 或停用: `mobile-h5/src/api/masterDataApi.ts`
- [ ] **Step 1: `crmOptionsApi.ts`(门户信封 client)**
```ts
import api from '@/utils/http'
export function fetchCustomerOptions(keyword?: string) {
return api.post<{ list?: Array<{ id: string; customerName: string }> }>({
url: '/web/crm/customer/list',
data: { page: 1, pageSize: 20, customerName: keyword || undefined }
})
}
export function fetchProductOptions(keyword?: string) {
return api.post<{ list?: Array<{ id: string; productName: string; model?: string }> }>({
url: '/web/crm/products/list',
data: { page: 1, pageSize: 20, productName: keyword || undefined }
})
}
```
按实际后端 list 入参字段名微调(与 `web-admin` `fetchCustomerList` / `fetchProductList` 一致)。解包后取 `list` 或 `rows`。
- [ ] **Step 2: `vehicleOptionsApi.ts`**
登录态:
```ts
import api from '@/utils/http' // 若 PL 走门户信封;否则用能解 PL 响应的 client
export function fetchVehicleOptions(q?: string) {
return api.get<{ list?: Array<{ id: string; licensePlate: string; vehicleType: string }> }>({
url: '/transport/vehicle',
params: { q, page: 1, pageSize: 100 }
})
}
```
响应形状对齐 `VehicleListResponse`(常用 `list`/`rows`/`items` — 以实现时实际 JSON 为准,与 `pickBxList` 同类解包)。
免登(Task 6 后端就绪后):
```ts
export function fetchPublicVehiclePlates(params: { q?: string; inventoryId?: string; planId?: string }) {
return api.get<Array<{ licensePlate: string; vehicleType?: string }>>({
url: '/public/db/vehicle-plates',
params
})
}
```
- [ ] **Step 3: 三个 Picker 组件**
共用模式:`van-popup` + `van-search` + 列表点击选中;**无**「手动输入」按钮。
- `DbCustomerPicker`:`v-model` = 客户名称字符串;emit 可选 `select(item)`
- `DbProductPicker`:`v-model:productId` + `v-model:productName` + `v-model:specification`(规格可用 `model`)
- `DbVehiclePicker`:`v-model` = `licensePlate`;prop `publicMode?: boolean` 决定调登录态或公共接口
空结果文案:`暂无匹配,请到门户维护`
- [ ] **Step 4: Commit**
```bash
git add mobile-h5/src/api/crmOptionsApi.ts mobile-h5/src/api/vehicleOptionsApi.ts \
mobile-h5/src/components/pickers/ mobile-h5/src/api/masterDataApi.ts
git commit -m "feat(mobile-h5): add CRM/PL option APIs and select-only pickers"
```
---
### Task 5: 业务表单接入选择器 + 去掉自动建档
**Files:**
- Modify: `mobile-h5/src/views/plans/detail.vue`
- Modify: `mobile-h5/src/views/inventory/detail.vue`
- Modify: `mobile-h5/src/views/weighing/detail.vue`
- Modify: `mobile-h5/src/views/plans/index.vue`(生成出入库 query 用 string id)
- Modify: `mobile-h5/src/views/inventory/index.vue`
- Modify: `mobile-h5/src/views/weighing/index.vue`
- Modify: `mobile-h5/src/views/dashboard/index.vue`(去掉基础资料入口)
- Modify: `mobile-h5/src/router/index.ts`(删除 master-data、user 路由)
- Delete: `mobile-h5/src/views/master-data/**`(可选,或留文件但无路由)
- [ ] **Step 1: 计划详情**
- 往来单位 → `DbCustomerPicker` → `formData.customerName`
- 产品 → `DbProductPicker` → `productId` + `product` + `specification`
- 提交 `planApi.create/update` 必须带 `productId`
- 删除对 `companyApi`/`cargoApi` 的引用
- [ ] **Step 2: 出入库详情**
- 同上客户/产品选择器
- 车牌 → `DbVehiclePicker`(登录态)
- **删除** `ensureVehicleExists`、`showManualVehicleInput`、手动输入 Dialog
- `purchasePlanId` 保持 string;去掉 `Number(route.query...)`
- [ ] **Step 3: 地磅单详情**
- 供货/收货单位用客户选择器(或沿用现有双单位字段 + 客户 Picker)
- 货物 → 产品选择器(`productId` 必填)
- `inboundOutboundId` / `purchasePlanId`:string;加载关联时 `inventoryApi.get(id)` 勿 `Number`
- [ ] **Step 4: 列表页**
- 所有 `handleDelete(id: number)` → `string`
- 跳转 query 一律字符串 id
- 仪表盘 `menuList` 去掉 `/master-data`
- 路由删除 `master-data*`;用户管理路由若存在一并删除
- [ ] **Step 5: 手工冒烟**
登录 → 新建计划(选客户+产品)→ 生成出入库(选车牌)→ 生成地磅 → 作废。
- [ ] **Step 6: Commit**
```bash
git add mobile-h5/src/views/ mobile-h5/src/router/index.ts
git commit -m "feat(mobile-h5): wire select-only master data into business forms"
```
---
### Task 6: 现场免登页 + 公共车牌接口
**Files:**
- Modify: `mobile-h5/src/views/inventory/fill.vue`
- Modify: `mobile-h5/src/views/inventory/plan.vue`
- Create or Modify: `backend/app/api/db/public_inventory.py`(或 `public_vehicles.py`)
- Modify: `backend/app/api/__init__.py` / `backend/app/main.py`(如需新 router)
- Create: `backend/tests/test_public_db_vehicle_plates.py`(可选轻量)
- [ ] **Step 1: 后端公共车牌 options**
在 `public_inventory` 同模块或新 router `prefix="/public/db")`:
```python
@router.get("/vehicle-plates")
def list_public_vehicle_plates(
q: Optional[str] = Query(None),
inventory_id: Optional[str] = Query(None, alias="inventoryId"),
plan_id: Optional[str] = Query(None, alias="planId"),
db: Session = Depends(get_db),
):
"""免登:按出入库/计划解析 tenant_no,返回 pl_vehicle 车牌(self+carrier)。"""
# 1) 由 inventory_id 或 plan_id 查 db_inbound_outbound / db_purchase_plan 得 tenant_no
# 2) 若都无则 400
# 3) query PlVehicle: tenant_no, del_flag=0, optional license_plate contains q
# 4) return [{"licensePlate": ..., "vehicleType": ...}, ...] (limit 100)
```
只读,禁止 POST。挂到 `API_V1_PREFIX`。
将路径加入 `API_ENCRYPT_EXCLUDE_PATHS`(若加密开启)。
- [ ] **Step 2: 单测或手工 curl**
```bash
curl -s "http://127.0.0.1:8000/api/public/db/vehicle-plates?inventoryId=<已有出入库id>"
```
Expected: JSON 数组含 `licensePlate`。
- [ ] **Step 3: fill.vue / plan.vue**
- 删除 `ensureVehicleExists` 与手输 UI
- 车牌用 `DbVehiclePicker` `publicMode` + 传入当前 `inventoryId` / `planId`
- 保存仍走现有免登 patch/create
- [ ] **Step 4: 二维码**
确认 `inventory/index` 二维码内容仍为出入库 id,打开 `#/inventory/fill/{id}`。
- [ ] **Step 5: Commit**
```bash
git add backend/app/api/db/ mobile-h5/src/views/inventory/fill.vue \
mobile-h5/src/views/inventory/plan.vue backend/tests/test_public_db_vehicle_plates.py
git commit -m "feat: public vehicle plate options for H5 fill pages; select-only plates"
```
---
### Task 7: 全链路验收与收尾
- [ ] **Step 1: 按 spec §10 验收**
1. 门户账号 RSA 登录 H5
2. 仪表盘无「基础资料」;图表有数据或空态正常
3. 计划 → 出入库 → 地磅 → 作废
4. 客户/产品/车辆无法手输新增
5. 无登录打开 `#/inventory/fill/{id}`,车牌只能选
6. Network 中业务 id 均为字符串
- [ ] **Step 2: 类型检查**
```bash
cd mobile-h5 && pnpm exec vue-tsc --noEmit
```
Expected: 无因 `number` id 引起的错误(可允许原有无关告警,但新建代码应干净)。
- [ ] **Step 3: 最终 commit(若有零散修复)**
```bash
git add mobile-h5 backend
git commit -m "fix(mobile-h5): polish dibang H5 adaptation after acceptance"
```
---
## Spec coverage checklist
| Spec 要求 | Task |
|-----------|------|
| 门户 RSA + JWT 登录 | Task 2 |
| db API 直出 JSON + 尾斜杠 | Task 2–3 |
| id→string、productId | Task 3、5 |
| 删基础资料;客户/产品/车辆只选 | Task 4–5 |
| 禁止手输自动建档(含现场) | Task 5–6 |
| 现场继续用出入库 id | Task 6 |
| H5 无权限 UI 门禁 | 全任务不添加角色判断 |
| 后端仅缺口补丁(公共车牌) | Task 6 |
| 验收清单 | Task 7 |
## Placeholder / consistency notes
- CRM list 请求体字段名以实现时 web-admin 实际调用为准(`customerName` / `productName` 等)。
- PL 车辆列表响应字段统一用 `licensePlate`(camelCase alias)。
- Hash 路由登出跳转统一 `#/login`。