Files
shanxia2.0/docs/superpowers/plans/2026-08-02-pl-vehicle-personnel-photo.md
T

18 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: 为 pl_vehicle / pl_personnel 增加可选 photo_urls(默认 1 张图),并在三方车辆、山峡自有车辆、三方/山峡驾押人员四页完成列表缩略图、表单上传与查看展示。

Architecture: 库表固定字段存逗号分隔 MinIO URL;后端 Schema/Create/Update/Read 透传 photoUrls;前端复用 AttachmentUploadField(limit=1、accept="image/*"),列表用 48×48 缩略图取第一段 URL。

Tech Stack: MySQL 5.7 · FastAPI · SQLAlchemy · Pydantic v2 · Vue 3 · Element Plus · MinIO

Spec: docs/superpowers/specs/2026-08-02-pl-vehicle-personnel-photo-design.md
Demo: docs/fresh-mint-pl-vehicle-personnel-photo-demo.html


文件结构

操作 文件 职责
Create backend/scripts/migrate_20260802_pl_vehicle_personnel_photo_urls.sql 加列迁移
Modify docs/sql/shanxia2.0.sql 基线 DDL 同步
Modify backend/app/models/pl_models.py PlVehicle / PlPersonnel 列
Modify backend/app/schemas/pl/base_data.py Create/Update/Base 增加 photoUrls
Modify backend/app/api/pl/base_data.py create 构造写入 photo_urls(update 经 setattr 自动)
Create backend/tests/test_pl_vehicle_personnel_photo_schema.py Schema 接受 photoUrls
Modify web-admin/src/api/pl/index.ts 类型增加 photoUrls
Create web-admin/src/utils/pl-photo.ts 取首图 URL + MinIO 完整地址
Modify web-admin/src/views/pl/vehicle/index.vue 三方车辆 UI
Modify web-admin/src/views/pl/self-vehicle/index.vue 自有车辆 UI
Modify web-admin/src/views/pl/personnel/index.vue 驾押人员 UI(双归属共用)

部署顺序

1. 执行 migrate_20260802_pl_vehicle_personnel_photo_urls.sql
2. 部署 backend
3. 部署 web-admin
4. 按 Task 8 验收

Task 1: DB 迁移与 SQL 基线

Files:

  • Create: backend/scripts/migrate_20260802_pl_vehicle_personnel_photo_urls.sql

  • Modify: docs/sql/shanxia2.0.sql(pl_vehicle / pl_personnel 的 CREATE TABLE)

  • Step 1: 写迁移脚本

-- backend/scripts/migrate_20260802_pl_vehicle_personnel_photo_urls.sql
-- 车辆/驾押人员:增加照片 URL 字段(逗号分隔,默认 1 张)

ALTER TABLE `pl_vehicle`
  ADD COLUMN `photo_urls` varchar(2000) DEFAULT NULL COMMENT '车辆照片URL,逗号分隔(默认1张)' AFTER `vehicle_status`;

ALTER TABLE `pl_personnel`
  ADD COLUMN `photo_urls` varchar(2000) DEFAULT NULL COMMENT '人员照片URL,逗号分隔(默认1张)' AFTER `status`;
  • Step 2: 更新 docs/sql/shanxia2.0.sql

在 CREATE TABLE pl_vehicle 中,于 `vehicle_status` ... 行后插入:

  `photo_urls` varchar(2000) DEFAULT NULL COMMENT '车辆照片URL,逗号分隔(默认1张)',

在 CREATE TABLE pl_personnel 中,于 `status` ... 行后插入:

  `photo_urls` varchar(2000) DEFAULT NULL COMMENT '人员照片URL,逗号分隔(默认1张)',

(若基线中有对应 INSERT 列清单,无需改历史 INSERT;新列可为 NULL。)

  • Step 3: 在目标库执行迁移(本地)

Run(按本地习惯选客户端):

# 示例:在 backend 环境对当前库执行
cd backend && source venv/bin/activate
# 使用项目惯用方式执行 SQL;或 mysql 客户端:
# mysql ... < scripts/migrate_20260802_pl_vehicle_personnel_photo_urls.sql

Expected: 两表均有 photo_urls 列,无报错。若列已存在则跳过或先检查 SHOW COLUMNS。

  • Step 4: Commit
git add backend/scripts/migrate_20260802_pl_vehicle_personnel_photo_urls.sql docs/sql/shanxia2.0.sql
git commit -m "$(cat <<'EOF'
chore(db): add photo_urls to pl_vehicle and pl_personnel

EOF
)"

Task 2: ORM 模型

