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>
27 KiB
小程序 · 司机运输计划单 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: 手工冒烟
- 人员 A 绑用户 U → 保存成功
- 人员 B 再绑 U → 失败提示
- 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 卡片结构 -
点击 →
navigateTodetail?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 验收清单逐项勾选
重点:
- 仅司机/押运员菜单
- 日历 + 卡片
- 张三→李四切换可见性;押运同理
- 押运可选可搜自动带出
- 人员绑用户一人一账号
- 公开司机链接仍可用
- 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。