Files
shanxia2.0/backend/app/services/pl_personnel_service.py
T

274 lines
11 KiB
Python

"""运输计划 - 人员 upsert 与车辆-人员关联保存"""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.orm import Session
from app.core.id_card import calc_age_from_chinese_id_card
from app.core.utils import generate_uuid
from app.models.pl_models import PlPersonnel, PlVehiclePersonnel
from app.schemas.pl.base_data import VehiclePersonnelRow
def _prepare_personnel_rows(db: Session, tenant_no: str, rows: list[VehiclePersonnelRow]):
"""解析整批人员身份及字段,不写 ORM;缺 ID 的歧义不能靠顺序消除。"""
ids = {(row.personnel_id or "").strip() for row in rows if row.personnel_id}
names = {(row.personnel_name or "").strip() for row in rows}
phones = {(row.phone_no or "").strip() for row in rows if row.phone_no}
people = db.query(PlPersonnel).filter(
PlPersonnel.tenant_no == tenant_no, PlPersonnel.del_flag == 0,
or_(PlPersonnel.id.in_(ids), PlPersonnel.personnel_name.in_(names),
PlPersonnel.phone_no.in_(phones)),
).with_for_update().all() if rows else []
by_id = {person.id: person for person in people}
aliases = {person.id: {"name": {person.personnel_name.strip()},
"phone": {(person.phone_no or "").strip()}} for person in people}
jobs, resolved = {}, []
def merge_fields(person, item):
fields = {}
if 'personnel_name' in item.model_fields_set:
name = (item.personnel_name or '').strip()
if not name:
raise HTTPException(422, '人员姓名不能为空')
fields['personnel_name'] = name
if 'phone_no' in item.model_fields_set:
fields['phone_no'] = (item.phone_no or '').strip() or None
if 'age' in item.model_fields_set:
fields['age'] = item.age
previous = jobs.setdefault(person.id, (person, {}))[1]
if any(field in previous and previous[field] != value for field, value in fields.items()):
raise HTTPException(422, '同一人员的姓名、手机号或年龄提交冲突,请核对后保存')
previous.update(fields)
aliases[person.id]['name'].add(fields.get('personnel_name', ''))
aliases[person.id]['phone'].add(fields.get('phone_no') or '')
# 显式身份全部先校验并预占;其他角色可复用同一个人,不能重复同一角色。
reserved = set()
for item in rows:
identity = (item.personnel_id or '').strip()
if not identity:
continue
person = by_id.get(identity)
if person is None:
raise HTTPException(422, '人员 id 无效、已删除或不属于当前租户')
key = identity, item.role
if key in reserved:
raise HTTPException(422, '同一人员角色不能重复提交')
reserved.add(key)
merge_fields(person, item)
seen = set()
for item in rows:
identity = (item.personnel_id or '').strip()
person = by_id.get(identity)
if not identity:
phone = (item.phone_no or '').strip()
name = (item.personnel_name or '').strip()
candidates = [by_id[pid] for pid, keys in aliases.items() if phone and phone in keys['phone']]
if not candidates:
candidates = [by_id[pid] for pid, keys in aliases.items() if name and name in keys['name']]
if len(candidates) > 1:
raise HTTPException(422, '姓名或手机号对应多名人员,请明确选择人员 ID 后保存')
person = candidates[0] if candidates else None
if person is None:
if not name:
raise HTTPException(422, '新增人员须提供姓名')
# 未加入 Session,整批验证失败不会留下部分新增的人员。
person = PlPersonnel(id=generate_uuid(), tenant_no=tenant_no, personnel_name=name)
by_id[person.id] = person
aliases[person.id] = {"name": {name}, "phone": {phone}}
if (person.id, item.role) in reserved:
raise HTTPException(422, '缺少人员 ID 的记录重复引用已提交人员角色')
if item.role not in ('driver', 'escort'):
raise HTTPException(422, '人员角色必须为司机或押运员')
key = person.id, item.role
if key in seen:
raise HTTPException(422, '同一人员角色不能重复提交')
seen.add(key)
merge_fields(person, item)
resolved.append((item, person))
return resolved, jobs
def _apply_personnel_jobs(db: Session, jobs, *, user_id: str, dept_id: str, vehicle_type: str) -> None:
affiliation = vehicle_type if vehicle_type in ('self', 'carrier') else 'self'
for person, fields in jobs.values():
if person not in db:
person.personnel_type = 'both'
person.personnel_affiliation = affiliation
person.status = '1'
person.create_by = user_id
person.create_dept_id = dept_id
db.add(person)
for field, value in fields.items():
setattr(person, field, value)
person.update_by = user_id
def upsert_personnel(
db: Session,
tenant_no: str,
row: VehiclePersonnelRow,
*,
user_id: str,
dept_id: str,
vehicle_type: str = "self",
) -> str:
"""兼容单人入口,与车辆集合使用相同的唯一身份匹配规则。"""
with db.no_autoflush:
resolved, jobs = _prepare_personnel_rows(db, tenant_no, [row])
_apply_personnel_jobs(db, jobs, user_id=user_id, dept_id=dept_id, vehicle_type=vehicle_type)
db.flush()
return resolved[0][1].id
def save_vehicle_personnel(
db: Session,
vehicle_id: str,
rows: list[VehiclePersonnelRow],
tenant_no: str,
*,
user_id: str,
dept_id: str,
vehicle_type: str = "self",
) -> None:
"""按人员 + 角色差量保存;整批身份、字段及最终默认司机先验证。"""
from app.services.pl_child_sync import prepare_keyed_rows, apply_keyed_rows
if rows is None:
return
with db.no_autoflush:
existing = db.query(PlVehiclePersonnel).filter(PlVehiclePersonnel.vehicle_id == vehicle_id,
PlVehiclePersonnel.tenant_no == tenant_no, PlVehiclePersonnel.del_flag == 0).with_for_update().all()
resolved, jobs = _prepare_personnel_rows(db, tenant_no, rows)
current = {(row.personnel_id, row.role): row for row in existing}
values, defaults = [], 0
for item, person in resolved:
previous = current.get((person.id, item.role))
is_default = item.is_default if 'is_default' in item.model_fields_set else (
previous.is_default if previous is not None else 0)
if item.role == 'escort' and is_default == 1:
raise HTTPException(400, '仅司机角色可设为默认')
defaults += int(item.role == 'driver' and is_default == 1)
value = dict(tenant_no=tenant_no, vehicle_id=vehicle_id, personnel_id=person.id,
role=item.role, is_default=is_default)
if 'remark' in item.model_fields_set:
value['remark'] = item.remark
values.append(value)
if defaults > 1:
raise HTTPException(400, '每辆车最多设置一名默认司机')
prepared = prepare_keyed_rows(existing, values, ('personnel_id', 'role'), '车辆人员关联')
_apply_personnel_jobs(db, jobs, user_id=user_id, dept_id=dept_id, vehicle_type=vehicle_type)
apply_keyed_rows(db, PlVehiclePersonnel, prepared, user_id=user_id, dept_id=dept_id)
def build_personnel_summary(
links: list[tuple[str, str, int]],
) -> tuple[str, Optional[str]]:
"""links: [(personnel_name, role, is_default)] → (summary, default_driver_name)."""
role_labels = {"driver": "驾", "escort": "押"}
parts: list[str] = []
default_name: Optional[str] = None
for name, role, is_default in links:
label = role_labels.get(role, role)
parts.append(f"{name}({label})")
if role == "driver" and int(is_default) == 1:
default_name = name
return "、".join(parts), default_name
def load_vehicle_personnel_rows(
db: Session, tenant_no: str, vehicle_id: str
) -> list[VehiclePersonnelRow]:
"""查询车辆关联人员子表行。"""
rows = (
db.query(PlVehiclePersonnel, PlPersonnel)
.join(
PlPersonnel,
PlPersonnel.id == PlVehiclePersonnel.personnel_id,
)
.filter(
PlVehiclePersonnel.vehicle_id == vehicle_id,
PlVehiclePersonnel.tenant_no == tenant_no,
PlVehiclePersonnel.del_flag == 0,
PlPersonnel.del_flag == 0,
)
.order_by(PlVehiclePersonnel.order_num.asc(), PlVehiclePersonnel.create_time.asc())
.all()
)
return [
VehiclePersonnelRow(
personnel_id=p.personnel_id,
personnel_name=person.personnel_name,
phone_no=person.phone_no,
age=person.age,
role=p.role,
is_default=p.is_default,
remark=p.remark,
)
for p, person in rows
]
def load_vehicle_personnel_summaries(
db: Session, tenant_no: str, vehicle_ids: list[str]
) -> dict[str, tuple[str, Optional[str]]]:
"""批量加载 personnelSummary / defaultDriverName。"""
if not vehicle_ids:
return {}
rows = (
db.query(
PlVehiclePersonnel.vehicle_id,
PlPersonnel.personnel_name,
PlVehiclePersonnel.role,
PlVehiclePersonnel.is_default,
)
.join(PlPersonnel, PlPersonnel.id == PlVehiclePersonnel.personnel_id)
.filter(
PlVehiclePersonnel.vehicle_id.in_(vehicle_ids),
PlVehiclePersonnel.tenant_no == tenant_no,
PlVehiclePersonnel.del_flag == 0,
PlPersonnel.del_flag == 0,
)
.order_by(PlVehiclePersonnel.order_num.asc())
.all()
)
grouped: dict[str, list[tuple[str, str, str]]] = {}
for vehicle_id, name, role, is_default in rows:
grouped.setdefault(vehicle_id, []).append((name, role, is_default))
return {
vid: build_personnel_summary(items) for vid, items in grouped.items()
}
def refresh_personnel_ages_from_id_card(db: Session) -> int:
"""按 18 位身份证号重算 pl_personnel.age,仅更新算出值与库中不一致的记录。返回更新条数。"""
rows = (
db.query(PlPersonnel)
.filter(
PlPersonnel.del_flag == 0,
PlPersonnel.id_card_no.isnot(None),
PlPersonnel.id_card_no != "",
)
.all()
)
now = datetime.now()
updated = 0
for row in rows:
age = calc_age_from_chinese_id_card(row.id_card_no)
if age is None or row.age == age:
continue
row.age = age
row.update_time = now
updated += 1
return updated