Files:

  • Modify: backend/app/models/pl_models.py

  • Step 1: 在 PlVehicle 增加列

在 vehicle_status = Column(...) 之后、tenant_no = Column(...) 之前插入:

    photo_urls = Column(String(2000), nullable=True, comment="车辆照片URL,逗号分隔(默认1张)")
  • Step 2: 在 PlPersonnel 增加列

在 status = Column(...) 之后、tenant_no = Column(...) 之前插入:

    photo_urls = Column(String(2000), nullable=True, comment="人员照片URL,逗号分隔(默认1张)")
  • Step 3: Commit
git add backend/app/models/pl_models.py
git commit -m "$(cat <<'EOF'
feat(pl): map photo_urls on vehicle and personnel models

EOF
)"

Task 3: Schema + API 透传(含单测)

Files:

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

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

  • Create: backend/tests/test_pl_vehicle_personnel_photo_schema.py

  • Step 1: 写失败测试

"""车辆/人员 Schema 接受 photoUrls。"""
from __future__ import annotations

from decimal import Decimal

from app.schemas.pl.base_data import PersonnelCreate, PersonnelUpdate, VehicleCreate, VehicleUpdate


def test_vehicle_create_accepts_photo_urls():
    body = VehicleCreate.model_validate(
        {
            "vehicleType": "carrier",
            "licensePlate": "皖A12345",
            "vehicleStatus": "available",
            "photoUrls": "pl/vehicle-photo/a.jpg",
        }
    )
    assert body.photo_urls == "pl/vehicle-photo/a.jpg"


def test_vehicle_update_accepts_photo_urls():
    body = VehicleUpdate.model_validate({"photoUrls": "pl/vehicle-photo/b.jpg"})
    assert body.photo_urls == "pl/vehicle-photo/b.jpg"


def test_personnel_create_accepts_photo_urls():
    body = PersonnelCreate.model_validate(
        {
            "personnelName": "张三",
            "personnelType": "driver",
            "personnelAffiliation": "self",
            "status": "1",
            "photoUrls": "pl/personnel-photo/c.jpg",
        }
    )
    assert body.photo_urls == "pl/personnel-photo/c.jpg"


def test_personnel_update_accepts_photo_urls():
    body = PersonnelUpdate.model_validate({"photoUrls": ""})
    assert body.photo_urls == ""
  • Step 2: 跑测确认失败

Run: cd backend && source venv/bin/activate && pytest tests/test_pl_vehicle_personnel_photo_schema.py -v
Expected: FAIL(photoUrls / photo_urls 额外字段被忽略或校验失败,断言失败)

  • Step 3: 改 Schema

在 VehicleBase(约 vehicle_status 行后、remark 前)增加:

    photo_urls: Optional[str] = Field(
        default=None, max_length=2000, alias="photoUrls", description="车辆照片URL,逗号分隔"
    )

在 VehicleUpdate(vehicle_status 与 remark 之间)增加:

    photo_urls: Optional[str] = Field(default=None, max_length=2000, alias="photoUrls")

在 PersonnelBase(status 与 remark 之间)增加:

    photo_urls: Optional[str] = Field(
        default=None, max_length=2000, alias="photoUrls", description="人员照片URL,逗号分隔"
    )

在 PersonnelUpdate(status 与 remark 之间)增加:

    photo_urls: Optional[str] = Field(default=None, max_length=2000, alias="photoUrls")

确保文件顶部已有 Optional 导入(已有则不动)。

  • Step 4: 改 API create 写入

create_vehicle 中 PlVehicle(...) 增加:

        photo_urls=payload.photo_urls,

create_personnel 中 PlPersonnel(...) 增加:

        photo_urls=payload.photo_urls,

update_vehicle / update_personnel 已用 model_dump(exclude_unset=True) + setattr,无需额外分支。

List/Detail:VehicleRead.model_validate(vehicle) / personnel 等同理,ORM 有列后自动带出。

  • Step 5: 跑测确认通过

Run: cd backend && source venv/bin/activate && pytest tests/test_pl_vehicle_personnel_photo_schema.py -v
Expected: PASS(4 passed)

  • Step 6: Commit
git add backend/app/schemas/pl/base_data.py backend/app/api/pl/base_data.py backend/tests/test_pl_vehicle_personnel_photo_schema.py
git commit -m "$(cat <<'EOF'
feat(pl): expose photoUrls on vehicle and personnel APIs

EOF
)"

Task 4: 前端类型与照片工具函数

Files:

  • Modify: web-admin/src/api/pl/index.ts

  • Create: web-admin/src/utils/pl-photo.ts

  • Step 1: 扩展类型

