提交
This commit is contained in:
1 parent
0e60b88cba
commit
87f7f6bf38
34 files changed
+18326
-6165
No files matched your search
@@ -0,0 +1,524 @@
|
||||
# 人员从业资格证拆分 — 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:** 将驾押人员必备证书从泛称「从业资格证」拆分为「驾驶员从业资格证」与「押运员从业资格证」;兼岗须 4 证齐全;徽章显示「业」「押」;押运员证作为押运员从业资格证别名。
|
||||
|
||||
**Architecture:** 规则单一来源为 `pl-personnel-cert.ts`(前端)与 `pl_cert_required_service.py`(后端),二者常数与 `match*CertName` 保持镜像;`both` 类型返回独立 4 项列表;历史数据先跑迁移 SQL 再部署代码。
|
||||
|
||||
**Tech Stack:** FastAPI + SQLAlchemy;Vue 3 + Element Plus;MySQL 5.7
|
||||
|
||||
**Status:** ✅ 已实现(2026-07-13)
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 操作 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| Create | `backend/scripts/migration_20260713_pl_personnel_cert_split.sql` | 历史 cert_name 重命名 + both 待人工清单查询 |
|
||||
| Create | `backend/tests/test_pl_personnel_cert_required.py` | 必备规则与匹配单元测试 |
|
||||
| Modify | `backend/app/services/pl_cert_required_service.py` | 后端必备常数、`both` 合并、`match_required_cert_name` |
|
||||
| Modify | `web-admin/src/utils/pl-personnel-cert.ts` | 前端规则源(与后端对齐) |
|
||||
| Modify | `web-admin/src/views/pl/personnel/index.vue` | 证书下拉选项 |
|
||||
| Modify | `docs/superpowers/specs/2026-07-12-pl-required-cert-slots-design.md` | 废止 D5/D6,指向新 spec |
|
||||
| Modify | `docs/sql/shanxia2.0.sql` | 字典 `pl_cert_name` 增量(可选,见 Task 6) |
|
||||
| 回归 | `web-admin/src/components/business/pl-personnel-cert-badges/index.vue` | 无需改代码,验徽章 |
|
||||
| 回归 | `web-admin/src/views/pl/self-vehicle/index.vue` | 关联人员证书校验 |
|
||||
| 回归 | `web-admin/src/views/pl/vehicle/index.vue` | 同上 |
|
||||
|
||||
---
|
||||
|
||||
## 部署顺序(强制)
|
||||
|
||||
```
|
||||
1. 执行 migration_20260713_pl_personnel_cert_split.sql
|
||||
2. 处理 both + 单条「从业资格证」人工清单
|
||||
3. 部署 backend + web-admin
|
||||
4. 按 §验收步骤 走查
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 后端单元测试(TDD 先行)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/tests/test_pl_personnel_cert_required.py`
|
||||
|
||||
- [ ] **Step 1: 编写失败测试**
|
||||
|
||||
```python
|
||||
"""pl_cert_required_service 人员必备证书规则测试。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.pl_cert_required_service import (
|
||||
collect_required_cert_errors,
|
||||
get_required_personnel_cert_names,
|
||||
match_required_cert_name,
|
||||
)
|
||||
|
||||
|
||||
def test_match_driver_qualification_exact():
|
||||
assert match_required_cert_name("驾驶员从业资格证", "驾驶员从业资格证") is True
|
||||
assert match_required_cert_name("从业资格证", "驾驶员从业资格证") is False
|
||||
assert match_required_cert_name("危化品从业资格证", "驾驶员从业资格证") is False
|
||||
|
||||
|
||||
def test_match_escort_qualification_and_alias():
|
||||
assert match_required_cert_name("押运员从业资格证", "押运员从业资格证") is True
|
||||
assert match_required_cert_name("押运员证", "押运员从业资格证") is True
|
||||
assert match_required_cert_name("从业资格证", "押运员从业资格证") is False
|
||||
|
||||
|
||||
def test_required_names_by_personnel_type():
|
||||
assert get_required_personnel_cert_names("driver") == (
|
||||
"身份证",
|
||||
"驾驶证",
|
||||
"驾驶员从业资格证",
|
||||
)
|
||||
assert get_required_personnel_cert_names("escort") == (
|
||||
"身份证",
|
||||
"押运员从业资格证",
|
||||
)
|
||||
assert get_required_personnel_cert_names("both") == (
|
||||
"身份证",
|
||||
"驾驶证",
|
||||
"驾驶员从业资格证",
|
||||
"押运员从业资格证",
|
||||
)
|
||||
|
||||
|
||||
def test_both_requires_four_certs():
|
||||
cert_rows = [
|
||||
{"certName": "身份证", "certNo": "1", "expireDate": "2030-01-01", "attachmentUrls": "http://a"},
|
||||
{"certName": "驾驶证", "certNo": "2", "expireDate": "2030-01-01", "attachmentUrls": "http://b"},
|
||||
{"certName": "驾驶员从业资格证", "certNo": "3", "expireDate": "2030-01-01", "attachmentUrls": "http://c"},
|
||||
]
|
||||
errors = collect_required_cert_errors(
|
||||
cert_rows,
|
||||
get_required_personnel_cert_names("both"),
|
||||
)
|
||||
assert any("押运员从业资格证" in e for e in errors)
|
||||
|
||||
|
||||
def test_escort_alias_counts_as_complete():
|
||||
cert_rows = [
|
||||
{"certName": "身份证", "certNo": "1", "expireDate": "2030-01-01", "attachmentUrls": "http://a"},
|
||||
{"certName": "押运员证", "certNo": "2", "expireDate": "2030-01-01", "attachmentUrls": "http://b"},
|
||||
]
|
||||
errors = collect_required_cert_errors(
|
||||
cert_rows,
|
||||
get_required_personnel_cert_names("escort"),
|
||||
)
|
||||
assert errors == []
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_pl_personnel_cert_required.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL(常数仍为「从业资格证」、`both` 仅 3 项)
|
||||
|
||||
- [ ] **Step 3: Commit 测试文件**
|
||||
|
||||
```bash
|
||||
git add backend/tests/test_pl_personnel_cert_required.py
|
||||
git commit -m "test: add personnel cert split required rules tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 后端规则实现
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/services/pl_cert_required_service.py`
|
||||
|
||||
- [ ] **Step 1: 更新常数与匹配函数**
|
||||
|
||||
将第 22–39 行替换为:
|
||||
|
||||
```python
|
||||
PERSONNEL_DRIVER_REQUIRED = ("身份证", "驾驶证", "驾驶员从业资格证")
|
||||
PERSONNEL_ESCORT_REQUIRED = ("身份证", "押运员从业资格证")
|
||||
PERSONNEL_BOTH_REQUIRED = (
|
||||
"身份证",
|
||||
"驾驶证",
|
||||
"驾驶员从业资格证",
|
||||
"押运员从业资格证",
|
||||
)
|
||||
|
||||
|
||||
def match_required_cert_name(actual_name: str | None, required_name: str) -> bool:
|
||||
name = (actual_name or "").strip()
|
||||
if not name:
|
||||
return False
|
||||
if required_name == "押运员从业资格证":
|
||||
return name == "押运员从业资格证" or name == "押运员证"
|
||||
return name == required_name
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 更新 `get_required_personnel_cert_names`**
|
||||
|
||||
将第 136–142 行替换为:
|
||||
|
||||
```python
|
||||
def get_required_personnel_cert_names(personnel_type: str | None) -> tuple[str, ...]:
|
||||
ptype = (personnel_type or "").strip()
|
||||
if ptype == "escort":
|
||||
return PERSONNEL_ESCORT_REQUIRED
|
||||
if ptype == "driver":
|
||||
return PERSONNEL_DRIVER_REQUIRED
|
||||
if ptype == "both":
|
||||
return PERSONNEL_BOTH_REQUIRED
|
||||
return PERSONNEL_BOTH_REQUIRED
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 运行测试**
|
||||
|
||||
```bash
|
||||
cd backend && pytest tests/test_pl_personnel_cert_required.py -v
|
||||
```
|
||||
|
||||
Expected: PASS(5 passed)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/services/pl_cert_required_service.py
|
||||
git commit -m "feat(pl): split driver and escort qualification cert requirements"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 前端规则源
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/utils/pl-personnel-cert.ts`
|
||||
|
||||
- [ ] **Step 1: 更新常数(第 5–11 行)**
|
||||
|
||||
```typescript
|
||||
export const PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES = [
|
||||
'身份证',
|
||||
'驾驶证',
|
||||
'驾驶员从业资格证'
|
||||
] as const
|
||||
|
||||
export const PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES = [
|
||||
'身份证',
|
||||
'押运员从业资格证'
|
||||
] as const
|
||||
|
||||
export const PL_PERSONNEL_BOTH_REQUIRED_CERT_NAMES = [
|
||||
'身份证',
|
||||
'驾驶证',
|
||||
'驾驶员从业资格证',
|
||||
'押运员从业资格证'
|
||||
] as const
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 更新 `getRequiredPersonnelCertNamesByType`(第 66–73 行)**
|
||||
|
||||
```typescript
|
||||
export function getRequiredPersonnelCertNamesByType(
|
||||
personnelType?: string | null
|
||||
): readonly string[] {
|
||||
if (personnelType === 'escort') {
|
||||
return PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES
|
||||
}
|
||||
if (personnelType === 'driver') {
|
||||
return PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES
|
||||
}
|
||||
return PL_PERSONNEL_BOTH_REQUIRED_CERT_NAMES
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 更新 `matchRequiredCertName`(第 114–128 行)**
|
||||
|
||||
```typescript
|
||||
export function matchRequiredCertName(
|
||||
actualName: string | undefined,
|
||||
requiredName: string
|
||||
): boolean {
|
||||
const name = actualName?.trim() || ''
|
||||
if (!name) return false
|
||||
if (requiredName === '押运员从业资格证') {
|
||||
return name === '押运员从业资格证' || name === '押运员证'
|
||||
}
|
||||
return name === requiredName
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 更新徽章 shortLabel(第 190–194 行)**
|
||||
|
||||
```typescript
|
||||
export const PL_PERSONNEL_CERT_SHORT_LABELS: Record<string, string> = {
|
||||
身份证: '身',
|
||||
驾驶证: '驾',
|
||||
驾驶员从业资格证: '业',
|
||||
押运员从业资格证: '押'
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/utils/pl-personnel-cert.ts
|
||||
git commit -m "feat(pl): align frontend personnel cert required rules with split design"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 人员页下拉选项
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/pl/personnel/index.vue:431-438`
|
||||
|
||||
- [ ] **Step 1: 更新 `certNameOptions`**
|
||||
|
||||
```typescript
|
||||
const certNameOptions = [
|
||||
'身份证',
|
||||
'驾驶证',
|
||||
'驾驶员从业资格证',
|
||||
'押运员从业资格证',
|
||||
'押运员证',
|
||||
'健康证'
|
||||
]
|
||||
```
|
||||
|
||||
说明:移除泛称 `从业资格证`、`危化品从业资格证`(避免误选;历史/other 证书仍可手输)。
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/pl/personnel/index.vue
|
||||
git commit -m "feat(pl): update personnel cert name dropdown for split qualifications"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 历史数据迁移脚本
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/scripts/migration_20260713_pl_personnel_cert_split.sql`
|
||||
|
||||
- [ ] **Step 1: 创建迁移 SQL**
|
||||
|
||||
```sql
|
||||
-- 人员从业资格证拆分:cert_name 重命名(可重复执行)
|
||||
-- 前置 spec: docs/superpowers/specs/2026-07-13-pl-personnel-cert-split-design.md
|
||||
|
||||
-- 1) 押运员证 → 押运员从业资格证(全部 personnel 证书)
|
||||
UPDATE pl_cert c
|
||||
INNER JOIN pl_personnel p ON p.id = c.owner_id AND p.del_flag = 0
|
||||
SET c.cert_name = '押运员从业资格证',
|
||||
c.update_time = NOW()
|
||||
WHERE c.del_flag = 0
|
||||
AND c.owner_type = 'personnel'
|
||||
AND c.cert_name = '押运员证';
|
||||
|
||||
-- 2) 泛称从业资格证 → 驾驶员从业资格证(driver / both,且非 both 单条歧义场景见注释)
|
||||
UPDATE pl_cert c
|
||||
INNER JOIN pl_personnel p ON p.id = c.owner_id AND p.del_flag = 0
|
||||
SET c.cert_name = '驾驶员从业资格证',
|
||||
c.update_time = NOW()
|
||||
WHERE c.del_flag = 0
|
||||
AND c.owner_type = 'personnel'
|
||||
AND c.cert_name = '从业资格证'
|
||||
AND p.personnel_type IN ('driver', 'both');
|
||||
|
||||
-- 3) 泛称从业资格证 → 押运员从业资格证(escort)
|
||||
UPDATE pl_cert c
|
||||
INNER JOIN pl_personnel p ON p.id = c.owner_id AND p.del_flag = 0
|
||||
SET c.cert_name = '押运员从业资格证',
|
||||
c.update_time = NOW()
|
||||
WHERE c.del_flag = 0
|
||||
AND c.owner_type = 'personnel'
|
||||
AND c.cert_name = '从业资格证'
|
||||
AND p.personnel_type = 'escort';
|
||||
|
||||
-- 4) both 人员:若迁移后仍缺驾/押任一从业证,供运营补录(验证查询,不修改数据)
|
||||
SELECT
|
||||
p.id AS personnel_id,
|
||||
p.personnel_name,
|
||||
p.personnel_type,
|
||||
GROUP_CONCAT(c.cert_name ORDER BY c.cert_name SEPARATOR '、') AS cert_names
|
||||
FROM pl_personnel p
|
||||
LEFT JOIN pl_cert c
|
||||
ON c.owner_id = p.id
|
||||
AND c.owner_type = 'personnel'
|
||||
AND c.del_flag = 0
|
||||
WHERE p.del_flag = 0
|
||||
AND p.personnel_type = 'both'
|
||||
GROUP BY p.id, p.personnel_name, p.personnel_type
|
||||
HAVING SUM(CASE WHEN c.cert_name IN ('驾驶员从业资格证') THEN 1 ELSE 0 END) = 0
|
||||
OR SUM(CASE WHEN c.cert_name IN ('押运员从业资格证', '押运员证') THEN 1 ELSE 0 END) = 0;
|
||||
|
||||
-- 5) 危化品从业资格证:不自动迁移(人工清单)
|
||||
SELECT
|
||||
p.id AS personnel_id,
|
||||
p.personnel_name,
|
||||
p.personnel_type,
|
||||
c.id AS cert_id,
|
||||
c.cert_name
|
||||
FROM pl_cert c
|
||||
INNER JOIN pl_personnel p ON p.id = c.owner_id AND p.del_flag = 0
|
||||
WHERE c.del_flag = 0
|
||||
AND c.owner_type = 'personnel'
|
||||
AND c.cert_name = '危化品从业资格证';
|
||||
|
||||
-- 6) both + 仍保留泛称「从业资格证」的歧义记录(需人工处理,不自动改)
|
||||
SELECT
|
||||
p.id AS personnel_id,
|
||||
p.personnel_name,
|
||||
c.id AS cert_id,
|
||||
c.cert_name
|
||||
FROM pl_cert c
|
||||
INNER JOIN pl_personnel p ON p.id = c.owner_id AND p.del_flag = 0
|
||||
WHERE c.del_flag = 0
|
||||
AND c.owner_type = 'personnel'
|
||||
AND c.cert_name = '从业资格证'
|
||||
AND p.personnel_type = 'both';
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在测试库执行并保存清单**
|
||||
|
||||
```bash
|
||||
mysql -h <host> -u <user> -p <database> < backend/scripts/migration_20260713_pl_personnel_cert_split.sql
|
||||
```
|
||||
|
||||
将步骤 4–6 的 SELECT 结果导出给运营。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/scripts/migration_20260713_pl_personnel_cert_split.sql
|
||||
git commit -m "chore(pl): add migration script for personnel cert name split"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 字典同步(可选)
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/sql/shanxia2.0.sql`(`pl_cert_name` 字典段)
|
||||
|
||||
- [ ] **Step 1: 追加字典项(若不存在)**
|
||||
|
||||
```sql
|
||||
-- pl_cert_name 字典增量(执行前检查是否已存在)
|
||||
INSERT INTO sys_dict_data (id, dict_label, dict_value, tag_type, dict_type, tenant_no, order_num, del_flag, create_time, update_time)
|
||||
SELECT 'PL_CERT_DRIVER_QUAL', '驾驶员从业资格证', '驾驶员从业资格证', 'primary', 'pl_cert_name', 'shanxia2.0', 6, 0, NOW(), NOW()
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE id = 'PL_CERT_DRIVER_QUAL');
|
||||
|
||||
INSERT INTO sys_dict_data (id, dict_label, dict_value, tag_type, dict_type, tenant_no, order_num, del_flag, create_time, update_time)
|
||||
SELECT 'PL_CERT_ESCORT_QUAL', '押运员从业资格证', '押运员从业资格证', 'warning', 'pl_cert_name', 'shanxia2.0', 7, 0, NOW(), NOW()
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE id = 'PL_CERT_ESCORT_QUAL');
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit(若做了字典变更)**
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 文档与图谱
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/specs/2026-07-12-pl-required-cert-slots-design.md`(§1.3 表格 + D5/D6 加注「已由 2026-07-13 spec 替代」)
|
||||
- Modify: `docs/superpowers/specs/2026-07-13-pl-personnel-cert-split-design.md`(状态改为「已批准」)
|
||||
|
||||
- [ ] **Step 1: 更新旧 spec 废止说明**
|
||||
- [ ] **Step 2: 运行 graphify 更新**
|
||||
|
||||
```bash
|
||||
cd /Users/zlwu/work/00code/shanxia2.0 && ./scripts/update-graph.sh backend
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/
|
||||
git commit -m "docs(pl): mark personnel cert split spec approved and supersede 2026-07-12 D5/D6"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验收步骤
|
||||
|
||||
### A. 自动化
|
||||
|
||||
```bash
|
||||
# 后端
|
||||
cd backend && pytest tests/test_pl_personnel_cert_required.py -v
|
||||
|
||||
# 前端 lint(可选)
|
||||
cd web-admin && pnpm lint
|
||||
```
|
||||
|
||||
### B. 人员主数据页 `/#/pl/base-info/personnel`
|
||||
|
||||
| # | 操作 | 预期 |
|
||||
|---|------|------|
|
||||
| 1 | 新增 → 选「驾驶员」 | 预置 3 槽:身份证、驾驶证、**驾驶员从业资格证**;徽章占位 **身驾业** |
|
||||
| 2 | 缺「驾驶员从业资格证」保存 | 阻断,提示「押运员从业资格证」或「驾驶员从业资格证」缺失文案 |
|
||||
| 3 | 新增 → 选「押运员」 | 预置 2 槽;列表徽章 **身押**(非「业」) |
|
||||
| 4 | 证书名选手输「押运员证」且三字段齐全 | 满足押运员必备,可保存 |
|
||||
| 5 | 新增 → 选「驾驶员兼押运员」 | 预置 **4** 槽;徽章 **身驾业押** |
|
||||
| 6 | both 仅填 3 证(缺押运员侧) | 保存阻断 |
|
||||
| 7 | 手输泛称「从业资格证」 | **不能**满足任一从业必备槽 |
|
||||
|
||||
### C. 车辆关联(山峡自有车)
|
||||
|
||||
| # | 操作 | 预期 |
|
||||
|---|------|------|
|
||||
| 8 | 关联 escort 角色,人员仅有押运员从业资格证(或押运员证) | 车辆保存通过 escort 证书校验 |
|
||||
| 9 | 关联 driver 角色,人员缺「驾驶员从业资格证」 | 车辆保存阻断 |
|
||||
|
||||
### D. 迁移后数据
|
||||
|
||||
| # | 检查 | 预期 |
|
||||
|---|------|------|
|
||||
| 10 | `SELECT cert_name FROM pl_cert WHERE cert_name='押运员证' AND owner_type='personnel'` | 0 行(已 rename) |
|
||||
| 11 | both 缺证清单(迁移 SQL §4) | 运营已补录或确认无遗漏 |
|
||||
| 12 | 危化品从业资格证清单(迁移 SQL §5) | 运营已人工归类 |
|
||||
|
||||
### E. API 直调(可选)
|
||||
|
||||
```bash
|
||||
# PUT /api/web/admin/transport/personnel/{id}
|
||||
# personnelType=both, certList 仅含 3 证 → 期望 400,detail 含「押运员从业资格证」
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec 覆盖自检
|
||||
|
||||
| Spec 要求 | 对应 Task |
|
||||
|-----------|-----------|
|
||||
| D1 拆分必备证种 | Task 2, 3 |
|
||||
| D2 兼岗 4 证 | Task 2, 3 |
|
||||
| D3 徽章 业/押 | Task 3 Step 4 |
|
||||
| D4 押运员证别名 | Task 2, 3 match 函数 |
|
||||
| D5 严格匹配 | Task 2, 3 删除模糊规则 |
|
||||
| D6/D7 双校验 | Task 2 + 3 + 验收 B/E |
|
||||
| D8 迁移 | Task 5 |
|
||||
| 验收 1–8 | §验收步骤 B–D |
|
||||
|
||||
---
|
||||
|
||||
## 工作量
|
||||
|
||||
| Task | 预估 |
|
||||
|------|------|
|
||||
| Task 1–2 后端 | 2h |
|
||||
| Task 3–4 前端 | 1h |
|
||||
| Task 5 迁移 | 1h |
|
||||
| Task 6–7 文档 | 0.5h |
|
||||
| 验收走查 | 1h |
|
||||
| **合计** | **约 5.5h** |
|
||||
@@ -45,8 +45,8 @@
|
||||
| D2 | **必备字段:证号 + 有效期 + 附件** | 提醒天数维持默认 30,不作为必备条件 |
|
||||
| D3 | **仅山峡自有车强制车辆证** | `vehicleType=self` 时校验主车 3 证、有挂车号时挂车 4 证;`carrier` 不校验 |
|
||||
| D4 | **人员类型三选一** | `driver` 驾驶员 / `escort` 押运员 / `both` 驾驶员兼押运员;人员主数据必填 |
|
||||
| D5 | **人员必备按类型** | driver、both → 身份证+驾驶证+从业资格证;escort → 身份证+从业资格证 |
|
||||
| D6 | **从业资格证别名** | 「危化品从业资格证」等等价匹配「从业资格证」(沿用 `matchRequiredCertName`) |
|
||||
| D5 | **人员必备按类型** | ~~driver、both → 身份证+驾驶证+从业资格证;escort → 身份证+从业资格证~~ **已由 `2026-07-13-pl-personnel-cert-split-design.md` 替代**:驾/押从业证拆分,both 须 4 证 |
|
||||
| D6 | **从业资格证别名** | ~~「危化品从业资格证」等等价匹配「从业资格证」~~ **已废止**;押运员侧仅 `押运员证` 可匹配 `押运员从业资格证` |
|
||||
| D7 | **保存一律阻断** | 历史数据缺必备证时,任何字段保存均须先补齐 |
|
||||
| D8 | **前后端双校验** | 前端 UX + 后端 API 400,防绕过 |
|
||||
| D9 | **关联人员角色兼容** | 车辆子表 `role` 保留;保存山峡自有车时校验人员类型与 role 可匹配(见 §三) |
|
||||
@@ -74,9 +74,9 @@
|
||||
|
||||
| personnel_type | 展示文案 | 必备证书 |
|
||||
|----------------|---------|---------|
|
||||
| `driver` | 驾驶员 | 身份证、驾驶证、从业资格证 |
|
||||
| `escort` | 押运员 | 身份证、从业资格证 |
|
||||
| `both` | 驾驶员兼押运员 | 身份证、驾驶证、从业资格证 |
|
||||
| `driver` | 驾驶员 | 身份证、驾驶证、驾驶员从业资格证 |
|
||||
| `escort` | 押运员 | 身份证、押运员从业资格证 |
|
||||
| `both` | 驾驶员兼押运员 | 身份证、驾驶证、驾驶员从业资格证、押运员从业资格证 |
|
||||
|
||||
常数来源:`PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES`、`PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES`。
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# 运输管理 · 人员从业资格证拆分 — 设计文档
|
||||
|
||||
> **日期**: 2026-07-13
|
||||
> **状态**: 已批准
|
||||
> **前置**: `docs/superpowers/specs/2026-07-12-pl-required-cert-slots-design.md`(D5/D6 需废止并替换)
|
||||
> **界面**: `/#/pl/base-info/personnel`(驾押人员信息)
|
||||
|
||||
## 背景
|
||||
|
||||
2026-07-12 实现将「从业资格证」视为**驾押共用**的一种证,并通过 `matchRequiredCertName` 模糊匹配(含「危化品从业资格证」、名称含「从业资格证」)。业务上这是**错误建模**:
|
||||
|
||||
- **驾驶员从业资格证** 与 **押运员从业资格证** 是两张不同的证
|
||||
- **驾驶员兼押运员** 须同时持有上述两种从业资格证(共 4 项必备,非 3 项)
|
||||
|
||||
## 已确认决策
|
||||
|
||||
| # | 决策 | 说明 |
|
||||
|---|------|------|
|
||||
| D1 | **拆分必备证种** | 不再使用泛称「从业资格证」作为必备槽名称 |
|
||||
| D2 | **兼岗 4 证** | `both` = 身份证 + 驾驶证 + 驾驶员从业资格证 + 押运员从业资格证 |
|
||||
| D3 | **徽章 shortLabel** | 驾驶员从业资格证 → **业**;押运员从业资格证 → **押** |
|
||||
| D4 | **押运员证别名** | 历史/录入名称「押运员证」**等价于**「押运员从业资格证」(匹配与迁移均认可) |
|
||||
| D5 | **严格匹配从业证** | 取消「名称含从业资格证」的宽泛规则;两类从业证分别精确匹配(押运员侧含 D4 别名) |
|
||||
| D6 | **保存一律阻断** | 沿用 2026-07-12 D7:缺必备证时任何保存均 400 / 前端 warning |
|
||||
| D7 | **前后端双校验** | 规则以 util + service 两处硬编码保持一致 |
|
||||
| D8 | **历史数据迁移** | 上线前执行 SQL/脚本,见 §三 |
|
||||
|
||||
## 一、修订后必备证书矩阵
|
||||
|
||||
| personnel_type | 展示文案 | 必备证书 | 列表徽章顺序 |
|
||||
|----------------|---------|---------|-------------|
|
||||
| `driver` | 驾驶员 | 身份证、驾驶证、驾驶员从业资格证 | 身、驾、业 |
|
||||
| `escort` | 押运员 | 身份证、押运员从业资格证 | 身、押 |
|
||||
| `both` | 驾驶员兼押运员 | 身份证、驾驶证、驾驶员从业资格证、押运员从业资格证 | 身、驾、业、押 |
|
||||
|
||||
每项必备仍要求:**证号 + 有效期 + 附件** 均非空(与 2026-07-12 §1.4 一致)。
|
||||
|
||||
### 1.1 证书名称匹配规则
|
||||
|
||||
| 必备槽目标名称 | 可接受的 cert_name |
|
||||
|---------------|-------------------|
|
||||
| 身份证 | 精确:`身份证` |
|
||||
| 驾驶证 | 精确:`驾驶证` |
|
||||
| 驾驶员从业资格证 | 精确:`驾驶员从业资格证` |
|
||||
| 押运员从业资格证 | 精确:`押运员从业资格证`;别名:`押运员证`(D4) |
|
||||
|
||||
**不再接受**作为任一从业必备槽的匹配:
|
||||
|
||||
- 泛称 `从业资格证`
|
||||
- `危化品从业资格证`(除非业务后续单独确认可映射到驾驶员侧,**本期不自动映射**)
|
||||
- `name.includes('从业资格证')` 类模糊规则
|
||||
|
||||
### 1.2 与 2026-07-12 的差异
|
||||
|
||||
| 项 | 2026-07-12 | 本变更 |
|
||||
|----|-----------|--------|
|
||||
| driver 必备 | 身份证、驾驶证、从业资格证 | 身份证、驾驶证、**驾驶员**从业资格证 |
|
||||
| escort 必备 | 身份证、从业资格证 | 身份证、**押运员**从业资格证 |
|
||||
| both 必备 | 同 driver(3 项) | **4 项**(驾+押从业证均有) |
|
||||
| 从业证匹配 | 模糊共用 | 分类型精确匹配 + 押运员证别名 |
|
||||
|
||||
## 二、实现范围
|
||||
|
||||
### 2.1 前端
|
||||
|
||||
**文件**: `web-admin/src/utils/pl-personnel-cert.ts`
|
||||
|
||||
- 常数:
|
||||
- `PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES` → `('身份证', '驾驶证', '驾驶员从业资格证')`
|
||||
- `PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES` → `('身份证', '押运员从业资格证')`
|
||||
- `getRequiredPersonnelCertNamesByType`:
|
||||
- `escort` → escort 列表
|
||||
- `driver` → driver 列表
|
||||
- `both` → **合并去重** driver ∪ escort(顺序:身份证、驾驶证、驾驶员从业资格证、押运员从业资格证)
|
||||
- `matchRequiredCertName`:
|
||||
- 删除对泛称「从业资格证」的模糊分支
|
||||
- 新增:`押运员从业资格证` 可匹配 `押运员证`
|
||||
- `驾驶员从业资格证` 仅精确匹配
|
||||
- `PL_PERSONNEL_CERT_SHORT_LABELS`:
|
||||
- `驾驶员从业资格证: '业'`
|
||||
- `押运员从业资格证: '押'`
|
||||
- 移除泛称 `从业资格证: '业'`
|
||||
|
||||
**文件**: `web-admin/src/views/pl/personnel/index.vue`
|
||||
|
||||
- `certNameOptions`:必备相关选项改为 `驾驶员从业资格证`、`押运员从业资格证`;保留 `押运员证` 于「其他证书」可选(或标注为别名,非必备槽名)
|
||||
- 移除泛称 `从业资格证` 作为必备槽预置名(可选是否保留于自由添加下拉)
|
||||
|
||||
**连带组件**(逻辑随 util 自动生效,需回归):
|
||||
|
||||
- `pl-personnel-cert-badges`
|
||||
- `pl-cert-form-list` / `pl-cert-slots`
|
||||
- `pl-vehicle-personnel-view-list` / `PersonnelViewPanel`(车辆关联查看态缺失 suffix)
|
||||
- `pl/self-vehicle`、`pl/vehicle`(关联人员证书校验提示)
|
||||
|
||||
### 2.2 后端
|
||||
|
||||
**文件**: `backend/app/services/pl_cert_required_service.py`
|
||||
|
||||
- `PERSONNEL_DRIVER_REQUIRED`、`PERSONNEL_ESCORT_REQUIRED` 与前端对齐
|
||||
- `get_required_personnel_cert_names`:`both` 返回 4 项合并列表
|
||||
- `match_required_cert_name`:与前端 D4/D5 一致
|
||||
- `validate_vehicle_personnel_cert_links`:按 `role` 使用 driver/escort 必备列表(escort 侧含押运员从业资格证 + 押运员证别名)
|
||||
|
||||
### 2.3 字典(可选同步)
|
||||
|
||||
`sys_dict_data` / `pl_cert_name`:
|
||||
|
||||
- 新增:`驾驶员从业资格证`、`押运员从业资格证`
|
||||
- `押运员证` 保留(作为别名来源,非必备槽展示名)
|
||||
- 泛称 `从业资格证`、`危化品从业资格证`:可保留于字典供历史/other 证书,**不参与必备匹配**
|
||||
|
||||
## 三、历史数据迁移
|
||||
|
||||
### 3.1 证书名称重命名
|
||||
|
||||
| 原 cert_name | 迁移后 cert_name | 条件 |
|
||||
|-------------|-----------------|------|
|
||||
| `押运员证` | `押运员从业资格证` | 全部 personnel owner |
|
||||
| `从业资格证` | `驾驶员从业资格证` | 该人员 `personnel_type IN ('driver', 'both')` |
|
||||
| `从业资格证` | `押运员从业资格证` | 该人员 `personnel_type = 'escort'` |
|
||||
| `从业资格证` | **不自动迁移** | `personnel_type = 'both'` 且仅有一条 → **导出人工清单**;或按业务规则复制为两条(若证号相同则拆两条记录,**需运营确认**,默认不自动复制) |
|
||||
|
||||
**both + 单条泛称「从业资格证」**:迁移脚本应 **输出待人工处理清单**(personnel_id、姓名、原 cert_name),不静默映射,避免一张证冒充两种从业证。
|
||||
|
||||
**危化品从业资格证**:本期 **不自动映射**;进入缺证清单,由运营按实际类型补录或重命名。
|
||||
|
||||
### 3.2 迁移脚本
|
||||
|
||||
- 路径建议:`backend/scripts/migration_20260713_pl_personnel_cert_split.sql`
|
||||
- 要求:可重复执行(UPDATE 仅作用于未迁移行);执行后附 `SELECT` 验证 both 人员是否仍缺押运/驾从业证
|
||||
|
||||
### 3.3 上线顺序
|
||||
|
||||
1. 执行迁移脚本 + 人工处理 both ambiguous 清单
|
||||
2. 部署前后端新规则
|
||||
3. 运营抽查列表徽章(身驾业押)与保存阻断
|
||||
|
||||
## 四、业务规则(保持不变部分)
|
||||
|
||||
- 人员主数据:按 `personnel_type` 校验必备(`both` 为 4 项)
|
||||
- 山峡自有车关联人员:按关联行 `role` 校验 driver/escort 必备子集
|
||||
- `both` 类型人员主数据仍须驾驶证,即使仅关联为 escort(沿用 2026-07-12 §3.2 说明)
|
||||
- 承运商车辆不强制车辆证;人员主数据始终强制
|
||||
|
||||
## 五、验收标准
|
||||
|
||||
1. 新增驾驶员:预置 3 必备槽(身份证、驾驶证、驾驶员从业资格证);缺任一项无法保存。
|
||||
2. 新增押运员:预置 2 必备槽(身份证、押运员从业资格证);列表徽章 **身、押**。
|
||||
3. 新增兼岗:预置 4 必备槽;列表徽章 **身、驾、业、押**;缺押运员从业资格证无法保存。
|
||||
4. 录入证书名称为「押运员证」时,满足押运员从业资格证必备槽(D4)。
|
||||
5. 泛称「从业资格证」**不能**满足任一从业必备槽(迁移后)。
|
||||
6. 车辆关联 escort role:校验身份证 + 押运员从业资格证(含押运员证别名)。
|
||||
7. 前后端错误文案一致,含缺失字段(证号/有效期/附件)。
|
||||
8. 迁移脚本执行后,`押运员证` 行已更名为 `押运员从业资格证`。
|
||||
|
||||
## 六、工作量估算
|
||||
|
||||
| 模块 | 预估 |
|
||||
|------|------|
|
||||
| 前后端 util/service 规则对齐 | 0.5 天 |
|
||||
| 人员页下拉/槽位 + 徽章回归 | 0.25 天 |
|
||||
| 迁移脚本 + both 人工清单 | 0.25 天 |
|
||||
| 车辆页关联校验回归 + 自测 | 0.25 天 |
|
||||
| **合计** | **约 1~1.25 人天** |
|
||||
|
||||
## 七、风险
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|------|------|
|
||||
| both 人员仅有一条泛称从业证 | 迁移输出人工清单,不上线前不部署新规则 |
|
||||
| 兼岗人员大量缺「押运员从业资格证」 | 迁移押运员证 + 运营补录 |
|
||||
| 危化品从业资格证无法自动归类 | 本期不映射,运营手动改为驾驶员/押运员从业资格证 |
|
||||
|
||||
## 八、OpenSpec
|
||||
|
||||
建议在 `transport-plan-management` 或新 change 中增补 Scenario,**废止** 2026-07-12 的 D5/D6 表述,引用本设计 §1 矩阵。
|
||||
Reference in new issue
Block a user