From 9edbde7a63e6570e181a15a74bbee714a5202cc6 Mon Sep 17 00:00:00 2001 From: Misaka Date: Sun, 24 May 2026 17:25:47 +0800 Subject: [PATCH] docs: add accessory module PRD and implementation plan Co-Authored-By: Claude Opus 4.6 --- docs/plans/2026-05-24-accessory-impl.md | 750 ++++++++++++++++++++++ docs/plans/2026-05-24-accessory-module.md | 646 +++++++++++++++++++ 2 files changed, 1396 insertions(+) create mode 100644 docs/plans/2026-05-24-accessory-impl.md create mode 100644 docs/plans/2026-05-24-accessory-module.md diff --git a/docs/plans/2026-05-24-accessory-impl.md b/docs/plans/2026-05-24-accessory-impl.md new file mode 100644 index 0000000..2320b90 --- /dev/null +++ b/docs/plans/2026-05-24-accessory-impl.md @@ -0,0 +1,750 @@ +# Accessory Module Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add an independent accessory registration module to CargoTrace, allowing warehouse workers to manually register, shelve, and box order accessories that arrive without execution cards. + +**Architecture:** New `finished_goods_accessory` table + `accessory_type` preset table in PostgreSQL. New FastAPI service layer (`accessory_service.py`) and API routes. Flutter gets a new `AccessoryPage` on the home screen. Existing boxing module is extended to show and box accessories via a new `item_type` column on `finished_goods_box_item`. + +**Tech Stack:** FastAPI + SQLAlchemy 2.0 (sync) + Pydantic v2 / Flutter 3.x + Dart / PostgreSQL + +**Branching:** Create a `dev` branch from `master` in both `services/fastapi` and `apps/pad_scanner` submodules. + +--- + +## Task 1: Create dev branches in both submodules + +**Step 1: Create dev branch in fastapi submodule** + +```bash +cd D:/FileLib/Projects/CargoTrace/services/fastapi +git checkout -b dev +git push -u origin dev +``` + +**Step 2: Create dev branch in pad_scanner submodule** + +```bash +cd D:/FileLib/Projects/CargoTrace/apps/pad_scanner +git checkout -b dev +git push -u origin dev +``` + +**Step 3: Commit plan document in fastapi** + +The PRD `2026-05-24-accessory-module.md` is already in the working tree. + +```bash +cd D:/FileLib/Projects/CargoTrace/services/fastapi +git add docs/plans/2026-05-24-accessory-module.md docs/plans/2026-05-24-accessory-impl.md +git commit -m "docs: add accessory module PRD and implementation plan" +``` + +--- + +## Task 2: Database migration — new tables + ALTER existing table + +**Files:** +- Create: `services/fastapi/app/models/accessory.py` +- Modify: `services/fastapi/app/models/finished_goods.py` (add `item_type` column to `FinishedGoodsBoxItem`) +- Modify: `services/fastapi/app/models/__init__.py` (register new models for auto-import) + +**Step 1: Create the accessory model file** + +```python +# app/models/accessory.py +from datetime import datetime + +from sqlalchemy import BigInteger, DateTime, Index, Integer, String, func +from sqlalchemy.orm import Mapped, mapped_column + +from app.core.database import Base + +SCHEMA = "CargoTrace" + + +class FinishedGoodsAccessory(Base): + __tablename__ = "finished_goods_accessory" + __table_args__ = ( + Index("idx_fga_paichan_no", "paichan_no"), + Index("idx_fga_zongpai_no", "zongpai_no"), + Index("idx_fga_location_code", "location_code"), + {"schema": SCHEMA}, + ) + + id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) + paichan_no: Mapped[str] = mapped_column(String(64), nullable=False) + zongpai_no: Mapped[str] = mapped_column(String(64), nullable=False) + accessory_type: Mapped[str] = mapped_column(String(64), nullable=False) + quantity: Mapped[int] = mapped_column(Integer, nullable=False) + location_code: Mapped[str | None] = mapped_column(String(64), nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime, + nullable=False, + default=func.current_timestamp(), + server_default=func.current_timestamp(), + ) + + +class AccessoryType(Base): + __tablename__ = "accessory_type" + __table_args__ = ( + Index("uk_accessory_type_name", "name", unique=True), + {"schema": SCHEMA}, + ) + + id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(64), nullable=False) + sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) +``` + +**Step 2: Add `item_type` column to `FinishedGoodsBoxItem`** + +In `app/models/finished_goods.py`, add to the `FinishedGoodsBoxItem` class: + +```python +item_type: Mapped[str] = mapped_column( + String(16), nullable=False, default="product", server_default="product" +) +``` + +**Step 3: Run the SQL migration manually** + +Execute against the PostgreSQL database: + +```sql +-- 1. 附件记录表 +CREATE TABLE IF NOT EXISTS "CargoTrace".finished_goods_accessory ( + id SERIAL PRIMARY KEY, + paichan_no VARCHAR(64) NOT NULL, + zongpai_no VARCHAR(64) NOT NULL, + accessory_type VARCHAR(64) NOT NULL, + quantity INT NOT NULL, + location_code VARCHAR(64) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_fga_paichan_no ON "CargoTrace".finished_goods_accessory (paichan_no); +CREATE INDEX IF NOT EXISTS idx_fga_zongpai_no ON "CargoTrace".finished_goods_accessory (zongpai_no); +CREATE INDEX IF NOT EXISTS idx_fga_location_code ON "CargoTrace".finished_goods_accessory (location_code); + +-- 2. 附件类型预设表 +CREATE TABLE IF NOT EXISTS "CargoTrace".accessory_type ( + id SERIAL PRIMARY KEY, + name VARCHAR(64) NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + CONSTRAINT uk_accessory_type_name UNIQUE (name) +); + +-- 3. 装箱明细表新增 item_type 字段 +ALTER TABLE "CargoTrace".finished_goods_box_item +ADD COLUMN IF NOT EXISTS item_type VARCHAR(16) NOT NULL DEFAULT 'product'; +``` + +**Step 4: Verify tables exist** + +```bash +cd D:/FileLib/Projects/CargoTrace/services/fastapi +# Activate venv and run a quick check +``` + +**Step 5: Commit** + +```bash +git add app/models/accessory.py app/models/finished_goods.py +git commit -m "feat: add accessory models and box_item item_type column" +``` + +--- + +## Task 3: Pydantic schemas for accessory endpoints + +**Files:** +- Create: `services/fastapi/app/schemas/accessory.py` + +**Step 1: Write the schema file** + +```python +# app/schemas/accessory.py +import re + +from pydantic import BaseModel, Field, field_validator + +PAICHAN_PATTERN = re.compile(r"^[A-Z]{1,2}\d{5}(-J)?$") +LOCATION_NORMAL_PATTERN = re.compile(r"^[A-Z]+\d+-\d+-\d+$") +LOCATION_TRANS_PATTERN = re.compile(r"^TRANS-\d+") + + +# ── Work order query ── + +class WorkOrderGroup(BaseModel): + work_order_no: str + zongpai_nos: list[str] + + +class WorkOrderQueryResponse(BaseModel): + paichan_no: str + work_orders: list[WorkOrderGroup] + + +# ── Accessory CRUD ── + +class AccessoryCreateRequest(BaseModel): + paichan_no: str + zongpai_no: str + accessory_type: str + quantity: int + location_code: str | None = None + + @field_validator("paichan_no") + @classmethod + def validate_paichan_no(cls, v: str) -> str: + v = v.strip().upper() + if not PAICHAN_PATTERN.match(v): + raise ValueError("INVALID_PAICHAN") + return v + + @field_validator("zongpai_no") + @classmethod + def validate_zongpai_no(cls, v: str) -> str: + v = v.strip().upper() + if not re.match(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$", v): + raise ValueError("INVALID_ZONGPAI") + return v + + @field_validator("quantity") + @classmethod + def validate_quantity(cls, v: int) -> int: + if v <= 0: + raise ValueError("INVALID_QUANTITY") + return v + + @field_validator("location_code") + @classmethod + def validate_location_code(cls, v: str | None) -> str | None: + if v is None: + return v + v = v.strip().upper() + if not LOCATION_NORMAL_PATTERN.match(v) and not LOCATION_TRANS_PATTERN.match(v): + raise ValueError("INVALID_LOCATION") + return v + + +class AccessoryResponse(BaseModel): + id: int + paichan_no: str + zongpai_no: str + accessory_type: str + quantity: int + location_code: str | None = None + is_boxed: bool = False + created_at: str + + +class AccessoryListResponse(BaseModel): + paichan_no: str + items: list[AccessoryResponse] + + +class AccessoryUpdateRequest(BaseModel): + accessory_type: str | None = None + quantity: int | None = None + location_code: str | None = None + + @field_validator("quantity") + @classmethod + def validate_quantity(cls, v: int | None) -> int | None: + if v is not None and v <= 0: + raise ValueError("INVALID_QUANTITY") + return v + + @field_validator("location_code") + @classmethod + def validate_location_code(cls, v: str | None) -> str | None: + if v is None: + return v + v = v.strip().upper() + if not LOCATION_NORMAL_PATTERN.match(v) and not LOCATION_TRANS_PATTERN.match(v): + raise ValueError("INVALID_LOCATION") + return v + + +# ── Accessory type management ── + +class AccessoryTypeItem(BaseModel): + id: int + name: str + sort_order: int + + +class AccessoryTypeListResponse(BaseModel): + types: list[AccessoryTypeItem] + + +class AccessoryTypeCreateRequest(BaseModel): + name: str + sort_order: int = 0 + + @field_validator("name") + @classmethod + def validate_name(cls, v: str) -> str: + v = v.strip() + if not v: + raise ValueError("INVALID_TYPE_NAME") + return v + + +class AccessoryTypeUpdateRequest(BaseModel): + name: str | None = None + sort_order: int | None = None +``` + +**Step 2: Commit** + +```bash +git add app/schemas/accessory.py +git commit -m "feat: add accessory Pydantic schemas" +``` + +--- + +## Task 4: Accessory service layer — business logic + +**Files:** +- Create: `services/fastapi/app/services/accessory_service.py` + +This service handles: work order query, accessory CRUD, shelving/off-shelf logic, and type management. + +**Step 1: Write the service** + +Key functions to implement: + +1. `query_work_orders(db, paichan_no)` — Query ERP for all work orders under a paichan_no, group by work_order_no, return `WorkOrderQueryResponse`. +2. `list_accessories(db, paichan_no)` — List all accessories for a paichan_no, check `is_boxed` via `finished_goods_box_item`. +3. `create_accessory(db, req)` — Create accessory record with shelving logic (same off-shelf rules as location_service). +4. `update_accessory(db, accessory_id, req)` — Partial update of accessory_type/quantity/location_code. +5. `delete_accessory(db, accessory_id)` — Delete if not boxed. +6. `list_accessory_types(db)` / `create_accessory_type(db, req)` / `update_accessory_type(db, type_id, req)` / `delete_accessory_type(db, type_id)` + +**Error classes to define:** + +```python +class InvalidPaichanError(Exception): + pass + +class PaichanNotFoundError(Exception): + pass + +class AccessoryNotFoundError(Exception): + pass + +class AccessoryAlreadyBoxedError(Exception): + pass + +class AccessoryDuplicateLocationError(Exception): + def __init__(self, location_code: str, registered_at: str): + self.location_code = location_code + self.registered_at = registered_at + +class AccessoryAlreadyOffShelfError(Exception): + def __init__(self, location_code: str, registered_at: str): + self.location_code = location_code + self.registered_at = registered_at +``` + +**Shelving logic** (in `create_accessory` when `location_code` is provided): + +- If the accessory row already has a `location_code`: + - If existing starts with `TRANS-` → raise `AccessoryAlreadyOffShelfError` + - If new target starts with `TRANS-` → update record (off-shelf / down-shelf) + - If both are normal locations → raise `AccessoryDuplicateLocationError` + +**is_boxed check**: Query `finished_goods_box_item` where `zongpai_no` matches and `item_type = 'accessory'` and there exists a `finished_goods_box` with `paichan_no` matching. This checks if the accessory has been packed into a box. + +**Step 2: Commit** + +```bash +git add app/services/accessory_service.py +git commit -m "feat: add accessory service layer with CRUD and shelving logic" +``` + +--- + +## Task 5: Accessory API routes + +**Files:** +- Create: `services/fastapi/app/api/v1/accessory.py` +- Modify: `services/fastapi/app/main.py` — register new router + exception handlers + +**Step 1: Create the accessory router** + +```python +# app/api/v1/accessory.py +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.schemas.accessory import ( + AccessoryCreateRequest, + AccessoryListResponse, + AccessoryResponse, + AccessoryTypeCreateRequest, + AccessoryTypeItem, + AccessoryTypeListResponse, + AccessoryTypeUpdateRequest, + AccessoryUpdateRequest, + WorkOrderQueryResponse, +) +from app.services.accessory_service import ( + create_accessory, + create_accessory_type, + delete_accessory, + delete_accessory_type, + list_accessories, + list_accessory_types, + update_accessory, + update_accessory_type, + query_work_orders, +) + +router = APIRouter(tags=["accessory"]) +``` + +Endpoints: + +| Method | Path | Handler | +|---|---|---| +| GET | `/accessory/work-orders` | `query_work_orders` | +| GET | `/accessory` | `list_accessories` | +| POST | `/accessory` | `create_accessory` | +| PATCH | `/accessory/{id}` | `update_accessory` | +| DELETE | `/accessory/{id}` | `delete_accessory` | +| GET | `/accessory-type` | `list_accessory_types` | +| POST | `/accessory-type` | `create_accessory_type` | +| PATCH | `/accessory-type/{id}` | `update_accessory_type` | +| DELETE | `/accessory-type/{id}` | `delete_accessory_type` | + +**Step 2: Register router + exception handlers in `main.py`** + +Add to `app/main.py`: + +```python +from app.api.v1.accessory import router as accessory_router +from app.services.accessory_service import ( + AccessoryAlreadyBoxedError, + AccessoryAlreadyOffShelfError, + AccessoryDuplicateLocationError, + AccessoryNotFoundError, + InvalidPaichanError, + PaichanNotFoundError, +) + +app.include_router(accessory_router, prefix="/CargoTrace") +``` + +Add exception handlers mirroring the existing pattern (400 for `InvalidPaichanError`, 404 for `PaichanNotFoundError`/`AccessoryNotFoundError`, 409 for duplicate/off-shelf/boxed errors). + +**Step 3: Commit** + +```bash +git add app/api/v1/accessory.py app/main.py +git commit -m "feat: add accessory API routes and exception handlers" +``` + +--- + +## Task 6: Extend boxing module to support accessories + +**Files:** +- Modify: `services/fastapi/app/services/box_service.py` — `get_box_info` returns `pending_accessories`; `save_box_record` handles `item_type=accessory` +- Modify: `services/fastapi/app/schemas/box.py` — add `PendingAccessory`, `BoxSaveRequest.item_type`, `BoxSaveRequest.accessory_id` +- Modify: `services/fastapi/app/api/v1/box.py` — no structural change, just forward new fields + +**Step 1: Extend box schemas** + +In `app/schemas/box.py`, add: + +```python +class PendingAccessory(BaseModel): + accessory_id: int + accessory_type: str + quantity: int + location_code: str | None = None +``` + +Add to `BoxInfoResponse`: + +```python +pending_accessories: list[PendingAccessory] = Field(default_factory=list) +``` + +Add to `BoxSaveRequest`: + +```python +item_type: str = "product" +accessory_id: int | None = None +``` + +**Step 2: Extend `get_box_info` in `box_service.py`** + +After building the existing response dict, query `finished_goods_accessory` for the same `zongpai_no` where `location_code IS NOT NULL` (shelved but not yet boxed). Add as `pending_accessories`. + +**Step 3: Extend `save_box_record` in `box_service.py`** + +When `item_type == "accessory"`: +- Skip the ERP quantity validation (accessories don't have ERP quantities) +- Use the `zongpai_no` and `paichan_no` from the `accessory_id` record +- Create `FinishedGoodsBoxItem` with `item_type="accessory"` + +**Step 4: Commit** + +```bash +git add app/schemas/box.py app/services/box_service.py +git commit -m "feat: extend boxing module to support accessory items" +``` + +--- + +## Task 7: FastAPI — integration test + +**Files:** +- Create: `services/fastapi/tests/test_accessory.py` + +**Step 1: Write integration tests** + +Test the following scenarios against the running API: + +1. `GET /CargoTrace/accessory/work-orders?paichan_no=W00009` — returns work order groups +2. `POST /CargoTrace/accessory` — create accessory with location +3. `GET /CargoTrace/accessory?paichan_no=W00009` — list accessories +4. `PATCH /CargoTrace/accessory/{id}` — update quantity +5. `DELETE /CargoTrace/accessory/{id}` — delete +6. `GET /CargoTrace/accessory-type` — list types +7. `POST /CargoTrace/accessory-type` — create type +8. Off-shelf scenario: create with normal location, then update to TRANS- location +9. Error scenario: duplicate location, already off-shelf, invalid paichan + +**Step 2: Run tests** + +```bash +cd D:/FileLib/Projects/CargoTrace/services/fastapi +source .venv/Scripts/activate +pytest tests/test_accessory.py -v +``` + +**Step 3: Commit** + +```bash +git add tests/test_accessory.py +git commit -m "test: add accessory module integration tests" +``` + +--- + +## Task 8: Flutter — API service extension + +**Files:** +- Modify: `apps/pad_scanner/lib/services/api_service.dart` — add accessory API methods + +**Step 1: Add accessory API methods to `ApiService`** + +Methods to add: + +```dart +// Work order query +Future queryWorkOrders(String paichanNo) + +// Accessory CRUD +Future createAccessory(AccessoryCreateRequest req) +Future listAccessories(String paichanNo) +Future updateAccessory(int id, AccessoryUpdateRequest req) +Future deleteAccessory(int id) + +// Accessory types +Future> listAccessoryTypes() +Future createAccessoryType(String name, {int sortOrder = 0}) +Future deleteAccessoryType(int id) +``` + +Define corresponding result/model classes. + +**Step 2: Commit** + +```bash +git add lib/services/api_service.dart +git commit -m "feat: add accessory API methods to Flutter API service" +``` + +--- + +## Task 9: Flutter — Accessory registration page + +**Files:** +- Create: `apps/pad_scanner/lib/pages/accessory_page.dart` +- Create: `apps/pad_scanner/lib/pages/accessory/` directory with part files for separation + +**Step 1: Create the main page structure** + +`accessory_page.dart` as StatefulWidget, following the same pattern as `registration_page.dart`: + +- Part files for scan handling, submit logic, status display, list management +- State machine: `idle` → `paichanQueried` → `workOrderSelected` → `accessoryTypeSelected` → `readyToSubmit` + +**Step 2: Implement the step-by-step form** + +- Paichan_no input field + query button (hardware Enter key triggers query) +- Work order dropdown (populated after query) +- Zongpai_no auto-fill or dropdown (based on selected work order) +- Accessory type selector (dropdown from presets + manual input option) +- Quantity input field +- Location code field (supports scanner input) +- Submit button + +**Step 3: Implement the registered accessories list** + +Bottom half of the page, showing all accessories for the current paichan_no with inline edit/delete. + +**Step 4: Commit** + +```bash +git add lib/pages/accessory_page.dart lib/pages/accessory/ +git commit -m "feat: add accessory registration page" +``` + +--- + +## Task 10: Flutter — Update home page with accessory card + +**Files:** +- Modify: `apps/pad_scanner/lib/pages/home_page.dart` + +**Step 1: Add accessory module card** + +Add a third card between "装箱编号" and "货架查询": + +```dart +ModuleCard( + icon: Icons.build, + title: '附件登记', + description: '手动登记订单附件上架与装箱', + status: ModuleStatus.developing, + onTap: () => Navigator.pushNamed(context, '/accessory'), +) +``` + +**Step 2: Register route in `main.dart`** + +```dart +'/accessory': (context) => const AccessoryPage(), +``` + +**Step 3: Commit** + +```bash +git add lib/pages/home_page.dart lib/main.dart +git commit -m "feat: add accessory module card to home page" +``` + +--- + +## Task 11: Flutter — Extend boxing page for accessories + +**Files:** +- Modify: `apps/pad_scanner/lib/pages/boxing_page.dart` or relevant part files +- Modify: `apps/pad_scanner/lib/pages/boxing/boxing_models.dart` — add `PendingAccessory` model + +**Step 1: Parse `pending_accessories` from box info API response** + +When `get_box_info` returns data, extract `pending_accessories` list. + +**Step 2: Add "待装箱附件" section in boxing UI** + +Below the existing assigned items list, show pending accessories with "加入本箱" buttons. + +**Step 3: Handle accessory boxing submission** + +When boxing an accessory, send `item_type: 'accessory'` and `accessory_id` in the request. + +**Step 4: Show accessory items in boxing detail page** + +In `boxing_detail_page.dart`, distinguish accessories from products (e.g., different row color or icon). + +**Step 5: Commit** + +```bash +git add lib/pages/boxing_page.dart lib/pages/boxing/ lib/pages/boxing_detail_page.dart +git commit -m "feat: extend boxing module to display and box accessories" +``` + +--- + +## Task 12: Manual testing & polish + +**Step 1: Start FastAPI dev server** + +```bash +cd D:/FileLib/Projects/CargoTrace/services/fastapi +source .venv/Scripts/activate +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +**Step 2: Build and run Flutter app** + +```bash +cd D:/FileLib/Projects/CargoTrace/apps/pad_scanner +flutter run +``` + +**Step 3: Test the complete flow** + +1. Home page → 附件登记 card visible, tap to enter +2. Input paichan_no → work orders load in dropdown +3. Select work order → zongpai_no auto-fills +4. Select accessory type → input quantity +5. Scan location code → submit +6. Verify accessory appears in registered list +7. Go to boxing page → scan a zongpai_no that has accessories +8. Verify "待装箱附件" section shows accessories +9. Box an accessory → verify in detail page + +**Step 4: Commit any fixes** + +```bash +git add -A +git commit -m "fix: address issues found during manual testing" +``` + +--- + +## Task 13: Final commit and merge to master + +**Step 1: In fastapi submodule** + +```bash +cd D:/FileLib/Projects/CargoTrace/services/fastapi +# Ensure all changes committed on dev +git log --oneline master..dev +# Merge dev into master +git checkout master +git merge dev +git push +``` + +**Step 2: In pad_scanner submodule** + +```bash +cd D:/FileLib/Projects/CargoTrace/apps/pad_scanner +git log --oneline master..dev +git checkout master +git merge dev +git push +``` + +**Step 3: Update parent repo submodules** + +```bash +cd D:/FileLib/Projects/CargoTrace +git add apps/pad_scanner services/fastapi +git commit -m "feat: add accessory registration module" +git push +``` diff --git a/docs/plans/2026-05-24-accessory-module.md b/docs/plans/2026-05-24-accessory-module.md new file mode 100644 index 0000000..3af9d20 --- /dev/null +++ b/docs/plans/2026-05-24-accessory-module.md @@ -0,0 +1,646 @@ +# PRD · 模块三:附件登记 + +**版本:** v1.0 +**状态:** 待实现 +**适用端:** 安卓扫码枪 Flutter 应用 + FastAPI 后端 + +--- + +## 1. 功能概述 + +附件登记模块是为成品库房中**无执行卡的订单附件**提供信息化管理的独立模块。 + +在实际作业中,产品的附件(安装配件、说明书、包装材料等)可能在主产品生产完成之前就运抵库房,需要临时存放。这些附件不携带执行卡,无法通过扫码获取总排号,因此需要通过手动输入排产号、选择工令号的方式定位到具体总排号,完成上架和装箱登记。 + +本模块覆盖附件的**上架登记、下架操作、装箱处理**全流程,与现有的上架登记和装箱编号模块并行存在于首页导航。 + +--- + +## 2. 用户与使用场景 + +**使用人员:** 成品库房工人 + +**典型场景:** + +| 场景 | 描述 | +|---|---| +| 附件到货上架 | 附件先于产品到达库房,工人根据送货单上的排产号,手动输入后查询工令号,选择后登记到货位 | +| 附件下架 | 已上架的附件需要转移到转运区域,与产品下架逻辑一致 | +| 附件与产品混装 | 装箱时将附件与主产品装入同一箱号 | +| 附件单独装箱 | 装箱时将附件单独装箱,关联到同一排产号 | +| 附件类型管理 | 管理员在系统中预设附件类型列表,工人操作时从中点选 | + +--- + +## 3. 设备能力约定 + +与现有模块一致: + +| 项目 | 说明 | +|---|---| +| 扫码输出方式 | 广播输出(Android Intent) | +| 屏幕尺寸 | 3.5 英寸,宽度 640px,高度 960px | +| 键盘 | 31 键实体键盘 | +| 网络 | WiFi / 蓝牙 / 蜂窝 | + +--- + +## 4. 码值识别规则 + +本模块中,货位号仍通过扫码获取,复用现有码值解析规则。排产号和工令号通过手动输入和选择操作。 + +| 码值类型 | 获取方式 | 识别规则 | +|---|---|---| +| 排产号 | 手动输入 | 正则 `^[A-Z]{1,2}\d{5}(-J)?$` | +| 工令号 | 从列表选择 | 由 ERP 查询返回 | +| 总排号 | 系统自动关联 | 由 排产号+工令号 从 ERP 关联得出 | +| 普通货位号 | 扫码或手动输入 | 正则 `^[A-Z]+\d+-\d+-\d+$` | +| 转运特殊货位号 | 扫码或手动输入 | 以 `TRANS-` 开头 | +| 附件类型 | 预设点选或自由输入 | 字符串,最长 64 字符 | + +--- + +## 5. 数据模型 + +### 5.1 新增表:附件记录表 + +```sql +CREATE TABLE "CargoTrace".finished_goods_accessory ( + id SERIAL PRIMARY KEY, + paichan_no VARCHAR(64) NOT NULL, + zongpai_no VARCHAR(64) NOT NULL, + accessory_type VARCHAR(64) NOT NULL, + quantity INT NOT NULL, + location_code VARCHAR(64) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_fga_paichan_no ON "CargoTrace".finished_goods_accessory (paichan_no); +CREATE INDEX idx_fga_zongpai_no ON "CargoTrace".finished_goods_accessory (zongpai_no); +CREATE INDEX idx_fga_location_code ON "CargoTrace".finished_goods_accessory (location_code); +``` + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | SERIAL | 主键 | +| paichan_no | VARCHAR(64) | 排产号,工人手动输入 | +| zongpai_no | VARCHAR(64) | 总排号,由排产号+工令号从 ERP 关联得出 | +| accessory_type | VARCHAR(64) | 附件类型名称,来自预设列表或自由输入 | +| quantity | INT | 附件数量,精确计量 | +| location_code | VARCHAR(64) | 货位号,上架后填入;未上架时为 NULL | +| created_at | TIMESTAMP | 创建时间 | + +**设计说明:** +- 无唯一约束:同一总排号可登记多种附件,每种附件也可分多次登记。 +- `location_code` 为 NULL 表示未上架,有值表示已上架或已下架到转运区域。 +- 下架逻辑复用现有模式:`location_code` 以 `TRANS-` 开头即为已下架。 + +### 5.2 新增表:附件类型预设表 + +```sql +CREATE TABLE "CargoTrace".accessory_type ( + id SERIAL PRIMARY KEY, + name VARCHAR(64) NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + + CONSTRAINT uk_accessory_type_name UNIQUE (name) +); +``` + +| 字段 | 类型 | 说明 | +|---|---|---| +| id | SERIAL | 主键 | +| name | VARCHAR(64) | 类型名称,唯一 | +| sort_order | INT | 排序权重,值越小越靠前 | + +### 5.3 现有表变更:装箱明细表 + +在 `finished_goods_box_item` 表新增 `item_type` 字段: + +```sql +ALTER TABLE "CargoTrace".finished_goods_box_item +ADD COLUMN item_type VARCHAR(16) NOT NULL DEFAULT 'product'; + +COMMENT ON COLUMN "CargoTrace".finished_goods_box_item.item_type +IS '条目类型:product = 产品,accessory = 附件'; +``` + +--- + +## 6. 页面设计 + +### 6.1 首页更新 + +在现有首页功能卡片列表中,在"装箱编号"卡片下方新增"附件登记"卡片: + +``` +┌──────────────────────────────┐ +│ 📦 上架登记 │ ← 已上线 +│ ──────────────────────── │ +│ 📋 装箱编号 │ ← 已上线 +│ ──────────────────────── │ +│ 🔧 附件登记 │ ← 新增,开发中 +│ 扫码或手动登记订单附件 │ +│ 开发中 ○ │ +│ ──────────────────────── │ +│ 🔍 货架查询 │ ← 规划中(已并入总览) +│ 规划中 ○ │ +└──────────────────────────────┘ +``` + +### 6.2 附件登记主页面 + +页面分为上下两个区域:上方为操作区,下方为已登记附件列表。整体可滚动。 + +**初始状态(等待输入):** + +``` +┌──────────────────────────────────┐ +│ 附件登记 │ +├──────────────────────────────────┤ +│ ▌ 操作区 │ +│ │ +│ 排产号 [ ] [查询] │ ← 手动输入框 + 查询按钮 +│ │ +│ 工令号 (请先查询排产号) │ ← 置灰,查询后填充下拉列表 +│ 总排号 (请先选择工令号) │ ← 置灰,选择工令号后自动填入 +│ 附件类型 ▼ [ ] │ ← 置灰,选择总排号后激活 +│ 数量 [ ] │ ← 置灰,选择附件类型后激活 +│ 货位号 [ ] │ ← 置灰,输入数量后激活,支持扫码 +│ │ +│ ┌────────────────────────┐ │ +│ │ 确 认 上 架 │ │ ← 置灰,全部填入后激活 +│ └────────────────────────┘ │ +│ │ +├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ +│ ▌ 已登记附件 │ ← 初始隐藏,查询排产号后展示 +│ (请先查询排产号) │ +│ │ +├──────────────────────────────────┤ +│ ● 等待输入排产号… │ +└──────────────────────────────────┘ +``` + +**查询排产号后的状态:** + +``` +┌──────────────────────────────────┐ +│ 附件登记 │ +├──────────────────────────────────┤ +│ ▌ 操作区 │ +│ │ +│ 排产号 [ W00009 ] [查询] │ +│ │ +│ 工令号 ▼ [ 6-1(7) ] │ ← 下拉展示该排产号下所有工令号 +│ │ │ 可点选 +│ 总排号 26BW0011 │ ← 自动填入(工令号选定后关联) +│ │ +│ 附件类型 ▼ [ 安装配件 ] │ ← 下拉预设 + 自由输入 +│ 数量 [ 5 ] │ +│ 货位号 [ A01-02-03 ] │ ← 扫码或手动输入 +│ │ +│ ┌────────────────────────┐ │ +│ │ 确 认 上 架 │ │ +│ └────────────────────────┘ │ +│ │ +├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ +│ ▌ 已登记附件 · W00009 │ +│ ┌────────┬──────────┬────┬──────┬──────────┐ +│ │总排号 │附件类型 │数量│货位号│操作 │ +│ ├────────┼──────────┼────┼──────┼──────────┤ +│ │26BW0011│安装配件 │ 5 │A01-02│ [改][删] │ +│ │26BW0011│说明书 │ 10 │A01-02│ [改][删] │ +│ │26BW0010│包装材料 │ 2 │— │ [改][删] │ ← 未上架,货位显示 — +│ └────────┴──────────┴────┴──────┴──────────┘ +│ │ +├──────────────────────────────────┤ +│ ● 已填入,请确认或继续编辑 │ +└──────────────────────────────────┘ +``` + +### 6.3 字段联动规则 + +操作区采用**逐步解锁**模式,每一步完成后才激活下一步: + +| 步骤 | 操作 | 激活条件 | 说明 | +|---|---|---|---| +| 1 | 输入排产号并查询 | 无 | 手动输入排产号,点击查询或按回车 | +| 2 | 选择工令号 | 排产号查询成功 | 下拉列表展示该排产号下所有去重工令号 | +| 3 | 总排号自动填入 | 工令号选定 | 系统根据排产号+工令号从 ERP 关联总排号;若只有1个则自动填入,若多个则展示下拉供选择 | +| 4 | 选择附件类型 | 总排号已填入 | 下拉展示预设类型列表,支持手动输入自定义类型 | +| 5 | 输入数量 | 附件类型已填入 | 手动输入正整数 | +| 6 | 输入货位号 | 数量已填入 | 扫码或手动输入,复用现有货位号格式校验 | +| 7 | 确认上架 | 全部字段有效 | 提交到后端 | + +### 6.4 已登记列表 + +已登记列表在首次查询排产号成功后展示,展示该排产号下所有已登记的附件记录。 + +**列定义:** + +| 列 | 内容 | 说明 | +|---|---|---| +| 总排号 | zongpai_no | 该附件关联的总排号 | +| 附件类型 | accessory_type | 类型名称 | +| 数量 | quantity | 登记的附件数量 | +| 货位号 | location_code | 已上架显示货位号,未上架显示 "—",转运显示 TRANS-xx | +| 操作 | [改] [删] | 行内编辑和删除 | + +**行内编辑(点击 [改]):** + +``` +│ 26BW0011 │ [安装配件] │ [5] │ A01-02 │ [✓][✗] │ +``` + +- 仅附件类型和数量可编辑,总排号和货位号不可修改 +- 点 [✓] 调用 PATCH 接口保存 +- 点 [✗] 取消恢复原值 + +**行内删除(点击 [删]):** + +``` +│ 确认删除此条附件记录? [是] [否] +``` + +- 确认后调用 DELETE 接口删除 + +**已装箱附件标记:** 已完成装箱的附件记录在列表中显示 `[已装箱]` 标签,不支持编辑和删除。 + +--- + +## 7. 功能详细说明 + +### 7.1 操作流程图 + +```mermaid +flowchart TD + A([进入附件登记页]) --> B[等待输入排产号] + B --> C[手动输入排产号\n点击查询或回车] + C --> D{排产号格式校验} + D -->|无效| E[底部红条提示:排产号格式无效] + E --> B + D -->|有效| F[调用后端查询工令号列表] + F --> G{查询结果} + G -->|未找到| H[底部红条提示:未找到该排产号信息] + H --> B + G -->|成功| I[工令号下拉列表填充\n已登记列表刷新] + + I --> J[选择工令号] + J --> K{工令号关联的总排号} + K -->|仅1个| L[自动填入总排号] + K -->|多个| M[总排号下拉供选择] + + L --> N[选择/输入附件类型] + M --> N + N --> O[输入数量] + O --> P[扫描或输入货位号] + P --> Q{全部字段有效?} + Q -->|否| R[相应字段标红提示] + Q -->|是| S[确认按钮激活] + + S --> T[点击确认或回车提交] + T --> U[调用后端保存接口] + U --> V{结果} + V -->|成功| W[成功反馈\n已登记列表刷新\n操作区部分重置\n保留排产号和工令号选择] + V -->|失败| X[保留数据\n底部状态栏显示错误] + + W --> Y{是否继续登记?} + Y -->|同排产号继续| N + Y -->|换排产号| B + + P --> P2{货位号类型判断} + P2 -->|现有记录在普通货架\n且目标为转运货架| DOWN[下架操作\n后端更新记录] + P2 -->|现有记录在转运货架| ERR[错误:已下架不可重新上架] +``` + +### 7.2 下架逻辑 + +附件的下架逻辑与产品完全一致: + +| 现有状态 | 目标货位 | 结果 | +|---|---|---| +| 未上架(location_code 为 NULL) | 普通货架 | 上架成功,新建 location_code | +| 未上架 | 转运货架 | 直接转运成功 | +| 普通货架 | 普通货架 | 错误:该附件已有货位记录 | +| 普通货架 | 转运货架 | 下架成功,更新 location_code | +| 转运货架 | 任何 | 错误:已下架不可重新上架 | + +### 7.3 已登记列表刷新策略 + +| 时机 | 行为 | +|---|---| +| 排产号查询成功 | 加载该排产号下全部附件记录 | +| 上架/下架提交成功 | 刷新列表 | +| 编辑/删除操作完成 | 刷新列表 | +| 切换排产号 | 替换为新排产号的记录 | + +--- + +## 8. 装箱集成 + +附件的装箱操作在**现有装箱编号模块**中完成,附件登记模块只负责上架和下架。 + +### 8.1 装箱模块改动 + +#### 8.1.1 接口响应扩展 + +`GET /CargoTrace/box/info` 接口的响应新增 `pending_accessories` 字段: + +```json +{ + "zongpai_no": "26BW0011", + "paichan_no": "W00009", + "work_order_no": "6-1(7)", + "quantity": 80, + "current_zongpai_boxes": [...], + "existing_boxes": [...], + "max_box_no": 5, + "suggested_box_no": 6, + "pending_accessories": [ + { + "accessory_id": 1, + "accessory_type": "安装配件", + "quantity": 5, + "location_code": "A01-02-03" + }, + { + "accessory_id": 2, + "accessory_type": "说明书", + "quantity": 10, + "location_code": "TRANS-01" + } + ] +} +``` + +#### 8.1.2 装箱页面展示 + +在装箱页面已分配列表下方,新增"待装箱附件"区域: + +``` +┌──────────────────────────────┐ +│ ...已分配产品列表... │ +├──────────────────────────────┤ +│ 待装箱附件(2 项) │ +│ 安装配件 × 5 [加入本箱] │ +│ 说明书 × 10 [加入本箱] │ +└──────────────────────────────┘ +``` + +- 工人点击 [加入本箱],将附件作为一条装箱明细写入 `finished_goods_box_item`,`item_type` 为 `accessory` +- 附件在装箱详情页中通过不同的行样式或标签区分 + +#### 8.1.3 装箱记录保存 + +`POST /CargoTrace/box` 接口的请求体新增可选字段: + +```json +{ + "zongpai_no": "26BW0011", + "box_no": 3, + "quantity": 80, + "item_type": "accessory", + "accessory_id": 1 +} +``` + +- `item_type`:默认为 `product`,附件装箱时传 `accessory` +- `accessory_id`:当 `item_type` 为 `accessory` 时必填,关联附件记录 + +--- + +## 9. 接口依赖 + +### 9.1 工令号查询接口(新增) + +**GET** `/CargoTrace/accessory/work-orders?paichan_no={paichan_no}` + +根据排产号查询该排产号下所有去重的工令号及其对应的总排号。 + +**请求参数:** + +| 参数 | 类型 | 位置 | 说明 | +|---|---|---|---| +| paichan_no | string | Query | 排产号 | + +**成功响应 200:** + +```json +{ + "paichan_no": "W00009", + "work_orders": [ + { + "work_order_no": "6-1(7)", + "zongpai_nos": ["26BW0010", "26BW0011", "26BW0012"] + }, + { + "work_order_no": "6-2(3)", + "zongpai_nos": ["26BW0013", "26BW0014"] + } + ] +} +``` + +**失败响应:** + +| HTTP 状态码 | 错误码 | 含义 | +|---|---|---| +| 400 | INVALID_PAICHAN | 排产号格式不合法 | +| 404 | PAICHA_NOT_FOUND | 未找到该排产号 | + +### 9.2 附件登记接口(新增) + +**POST** `/CargoTrace/accessory` + +**请求体:** + +```json +{ + "paichan_no": "W00009", + "zongpai_no": "26BW0011", + "accessory_type": "安装配件", + "quantity": 5, + "location_code": "A01-02-03" +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| paichan_no | string | 是 | 排产号 | +| zongpai_no | string | 是 | 总排号 | +| accessory_type | string | 是 | 附件类型名称 | +| quantity | int | 是 | 数量,必须为正整数 | +| location_code | string | 否 | 货位号,为空表示仅登记不上架 | + +**成功响应 200:** + +```json +{ + "id": 1, + "paichan_no": "W00009", + "zongpai_no": "26BW0011", + "accessory_type": "安装配件", + "quantity": 5, + "location_code": "A01-02-03", + "created_at": "2026-05-24T10:30:00" +} +``` + +### 9.3 附件记录查询接口(新增) + +**GET** `/CargoTrace/accessory?paichan_no={paichan_no}` + +查询指定排产号下所有附件记录。 + +**成功响应 200:** + +```json +{ + "paichan_no": "W00009", + "items": [ + { + "id": 1, + "zongpai_no": "26BW0011", + "accessory_type": "安装配件", + "quantity": 5, + "location_code": "A01-02-03", + "is_boxed": false, + "created_at": "2026-05-24T10:30:00" + } + ] +} +``` + +### 9.4 附件记录更新接口(新增) + +**PATCH** `/CargoTrace/accessory/{id}` + +**请求体:** + +```json +{ + "accessory_type": "安装配件", + "quantity": 8, + "location_code": "A01-02-04" +} +``` + +支持部分更新,仅传需要修改的字段。 + +### 9.5 附件记录删除接口(新增) + +**DELETE** `/CargoTrace/accessory/{id}` + +已装箱的附件不可删除,返回 `409 ALREADY_BOXED`。 + +### 9.6 附件类型管理接口(新增) + +**GET** `/CargoTrace/accessory-type** + +返回所有预设附件类型,按 sort_order 排序。 + +```json +{ + "types": [ + {"id": 1, "name": "安装配件", "sort_order": 0}, + {"id": 2, "name": "说明书", "sort_order": 1}, + {"id": 3, "name": "包装材料", "sort_order": 2} + ] +} +``` + +**POST** `/CargoTrace/accessory-type` — 新增类型 + +**PATCH** `/CargoTrace/accessory-type/{id}` — 修改类型名称或排序 + +**DELETE** `/CargoTrace/accessory-type/{id}` — 删除类型 + +--- + +## 10. 异常与错误处理 + +| 异常情况 | 触发条件 | 系统表现 | +|---|---|---| +| 排产号格式无效 | 不符合正则 | 底部红条提示"排产号格式无效" | +| 排产号未找到 | ERP 无记录 | 底部红条提示"未找到该排产号信息" | +| 附件类型为空 | 未选择或输入 | 提示"请选择或输入附件类型" | +| 数量无效 | 非正整数 | 提示"数量必须为正整数" | +| 货位号格式无效 | 不符合货位号规则 | 底部红条提示"无效的货位号" | +| 附件已有货位 | 重复上架普通货架 | 错误弹窗,显示已有货位信息 | +| 已下架附件 | 转运货架→任何 | 底部红条提示"该附件已下架不可重新上架" | +| 已装箱附件 | 尝试编辑/删除已装箱记录 | 底部红条提示"该附件已装箱不可操作" | +| 网络异常 | 超时或断网 | 底部黄条提示"网络异常,请检查网络连接" | + +--- + +## 11. 反馈机制 + +复用现有统一反馈标准: + +| 事件 | 屏幕 | 声音 | 振动 | +|---|---|---|---| +| 排产号查询成功 | 工令号下拉填充,绿色高亮 | 短促提示音 | 短震 | +| 上架成功 | 底部状态栏绿色提示 1.5 秒 | 成功音 | 长震 | +| 下架成功 | 底部状态栏绿色提示"下架成功" | 成功音 | 长震 | +| 操作失败 | 底部状态栏红色提示 2 秒 | 错误音 | 短震 | +| 网络异常 | 底部状态栏黄色提示 | 失败音 | 短震 | + +--- + +## 12. 非功能需求 + +| 项目 | 要求 | +|---|---| +| 工令号查询响应时间 | ≤ 500ms | +| 附件保存响应时间 | ≤ 500ms | +| 扫码到货位号填入延迟 | ≤ 100ms | +| 离线处理策略 | 断网时提示检查网络,数据保留在页面 | +| 软键盘控制 | 附件类型选择弹出选择面板时使用应用内 UI,不依赖系统软键盘 | + +--- + +## 13. 项目结构变更 + +### 13.1 Flutter 端 + +``` +lib/ +├── pages/ +│ ├── accessory_page.dart # 新增:附件登记主页面 +│ ├── boxing_page.dart # 修改:增加附件装箱展示 +│ └── boxing_detail_page.dart # 修改:附件装箱明细展示 +├── services/ +│ └── api_service.dart # 修改:新增附件相关 API 调用 +└── widgets/ + └── accessory_type_selector.dart # 新增:附件类型选择器组件 +``` + +### 13.2 FastAPI 端 + +``` +app/ +├── api/v1/ +│ ├── accessory.py # 新增:附件登记 API +│ ├── accessory_type.py # 新增:附件类型管理 API +│ └── box.py # 修改:装箱接口支持附件 +├── models/ +│ ├── finished_goods.py # 修改:新增附件模型 +│ └── accessory_type.py # 新增:附件类型模型 +├── schemas/ +│ ├── accessory.py # 新增:附件请求/响应 Schema +│ └── box.py # 修改:装箱 Schema 支持附件 +└── services/ + ├── accessory_service.py # 新增:附件业务逻辑 + └── box_service.py # 修改:装箱逻辑支持附件 +``` + +--- + +## 14. 超出当前版本范围 + +以下内容在当前版本中不实现: + +- 附件到货数量与 ERP BOM 对比校验(附件数量是否与订单需求匹配) +- 附件条码标签生成(为附件打印临时标签) +- 附件库存盘点功能 +- 附件出入库历史查询