在 Vehicle 接口(remark?: string 附近)增加:

  photoUrls?: string

在 VehiclePayload 同样增加:

  photoUrls?: string

在 Personnel 与 PersonnelPayload 同样增加:

  photoUrls?: string
  • Step 2: 创建 pl-photo.ts
import { normalizeAttachmentUrls } from '@/utils/download-attachment'

/** 取逗号分隔附件中的第一张 URL(相对或绝对) */
export function firstPhotoUrl(raw?: string | null): string {
  const normalized = normalizeAttachmentUrls(raw)
  if (!normalized) return ''
  return normalized.split(',').map((s) => s.trim()).filter(Boolean)[0] || ''
}

/** 拼 MinIO 完整访问地址(与 AttachmentUploadField 一致) */
export function resolvePhotoDisplayUrl(url?: string | null): string {
  const trimmed = String(url || '').trim()
  if (!trimmed) return ''
  if (/^https?:\/\//i.test(trimmed)) return trimmed
  const base = String(import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
  return base ? `${base}/${trimmed.replace(/^\//, '')}` : trimmed
}
  • Step 3: Commit
git add web-admin/src/api/pl/index.ts web-admin/src/utils/pl-photo.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): add photoUrls types and pl-photo helpers

EOF
)"

Task 5: 三方车辆页 vehicle/index.vue

Files:

  • Modify: web-admin/src/views/pl/vehicle/index.vue

  • Step 1: 导入与 ref

在 script 中增加(保持与现有 import 风格一致):

import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import { firstPhotoUrl, resolvePhotoDisplayUrl } from '@/utils/pl-photo'
const photoUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
  • Step 2: 列定义与持久化 key

listColumnDefs 最前增加:

  { prop: 'photoUrls', label: '照片' },

将 usePersistedListColumns('pl-vehicle-list-columns-v2', ...) 改为:

} = usePersistedListColumns('pl-vehicle-list-columns-v3', listColumnDefs)
  • Step 3: 表格列(序号后、车牌号前)
          <el-table-column
            v-if="isColumnVisible('photoUrls')"
            label="照片"
            min-width="80"
            align="center"
            header-align="center"
          >
            <template #default="{ row }">
              <img
                v-if="firstPhotoUrl(row.photoUrls)"
                class="pl-photo-thumb"
                :src="resolvePhotoDisplayUrl(firstPhotoUrl(row.photoUrls))"
                alt="车辆照片"
              />
              <span v-else class="pl-photo-thumb pl-photo-thumb--empty">未上传</span>
            </template>
          </el-table-column>
  • Step 4: 表单字段与 formData

formData 增加 photoUrls: ''。

在备注 el-form-item 之后、关联人员区块之前增加:

        <el-form-item label="车辆照片" class="form-col-span-2" style="grid-column: 1 / -1">
          <AttachmentUploadField
            ref="photoUploadRef"
            v-model="formData.photoUrls"
            :limit="1"
            accept="image/*"
            upload-folder="pl/vehicle-photo"
          />
        </el-form-item>

(若当前表单用 form-grid-3 且无 form-col-span-2 全局样式生效,用内联 grid-column: 1 / -1 或包一层全宽容器。)

  • Step 5: 查看弹窗

viewSections 的「车辆信息」fields 中,在备注前增加一项不够直观时,改为增加独立 section:

    {
      title: '车辆照片',
      fields: [],
      slot: 'photo'
    },

放在「车辆信息」section 之后(或之前均可;推荐车辆信息后、证书相关前)。

在 SystemViewDialog 内:

      <template #photo>
        <AttachmentUploadField
          v-if="viewDetail?.photoUrls"
          :model-value="viewDetail.photoUrls"
          disabled
          :limit="1"
          accept="image/*"
        />
        <span v-else>—</span>
      </template>
  • Step 6: 读写与提交

  • handleAdd 的 Object.assign 增加 photoUrls: ''

  • handleEdit 赋值 formData 时增加 photoUrls: detail.photoUrls || ''(或 row 详情字段)

  • handleSubmit:在证书上传检查旁增加:

  if (photoUploadRef.value?.hasUploading?.()) {
    ElMessage.warning('附件上传中,请稍候')
    return
  }

payload 增加:

      photoUrls: formData.photoUrls || undefined,
  • Step 7: 样式

在该页 scoped style 增加:

.pl-photo-thumb {
  width: 48px;
  height: 48px;
  border-radius: 8px;
  object-fit: cover;
  border: 1px solid var(--el-border-color, #d9e8e2);
  display: inline-block;
  vertical-align: middle;
  background: #f8fafc;
}

.pl-photo-thumb--empty {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  color: #94a3b8;
  box-sizing: border-box;
}
  • Step 8: Commit
git add web-admin/src/views/pl/vehicle/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add vehicle photo on tripartite vehicle page

EOF
)"

