Prechádzať zdrojové kódy

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>
zlwu 2 mesiacov pred
rodič
commit
26192676a2

+ 494 - 0
docs/superpowers/plans/2026-08-03-mobile-h5-dibang-adapt.md

@@ -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`。