Files
shanxia2.0/docs/superpowers/plans/2026-09-13-mini-pl-driver-plan.md
T
wuzilongandCursor 5102817782 docs: add mini driver transport plan implementation plan
Break the approved driver-plan design into schema, web arrange/escort,
login APIs, menu SQL, and miniprogram list/detail tasks.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 10:26:07 +08:00

27 KiB
Raw Blame History

小程序 · 司机运输计划单 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: 在小程序「运输管理」下新增「司机运输计划单」,登录态完成与 H5 司机链接相同的运输记录列表与司机回执;安排运输支持可选押运员;驾押人员可关联系统用户;仅「司机」「押运员」角色有菜单权限。

Architecture: Schema 增加 pl_personnel.sys_user_id 与运输/计划押运字段;从 public_transport 抽出按 tenant_no/plan_id/personnel_id 构建列表与详情、保存回执的 service;新增 JWT 路由 /api/transport/driver-plan/*;小程序原生 list/detail 对齐现场「运输计划单」壳子 + H5 司机页正文。公开 token API 与司机链接保留不变。

Tech Stack: FastAPI + SQLAlchemy、Vue3 Element Plus(web-admin)、uni-app Vue3(miniprogram)、dbRequest Bearer、uni-calendar、uni.uploadFile、sys_menu/sys_role/sys_role_menu 迁移 SQL。

Spec: docs/superpowers/specs/2026-09-13-mini-pl-driver-plan-design.md


文件结构

路径 职责
docs/sql/migrations/2026-09-13-pl-driver-plan-schema.sql sys_user_id + escort 字段 DDL
docs/sql/migrations/2026-09-13-mini-pl-driver-plan-menus.sql 菜单 + 角色补齐 + 授权
backend/app/models/pl_models.py ORM 字段
backend/app/schemas/pl/base_data.py Personnel 读写 sysUserId
backend/app/schemas/pl/resource_snapshot.py selectedEscortId
backend/app/schemas/pl/transport.py / plan.py escort 入参/出参
backend/app/api/pl/base_data.py 人员 CRUD 校验一人一账号
backend/app/services/pl_plan_service.py 等安排路径 写入 escort + 快照
backend/app/services/pl_driver_plan_service.py 登录态/公开态共用:history、详情、保存、上传
backend/app/api/pl/driver_plan.py JWT 路由 /driver-plan
backend/app/api/pl/public_transport.py 尽量改为调用 service(行为不变)
backend/app/api/pl/__init__.py include_router(driver_plan.router)
backend/app/core/config.py /api/transport/driver-plan 加解密排除
backend/tests/test_pl_driver_plan_service.py 可见性/日期/人员解析单测
web-admin/.../pl-personnel-form-dialog + personnel 列表 关联用户
web-admin/.../pl-plan-form-dialog(及安排入口) 选择押运员
miniprogram/api/pl.js driverPlanApi
miniprogram/config/api.js / workbench-menus.js / pages.json 菜单与路由
miniprogram/pages/pl/driver-plan/list.vue 运输记录列表
miniprogram/pages/pl/driver-plan/detail.vue 司机回执
miniprogram/styles/pl-driver-plan.scss 正文样式(三墙绿,可对照 pl-site-plan.scss)

Task 1: Schema 迁移 + ORM

Files:

  • Create: docs/sql/migrations/2026-09-13-pl-driver-plan-schema.sql

  • Modify: backend/app/models/pl_models.py(PlPersonnel、PlTransport、PlPlan)

  • Step 1: 写迁移 SQL

-- docs/sql/migrations/2026-09-13-pl-driver-plan-schema.sql
SET NAMES utf8mb4;

-- 驾押人员关联系统用户
ALTER TABLE `pl_personnel`
  ADD COLUMN `sys_user_id` varchar(64) DEFAULT NULL COMMENT '关联系统用户ID' AFTER `photo_urls`;

-- 运输单押运(与司机对称)
ALTER TABLE `pl_transport`
  ADD COLUMN `escort_id` varchar(64) DEFAULT NULL COMMENT '押运员人员ID' AFTER `driver_phone`,
  ADD COLUMN `escort_name` varchar(100) DEFAULT NULL COMMENT '押运员姓名' AFTER `escort_id`,
  ADD COLUMN `escort_phone` varchar(32) DEFAULT NULL COMMENT '押运员电话' AFTER `escort_name`;

-- 计划冗余押运(与 driver_id/driver_name 对称)
ALTER TABLE `pl_plan`
  ADD COLUMN `escort_id` varchar(64) DEFAULT NULL COMMENT '押运员人员ID' AFTER `driver_name`,
  ADD COLUMN `escort_name` varchar(100) DEFAULT NULL COMMENT '押运员姓名' AFTER `escort_id`,
  ADD COLUMN `escort_phone` varchar(32) DEFAULT NULL COMMENT '押运员电话' AFTER `escort_name`;

若某列已存在,执行前用 INFORMATION_SCHEMA 检查或手工跳过(MySQL 5.7 无 IF NOT EXISTS 列语法)。

  • Step 2: 更新 ORM

在 PlPersonnel 增加:

sys_user_id = Column(String(64), nullable=True, comment="关联系统用户ID")

在 PlTransport(driver_phone 后)与 PlPlan(driver_name 后)各增加:

escort_id = Column(String(64), nullable=True, comment="押运员人员ID")
escort_name = Column(String(100), nullable=True, comment="押运员姓名")
escort_phone = Column(String(32), nullable=True, comment="押运员电话")
  • Step 3: 本地/测试库执行 SQL 并确认列存在
# 按环境执行迁移后:
mysql ... -e "SHOW COLUMNS FROM pl_personnel LIKE 'sys_user_id'; SHOW COLUMNS FROM pl_transport LIKE 'escort%';"

Expected: 列存在

  • Step 4: Commit
git add docs/sql/migrations/2026-09-13-pl-driver-plan-schema.sql backend/app/models/pl_models.py
git commit -m "feat(pl): add personnel sys_user_id and transport escort columns"

Task 2: Schema/API — 人员关联用户 + 快照 selectedEscortId

Files:

  • Modify: backend/app/schemas/pl/base_data.py

  • Modify: backend/app/schemas/pl/resource_snapshot.py

  • Modify: backend/app/services/pl_transport_resource_snapshot_service.py

  • Modify: backend/app/api/pl/base_data.py(或 pl_personnel_service 若字段写入在此)

  • Create: backend/tests/test_pl_personnel_sys_user_bind.py

  • Step 1: 写失败单测(一人一账号)

# backend/tests/test_pl_personnel_sys_user_bind.py
from app.services.pl_personnel_user_bind import assert_sys_user_bind_available


class FakeRow:
    def __init__(self, id, sys_user_id, del_flag=0):
        self.id = id
        self.sys_user_id = sys_user_id
        self.del_flag = del_flag


def test_bind_ok_when_empty():
    assert_sys_user_bind_available(
        existing=[], personnel_id="p1", sys_user_id="u1"
    )


def test_bind_rejects_other_personnel():
    try:
        assert_sys_user_bind_available(
            existing=[FakeRow("p2", "u1")],
            personnel_id="p1",
            sys_user_id="u1",
        )
        assert False, "expected ValueError"
    except ValueError as e:
        assert "已关联" in str(e)


def test_bind_allows_self():
    assert_sys_user_bind_available(
        existing=[FakeRow("p1", "u1")],
        personnel_id="p1",
        sys_user_id="u1",
    )
  • Step 2: 跑测确认失败
cd backend && source venv/bin/activate
pytest tests/test_pl_personnel_sys_user_bind.py -v

Expected: FAIL(模块不存在)

  • Step 3: 实现校验 helper + schema 字段
# backend/app/services/pl_personnel_user_bind.py
from typing import Iterable, Optional


def assert_sys_user_bind_available(
    *, existing: Iterable, personnel_id: str, sys_user_id: Optional[str]
) -> None:
    uid = (sys_user_id or "").strip()
    if not uid:
        return
    for row in existing:
        if getattr(row, "del_flag", 0) == 1:
            continue
        if (getattr(row, "sys_user_id", None) or "").strip() != uid:
            continue
        if row.id == personnel_id:
            continue
        raise ValueError("该系统用户已关联其他驾押人员")

在 PersonnelBase / PersonnelUpdate / PersonnelRead 增加:

sys_user_id: Optional[str] = Field(default=None, alias="sysUserId", max_length=64)

在 TransportResourceSnapshot 增加:

selected_escort_id: Optional[str] = Field(default=None, alias="selectedEscortId")

build_transport_resource_snapshot(..., selected_escort_id: Optional[str] = None) 写入该字段。

在人员 create/update 路径:查询同租户 sys_user_id 冲突后调用 assert_sys_user_bind_available,再写入。

  • Step 4: 跑测通过
pytest tests/test_pl_personnel_sys_user_bind.py -v

Expected: PASS

  • Step 5: Commit
git add backend/app/schemas/pl/base_data.py backend/app/schemas/pl/resource_snapshot.py \
  backend/app/services/pl_personnel_user_bind.py backend/app/services/pl_transport_resource_snapshot_service.py \
  backend/app/api/pl/base_data.py backend/tests/test_pl_personnel_sys_user_bind.py
git commit -m "feat(pl): bind sys user on personnel and snapshot selectedEscortId"

Task 3: Web — 驾押人员「关联用户账号」

Files:

  • Modify: web-admin/src/api/pl/index.ts(Personnel 类型加 sysUserId)

  • Modify: web-admin/src/components/business/pl-personnel-form-dialog/index.vue

  • Modify: web-admin/src/views/pl/personnel/index.vue(列表列可选;查看态展示)

  • Step 1: 表单增加关联用户

在表单中(姓名附近)增加:

<el-form-item label="关联用户账号">
  <el-select
    v-model="formData.sysUserId"
    placeholder="可选,绑定小程序登录账号"
    filterable
    clearable
    :loading="userSelectLoading"
  >
    <el-option
      v-for="u in userOptions"
      :key="u.userId"
      :label="u.displayName"
      :value="u.userId"
    />
  </el-select>
</el-form-item>
  • fetchUserOptions 来自 @/api/admin(与加油登记页相同)

  • formData.sysUserId 读写;提交 payload 带 sysUserId(空则清空)

  • 后端 400「已关联」时用 ElMessage.error 展示

  • Step 2: 列表/查看展示(精简)

列表可增加「关联用户」列(min-width),无则显示 —。查看弹窗同步展示。

  • Step 3: 手工冒烟
  1. 人员 A 绑用户 U → 保存成功
  2. 人员 B 再绑 U → 失败提示
  3. A 清空关联 → 成功
  • Step 4: Commit
git add web-admin/src/api/pl/index.ts \
  web-admin/src/components/business/pl-personnel-form-dialog/index.vue \
  web-admin/src/views/pl/personnel/index.vue
git commit -m "feat(web-admin): link system user on personnel form"

Task 4: 安排运输 — 选择押运员(后端写入 + Web UI)

Files:

  • Modify: backend/app/schemas/pl/transport.py、plan.py(transport 块增加 escortId)

  • Modify: 安排写入路径(pl_plan_service.py / transport.py 中设置 driver_id 处对称写 escort,并 build_transport_resource_snapshot(..., selected_escort_id=...))

  • Modify: web-admin/src/components/business/pl-plan-form-dialog/index.vue

  • 若安排入口还在 usePlPlanArrangeSubmit.ts / 其它 dialog,同步字段

  • Step 1: 后端写入 escort

在解析司机信息旁增加 _resolve_personnel_info(可复用司机解析)并:

transport.escort_id = escort_info["id"] if escort_info else None
transport.escort_name = escort_info["name"] if escort_info else None
transport.escort_phone = escort_info.get("phone") if escort_info else None
plan.escort_id = transport.escort_id
plan.escort_name = transport.escort_name
# plan.escort_phone 若模型已加则同步
snapshot = build_transport_resource_snapshot(
    db, tenant_no, vehicle_id,
    selected_driver_id=driver_id,
    selected_escort_id=transport.escort_id,
)

清空安排时同步清空 escort 字段。

  • Step 2: Web UI — 选择押运员

紧挨「选择司机」:

<el-form-item label="选择押运员" class="pl-plan-transport-row__escort">
  <el-select
    v-model="selectedEscortId"
    placeholder="请选择押运员(可选)"
    filterable
    remote
    clearable
    :remote-method="searchEscorts"
    :loading="escortSelectLoading"
    :disabled="!formData.vehicleId"
    @focus="onEscortSelectFocus"
  >
    <el-option
      v-for="d in escortList"
      :key="d.id"
      :label="formatDriverOptionLabel(d)"
      :value="d.id"
    />
  </el-select>
</el-form-item>

行为对齐司机:

  • 选车后:从 vehicleBundle.personnelList 取 role === 'escort',默认 isDefault===1 或第一个

  • searchEscorts:人员库检索,过滤 personnelType 含 escort/both(与现有人员 API 参数对齐)

  • 提交 transport.escortId: selectedEscortId || undefined

  • 回填已安排计划的 escortId

  • Step 3: 冒烟

选车自动带出押运 → 可改可清空 → 保存后 DB pl_transport.escort_id 正确;改押运后旧 ID 被覆盖。

  • Step 4: Commit
git add backend/app/schemas/pl/transport.py backend/app/schemas/pl/plan.py \
  backend/app/services/pl_plan_service.py backend/app/api/pl/transport.py \
  web-admin/src/components/business/pl-plan-form-dialog/index.vue
git commit -m "feat(pl): optional escort selection on transport arrange"

Task 5: Service — 司机计划可见性 + history 构建

Files:

  • Create: backend/app/services/pl_driver_plan_service.py

  • Create: backend/tests/test_pl_driver_plan_service.py

  • Step 1: 写失败单测

# backend/tests/test_pl_driver_plan_service.py
from datetime import date, datetime

from app.api.pl.public_transport import _history_matches_date
from app.services.pl_driver_plan_service import transport_visible_to_personnel


def test_history_date_or_logic():
    day = date(2026, 9, 13)
    assert _history_matches_date(datetime(2026, 9, 13, 9, 0), None, day) is True
    assert _history_matches_date(None, datetime(2026, 9, 13, 18, 0), day) is True
    assert _history_matches_date(
        datetime(2026, 9, 12, 9, 0), datetime(2026, 9, 14, 18, 0), day
    ) is False


def test_visible_as_driver_or_escort():
    class T:
        def __init__(self, driver_id=None, escort_id=None):
            self.driver_id = driver_id
            self.escort_id = escort_id

    assert transport_visible_to_personnel(T(driver_id="p1"), "p1") is True
    assert transport_visible_to_personnel(T(escort_id="p1"), "p1") is True
    assert transport_visible_to_personnel(T(driver_id="p2", escort_id="p3"), "p1") is False
    assert transport_visible_to_personnel(T(), "p1") is False
  • Step 2: 跑测确认失败
pytest tests/test_pl_driver_plan_service.py -v

Expected: FAIL(transport_visible_to_personnel 不存在)

  • Step 3: 实现 service 核心
# backend/app/services/pl_driver_plan_service.py
"""运输计划 · 司机/押运登录态(history / page / feedback / upload)"""
from __future__ import annotations

from typing import Optional, Tuple

from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session

from app.models.pl_models import PlPersonnel, PlPlan, PlPlanTransport, PlTransport
from app.services.pl_public_transport_helpers import _parse_history_date


def transport_visible_to_personnel(transport: PlTransport, personnel_id: str) -> bool:
    pid = (personnel_id or "").strip()
    if not pid:
        return False
    return (transport.driver_id or "").strip() == pid or (transport.escort_id or "").strip() == pid


def resolve_personnel_for_user(
    db: Session, *, tenant_no: str, user_id: str
) -> Optional[PlPersonnel]:
    uid = (user_id or "").strip()
    if not uid:
        return None
    return (
        db.query(PlPersonnel)
        .filter(
            PlPersonnel.tenant_no == tenant_no,
            PlPersonnel.sys_user_id == uid,
            PlPersonnel.del_flag == 0,
        )
        .first()
    )


def resolve_transport_for_driver_plan(
    db: Session, *, tenant_no: str, plan_id: str, personnel_id: str
) -> Tuple[Optional[PlPlan], Optional[PlTransport]]:
    """本租户计划 + 运输,且当前人员是司机或押运。"""
    plan = (
        db.query(PlPlan)
        .filter(PlPlan.id == plan_id, PlPlan.tenant_no == tenant_no, PlPlan.del_flag == 0)
        .first()
    )
    if not plan:
        return None, None
    link = (
        db.query(PlPlanTransport)
        .filter(PlPlanTransport.plan_id == plan.id, PlPlanTransport.del_flag == 0)
        .order_by(PlPlanTransport.create_time.desc())
        .first()
    )
    if not link or not link.transport_id:
        return plan, None
    transport = (
        db.query(PlTransport)
        .filter(
            PlTransport.id == link.transport_id,
            PlTransport.tenant_no == tenant_no,
            PlTransport.del_flag == 0,
        )
        .first()
    )
    if not transport or not transport_visible_to_personnel(transport, personnel_id):
        return plan, None
    return plan, transport

继续实现 build_driver_history_page:复制 public_transport.get_public_transport_history 的查询骨架,将 PlTransport.license_plate == plate 改为:

or_(
    PlTransport.driver_id == personnel_id,
    PlTransport.escort_id == personnel_id,
)

日期条件保持 pickup/delivery OR(不要加 plan_date)。
详情/保存:调用现有 _build_public_page / feedback / upload 逻辑(从 public_transport 抽出或直接 import 内部函数后改为 service 导出,行为不变)。

  • Step 4: 跑测通过
pytest tests/test_pl_driver_plan_service.py tests/test_pl_public_transport_history.py -v

Expected: PASS

  • Step 5: Commit
git add backend/app/services/pl_driver_plan_service.py backend/tests/test_pl_driver_plan_service.py \
  backend/app/api/pl/public_transport.py
git commit -m "feat(pl): add driver-plan service for personnel-scoped history"

Task 6: JWT 路由 /api/transport/driver-plan

Files:

  • Create: backend/app/api/pl/driver_plan.py

  • Modify: backend/app/api/pl/__init__.py

  • Modify: backend/app/core/config.py(API_ENCRYPT_EXCLUDE_PATHS 增加 /api/transport/driver-plan,)

  • Step 1: 实现路由(对齐 site_plan.py)

# backend/app/api/pl/driver_plan.py
"""小程序/门户登录态 · 司机运输计划单"""
from __future__ import annotations

from typing import Optional

from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
from sqlalchemy.orm import Session

from app.core.database import get_db
from app.core.db_auth import DbUserContext, get_db_current_user
from app.core.response import error_response, success_response
from app.core.tenant_context import get_tenant_no
from app.schemas.pl.transport import (
    PublicTransportDriverStopFeedback,
    PublicTransportFeedback,
)
from app.services import pl_driver_plan_service as drv_svc

router = APIRouter(prefix="/driver-plan", tags=["运输计划-司机计划单"])


@router.get("/history")
def get_driver_plan_history(
    date: str = Query(..., description="筛选日 YYYY-MM-DD"),
    db: Session = Depends(get_db),
    user: DbUserContext = Depends(get_db_current_user),
):
    tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
    page, err = drv_svc.build_driver_history_page(
        db,
        tenant_no=tenant_no,
        user_id=user.id,
        date_str=date,
    )
    if err:
        return error_response(400, err)
    return success_response(page.model_dump(by_alias=True))


@router.get("/{plan_id}")
def get_driver_plan_detail(...):
    # resolve personnel + transport; 404 if not visible
    ...


@router.post("/{plan_id}/feedback")
def save_driver_plan_feedback(...):
    ...


@router.post("/{plan_id}/driver-feedback")
def save_driver_plan_stop_feedback(...):
    ...


@router.post("/{plan_id}/attachments")
def upload_driver_plan_attachment(...):
    ...

DbUserContext 使用 user.id(见 backend/app/core/db_auth.py)。

  • Step 2: 注册 router

在 backend/app/api/pl/__init__.py:include_router(driver_plan.router)(与 site_plan 同级)。

  • Step 3: 冒烟 curl(需登录 token + 已绑人员的测试用户)
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:8000/api/transport/driver-plan/history?date=$(date +%F)" | head

Expected: code=0,records 数组

  • Step 4: Commit
git add backend/app/api/pl/driver_plan.py backend/app/api/pl/__init__.py backend/app/core/config.py
git commit -m "feat(pl): add JWT driver-plan routes for miniprogram"

Task 7: 菜单 / 角色 SQL

Files:

  • Create: docs/sql/migrations/2026-09-13-mini-pl-driver-plan-menus.sql

  • Step 1: 写幂等 SQL

-- 小程序:运输管理 → 司机运输计划单(角色「司机」「押运员」)
SET NAMES utf8mb4;

-- 测试库补角色(生产已有则跳过)
INSERT INTO `sys_role` (
  `id`, `role_name`, `data_scope`, `status`, `tenant_no`, `order_num`, `remark`,
  `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
  'MINI_ROLE_DRIVER', '司机', '1', '1', 'shanxia2.0', 60, '小程序司机端',
  'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
  SELECT 1 FROM `sys_role` WHERE `role_name` = '司机' AND `del_flag` = 0
);

INSERT INTO `sys_role` (
  `id`, `role_name`, `data_scope`, `status`, `tenant_no`, `order_num`, `remark`,
  `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
  'MINI_ROLE_ESCORT', '押运员', '1', '1', 'shanxia2.0', 61, '小程序押运端',
  'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
  SELECT 1 FROM `sys_role` WHERE `role_name` = '押运员' AND `del_flag` = 0
);

INSERT INTO `sys_menu` (...)
SELECT
  'MINI_PL_ROOT', '运输管理', '1', 'M', '/mini/pl', NULL, NULL,
  ...
WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_PL_ROOT' AND `del_flag` = 0);

INSERT INTO `sys_menu` (...)
SELECT
  'MINI_PL_DRIVER_PLAN', '司机运输计划单', 'MINI_PL_ROOT', 'C', 'driver-plan', NULL,
  'mini:pl:driverPlan:list',
  1, '1', '1', 'shanxia2.0', 3, '小程序司机/押运运输记录与回执',
  'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
  SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_PL_DRIVER_PLAN' AND `del_flag` = 0
);

INSERT INTO `sys_role_menu` (...)
SELECT
  MD5(CONCAT(r.id, '|', m.menu_id, '|mini-pl-driver-plan|v1')),
  r.id, m.menu_id, IFNULL(NULLIF(r.tenant_no, ''), 'shanxia2.0'),
  0, '小程序司机运输计划单',
  'system', '', NOW(), NULL, NOW(), 0
FROM `sys_role` r
JOIN (
  SELECT '1' AS menu_id UNION ALL
  SELECT 'MINI_PL_ROOT' UNION ALL
  SELECT 'MINI_PL_DRIVER_PLAN'
) m
WHERE r.del_flag = 0
  AND r.role_name IN ('司机', '押运员')
  AND NOT EXISTS (
    SELECT 1 FROM `sys_role_menu` x
    WHERE x.role_id = r.id AND x.menu_id = m.menu_id AND x.del_flag = 0
  );

完整列清单复制 2026-09-13-mini-pl-transport-plan-menus.sql 风格。

  • Step 2: 测试库执行;生产库执行前核对已有「司机」「押运员」角色名

  • Step 3: Commit

git add docs/sql/migrations/2026-09-13-mini-pl-driver-plan-menus.sql
git commit -m "docs(sql): add mini driver-plan menu and role bindings"

Task 8: 小程序 — 菜单配置 + API + 列表页

Files:

  • Modify: miniprogram/api/pl.js

  • Modify: miniprogram/config/api.js

  • Modify: miniprogram/config/workbench-menus.js

  • Modify: miniprogram/pages.json

  • Create: miniprogram/pages/pl/driver-plan/list.vue

  • Create: miniprogram/styles/pl-driver-plan.scss(可从 pl-site-plan.scss 拷贝后改类名)

  • Step 1: API 与路由

// miniprogram/api/pl.js
export const driverPlanApi = {
  history: async (params = {}) => {
    const data = unwrapData(await db.get('/transport/driver-plan/history', params))
    return {
      date: data?.date || params.date,
      records: Array.isArray(data?.records) ? data.records : []
    }
  },
  get: async (planId) => unwrapData(await db.get(`/transport/driver-plan/${planId}`)),
  saveFeedback: (planId, payload) =>
    db.post(`/transport/driver-plan/${planId}/feedback`, payload),
  saveDriverFeedback: (planId, payload) =>
    db.post(`/transport/driver-plan/${planId}/driver-feedback`, payload)
}

PAGE_PATHS.PL_DRIVER_PLAN_LIST = '/pages/pl/driver-plan/list'
PAGE_PATHS.PL_DRIVER_PLAN_DETAIL = '/pages/pl/driver-plan/detail'
WORKBENCH_ROUTE_MAP['pl-driver-plan'] → list
workbench 菜单项:key/code: pl-driver-plan,perms: mini:pl:driverPlan:list,标题「司机运输计划单」
pages.json 分包 pages/pl 注册 list/detail,navigationStyle: "custom"

  • Step 2: 列表页

对照 miniprogram/pages/pl/transport-plan/list.vue:

  • 顶栏标题「司机运输计划单」,bx-page + safeAreaStyle

  • uni-calendar 选日,调用 driverPlanApi.history({ date })

  • 不要搜索框

  • 卡片对齐 H5 history.vue / 现场 list 卡片结构

  • 点击 → navigateTo detail ?planId=

  • 空态文案「暂无运输记录」

  • Step 3: 真机/开发者工具:司机角色可见菜单;列表按日加载

  • Step 4: Commit

git add miniprogram/api/pl.js miniprogram/config/api.js miniprogram/config/workbench-menus.js \
  miniprogram/pages.json miniprogram/pages/pl/driver-plan/list.vue \
  miniprogram/styles/pl-driver-plan.scss
git commit -m "feat(miniprogram): add driver-plan list and workbench menu"

Task 9: 小程序 — 司机回执详情页

Files:

  • Create: miniprogram/pages/pl/driver-plan/detail.vue

  • Modify: miniprogram/styles/pl-driver-plan.scss(详情样式)

  • 对照:web-admin/src/views/public/pl/transport/index.vue + miniprogram/pages/pl/transport-plan/detail.vue

  • Step 1: 实现详情

  • 顶栏标题「司机回执」

  • onLoad 读 planId → driverPlanApi.get

  • 展示公开回执页同等字段:计划摘要、人员、停靠/产品、反馈录入、附件

  • 只读态:planVoided / planArchived

  • 附件:uni.chooseImage + uni.uploadFile 到 /transport/driver-plan/{planId}/attachments(Bearer + folder),路径须在加密排除中

  • 保存调用与 H5 对等的 feedback / driver-feedback

  • 底栏:回列表 + 保存回执

实现时优先对照公开页字段与交互,壳子复用现场 detail 的 bx-page / section-card 模式;若公开页能力多于现场(如磅单二维码),按公开页一并带上(若依赖公开 URL,用 WEB_ORIGIN 拼链或后续任务补齐,但不得静默丢主流程回执保存)。

  • Step 2: 冒烟

司机账号打开今日单 → 保存回执 → 管理端/公开链接可见同一数据;改司机后旧账号进详情失败/空态。

  • Step 3: Commit
git add miniprogram/pages/pl/driver-plan/detail.vue miniprogram/styles/pl-driver-plan.scss
git commit -m "feat(miniprogram): add driver-plan receipt detail page"

Task 10: 验收对照 + graphify

  • Step 1: 按 spec §11 验收清单逐项勾选

重点:

  1. 仅司机/押运员菜单
  2. 日历 + 卡片
  3. 张三→李四切换可见性;押运同理
  4. 押运可选可搜自动带出
  5. 人员绑用户一人一账号
  6. 公开司机链接仍可用
  • Step 2: 更新知识图谱
./scripts/update-graph.sh backend
# 若改了 web-admin:
./scripts/update-graph.sh frontend
  • Step 3: Commit graphify(若有变更)
git add backend/graphify-out web-admin/graphify-out graphify-out
git commit -m "chore: update graphify after driver-plan feature"

Spec 覆盖自检

Spec 项 Task
D1 原生 + 登录态 API 5–9
D2/D3 列表/详情对齐 + 备用金顶栏 8–9
D4 当前司机/押运可见性 5
D5/D6 选择押运员 4
D7 关联用户 2–3
D8 角色菜单 7
D9 公开链接保留 5(不改公开行为)、验收
Schema escort + sys_user_id 1
加密排除 6

占位符扫描

无 TBD/TODO;路由与 perms 字面量已写死;鉴权用户主键为 DbUserContext.id。