Task 6: 山峡自有车辆页 self-vehicle/index.vue

Files:

  • Modify: web-admin/src/views/pl/self-vehicle/index.vue

与 Task 5 同样改动,差异仅:

项 值
列表持久化 key pl-self-vehicle-list-columns-v2 → pl-self-vehicle-list-columns-v3
表单 label 车辆照片
upload-folder pl/vehicle-photo
查看 slot #photo + AttachmentUploadField disabled

导入、photoUploadRef、listColumnDefs 照片列、表格列、formData、handleAdd/Edit/Submit、scoped 样式与 Task 5 相同模式。

  • Step 1: 按上表完成全部改动
  • Step 2: Commit
git add web-admin/src/views/pl/self-vehicle/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add vehicle photo on self-vehicle page

EOF
)"

Task 7: 驾押人员页 personnel/index.vue

Files:

  • Modify: web-admin/src/views/pl/personnel/index.vue

  • Step 1: 导入与 ref(同 Task 5,folder 不同)

import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import { firstPhotoUrl, resolvePhotoDisplayUrl } from '@/utils/pl-photo'

const photoUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
  • Step 2: 列与 key

listColumnDefs 最前:

  { prop: 'photoUrls', label: '照片' },

持久化 key:pl-personnel-list-columns-v2 → pl-personnel-list-columns-v3

  • Step 3: 表格列(序号后、姓名前)— 模板同车辆页,alt="人员照片"

  • Step 4: 表单

formData 增加 photoUrls: ''。

在备注之后、证书区块之前:

        <el-form-item label="人员照片" class="form-col-span-2" style="grid-column: 1 / -1">
          <AttachmentUploadField
            ref="photoUploadRef"
            v-model="formData.photoUrls"
            :limit="1"
            accept="image/*"
            upload-folder="pl/personnel-photo"
          />
        </el-form-item>
  • Step 5: 查看

viewSections 增加:

    {
      title: '人员照片',
      fields: [],
      slot: 'photo'
    },

(放在「基本信息」与「人员证书」之间。)

      <template #photo>
        <AttachmentUploadField
          v-if="viewDetail?.photoUrls"
          :model-value="viewDetail.photoUrls"
          disabled
          :limit="1"
          accept="image/*"
        />
        <span v-else>—</span>
      </template>
  • Step 6: 提交链路

  • add/edit 重置与回填 photoUrls

  • submit 前 photoUploadRef.hasUploading 检查

  • payload:photoUrls: formData.photoUrls || undefined

  • Step 7: 样式(同 Task 5 的 .pl-photo-thumb)

  • Step 8: Commit

git add web-admin/src/views/pl/personnel/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add personnel photo on driver/escort pages

EOF
)"

Task 8: 验收与图谱更新

Files: 无新文件(或按仓库惯例更新 graphify)

  • Step 1: 手工验收清单
  1. 迁移后四页列表出现「照片」列且默认可见
  2. 车辆新增上传 1 张 → 列表缩略图 → 查看可见
  3. 人员新增上传 1 张 → 同上
  4. 不上传可保存
  5. limit=1 无法再选第二张
  6. 编辑删除照片后列表显示「未上传」
  7. 三方车辆 / 自有车辆 / 三方人员 / 山峡人员行为一致
  • Step 2: 更新图谱
./scripts/update-graph.sh all
# 或分别:backend / frontend
  • Step 3: 将设计文档状态改为已实现(可选)

docs/superpowers/specs/2026-08-02-pl-vehicle-personnel-photo-design.md 顶部状态:待审阅 → 已实现

  • Step 4: Commit(若有图谱/状态变更)
git add graphify-out web-admin/graphify-out docs/superpowers/specs/2026-08-02-pl-vehicle-personnel-photo-design.md
git commit -m "$(cat <<'EOF'
chore: update graphify and mark vehicle/personnel photo design implemented

EOF
)"

Spec 覆盖自检

Spec 要求 Task
pl_vehicle.photo_urls / pl_personnel.photo_urls 1–2
Schema/API photoUrls 透传、非必填 3
前端类型 4
列表方案 A 48×48 5–7
表单 AttachmentUploadField limit=1 5–7
folder pl/vehicle-photo / pl/personnel-photo 5–7
查看弹窗只读 5–7
列默认可见 + 持久化 key 升版 5–7
上传中拦截 5–7
不做多图/OCR/小程序 无任务(YAGNI)