This commit is contained in:
zlwu committed 2026-07-12 17:14:53 +08:00
1 parent ffc89d9b57
commit c2cba3e668
53 files changed
+11794 -6264

No files matched your search

BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
47712
6907
+63
View File
@@ -48,6 +48,10 @@ from app.services.pl_cert_service import (
nearest_expire_date,
save_owner_certs,
)
from app.services.pl_cert_required_service import (
validate_personnel_required_certs,
validate_vehicle_save_certs,
)
from app.services.pl_carrier_service import (
carrier_eligible_for_transport,
carrier_qualification_complete,
@@ -409,6 +413,15 @@ def create_vehicle(
if existing:
raise HTTPException(status_code=400, detail="车牌号已存在")
validate_vehicle_save_certs(
db,
tenant_no,
vehicle_type=payload.vehicle_type,
trailer_no=payload.trailer_no,
cert_rows=payload.cert_list,
personnel_rows=payload.personnel_list,
)
vehicle = PlVehicle(
id=generate_uuid(),
tenant_no=tenant_no,
@@ -434,6 +447,7 @@ def create_vehicle(
tenant_no,
user_id=user.id,
dept_id=user.dept_id,
vehicle_type=payload.vehicle_type,
)
save_owner_certs(
db,
@@ -483,6 +497,26 @@ def update_vehicle(
exclude_unset=True, exclude={"personnel_list", "cert_list"}
)
effective_vehicle_type = data.get("vehicle_type", vehicle.vehicle_type)
effective_trailer_no = data.get("trailer_no", vehicle.trailer_no)
effective_cert_rows = (
cert_list
if cert_list is not None
else list_owner_certs(db, tenant_no, "vehicle", vehicle.id)
)
effective_personnel_rows = personnel_list
if effective_personnel_rows is None:
effective_personnel_rows = load_vehicle_personnel_rows(db, tenant_no, vehicle.id)
validate_vehicle_save_certs(
db,
tenant_no,
vehicle_type=effective_vehicle_type,
trailer_no=effective_trailer_no,
cert_rows=effective_cert_rows,
personnel_rows=effective_personnel_rows,
)
for key, value in data.items():
setattr(vehicle, key, value)
@@ -497,6 +531,7 @@ def update_vehicle(
tenant_no,
user_id=user.id,
dept_id=user.dept_id,
vehicle_type=effective_vehicle_type,
)
if cert_list is not None:
save_owner_certs(
@@ -581,6 +616,9 @@ def _personnel_to_detail(
def list_personnel(
q: Optional[str] = Query(default=None, description="模糊搜索姓名/手机号"),
status: Optional[str] = Query(default=None, description="状态"),
personnel_affiliation: Optional[str] = Query(
default=None, alias="personnelAffiliation", description="人员归属 self/carrier"
),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize"),
db: Session = Depends(get_db),
@@ -595,6 +633,9 @@ def list_personnel(
if status:
query = query.filter(PlPersonnel.status == status)
if personnel_affiliation:
query = query.filter(PlPersonnel.personnel_affiliation == personnel_affiliation)
if q:
query = query.filter(
or_(
@@ -626,6 +667,12 @@ def create_personnel(
) -> PersonnelDetailRead:
tenant_no = get_tenant_no(db, user.to_tenant_user_dict())
validate_personnel_required_certs(
payload.cert_list,
payload.personnel_type,
subject_label=f"「{payload.personnel_name}」",
)
personnel = PlPersonnel(
id=generate_uuid(),
tenant_no=tenant_no,
@@ -633,6 +680,8 @@ def create_personnel(
phone_no=payload.phone_no,
age=payload.age,
id_card_no=payload.id_card_no,
personnel_type=payload.personnel_type,
personnel_affiliation=payload.personnel_affiliation,
status=payload.status,
remark=payload.remark,
create_by=user.id,
@@ -684,6 +733,20 @@ def update_personnel(
cert_list = payload.cert_list
data = payload.model_dump(exclude_unset=True, exclude={"cert_list"})
effective_personnel_type = data.get("personnel_type", personnel.personnel_type)
effective_cert_rows = (
cert_list
if cert_list is not None
else list_owner_certs(db, tenant_no, "personnel", personnel.id)
)
subject_name = data.get("personnel_name", personnel.personnel_name)
validate_personnel_required_certs(
effective_cert_rows,
effective_personnel_type,
subject_label=f"「{subject_name}」",
)
for key, value in data.items():
setattr(personnel, key, value)
+14
View File
@@ -72,6 +72,7 @@ from app.services.pl_plan_service import (
sync_transport_prefill_for_plan,
update_plan_product_certs,
validate_create_full,
validate_hazard_cert_ready_for_transport,
)
router = APIRouter(prefix="/plan", tags=["运输计划-计划管理"])
@@ -1079,6 +1080,19 @@ def archive_plan(
already_archived = plan.archive_status == "archived"
if not already_archived:
products = (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.plan_id == plan.id,
PlPlanProduct.tenant_no == tenant_no,
PlPlanProduct.del_flag == 0,
)
.order_by(PlPlanProduct.order_num)
.all()
)
validate_hazard_cert_ready_for_transport(db, tenant_no, plan, products)
if payload is None:
if already_archived:
raise HTTPException(status_code=400, detail="计划已归档,请提交归档表单以调整内容")
+12
View File
@@ -90,6 +90,18 @@ class PlPersonnel(Base):
phone_no = Column(String(32), nullable=True, comment="手机号(非必填)")
age = Column(Integer, nullable=True, comment="年龄")
id_card_no = Column(String(32), nullable=True, comment="身份证号")
personnel_type = Column(
String(20),
nullable=False,
default="both",
comment="人员类型 driver-驾驶员 escort-押运员 both-驾驶员兼押运员",
)
personnel_affiliation = Column(
String(20),
nullable=False,
default="self",
comment="人员归属 self-山峡 carrier-非山峡",
)
status = Column(CHAR(1), nullable=False, default="1", comment="状态 1-在职 0-停用")
tenant_no = Column(String(64), nullable=False, default="shanxia2.0", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
+16
View File
@@ -272,6 +272,18 @@ class PersonnelBase(BaseModel):
phone_no: Optional[str] = Field(default=None, max_length=32, alias="phoneNo", description="手机号")
age: Optional[int] = Field(default=None, description="年龄")
id_card_no: Optional[str] = Field(default=None, max_length=32, alias="idCardNo", description="身份证号")
personnel_type: str = Field(
...,
max_length=20,
alias="personnelType",
description="人员类型 driver/escort/both",
)
personnel_affiliation: str = Field(
default="self",
max_length=20,
alias="personnelAffiliation",
description="人员归属 self-山峡 carrier-非山峡",
)
status: str = Field(default="1", max_length=1, description="状态 1-在职 0-停用")
remark: Optional[str] = None
@@ -286,6 +298,10 @@ class PersonnelUpdate(BaseModel):
phone_no: Optional[str] = Field(default=None, max_length=32, alias="phoneNo")
age: Optional[int] = None
id_card_no: Optional[str] = Field(default=None, max_length=32, alias="idCardNo")
personnel_type: Optional[str] = Field(default=None, max_length=20, alias="personnelType")
personnel_affiliation: Optional[str] = Field(
default=None, max_length=20, alias="personnelAffiliation"
)
status: Optional[str] = Field(default=None, max_length=1)
remark: Optional[str] = None
cert_list: Optional[list[CertRow]] = Field(default=None, alias="certList")
@@ -0,0 +1,314 @@
"""运输计划 - 必备证书校验"""
from __future__ import annotations
from typing import Iterable, Optional, Sequence
from fastapi import HTTPException
from sqlalchemy.orm import Session
from app.models.pl_models import PlPersonnel
from app.schemas.pl.base_data import CertRow, VehiclePersonnelRow
from app.services.pl_cert_service import list_owner_certs
VEHICLE_TYPE_SELF = "self"
VEHICLE_TYPE_CARRIER = "carrier"
SELF_HEAD_REQUIRED = ("行驶证", "营运证", "保险单")
SELF_TRAILER_REQUIRED = ("行驶证", "营运证", "保险单", "罐检报告")
CARRIER_HEAD_REQUIRED = ("行驶证", "营运证")
CARRIER_TRAILER_REQUIRED = ("行驶证", "营运证")
PERSONNEL_DRIVER_REQUIRED = ("身份证", "驾驶证", "从业资格证")
PERSONNEL_ESCORT_REQUIRED = ("身份证", "从业资格证")
VEHICLE_PART_HEAD = "head"
VEHICLE_PART_TRAILER = "trailer"
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 == "危化品从业资格证"
or "从业资格证" in name
)
return name == required_name
def _cert_row_complete(cert: CertRow | dict, required_name: str) -> bool:
if isinstance(cert, dict):
cert_name = cert.get("cert_name") or cert.get("certName")
cert_no = cert.get("cert_no") or cert.get("certNo")
expire_date = cert.get("expire_date") or cert.get("expireDate")
attachment_urls = cert.get("attachment_urls") or cert.get("attachmentUrls")
else:
cert_name = cert.cert_name
cert_no = cert.cert_no
expire_date = cert.expire_date
attachment_urls = cert.attachment_urls
if not match_required_cert_name(cert_name, required_name):
return False
if not (cert_no or "").strip():
return False
if not expire_date:
return False
if not (attachment_urls or "").strip():
return False
return True
def _find_cert_for_required(
cert_rows: Sequence[CertRow | dict],
required_name: str,
*,
vehicle_part: str | None = None,
) -> CertRow | dict | None:
for cert in cert_rows:
if isinstance(cert, dict):
name = cert.get("cert_name") or cert.get("certName")
part = cert.get("vehicle_part") or cert.get("vehiclePart")
else:
name = cert.cert_name
part = cert.vehicle_part
if vehicle_part is not None and (part or VEHICLE_PART_HEAD) != vehicle_part:
continue
if match_required_cert_name(name, required_name):
return cert
return None
def _missing_field_labels(cert: CertRow | dict | None, required_name: str) -> list[str]:
if cert is None:
return ["证号", "有效期", "附件"]
missing: list[str] = []
if isinstance(cert, dict):
cert_no = cert.get("cert_no") or cert.get("certNo")
expire_date = cert.get("expire_date") or cert.get("expireDate")
attachment_urls = cert.get("attachment_urls") or cert.get("attachmentUrls")
cert_name = cert.get("cert_name") or cert.get("certName")
else:
cert_no = cert.cert_no
expire_date = cert.expire_date
attachment_urls = cert.attachment_urls
cert_name = cert.cert_name
if not match_required_cert_name(cert_name, required_name):
return ["证号", "有效期", "附件"]
if not (cert_no or "").strip():
missing.append("证号")
if not expire_date:
missing.append("有效期")
if not (attachment_urls or "").strip():
missing.append("附件")
return missing
def collect_required_cert_errors(
cert_rows: Sequence[CertRow | dict],
required_names: Iterable[str],
*,
part_label: str = "",
) -> list[str]:
errors: list[str] = []
prefix = f"{part_label}" if part_label else ""
vehicle_part = None
if part_label in ("主车", "head"):
vehicle_part = VEHICLE_PART_HEAD
elif part_label in ("挂车", "trailer"):
vehicle_part = VEHICLE_PART_TRAILER
for required_name in required_names:
cert = _find_cert_for_required(
cert_rows, required_name, vehicle_part=vehicle_part
)
missing = _missing_field_labels(cert, required_name)
if missing:
label = f"{prefix}「{required_name}」" if prefix else f"「{required_name}」"
errors.append(f"{label}缺少:{'、'.join(missing)}")
return errors
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 in ("driver", "both"):
return PERSONNEL_DRIVER_REQUIRED
return PERSONNEL_DRIVER_REQUIRED
def validate_personnel_type_role_compat(personnel_type: str | None, role: str | None) -> None:
ptype = (personnel_type or "").strip()
r = (role or "").strip()
if not ptype or not r:
return
if ptype == "driver" and r == "escort":
raise HTTPException(status_code=400, detail="驾驶员类型人员不可关联为押运员")
if ptype == "escort" and r == "driver":
raise HTTPException(status_code=400, detail="押运员类型人员不可关联为驾驶员")
def validate_personnel_required_certs(
cert_rows: Sequence[CertRow | dict],
personnel_type: str | None,
*,
subject_label: str = "",
) -> None:
ptype = (personnel_type or "").strip()
if not ptype:
raise HTTPException(status_code=400, detail="人员类型不能为空")
required = get_required_personnel_cert_names(ptype)
errors = collect_required_cert_errors(cert_rows, required)
if errors:
prefix = f"人员{subject_label}" if subject_label else "人员"
raise HTTPException(
status_code=400,
detail=f"必备证书未齐全:{prefix}{';'.join(errors)}",
)
def validate_self_vehicle_required_certs(
cert_rows: Sequence[CertRow | dict],
*,
trailer_no: str | None = None,
) -> None:
errors: list[str] = []
errors.extend(
collect_required_cert_errors(
cert_rows, SELF_HEAD_REQUIRED, part_label="主车"
)
)
if (trailer_no or "").strip():
errors.extend(
collect_required_cert_errors(
cert_rows, SELF_TRAILER_REQUIRED, part_label="挂车"
)
)
if errors:
raise HTTPException(
status_code=400,
detail=f"必备证书未齐全:{';'.join(errors)}",
)
def validate_carrier_vehicle_required_certs(
cert_rows: Sequence[CertRow | dict],
*,
trailer_no: str | None = None,
) -> None:
errors: list[str] = []
errors.extend(
collect_required_cert_errors(
cert_rows, CARRIER_HEAD_REQUIRED, part_label="主车"
)
)
if (trailer_no or "").strip():
errors.extend(
collect_required_cert_errors(
cert_rows, CARRIER_TRAILER_REQUIRED, part_label="挂车"
)
)
if errors:
raise HTTPException(
status_code=400,
detail=f"必备证书未齐全:{';'.join(errors)}",
)
def validate_vehicle_personnel_affiliation(
db: Session,
tenant_no: str,
vehicle_type: str | None,
personnel_rows: Sequence[VehiclePersonnelRow],
) -> None:
vtype = (vehicle_type or "").strip()
if vtype not in (VEHICLE_TYPE_SELF, VEHICLE_TYPE_CARRIER):
return
label = "山峡" if vtype == VEHICLE_TYPE_SELF else "非山峡"
for row in personnel_rows:
personnel_id = (row.personnel_id or "").strip()
if not personnel_id:
continue
personnel = (
db.query(PlPersonnel)
.filter(
PlPersonnel.id == personnel_id,
PlPersonnel.tenant_no == tenant_no,
PlPersonnel.del_flag == 0,
)
.first()
)
if not personnel:
continue
affiliation = (personnel.personnel_affiliation or VEHICLE_TYPE_SELF).strip()
if affiliation != vtype:
name = (row.personnel_name or personnel.personnel_name or personnel_id).strip()
raise HTTPException(
status_code=400,
detail=f"关联人员「{name}」归属与车辆类型不匹配,{label}车辆仅可关联{label}人员",
)
def validate_vehicle_personnel_cert_links(
db: Session,
tenant_no: str,
personnel_rows: Sequence[VehiclePersonnelRow],
) -> None:
for row in personnel_rows:
personnel_id = (row.personnel_id or "").strip()
if not personnel_id:
continue
personnel = (
db.query(PlPersonnel)
.filter(
PlPersonnel.id == personnel_id,
PlPersonnel.tenant_no == tenant_no,
PlPersonnel.del_flag == 0,
)
.first()
)
if not personnel:
continue
validate_personnel_type_role_compat(personnel.personnel_type, row.role)
cert_rows = list_owner_certs(db, tenant_no, "personnel", personnel_id)
role = (row.role or "").strip()
required = (
PERSONNEL_DRIVER_REQUIRED
if role == "driver"
else PERSONNEL_ESCORT_REQUIRED
)
errors = collect_required_cert_errors(cert_rows, required)
if errors:
name = (row.personnel_name or personnel.personnel_name or personnel_id).strip()
raise HTTPException(
status_code=400,
detail=(
f"关联人员「{name}」必备证书未齐全:{';'.join(errors)}"
),
)
def validate_vehicle_save_certs(
db: Session,
tenant_no: str,
*,
vehicle_type: str | None,
trailer_no: str | None,
cert_rows: Sequence[CertRow | dict],
personnel_rows: Sequence[VehiclePersonnelRow] | None = None,
) -> None:
vtype = (vehicle_type or "").strip()
if vtype == VEHICLE_TYPE_SELF:
validate_self_vehicle_required_certs(cert_rows, trailer_no=trailer_no)
elif vtype == VEHICLE_TYPE_CARRIER:
validate_carrier_vehicle_required_certs(cert_rows, trailer_no=trailer_no)
if vtype in (VEHICLE_TYPE_SELF, VEHICLE_TYPE_CARRIER) and personnel_rows:
validate_vehicle_personnel_affiliation(
db, tenant_no, vtype, personnel_rows
)
validate_vehicle_personnel_cert_links(db, tenant_no, personnel_rows)
+14 -1
View File
@@ -18,6 +18,7 @@ def upsert_personnel(
*,
user_id: str,
dept_id: str,
vehicle_type: str = "self",
) -> str:
"""按 personnelId → phone → name 弱匹配 upsert,返回 personnel_id。"""
personnel: Optional[PlPersonnel] = None
@@ -65,12 +66,18 @@ def upsert_personnel(
personnel.update_by = user_id
return personnel.id
affiliation = (vehicle_type or "self").strip()
if affiliation not in ("self", "carrier"):
affiliation = "self"
personnel = PlPersonnel(
id=generate_uuid(),
tenant_no=tenant_no,
personnel_name=name,
phone_no=phone,
age=row.age,
personnel_type="both",
personnel_affiliation=affiliation,
status="1",
create_by=user_id,
create_dept_id=dept_id,
@@ -88,6 +95,7 @@ def save_vehicle_personnel(
*,
user_id: str,
dept_id: str,
vehicle_type: str = "self",
) -> None:
"""软删旧关联后批量插入;校验默认司机唯一。"""
default_count = sum(
@@ -107,7 +115,12 @@ def save_vehicle_personnel(
raise HTTPException(status_code=400, detail="仅司机角色可设为默认")
personnel_id = upsert_personnel(
db, tenant_no, row, user_id=user_id, dept_id=dept_id
db,
tenant_no,
row,
user_id=user_id,
dept_id=dept_id,
vehicle_type=vehicle_type,
)
link = PlVehiclePersonnel(
id=generate_uuid(),
+3 -5
View File
@@ -122,6 +122,7 @@
"120": "Community 120",
"121": "Community 121",
"122": "Community 122",
"123": "Community 123",
"124": "Community 124",
"125": "Community 125",
"126": "Community 126",
@@ -148,6 +149,7 @@
"147": "Community 147",
"148": "Community 148",
"149": "Community 149",
"150": "Community 150",
"151": "Community 151",
"152": "Community 152",
"153": "Community 153",
@@ -155,9 +157,5 @@
"155": "Community 155",
"156": "Community 156",
"157": "Community 157",
"158": "Community 158",
"159": "Community 159",
"160": "Community 160",
"162": "Community 162",
"169": "Community 169"
"162": "Community 162"
}
+203 -213
View File
@@ -1,16 +1,16 @@
# Graph Report - backend (2026-07-12)
## Corpus Check
- 245 files · ~172,306 words
- 248 files · ~173,671 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 5436 nodes · 38642 edges · 161 communities (159 shown, 2 thin omitted)
- Extraction: 51% EXTRACTED · 49% INFERRED · 0% AMBIGUOUS · INFERRED: 18828 edges (avg confidence: 0.5)
- 5502 nodes · 38811 edges · 159 communities (157 shown, 2 thin omitted)
- Extraction: 51% EXTRACTED · 49% INFERRED · 0% AMBIGUOUS · INFERRED: 18852 edges (avg confidence: 0.5)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `7b1d4c87`
- Built from commit: `ffc89d9b`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -138,6 +138,7 @@
- [[_COMMUNITY_Community 120|Community 120]]
- [[_COMMUNITY_Community 121|Community 121]]
- [[_COMMUNITY_Community 122|Community 122]]
- [[_COMMUNITY_Community 123|Community 123]]
- [[_COMMUNITY_Community 124|Community 124]]
- [[_COMMUNITY_Community 125|Community 125]]
- [[_COMMUNITY_Community 126|Community 126]]
@@ -154,6 +155,7 @@
- [[_COMMUNITY_Community 138|Community 138]]
- [[_COMMUNITY_Community 148|Community 148]]
- [[_COMMUNITY_Community 149|Community 149]]
- [[_COMMUNITY_Community 150|Community 150]]
- [[_COMMUNITY_Community 151|Community 151]]
- [[_COMMUNITY_Community 152|Community 152]]
- [[_COMMUNITY_Community 153|Community 153]]
@@ -161,11 +163,7 @@
- [[_COMMUNITY_Community 155|Community 155]]
- [[_COMMUNITY_Community 156|Community 156]]
- [[_COMMUNITY_Community 157|Community 157]]
- [[_COMMUNITY_Community 158|Community 158]]
- [[_COMMUNITY_Community 159|Community 159]]
- [[_COMMUNITY_Community 160|Community 160]]
- [[_COMMUNITY_Community 162|Community 162]]
- [[_COMMUNITY_Community 169|Community 169]]
## God Nodes (most connected - your core abstractions)
1. `Session` - 1521 edges
@@ -191,139 +189,139 @@
- `Any` --uses--> `OperLogMeta` [INFERRED]
scripts/migrate_dibang_data.py → app/core/oper_log_context.py
## Communities (161 total, 2 thin omitted)
## Communities (159 total, 2 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.10
Nodes (112): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+104 more)
Cohesion: 0.12
Nodes (102): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+94 more)
### Community 1 - "Community 1"
Cohesion: 0.35
Nodes (219): Any, bool, str, UploadFile, CRM 线索管理 Pydantic Schemas, CRM 客户管理 Pydantic Schemas, CRM 商机管理 Pydantic Schemas, CRM 客户公海池规则 Pydantic Schemas (+211 more)
### Community 2 - "Community 2"
Cohesion: 0.22
Nodes (29): Any, int, Session, str, create_instance(), create_process_def(), create_task(), get_all_tasks_for_business() (+21 more)
Cohesion: 0.08
Nodes (54): CrmContract, CrmContractProductPriceTier, date, datetime, Decimal, Query, _apply_contract_entity_fields(), _apply_contract_scalar_fields() (+46 more)
### Community 3 - "Community 3"
Cohesion: 0.07
Nodes (58): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, str, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody, ContractApprovalExportBody, ContractApprovalListBody (+50 more)
Cohesion: 0.06
Nodes (53): str, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody, ContractApprovalExportBody, ContractApprovalListBody, ContractApprovalListQuery (+45 more)
### Community 4 - "Community 4"
Cohesion: 0.07
Nodes (112): bool, DbUserContext, int, 运输计划管理 - 基础数据 API(承运商、车辆、人员), PlCarrier, PlVehicle, Session, str (+104 more)
Cohesion: 0.05
Nodes (109): DbUserContext, int, 运输计划管理 - 基础数据 API(承运商、车辆、人员), PlVehicle, Session, str, VehicleRead, date (+101 more)
### Community 5 - "Community 5"
Cohesion: 0.21
Nodes (36): register_quote_routes(), bool, CRM 报价单 CRUD — 对照 CrmQuoteServiceImpl.java(P2 全量), Session, str, accept_quote(), create_quote(), _enrich_contact_fields() (+28 more)
Cohesion: 0.16
Nodes (44): register_quote_routes(), bool, CRM 报价单 CRUD — 对照 CrmQuoteServiceImpl.java(P2 全量), Session, str, accept_quote(), create_quote(), _enrich_contact_fields() (+36 more)
### Community 6 - "Community 6"
Cohesion: 0.05
Nodes (78): 运输计划管理 - 基础数据 Schema(承运商、车辆、人员、证书), Session, str, TransportInventoryLinkRead, str, TransportSiteLinkRead, CarrierAuditReject, CarrierBase (+70 more)
Cohesion: 0.16
Nodes (12): Decimal, str, VehicleRechargeApplyCreate, VehicleRechargeApplyListResponse, VehicleRechargeApplyOption, VehicleRechargeApplyRead, VehicleRechargeApplyUpdate, VehicleRechargeDisburseBoardListResponse (+4 more)
### Community 7 - "Community 7"
Cohesion: 0.06
Nodes (63): int, PlPlan, PlTransport, Session, str, UploadFile, Any, PlPlanTransport (+55 more)
Nodes (163): bool, PlCarrier, VehicleCreate, VehicleUpdate, bool, DbUserContext, float, int (+155 more)
### Community 8 - "Community 8"
Cohesion: 0.04
Nodes (118): Any, bool, CRM 产品模块 CRUD(产品分类 + 产品 + 附件), int, Session, str, SysOss, int (+110 more)
Nodes (117): Any, bool, CRM 产品模块 CRUD(产品分类 + 产品 + 附件), int, Session, str, SysOss, int (+109 more)
### Community 9 - "Community 9"
Cohesion: 0.07
Nodes (72): CertRead, date, int, Session, str, int, Session, str (+64 more)
Cohesion: 0.04
Nodes (107): Any, bool, CertRow, Session, str, VehiclePersonnelRow, CertRead, CertRow (+99 more)
### Community 10 - "Community 10"
Cohesion: 0.05
Nodes (126): bool, Decimal, int, PlPlan, PlPlanProduct, PlTransport, PlVehicle, Session (+118 more)
Cohesion: 0.03
Nodes (137): Decimal, PlPlanProduct, Session, str, Decimal, PlPlanChange, 计划变更记录表 pl_plan_change, 计划产品明细表 pl_plan_product (+129 more)
### Community 11 - "Community 11"
Cohesion: 0.11
Nodes (98): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+90 more)
Nodes (101): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+93 more)
### Community 12 - "Community 12"
Cohesion: 0.06
Nodes (169): _apply_default_invoice_info_to_customers(), category_add(), category_delete(), category_tree(), category_update(), clue_assign(), clue_check_convert(), clue_check_duplicate() (+161 more)
Cohesion: 0.05
Nodes (227): attachment_preview_url(), create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post(), create_role_management(), create_site() (+219 more)
### Community 13 - "Community 13"
Cohesion: 0.05
Nodes (44): get_plan_arrange_info(), patch_plan_product_certs(), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输) (+36 more)
Cohesion: 0.04
Nodes (46): get_plan_arrange_info(), patch_plan_product_certs(), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输) (+38 more)
### Community 14 - "Community 14"
Cohesion: 0.05
Nodes (74): BxUserContext, Session, str, UploadFile, Any, bool, bytes, int (+66 more)
Cohesion: 0.06
Nodes (60): Any, bool, bytes, int, str, Exception, bool, bytes (+52 more)
### Community 15 - "Community 15"
Cohesion: 0.11
Nodes (126): date, DbUserContext, int, PlanCreateFull, PlanDriverFeedbackUpdateBody, PlanSiteFeedbackUpdateBody, PlPlan, Session (+118 more)
Cohesion: 0.10
Nodes (132): date, DbUserContext, int, PlanCreateFull, PlanDriverFeedbackUpdateBody, PlanSiteFeedbackUpdateBody, PlPlan, Session (+124 more)
### Community 16 - "Community 16"
Cohesion: 0.03
Nodes (67): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+59 more)
Cohesion: 0.02
Nodes (104): 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+96 more)
### Community 17 - "Community 17"
Cohesion: 0.09
Nodes (57): Any, bool, CrmContract, datetime, int, Query, Session, str (+49 more)
Cohesion: 0.08
Nodes (70): CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, bool, CrmContract, datetime, int, Query (+62 more)
### Community 18 - "Community 18"
Cohesion: 0.16
Nodes (33): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+25 more)
Nodes (36): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+28 more)
### Community 19 - "Community 19"
Cohesion: 0.19
Nodes (18): str, Any, bool, Session, str, CRM 模块 Excel 导出列定义(字段与字典类型对齐 Java *Vo @ExcelDictFormat)。, _yes_no(), _build_workbook() (+10 more)
Cohesion: 0.06
Nodes (31): 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo (+23 more)
### Community 20 - "Community 20"
Cohesion: 0.08
Nodes (40): AttachmentUploadResponse, ChangePasswordRequest, Config, LoginRequest, LoginResponse, Pydantic schemas - 用户认证, ResetPasswordRequest, UserCreate (+32 more)
Cohesion: 0.07
Nodes (41): AttachmentUploadResponse, ChangePasswordRequest, Config, LoginRequest, LoginResponse, Pydantic schemas - 用户认证, ResetPasswordRequest, UserCreate (+33 more)
### Community 21 - "Community 21"
Cohesion: 0.11
Nodes (57): Any, bool, int, Session, str, Any, bool, CRM 线索 CRUD(线索主表 + 转化/分配/无效/等级操作) (+49 more)
Nodes (58): Any, bool, int, Session, str, Any, bool, CRM 线索 CRUD(线索主表 + 转化/分配/无效/等级操作) (+50 more)
### Community 22 - "Community 22"
Cohesion: 0.19
Nodes (37): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, UploadFile, bool, int (+29 more)
Cohesion: 0.09
Nodes (75): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, Session, str, UploadFile (+67 more)
### Community 23 - "Community 23"
Cohesion: 0.09
Nodes (116): date, DbUserContext, int, Session, str, VehicleRechargeApplyCreate, VehicleRechargeApplyRead, VehicleRechargeApplyUpdate (+108 more)
Cohesion: 0.11
Nodes (108): date, DbUserContext, int, Session, str, VehicleRechargeApplyCreate, VehicleRechargeApplyRead, VehicleRechargeApplyUpdate (+100 more)
### Community 24 - "Community 24"
Cohesion: 0.25
Nodes (18): BxUserContext, Session, str, bool, BxUserContext, Session, str, create_expense_item() (+10 more)
### Community 25 - "Community 25"
Cohesion: 0.14
Nodes (29): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+21 more)
Cohesion: 0.11
Nodes (35): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+27 more)
### Community 26 - "Community 26"
Cohesion: 0.18
Nodes (50): Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal, int, Session (+42 more)
Nodes (49): Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal, int, Session (+41 more)
### Community 27 - "Community 27"
Cohesion: 0.10
Nodes (58): bool, date, DbInboundOutbound, DbPurchasePlan, float, PlPlan, PlPlanProduct, PlTransport (+50 more)
Nodes (58): bool, date, datetime, DbInboundOutbound, DbPurchasePlan, float, PlPlan, PlPlanProduct (+50 more)
### Community 28 - "Community 28"
Cohesion: 0.12
Nodes (30): Session, str, _append_user_scope_conditions(), apply_data_scope(), _collect_scope_ids(), _get_child_dept_ids(), get_user_dept_id(), 数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤) data_scope 值: 1 = 全部数据 2 = 本部门数据 3 (+22 more)
Nodes (32): bool, Session, str, _append_user_scope_conditions(), apply_data_scope(), assert_row_in_data_scope(), _collect_scope_ids(), _get_child_dept_ids() (+24 more)
### Community 29 - "Community 29"
Cohesion: 0.10
Nodes (90): Any, register_contract_routes(), CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), Session (+82 more)
Cohesion: 0.08
Nodes (85): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, register_contract_routes(), bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), Session, str, batch_assign_owner(), batch_submit_approval() (+77 more)
### Community 30 - "Community 30"
Cohesion: 0.16
Nodes (51): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+43 more)
Cohesion: 0.36
Nodes (31): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+23 more)
### Community 31 - "Community 31"
Cohesion: 0.12
Nodes (44): 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), refresh_token(), bool, Redis, Request, str, _T, bool (+36 more)
Cohesion: 0.13
Nodes (43): 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), refresh_token(), bool, Redis, Request, str, _T, bool (+35 more)
### Community 32 - "Community 32"
Cohesion: 0.07
Nodes (118): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, bool (+110 more)
Nodes (123): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, bool (+115 more)
### Community 33 - "Community 33"
Cohesion: 0.15
@@ -331,11 +329,11 @@ Nodes (30): Any, bool, int, Session, str, approve_quote(), can_user_approve(), c
### Community 34 - "Community 34"
Cohesion: 0.04
Nodes (48): create_plan_full(), _generate_plan_no(), 创建运输计划(完整表单:合同产品 + 可选运输安排), 创建运输计划(完整表单:合同产品 + 可选运输安排), 创建运输计划(完整表单:合同产品 + 可选运输安排), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输), 创建运输计划(完整表单:合同产品 + 可选预选车辆,不自动安排运输) (+40 more)
Nodes (48): PlInventoryTransaction, PlInventoryWarning, 计划-运输关联表 pl_plan_transport, 运费结算表 pl_freight_settlement, 库存流水表 pl_inventory_transaction, 运费结算表 pl_freight_settlement, 结算单-运输关联表 pl_freight_settlement_transport, 结算单-运输关联表 pl_freight_settlement_transport (+40 more)
### Community 35 - "Community 35"
Cohesion: 0.21
Nodes (60): InboundOutboundRead, bool, PurchasePlanRead, PurchasePlanWithFillLink, DbUserContext, DbWeighingSlip, Session, str (+52 more)
Cohesion: 0.17
Nodes (39): DbPurchasePlan, DbUserContext, Session, str, InboundOutboundRead, _apply_plan_product_update(), _apply_plan_update(), _base_query() (+31 more)
### Community 36 - "Community 36"
Cohesion: 0.40
@@ -346,24 +344,24 @@ Cohesion: 0.13
Nodes (40): Any, bool, date, int, Session, str, Session, str (+32 more)
### Community 38 - "Community 38"
Cohesion: 0.18
Nodes (44): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, datetime, str (+36 more)
Cohesion: 0.17
Nodes (64): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, BxUserContext, int (+56 more)
### Community 39 - "Community 39"
Cohesion: 0.14
Nodes (40): DbInboundOutbound, DbUserContext, Session, str, Session, str, user_info_from_context(), _apply_inventory_product_update() (+32 more)
Cohesion: 0.21
Nodes (37): DbInboundOutbound, DbUserContext, Session, str, CreateLinkForPlanPayload, _apply_inventory_product_update(), _apply_inventory_update(), _base_query() (+29 more)
### Community 40 - "Community 40"
Cohesion: 0.15
Nodes (33): CRM 报价单 API 路由 — 挂接到 crm.router, CRM 报价单 Pydantic Schemas, BaseModel, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody (+25 more)
Cohesion: 0.12
Nodes (31): CRM 报价单 API 路由 — 挂接到 crm.router, CRM 报价单 Pydantic Schemas, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody, QuoteCreateBody (+23 more)
### Community 41 - "Community 41"
Cohesion: 0.07
Nodes (42): _db_pool_keepalive_loop(), _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str (+34 more)
Nodes (41): _db_pool_keepalive_loop(), _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str (+33 more)
### Community 42 - "Community 42"
Cohesion: 0.35
Nodes (30): BxUserContext, int, Session, str, approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse() (+22 more)
Cohesion: 0.05
Nodes (44): Any, datetime, Decimal, calculate_quote_amount(), _decimal_value(), _parse_optional_datetime(), _period_key(), Java: statisticsOverview (+36 more)
### Community 43 - "Community 43"
Cohesion: 0.13
@@ -371,39 +369,39 @@ Nodes (37): Any, bool, Decimal, int, Session, str, batch_insert_quote_products()
### Community 44 - "Community 44"
Cohesion: 0.07
Nodes (95): Any, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作), CrmCustomer (+87 more)
Nodes (98): pool_rule_toggle(), Any, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作) (+90 more)
### Community 45 - "Community 45"
Cohesion: 0.13
Nodes (15): Any, bool, int, str, get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块 支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.html, 统一下单接口 (V3) Args: out_trade_no: 商户订单号 total_amo (+7 more)
### Community 46 - "Community 46"
Cohesion: 0.13
Nodes (30): Any, bool, CRM 供方信息 CRUD — 对照 CrmSupplierInfoServiceImpl.java, int, Session, str, _apply_list_filters(), create_supplier_info() (+22 more)
Cohesion: 0.18
Nodes (11): generate_contract_no(), Java: generateContractNo — {供方编码}/X-JT + yyyyMMdd + 3位流水, Java: generateContractNo — {供方编码}/X-JT + yyyyMMdd + 3位流水, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号}, Java: generateContractNo — {供方编码}/X-JT{yyyyMMdd}{3位序号} (+3 more)
### Community 47 - "Community 47"
Cohesion: 0.16
Nodes (33): Any, bool, int, Session, str, Any, Session, str (+25 more)
### Community 48 - "Community 48"
Cohesion: 0.14
Nodes (121): date, DbUserContext, int, Session, str, bool, DbUserContext, float (+113 more)
Cohesion: 0.16
Nodes (94): date, DbUserContext, int, Session, str, DocumentReminderRead, DocumentReminderUpdate, InventoryLedgerResponse (+86 more)
### Community 49 - "Community 49"
Cohesion: 0.24
Nodes (20): Any, bool, str, _actor_ids(), _actor_names(), assert_row_deletable_by_creator(), assert_rows_deletable_by_creator(), can_delete_by_creator() (+12 more)
Cohesion: 0.38
Nodes (22): DbUserContext, int, Session, str, PlTransportAddress, 运输通用地址库 pl_transport_address, 运输通用地址库 pl_transport_address, 运输通用地址库 pl_transport_address (+14 more)
### Community 50 - "Community 50"
Cohesion: 0.12
Nodes (31): datetime, int, str, datetime, date, datetime, str, 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。 (+23 more)
Nodes (35): datetime, int, str, datetime, date, datetime, str, 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。 (+27 more)
### Community 51 - "Community 51"
Cohesion: 0.16
Nodes (31): Any, bool, CrmContractProductPriceTier, date, Decimal, int, Session, str (+23 more)
Cohesion: 0.17
Nodes (32): Any, bool, CrmContractProductPriceTier, date, Decimal, int, Session, str (+24 more)
### Community 52 - "Community 52"
Cohesion: 0.08
Nodes (25): batch_assign_owner(), download_contract_pdf(), Java: batchAssignOwner, Java: downloadContractPdf — 返回合同文件 URL/路径, Java: batchAssignOwner, Java: batchAssignOwner, Java: batchAssignOwner, Java: batchAssignOwner (+17 more)
Cohesion: 0.25
Nodes (8): Java Controller TODO — 保持 pending,记录备注, Java Controller TODO — 保持 pending,记录备注, Java Controller TODO — 保持 pending,记录备注, Java Controller TODO — 保持 pending,记录备注, Java Controller TODO — 保持 pending,记录备注, Java Controller TODO — 保持 pending,记录备注, Java Controller TODO — 保持 pending,记录备注, submit_change_approval()
### Community 53 - "Community 53"
Cohesion: 0.14
@@ -414,56 +412,56 @@ Cohesion: 0.23
Nodes (10): Any, bool, int, Session, str, camel_to_snake(), CRUDBase, get_model_attr() (+2 more)
### Community 55 - "Community 55"
Cohesion: 0.23
Nodes (30): Engine, int, Path, build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows() (+22 more)
Cohesion: 0.15
Nodes (35): Engine, Path, build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str() (+27 more)
### Community 56 - "Community 56"
Cohesion: 0.22
Nodes (12): Any, bool, str, WfProcessInstance, _LocalCallbackFn, _dispatch_local(), 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。 (+4 more)
Nodes (29): Any, int, Session, str, create_instance(), create_process_def(), create_task(), get_all_tasks_for_business() (+21 more)
### Community 57 - "Community 57"
Cohesion: 0.46
Nodes (13): bool, date, PlCarrier, str, bool, assert_carrier_eligible_for_transport(), carrier_agreement_expired(), carrier_audit_approved() (+5 more)
Cohesion: 0.22
Nodes (49): InboundOutboundRead, bool, PurchasePlanRead, PurchasePlanWithFillLink, DbUserContext, DbWeighingSlip, Session, str (+41 more)
### Community 58 - "Community 58"
Cohesion: 0.19
Nodes (20): int, str, Any, bool, int, Session, str, business_type_from_path() (+12 more)
Nodes (21): DbUserContext, Session, str, Session, str, int, Session, str (+13 more)
### Community 59 - "Community 59"
Cohesion: 0.33
Nodes (10): Any, int, Session, str, list_ethnics(), 民族码表 CRUD(全局 sys_ethnic), _row_to_dict(), 民族码表 sys_ethnic(国标 GB/T 3304,全局) (+2 more)
Cohesion: 0.24
Nodes (20): Any, bool, str, _actor_ids(), _actor_names(), assert_row_deletable_by_creator(), assert_rows_deletable_by_creator(), can_delete_by_creator() (+12 more)
### Community 60 - "Community 60"
Cohesion: 0.22
Nodes (9): Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval, Java: withdrawApproval (+1 more)
Cohesion: 0.19
Nodes (18): str, Any, bool, Session, str, CRM 模块 Excel 导出列定义(字段与字典类型对齐 Java *Vo @ExcelDictFormat)。, _yes_no(), _build_workbook() (+10 more)
### Community 61 - "Community 61"
Cohesion: 0.24
Nodes (25): DbUserContext, float, Session, str, Session, str, DashboardCargoStat, DashboardPartnerCargoStat (+17 more)
### Community 62 - "Community 62"
Cohesion: 0.10
Nodes (21): _build_contract_filters(), export_contracts(), Java: validateContractTypeAndProductsPresence, Java: queryList for export, Java: queryList for export, Java: queryList for export, Java: queryList for export, Java: buildQueryWrapper (+13 more)
Cohesion: 0.08
Nodes (40): Any, Any, str, Any, 与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。, _build_customer_address(), _change_row_to_dict(), contract_row_to_dict() (+32 more)
### Community 63 - "Community 63"
Cohesion: 0.08
Nodes (25): 技术栈, 1. 创建虚拟环境, 项目结构, 1. 创建虚拟环境, 2. 安装依赖, 2. 安装依赖, 3. 配置环境变量, 快速开始 (+17 more)
### Community 64 - "Community 64"
Cohesion: 0.05
Nodes (46): PlInventoryTransaction, PlInventoryWarning, 计划-运输关联表 pl_plan_transport, 运费结算表 pl_freight_settlement, 库存流水表 pl_inventory_transaction, 运费结算表 pl_freight_settlement, 结算单-运输关联表 pl_freight_settlement_transport, 结算单-运输关联表 pl_freight_settlement_transport (+38 more)
Cohesion: 0.21
Nodes (27): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), create_process_def(), delete_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields() (+19 more)
### Community 65 - "Community 65"
Cohesion: 0.28
Nodes (8): date, datetime, str, _date_to_str(), _dt_to_str(), PagedQuery, PagedResponse, bx 原系统 API 响应包装(code=0 / msg / data.rows),与门户 success_response 分离。
Cohesion: 0.12
Nodes (33): BxUserContext, int, Session, str, Any, str, bool, BxUserContext (+25 more)
### Community 66 - "Community 66"
Cohesion: 0.14
Nodes (28): str, T, bool, date, datetime, int, Session, str (+20 more)
Cohesion: 0.18
Nodes (24): bool, date, datetime, int, Session, str, ReimburseApplyOut, allocate_reimburse_title_seq() (+16 more)
### Community 67 - "Community 67"
Cohesion: 0.21
Nodes (17): bool, int, Redis, Request, str, _claims_from_auth(), _coerce_post_method_override_to_get(), _get_redis_client() (+9 more)
Cohesion: 0.20
Nodes (18): bool, int, Redis, Request, str, _claims_from_auth(), _coerce_post_method_override_to_get(), _get_redis_client() (+10 more)
### Community 68 - "Community 68"
Cohesion: 0.25
@@ -474,8 +472,8 @@ Cohesion: 0.21
Nodes (16): Any, int, str, BusinessException, InvalidStatusTransitionException, log_api_request(), log_delivery_operation(), log_operation() (+8 more)
### Community 70 - "Community 70"
Cohesion: 0.11
Nodes (32): Any, str, bool, BxUserContext, int, Session, str, Any (+24 more)
Cohesion: 0.14
Nodes (25): bool, BxUserContext, int, Session, str, Any, bool, int (+17 more)
### Community 71 - "Community 71"
Cohesion: 0.16
@@ -494,25 +492,25 @@ Cohesion: 0.06
Nodes (88): Any, Decimal, Session, str, _advance_after_approval(), _approval_to_dict(), can_user_approve_settlement(), cancel_draft_settlement() (+80 more)
### Community 75 - "Community 75"
Cohesion: 0.33
Nodes (3): 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。, 地磅(db)Pydantic schemas — 对齐原 simple-dibang-xiaoshou 契约。, 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。
Cohesion: 0.28
Nodes (11): operlog_detail(), operlog_list(), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), Session, OperLogDetailBody, OperLogListBody (+3 more)
### Community 76 - "Community 76"
Cohesion: 0.09
Nodes (49): API 路由包 — 山峡化工生产一体化门户, create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), submit_leave() (+41 more)
Cohesion: 0.27
Nodes (17): create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), submit_leave(), update_leave() (+9 more)
### Community 77 - "Community 77"
Cohesion: 0.33
Nodes (3): _coerce_id(), OffsetItem, ReimburseDetailCreate
Cohesion: 0.19
Nodes (20): int, str, Any, bool, int, Session, str, business_type_from_path() (+12 more)
### Community 78 - "Community 78"
Cohesion: 0.13
Nodes (37): CrmContract, CrmContractProductPriceTier, date, datetime, Decimal, int, Query, contract_row_to_dict() (+29 more)
### Community 79 - "Community 79"
Cohesion: 0.28
Nodes (18): Any, int, Session, str, _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept() (+10 more)
### Community 79 - "Community 79"
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_dict_type(), dict_type_code_exists(), get_dict_type() (+8 more)
### Community 80 - "Community 80"
Cohesion: 0.31
Nodes (20): Any, bool, CrmOpportunity, datetime, int, Session, str, create_opportunity_follow() (+12 more)
@@ -534,16 +532,16 @@ Cohesion: 0.34
Nodes (19): check_python(), create_venv(), ensure_dirs(), ensure_valid_venv(), ensure_venv_and_runtime(), install_dependencies(), is_running(), log_error() (+11 more)
### Community 85 - "Community 85"
Cohesion: 0.09
Nodes (27): Any, datetime, Decimal, _decimal_value(), _parse_optional_datetime(), _period_key(), Java: statisticsOverview, Java: statisticsOverview (+19 more)
Cohesion: 0.29
Nodes (16): Any, int, Session, str, BizLeaveRequest, create_leave(), get_leave(), list_leaves() (+8 more)
### Community 86 - "Community 86"
Cohesion: 0.35
Nodes (17): Any, bool, int, Session, str, _build_tree(), _cascade_soft_delete(), create_menu() (+9 more)
### Community 87 - "Community 87"
Cohesion: 0.10
Nodes (21): cancel_plan(), 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。 (+13 more)
Cohesion: 0.09
Nodes (22): cancel_plan(), 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。, 作废运输计划:取消跟进状态、作废关联地磅出入库单、取消运输单。 (+14 more)
### Community 88 - "Community 88"
Cohesion: 0.37
@@ -554,40 +552,40 @@ Cohesion: 0.22
Nodes (17): Any, bool, Session, str, list_share_logs(), _parse_user_agent(), CRM 报价单分享访问日志 CRUD — 对照 CrmQuoteShareLogServiceImpl.java, Java: statisticsVisits (+9 more)
### Community 90 - "Community 90"
Cohesion: 0.10
Nodes (22): date, Query, _build_quote_filters(), generate_quote_no(), _parse_optional_date(), Java: toContract — 返回 contractId, Java: toContract — 返回 contractId, Java: toContract — 返回 contractId (+14 more)
Cohesion: 0.38
Nodes (15): Any, bool, int, Session, str, create_site(), get_site(), list_sites() (+7 more)
### Community 91 - "Community 91"
Cohesion: 0.29
Nodes (16): Any, int, Session, str, BizLeaveRequest, create_leave(), get_leave(), list_leaves() (+8 more)
Cohesion: 0.10
Nodes (21): _generate_plan_no(), 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD###### (+13 more)
### Community 92 - "Community 92"
Cohesion: 0.45
Nodes (20): DbUserContext, int, Session, str, PlTransportAddress, 运输通用地址库 pl_transport_address, _address_to_read(), _build_address_query() (+12 more)
Cohesion: 0.10
Nodes (21): create_new_version(), Java: createNewVersion — 返回新报价单 id, Java: toContract — 返回 contractId, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id (+13 more)
### Community 93 - "Community 93"
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_tenant(), get_tenant(), list_tenants() (+8 more)
### Community 94 - "Community 94"
Cohesion: 0.39
Nodes (8): bool, BxUserContext, Session, str, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, WorkflowService, BxWorkflowDef
Cohesion: 0.41
Nodes (13): BxUserContext, Session, str, UploadFile, bind_biz(), delete_attachment(), download_file(), list_by_biz() (+5 more)
### Community 95 - "Community 95"
Cohesion: 0.23
Nodes (14): int, Session, str, _allocate_inbound_outbound_id(), _ensure_sequence_row(), max_numeric_inbound_outbound_id(), next_inbound_outbound_id(), 出入库单 id 生成 — 租户内纯数字自增,并发安全。 (+6 more)
### Community 96 - "Community 96"
Cohesion: 0.17
Nodes (20): Any, int, str, get_oper_log_meta(), OperLogMeta, 请求级操作日志元数据(ContextVar), 供异步落库线程使用,避免 ContextVar 跨线程丢失。, set_oper_log_meta() (+12 more)
Cohesion: 0.20
Nodes (10): Decimal, VehicleUreaApplyCreate, VehicleUreaApplyListResponse, VehicleUreaApplyRead, VehicleUreaApplyUpdate, VehicleUreaDisburseBoardListResponse, VehicleUreaDisburseCreate, VehicleUreaDisburseListResponse (+2 more)
### Community 97 - "Community 97"
Cohesion: 0.38
Nodes (15): Any, bool, int, Session, str, create_post(), get_post(), list_posts() (+7 more)
### Community 98 - "Community 98"
Cohesion: 0.38
Nodes (15): Any, bool, int, Session, str, create_site(), get_site(), list_sites() (+7 more)
Cohesion: 0.41
Nodes (11): Any, str, build_quote_html(), _default_value(), _format_date(), _format_discount(), _format_money(), generate_quote_pdf() (+3 more)
### Community 99 - "Community 99"
Cohesion: 0.33
@@ -599,23 +597,23 @@ Nodes (14): is_valid_dict_type(), load_portal_baseline(), load_sx(), load_zkxw()
### Community 101 - "Community 101"
Cohesion: 0.22
Nodes (17): Session, str, str, 附件上传 bizType 与 MinIO 目录映射(对齐原 CRM oss 上传语义), 优先使用显式 folder;否则按 bizType 映射默认目录。, 优先使用显式 folder;否则按 bizType 映射默认目录。, 优先使用显式 folder;否则按 bizType 映射默认目录。, resolve_upload_folder() (+9 more)
Nodes (18): Any, bool, bytes, int, Request, str, _claims_from_auth(), _format_body_bytes() (+10 more)
### Community 102 - "Community 102"
Cohesion: 0.35
Nodes (5): Any, str, WfProcessInstance, WorkflowEngine, WfProcessInstance
### Community 103 - "Community 103"
Cohesion: 0.12
Nodes (16): int, _fill_contract_linked_count(), list_quotes(), Java: fillContractLinkedCount, Java: fillContractLinkedCount, Java: fillContractLinkedCount, Java: fillContractLinkedCount, Java: fillContractLinkedCount (+8 more)
### Community 103 - "Community 103"
Cohesion: 0.41
Nodes (11): Any, str, build_quote_html(), _default_value(), _format_date(), _format_discount(), _format_money(), generate_quote_pdf() (+3 more)
### Community 104 - "Community 104"
Cohesion: 0.39
Nodes (8): int, LeaveCreate, LeaveUpdate, Config, LeaveCreate, LeaveResponse, LeaveUpdate, Pydantic schemas - 请假申请
Cohesion: 0.14
Nodes (15): date, Query, _build_quote_filters(), _parse_optional_date(), Java: toContract — 返回 contractId, Java: toContract — 返回 contractId, Java: buildQueryWrapper, Java: buildQueryWrapper (+7 more)
### Community 105 - "Community 105"
Cohesion: 0.16
Nodes (25): Any, bool, bytes, int, Request, str, bool, int (+17 more)
Cohesion: 0.17
Nodes (20): Any, int, str, get_oper_log_meta(), OperLogMeta, 请求级操作日志元数据(ContextVar), 供异步落库线程使用,避免 ContextVar 跨线程丢失。, set_oper_log_meta() (+12 more)
### Community 106 - "Community 106"
Cohesion: 0.36
@@ -626,49 +624,53 @@ Cohesion: 0.60
Nodes (10): Any, Session, str, _apply_create_range(), _base_query(), get_clue_level_statistics(), get_clue_overview_statistics(), get_clue_owner_statistics() (+2 more)
### Community 108 - "Community 108"
Cohesion: 0.35
Nodes (5): Any, str, WfProcessInstance, WorkflowEngine, WfProcessInstance
Cohesion: 0.22
Nodes (12): Any, bool, str, WfProcessInstance, _LocalCallbackFn, _dispatch_local(), 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。 (+4 more)
### Community 109 - "Community 109"
Cohesion: 0.14
Nodes (22): Session, str, bool, Session, str, get_user_menu_codes(), 菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms), 读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。 (+14 more)
### Community 110 - "Community 110"
Cohesion: 0.05
Nodes (38): create_contract(), create_from_opportunity(), generate_contract_name(), generate_contract_no(), _normalize_framework_contract_data(), _opportunity_products_to_contract_products(), Java: normalizeFrameworkContractData, Java: createFromOpportunity — 返回合同 id (+30 more)
Cohesion: 0.18
Nodes (11): generate_contract_name(), Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName, Java: generateContractName (+3 more)
### Community 111 - "Community 111"
Cohesion: 0.17
Nodes (42): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody, TaskActionBody, Any (+34 more)
Cohesion: 0.19
Nodes (38): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, StartInstanceBody, Any, bool, WfProcessInstance, bool (+30 more)
### Community 112 - "Community 112"
Cohesion: 0.08
Nodes (103): attachment_download_url(), attachment_preview_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post() (+95 more)
Cohesion: 0.26
Nodes (27): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, MenuCreateBody (+19 more)
### Community 113 - "Community 113"
Cohesion: 0.10
Nodes (20): create_from_quote(), _quote_products_to_contract_products(), Java: createFromQuote — 返回合同 id, Java: renewContract — 基础续签, Java: createFromQuote — 返回合同 id, Java: renewContract — 基础续签, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id (+12 more)
### Community 114 - "Community 114"
Cohesion: 0.19
Nodes (11): Decimal, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyListResponse, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder, VehicleMaintenanceApplyUpdate, VehicleMaintenanceProcessBoardListResponse, VehicleMaintenanceProcessCreate (+3 more)
### Community 115 - "Community 115"
Cohesion: 0.29
Nodes (9): AST, expr, field_has_alias(), iter_field_assignments(), main(), bool, int, str (+1 more)
### Community 116 - "Community 116"
Cohesion: 0.13
Nodes (15): calculate_quote_amount(), create_new_version(), Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id, Java: createNewVersion — 返回新报价单 id (+7 more)
### Community 117 - "Community 117"
Cohesion: 0.39
Nodes (6): _inject_user_info_after_tenant(), patch_contract_routes(), patch_quote_routes(), patch_route_file(), Path, str
### Community 118 - "Community 118"
Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_dict_type(), dict_type_code_exists(), get_dict_type() (+8 more)
### Community 119 - "Community 119"
Cohesion: 0.15
Nodes (13): ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相), ORM → camelCase(单点真相), Java: viewByShareCode, Java: viewByShareCode, Java: viewByShareCode, Java: viewByShareCode (+5 more)
### Community 117 - "Community 117"
Cohesion: 0.44
Nodes (8): bool, int, Session, str, ApiRequestLogCRUD, SysApiRequestLog, SysApiRequestLogHis, SysApiRequestLog
### Community 118 - "Community 118"
Cohesion: 0.20
Nodes (10): create_from_opportunity(), _opportunity_products_to_contract_products(), Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id, Java: createFromOpportunity — 返回合同 id (+2 more)
### Community 119 - "Community 119"
Cohesion: 0.29
Nodes (10): datetime, int, Session, str, BizNoKind, allocate_next_biz_no(), _format_biz_no(), _prefix_for() (+2 more)
### Community 120 - "Community 120"
Cohesion: 0.27
Nodes (11): Any, bool, str, decrypt_login_payload(), ensure_initialized(), extract_login_credentials(), get_public_pem(), 登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。 (+3 more)
@@ -681,6 +683,10 @@ Nodes (9): batch_submit_contract_approval(), Java: batchSubmitApproval, Java: ba
Cohesion: 0.32
Nodes (7): str, eq_unicode(), like_unicode(), MySQL 字符串比较/模糊匹配时统一排序规则,避免 Illegal mix of collations。, 跨表字符串等值比较(JOIN / WHERE)时统一为 utf8mb4_unicode_ci。, 将列显式设为 utf8mb4_unicode_ci。, unicode_ci()
### Community 123 - "Community 123"
Cohesion: 0.33
Nodes (10): Any, int, Session, str, list_ethnics(), 民族码表 CRUD(全局 sys_ethnic), _row_to_dict(), 民族码表 sys_ethnic(国标 GB/T 3304,全局) (+2 more)
### Community 124 - "Community 124"
Cohesion: 0.18
Nodes (9): 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, 报销(bx)Pydantic 模型 — 响应协议与 bx 原系统保持一致, bx_health(), 报销(bx)API 路由聚合 — REST 风格,协议对齐 bx 原系统。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。, P0 健康检查:验证 bx 路由已挂载且响应协议为 code=0。 (+1 more)
@@ -690,40 +696,40 @@ Cohesion: 0.22
Nodes (9): generate_template_no(), Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号}, Java: generateTemplateNo — TMPL{yyyyMMdd}{4位序号} (+1 more)
### Community 126 - "Community 126"
Cohesion: 0.17
Nodes (10): FreightSettlementApprovalActionBody, FreightSettlementByTransportRead, FreightSettlementCreate, FreightSettlementDisburseBody, FreightSettlementListItem, FreightSettlementSubmitApprovalBody, SettlementApprovalFlowItem, SettlementPlanSummary (+2 more)
Cohesion: 0.05
Nodes (48): BaseModel, AdvanceApplyOut, AdvanceApprovalCreate, AttachmentOut, PagedQuery, PagedResponse, ReimburseApplyCreate, ReimburseApplyUpdate (+40 more)
### Community 127 - "Community 127"
Cohesion: 0.16
Nodes (37): DbPurchasePlan, DbUserContext, Session, str, InboundOutboundRead, _apply_plan_product_update(), _apply_plan_update(), _base_query() (+29 more)
Cohesion: 0.32
Nodes (7): Session, str, apply_product_snapshot_to_weighing(), ProductSnapshot, 地磅模块 — CRM 产品解析与快照写入。, 校验 CRM 产品并返回名称/规格快照。新建单据 product_id 必填。, resolve_crm_product()
### Community 128 - "Community 128"
Cohesion: 0.24
Nodes (8): Decimal, VehicleUreaApplyCreate, VehicleUreaApplyListResponse, VehicleUreaApplyUpdate, VehicleUreaDisburseBoardListResponse, VehicleUreaDisburseCreate, VehicleUreaDisburseListResponse, VehicleUreaDisburseUpdate
Cohesion: 0.29
Nodes (5): API 路由包 — 山峡化工生产一体化门户, get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单), 运营端角色守卫:admin / super_admin 可访问。, require_admin_or_super_admin()
### Community 129 - "Community 129"
Cohesion: 0.18
Nodes (11): Java: createNewVersion — 返回新报价单 id, Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人), Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人) (+3 more)
Cohesion: 0.38
Nodes (6): date, datetime, str, _date_to_str(), _dt_to_str(), bx 原系统 API 响应包装(code=0 / msg / data.rows),与门户 success_response 分离。
### Community 130 - "Community 130"
Cohesion: 0.07
Nodes (34): _avatar_url_field(), _build_user_info_payload(), get_current_user_profile(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), public_quote_share(), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。 (+26 more)
Cohesion: 0.09
Nodes (29): _avatar_url_field(), _build_user_info_payload(), get_current_user_profile(), _jwt_user_payload(), logout(), public_quote_share(), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。 (+21 more)
### Community 131 - "Community 131"
Cohesion: 0.40
Nodes (3): BaseSettings, Config, Settings
### Community 132 - "Community 132"
Cohesion: 0.18
Nodes (11): Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus, Java: statisticsStatus (+3 more)
Cohesion: 0.33
Nodes (5): _coerce_optional_id(), WorkflowDefCreate, WorkflowDefUpdate, WorkflowNodeCreate, WorkflowNodeOut
### Community 134 - "Community 134"
Cohesion: 0.18
Nodes (11): 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。, 解析司机信息:优先 pl_personnel,兼容旧 pl_driver。 (+3 more)
Cohesion: 0.33
Nodes (6): attachment_download_url(), 返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。, 返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。, 返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。, 返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。, 返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。
### Community 135 - "Community 135"
Cohesion: 0.20
Nodes (10): Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds — 软删除, Java: deleteWithValidByIds, Java: deleteWithValidByIds — 软删除 (+2 more)
Cohesion: 0.33
Nodes (3): 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。, 地磅(db)Pydantic schemas — 对齐原 simple-dibang-xiaoshou 契约。, 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。
### Community 136 - "Community 136"
Cohesion: 0.18
@@ -739,11 +745,15 @@ Nodes (4): str, _coerce_id(), ExpenseItemCreate, ExpenseItemUpdate
### Community 149 - "Community 149"
Cohesion: 0.33
Nodes (5): _coerce_optional_id(), WorkflowDefCreate, WorkflowDefUpdate, WorkflowNodeCreate, WorkflowNodeOut
Nodes (3): _coerce_id(), OffsetItem, ReimburseDetailCreate
### Community 150 - "Community 150"
Cohesion: 0.40
Nodes (5): list_roles_for_designer(), 返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。 与 /roles 不同,此端点返回角色 UUID, 返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。 与 /roles 不同,此端点返回角色 UUID, 返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。 与 /roles 不同,此端点返回角色 UUID, 返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。 与 /roles 不同,此端点返回角色 UUID
### Community 151 - "Community 151"
Cohesion: 0.03
Nodes (60): 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll (+52 more)
Cohesion: 0.10
Nodes (21): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价) (+13 more)
### Community 152 - "Community 152"
Cohesion: 0.38
@@ -754,40 +764,20 @@ Cohesion: 0.40
Nodes (5): Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), Web 端登录用 RSA 公钥(PKCS#1 v1.5,与 JSEncrypt 一致), web_login_rsa_public_key()
### Community 154 - "Community 154"
Cohesion: 0.29
Nodes (10): datetime, int, Session, str, BizNoKind, allocate_next_biz_no(), _format_biz_no(), _prefix_for() (+2 more)
### Community 155 - "Community 155"
Cohesion: 0.40
Nodes (4): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID, ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID, ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
Nodes (5): get_web_auth_menu_codes(), 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。, 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。, 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。, 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。
### Community 156 - "Community 156"
Cohesion: 0.40
Nodes (4): ContactByCustomerBody, ContactDetailBody, ContactSetPrimaryBody, CRM 联系人 Pydantic Schemas
Nodes (3): AdvanceApplyCreate, AdvanceApplyUpdate, _coerce_optional_id()
### Community 157 - "Community 157"
Cohesion: 0.40
Nodes (4): CreateLinkForPlanPayload, InboundOutboundBase, InboundOutboundCreate, InboundOutboundUpdate
### Community 158 - "Community 158"
Cohesion: 0.50
Nodes (3): Config, PageResult, ResponseModel
### Community 159 - "Community 159"
Cohesion: 0.50
Nodes (3): PurchasePlanBase, PurchasePlanCreate, PurchasePlanUpdate
### Community 160 - "Community 160"
Cohesion: 0.50
Nodes (3): WeighingSlipBase, WeighingSlipCreate, WeighingSlipUpdate
Nodes (4): list_roles_for_select(), 返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。, 返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。, 返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。
### Community 162 - "Community 162"
Cohesion: 0.50
Nodes (3): Any, str, 与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。
### Community 169 - "Community 169"
Cohesion: 0.18
Nodes (12): Session, Session, get_current_user(), get_current_user_by_token(), 根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期), 根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期), 根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期), 获取当前用户(sync 依赖,配合 sync 路由在线程池执行,避免阻塞事件循环)。 (+4 more)
Cohesion: 0.12
Nodes (17): Session, Session, get_current_user(), get_current_user_by_token(), 根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期), 根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期), 根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期), 获取当前用户(sync 依赖,配合 sync 路由在线程池执行,避免阻塞事件循环)。 (+9 more)
## Knowledge Gaps
- **77 isolated node(s):** `Session`, `Request`, `bool`, `bool`, `Config` (+72 more)
@@ -797,12 +787,12 @@ Nodes (12): Session, Session, get_current_user(), get_current_user_by_token(),
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `Session` connect `Community 112` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 37`, `Community 38`, `Community 39`, `Community 42`, `Community 43`, `Community 44`, `Community 46`, `Community 47`, `Community 48`, `Community 51`, `Community 52`, `Community 53`, `Community 54`, `Community 58`, `Community 59`, `Community 60`, `Community 61`, `Community 62`, `Community 66`, `Community 68`, `Community 70`, `Community 71`, `Community 72`, `Community 73`, `Community 74`, `Community 76`, `Community 78`, `Community 79`, `Community 80`, `Community 81`, `Community 85`, `Community 86`, `Community 87`, `Community 89`, `Community 90`, `Community 91`, `Community 92`, `Community 93`, `Community 94`, `Community 95`, `Community 97`, `Community 98`, `Community 99`, `Community 101`, `Community 102`, `Community 104`, `Community 105`, `Community 107`, `Community 108`, `Community 109`, `Community 110`, `Community 111`, `Community 113`, `Community 116`, `Community 118`, `Community 119`, `Community 121`, `Community 125`, `Community 127`, `Community 129`, `Community 130`, `Community 132`, `Community 134`, `Community 135`, `Community 151`, `Community 154`, `Community 169`?**
- **Why does `Session` connect `Community 12` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 13`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 35`, `Community 37`, `Community 38`, `Community 39`, `Community 42`, `Community 43`, `Community 44`, `Community 46`, `Community 47`, `Community 48`, `Community 49`, `Community 50`, `Community 51`, `Community 52`, `Community 53`, `Community 54`, `Community 56`, `Community 57`, `Community 58`, `Community 60`, `Community 61`, `Community 62`, `Community 64`, `Community 65`, `Community 66`, `Community 68`, `Community 70`, `Community 71`, `Community 72`, `Community 73`, `Community 74`, `Community 75`, `Community 76`, `Community 77`, `Community 78`, `Community 79`, `Community 80`, `Community 81`, `Community 85`, `Community 86`, `Community 87`, `Community 89`, `Community 90`, `Community 91`, `Community 92`, `Community 93`, `Community 94`, `Community 95`, `Community 97`, `Community 99`, `Community 102`, `Community 103`, `Community 104`, `Community 107`, `Community 109`, `Community 111`, `Community 112`, `Community 113`, `Community 116`, `Community 117`, `Community 118`, `Community 119`, `Community 121`, `Community 123`, `Community 125`, `Community 126`, `Community 127`, `Community 130`, `Community 134`, `Community 150`, `Community 151`, `Community 154`, `Community 157`, `Community 162`?**
_High betweenness centrality (0.257) - this node is a cross-community bridge._
- **Why does `BaseModel` connect `Community 3` to `Community 0`, `Community 1`, `Community 128`, `Community 4`, `Community 6`, `Community 7`, `Community 9`, `Community 10`, `Community 11`, `Community 14`, `Community 15`, `Community 18`, `Community 148`, `Community 149`, `Community 22`, `Community 23`, `Community 24`, `Community 152`, `Community 20`, `Community 156`, `Community 29`, `Community 158`, `Community 30`, `Community 157`, `Community 159`, `Community 160`, `Community 35`, `Community 32`, `Community 38`, `Community 39`, `Community 40`, `Community 42`, `Community 48`, `Community 61`, `Community 65`, `Community 76`, `Community 77`, `Community 104`, `Community 111`, `Community 112`, `Community 114`, `Community 126`, `Community 127`?**
_High betweenness centrality (0.058) - this node is a cross-community bridge._
- **Why does `int` connect `Community 55` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 29`, `Community 30`, `Community 32`, `Community 33`, `Community 35`, `Community 37`, `Community 38`, `Community 41`, `Community 42`, `Community 43`, `Community 44`, `Community 45`, `Community 46`, `Community 47`, `Community 48`, `Community 50`, `Community 51`, `Community 52`, `Community 53`, `Community 54`, `Community 58`, `Community 59`, `Community 66`, `Community 67`, `Community 68`, `Community 69`, `Community 70`, `Community 71`, `Community 72`, `Community 73`, `Community 74`, `Community 76`, `Community 78`, `Community 79`, `Community 80`, `Community 83`, `Community 86`, `Community 91`, `Community 92`, `Community 93`, `Community 94`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 99`, `Community 101`, `Community 102`, `Community 104`, `Community 105`, `Community 108`, `Community 111`, `Community 112`, `Community 118`, `Community 121`, `Community 135`, `Community 154`?**
_High betweenness centrality (0.040) - this node is a cross-community bridge._
- **Why does `BaseModel` connect `Community 3` to `Community 0`, `Community 1`, `Community 132`, `Community 4`, `Community 6`, `Community 7`, `Community 9`, `Community 11`, `Community 15`, `Community 17`, `Community 18`, `Community 148`, `Community 149`, `Community 22`, `Community 23`, `Community 24`, `Community 152`, `Community 20`, `Community 155`, `Community 156`, `Community 30`, `Community 32`, `Community 35`, `Community 38`, `Community 39`, `Community 40`, `Community 48`, `Community 57`, `Community 58`, `Community 61`, `Community 64`, `Community 65`, `Community 66`, `Community 75`, `Community 76`, `Community 96`, `Community 111`, `Community 112`, `Community 114`, `Community 126`?**
_High betweenness centrality (0.055) - this node is a cross-community bridge._
- **Why does `int` connect `Community 38` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 26`, `Community 29`, `Community 30`, `Community 32`, `Community 33`, `Community 37`, `Community 41`, `Community 43`, `Community 44`, `Community 45`, `Community 47`, `Community 48`, `Community 49`, `Community 50`, `Community 51`, `Community 53`, `Community 54`, `Community 55`, `Community 56`, `Community 57`, `Community 62`, `Community 64`, `Community 65`, `Community 66`, `Community 67`, `Community 68`, `Community 69`, `Community 70`, `Community 71`, `Community 72`, `Community 73`, `Community 74`, `Community 76`, `Community 77`, `Community 78`, `Community 79`, `Community 80`, `Community 83`, `Community 85`, `Community 86`, `Community 90`, `Community 93`, `Community 95`, `Community 97`, `Community 99`, `Community 101`, `Community 102`, `Community 103`, `Community 105`, `Community 111`, `Community 112`, `Community 117`, `Community 119`, `Community 121`, `Community 123`?**
_High betweenness centrality (0.044) - this node is a cross-community bridge._
- **Are the 337 inferred relationships involving `str` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
_`str` has 337 INFERRED edges - model-reasoned connections that need verification._
- **Are the 333 inferred relationships involving `Session` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
@@ -810,4 +800,4 @@ _Questions this graph is uniquely positioned to answer:_
- **Are the 222 inferred relationships involving `int` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
_`int` has 222 INFERRED edges - model-reasoned connections that need verification._
- **What connects `定期 ping MySQL,避免远程库/NAT 回收空闲 TCP 后首请求长时间卡住。`, `数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误`, `未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin` to the rest of the system?**
_1493 weakly-connected nodes found - possible documentation gaps or missing edges._
_1525 weakly-connected nodes found - possible documentation gaps or missing edges._
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_base_data_py", "label": "base_data.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L1"}, {"id": "pl_base_data_list_carriers", "label": "list_carriers()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "carrierlistresponse", "label": "CarrierListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "pl_base_data_create_carrier", "label": "create_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L119"}, {"id": "carriercreate", "label": "CarrierCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L119"}, {"id": "carrierread", "label": "CarrierRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L119"}, {"id": "pl_base_data_get_carrier", "label": "get_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L149"}, {"id": "pl_base_data_update_carrier", "label": "update_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L160"}, {"id": "carrierupdate", "label": "CarrierUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L160"}, {"id": "pl_base_data_delete_carrier", "label": "delete_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L194"}, {"id": "pl_base_data_approve_carrier", "label": "approve_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L208"}, {"id": "pl_base_data_reject_carrier", "label": "reject_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L232"}, {"id": "carrierauditreject", "label": "CarrierAuditReject", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L232"}, {"id": "plcarrier", "label": "PlCarrier", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L254"}, {"id": "pl_base_data_vehicle_to_read", "label": "_vehicle_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L275"}, {"id": "plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L275"}, {"id": "vehicleread", "label": "VehicleRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L275"}, {"id": "pl_base_data_vehicle_to_detail", "label": "_vehicle_to_detail()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L288"}, {"id": "vehicledetailread", "label": "VehicleDetailRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L288"}, {"id": "pl_base_data_list_vehicles", "label": "list_vehicles()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L306"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L306"}, {"id": "vehiclelistresponse", "label": "VehicleListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L306"}, {"id": "pl_base_data_create_vehicle", "laLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_required_service_py", "label": "pl_cert_required_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L1"}, {"id": "services_pl_cert_required_service_match_required_cert_name", "label": "match_required_cert_name()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L25"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L25"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L25"}, {"id": "services_pl_cert_required_service_cert_row_complete", "label": "_cert_row_complete()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L38"}, {"id": "certrow", "label": "CertRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L38"}, {"id": "services_pl_cert_required_service_find_cert_for_required", "label": "_find_cert_for_required()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L61"}, {"id": "services_pl_cert_required_service_missing_field_labels", "label": "_missing_field_labels()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L81"}, {"id": "services_pl_cert_required_service_collect_required_cert_errors", "label": "collect_required_cert_errors()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L107"}, {"id": "services_pl_cert_required_service_get_required_personnel_cert_names", "label": "get_required_personnel_cert_names()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L132"}, {"id": "services_pl_cert_required_service_validate_personnel_type_role_compat", "label": "validate_personnel_type_role_compat()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L141"}, {"id": "services_pl_cert_required_service_validate_personnel_required_certs", "label": "validate_personnel_required_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L152"}, {"id": "services_pl_cert_required_service_validate_self_vehicle_required_certs", "label": "validate_self_vehicle_required_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L171"}, {"id": "services_pl_cert_required_service_validate_self_vehicle_personnel_links", "label": "validate_self_vehicle_personnel_links()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L195"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L195"}, {"id": "vehiclepersonnelrow", "label": "VehiclePersonnelRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L195"}, {"id": "services_pl_cert_required_service_validate_vehicle_save_certs", "label": "validate_vehicle_save_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L234"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_required_service_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_required_service_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_required_service_py", "target": "sqlalchemy_orm", "Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "label": "pl_personnel_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L1"}, {"id": "services_pl_personnel_service_upsert_personnel", "label": "upsert_personnel()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L14"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L14"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L14"}, {"id": "vehiclepersonnelrow", "label": "VehiclePersonnelRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L14"}, {"id": "services_pl_personnel_service_save_vehicle_personnel", "label": "save_vehicle_personnel()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L90"}, {"id": "services_pl_personnel_service_build_personnel_summary", "label": "build_personnel_summary()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L140"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L140"}, {"id": "services_pl_personnel_service_load_vehicle_personnel_rows", "label": "load_vehicle_personnel_rows()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L157"}, {"id": "services_pl_personnel_service_load_vehicle_personnel_summaries", "label": "load_vehicle_personnel_summaries()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L190"}, {"id": "services_pl_personnel_service_rationale_1", "label": "\u8fd0\u8f93\u8ba1\u5212 - \u4eba\u5458 upsert \u4e0e\u8f66\u8f86-\u4eba\u5458\u5173\u8054\u4fdd\u5b58", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L1"}, {"id": "services_pl_personnel_service_rationale_23", "label": "\u6309 personnelId \u2192 phone \u2192 name \u5f31\u5339\u914d upsert\uff0c\u8fd4\u56de personnel_id\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L23"}, {"id": "services_pl_personnel_service_rationale_143", "label": "links: [(personnel_name, role, is_default)] \u2192 (summary, default_driver_name).", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L143"}, {"id": "services_pl_personnel_service_rationale_193", "label": "\u6279\u91cf\u52a0\u8f7d personnelSummary / defaultDriverName\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L193"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "target": "app_core_utils", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L9", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "target": "app_models_pl_models", "relation": "imports_from"Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "label": "pl_personnel_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L1"}, {"id": "services_pl_personnel_service_upsert_personnel", "label": "upsert_personnel()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L14"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L14"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L14"}, {"id": "vehiclepersonnelrow", "label": "VehiclePersonnelRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L14"}, {"id": "services_pl_personnel_service_save_vehicle_personnel", "label": "save_vehicle_personnel()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L84"}, {"id": "services_pl_personnel_service_build_personnel_summary", "label": "build_personnel_summary()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L128"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L128"}, {"id": "services_pl_personnel_service_load_vehicle_personnel_rows", "label": "load_vehicle_personnel_rows()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L145"}, {"id": "services_pl_personnel_service_load_vehicle_personnel_summaries", "label": "load_vehicle_personnel_summaries()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L178"}, {"id": "services_pl_personnel_service_rationale_1", "label": "\u8fd0\u8f93\u8ba1\u5212 - \u4eba\u5458 upsert \u4e0e\u8f66\u8f86-\u4eba\u5458\u5173\u8054\u4fdd\u5b58", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L1"}, {"id": "services_pl_personnel_service_rationale_22", "label": "\u6309 personnelId \u2192 phone \u2192 name \u5f31\u5339\u914d upsert\uff0c\u8fd4\u56de personnel_id\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L22"}, {"id": "services_pl_personnel_service_rationale_131", "label": "links: [(personnel_name, role, is_default)] \u2192 (summary, default_driver_name).", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L131"}, {"id": "services_pl_personnel_service_rationale_181", "label": "\u6279\u91cf\u52a0\u8f7d personnelSummary / defaultDriverName\u3002", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L181"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L6", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "target": "app_core_utils", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py", "source_location": "L9", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_personnel_service_py", "target": "app_models_pl_models", "relation": "imports_from"Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_models_pl_models_py", "label": "pl_models.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L1"}, {"id": "models_pl_models_plcarrier", "label": "PlCarrier", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L28"}, {"id": "base", "label": "Base", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "models_pl_models_plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L57"}, {"id": "models_pl_models_plpersonnel", "label": "PlPersonnel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L83"}, {"id": "models_pl_models_pltransportaddress", "label": "PlTransportAddress", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L111"}, {"id": "models_pl_models_plcert", "label": "PlCert", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L135"}, {"id": "models_pl_models_pldriver", "label": "PlDriver", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L161"}, {"id": "models_pl_models_plvehiclepersonnel", "label": "PlVehiclePersonnel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L187"}, {"id": "models_pl_models_plvehicledriver", "label": "PlVehicleDriver", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L208"}, {"id": "models_pl_models_plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L233"}, {"id": "models_pl_models_plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L332"}, {"id": "models_pl_models_plplanchange", "label": "PlPlanChange", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L388"}, {"id": "models_pl_models_pltransport", "label": "PlTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L415"}, {"id": "models_pl_models_pltransportdblink", "label": "PlTransportDbLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L477"}, {"id": "models_pl_models_plplantransport", "label": "PlPlanTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L500"}, {"id": "models_pl_models_pltransportsitelink", "label": "PlTransportSiteLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L519"}, {"id": "models_pl_models_pltransportapproval", "label": "PlTransportApproval", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L552"}, {"id": "models_pl_models_plfreightsettlement", "label": "PlFreightSettlement", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L581"}, {"id": "models_pl_models_plfreightsettlementapproval", "label": "PlFreightSettlementApproval", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L613"}, {"id": "models_pl_models_plfreightsettlementtransport", "label": "PlFreightSettlementTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L640"}, {"id": "models_pl_models_plremittancevoucher", "label": "PlRemittanceVoucher", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L660"}, {"id": "models_pl_models_plinventory", "label": "PlInventory", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L687"}, {"id": "models_pl_models_plinventorytransaction", "label": "PlInventoryTransaction", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L719"}, {"id": "models_pl_models_plinventorywarning", "label": "PlInventoryWarning", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L1"}, {"id": "pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L84"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L84"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L84"}, {"id": "pl_plan_format_subpack_slip_no", "label": "_format_subpack_slip_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L116"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L116"}, {"id": "pl_plan_normalize_subpack_slip_no", "label": "_normalize_subpack_slip_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L120"}, {"id": "pl_plan_subpack_slip_no_seq", "label": "_subpack_slip_no_seq()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L145"}, {"id": "pl_plan_ensure_subpack_slip_no", "label": "_ensure_subpack_slip_no()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L153"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L153"}, {"id": "pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L185"}, {"id": "pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L190"}, {"id": "pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L199"}, {"id": "pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L220"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L220"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L220"}, {"id": "planlistresponse", "label": "PlanListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L220"}, {"id": "pl_plan_count_plans_by_contract", "label": "count_plans_by_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L332"}, {"id": "pl_plan_create_plan", "label": "create_plan()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L360"}, {"id": "plancreate", "label": "PlanCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L360"}, {"id": "planread", "label": "PlanRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L360"}, {"id": "pl_plan_create_plan_full", "label": "create_plan_full()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L443"}, {"id": "plancreatefull", "label": "PlanCreateFull", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L443"}, {"id": "planwithproducts", "label": "PlanWithProducts", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L443"}, {"id": "pl_plan_create_from_contract", "label": "create_from_contract()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L550"}, {"id": "createfromcontractrequest", "label": "CreateFromContractRequest", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py", "source_location": "L550"}, {"id": "pl_plan_get_plan", "label": "get_plan()", "file_type": "code",Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_required_service_py", "label": "pl_cert_required_service.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L1"}, {"id": "services_pl_cert_required_service_match_required_cert_name", "label": "match_required_cert_name()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L29"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L29"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L29"}, {"id": "services_pl_cert_required_service_cert_row_complete", "label": "_cert_row_complete()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L42"}, {"id": "certrow", "label": "CertRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L42"}, {"id": "services_pl_cert_required_service_find_cert_for_required", "label": "_find_cert_for_required()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L65"}, {"id": "services_pl_cert_required_service_missing_field_labels", "label": "_missing_field_labels()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L85"}, {"id": "services_pl_cert_required_service_collect_required_cert_errors", "label": "collect_required_cert_errors()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L111"}, {"id": "services_pl_cert_required_service_get_required_personnel_cert_names", "label": "get_required_personnel_cert_names()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L136"}, {"id": "services_pl_cert_required_service_validate_personnel_type_role_compat", "label": "validate_personnel_type_role_compat()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L145"}, {"id": "services_pl_cert_required_service_validate_personnel_required_certs", "label": "validate_personnel_required_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L156"}, {"id": "services_pl_cert_required_service_validate_self_vehicle_required_certs", "label": "validate_self_vehicle_required_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L175"}, {"id": "services_pl_cert_required_service_validate_carrier_vehicle_required_certs", "label": "validate_carrier_vehicle_required_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L199"}, {"id": "services_pl_cert_required_service_validate_vehicle_personnel_affiliation", "label": "validate_vehicle_personnel_affiliation()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L223"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L223"}, {"id": "vehiclepersonnelrow", "label": "VehiclePersonnelRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L223"}, {"id": "services_pl_cert_required_service_validate_vehicle_personnel_cert_links", "label": "validate_vehicle_personnel_cert_links()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L257"}, {"id": "services_pl_cert_required_service_validate_vehicle_save_certs", "label": "validate_vehicle_save_certs()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py", "source_location": "L296"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_services_pl_cert_required_service_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/woLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "label": "crm_contract_routes.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L1"}, {"id": "api_crm_contract_routes_register_contract_routes", "label": "register_contract_routes()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L75"}, {"id": "api_crm_contract_routes_rationale_1", "label": "CRM \u5408\u540c API \u8def\u7531 \u2014 GET/PUT/DELETE/POST \u5bf9\u9f50\u539f Java CRM", "file_type": "rationale", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L1"}], "edges": [{"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "datetime", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L4", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L5", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "fastapi", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L7", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "sqlalchemy_orm", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L8", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_database", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L10", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_menu_guard", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L11", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_oper_log_decorator", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L12", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_response", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L13", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_core_tenant_context", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L14", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_data_scope_helpers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L15", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_contract", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L16", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_crud_crm_contract_approval", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py", "source_location": "L47", "weight": 1.0}, {"source": "users_zlwu_work_00code_shanxia2_0_backend_app_api_crm_contract_routes_py", "target": "app_schemas_crm_contract", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/zlwu/work/00code/shLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_base_data_py", "label": "base_data.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L1"}, {"id": "pl_base_data_carrierbase", "label": "CarrierBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L19"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_base_data_carrierqualificationbase", "label": "CarrierQualificationBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L27"}, {"id": "pl_base_data_carriercreate", "label": "CarrierCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L45"}, {"id": "pl_base_data_carriercreate_validate_qualification", "label": ".validate_qualification()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L56"}, {"id": "pl_base_data_carrierupdate", "label": "CarrierUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L66"}, {"id": "pl_base_data_carrierupdate_validate_qualification_patch", "label": ".validate_qualification_patch()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L88"}, {"id": "pl_base_data_carrierauditreject", "label": "CarrierAuditReject", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L109"}, {"id": "pl_base_data_carrierread", "label": "CarrierRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L115"}, {"id": "pl_base_data_carrier_to_read", "label": "carrier_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L134"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L134"}, {"id": "pl_base_data_vehiclepersonnelrow", "label": "VehiclePersonnelRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L170"}, {"id": "pl_base_data_certrow", "label": "CertRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L181"}, {"id": "pl_base_data_certread", "label": "CertRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L196"}, {"id": "pl_base_data_vehiclebase", "label": "VehicleBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L212"}, {"id": "pl_base_data_vehiclecreate", "label": "VehicleCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L225"}, {"id": "pl_base_data_vehicleupdate", "label": "VehicleUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L231"}, {"id": "pl_base_data_vehicleread", "label": "VehicleRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L245"}, {"id": "pl_base_data_vehicledetailread", "label": "VehicleDetailRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L259"}, {"id": "pl_base_data_personnelbase", "label": "PersonnelBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L270"}, {"id": "pl_base_data_personnelcreate", "label": "PersonnelCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L285"}, {"id": "pl_base_data_personnelupdate", "label": "PersonnelUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L290"}, {"id": "pl_base_data_personnelread", "label": "PersonnelRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L302"}, {"id": "pl_base_data_personneldetailread", "label": "PersonnelDetailRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_schemas_pl_base_data_py", "label": "base_data.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L1"}, {"id": "pl_base_data_carrierbase", "label": "CarrierBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L19"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "pl_base_data_carrierqualificationbase", "label": "CarrierQualificationBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L27"}, {"id": "pl_base_data_carriercreate", "label": "CarrierCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L45"}, {"id": "pl_base_data_carriercreate_validate_qualification", "label": ".validate_qualification()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L56"}, {"id": "pl_base_data_carrierupdate", "label": "CarrierUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L66"}, {"id": "pl_base_data_carrierupdate_validate_qualification_patch", "label": ".validate_qualification_patch()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L88"}, {"id": "pl_base_data_carrierauditreject", "label": "CarrierAuditReject", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L109"}, {"id": "pl_base_data_carrierread", "label": "CarrierRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L115"}, {"id": "pl_base_data_carrier_to_read", "label": "carrier_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L134"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L134"}, {"id": "pl_base_data_vehiclepersonnelrow", "label": "VehiclePersonnelRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L170"}, {"id": "pl_base_data_certrow", "label": "CertRow", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L181"}, {"id": "pl_base_data_certread", "label": "CertRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L196"}, {"id": "pl_base_data_vehiclebase", "label": "VehicleBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L212"}, {"id": "pl_base_data_vehiclecreate", "label": "VehicleCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L225"}, {"id": "pl_base_data_vehicleupdate", "label": "VehicleUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L231"}, {"id": "pl_base_data_vehicleread", "label": "VehicleRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L245"}, {"id": "pl_base_data_vehicledetailread", "label": "VehicleDetailRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L259"}, {"id": "pl_base_data_personnelbase", "label": "PersonnelBase", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L270"}, {"id": "pl_base_data_personnelcreate", "label": "PersonnelCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L291"}, {"id": "pl_base_data_personnelupdate", "label": "PersonnelUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L296"}, {"id": "pl_base_data_personnelread", "label": "PersonnelRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py", "source_location": "L311"}, {"id": "pl_base_data_personneldetailread", "label": "PersonnelDetailRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_models_pl_models_py", "label": "pl_models.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L1"}, {"id": "models_pl_models_plcarrier", "label": "PlCarrier", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L28"}, {"id": "base", "label": "Base", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "models_pl_models_plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L57"}, {"id": "models_pl_models_plpersonnel", "label": "PlPersonnel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L83"}, {"id": "models_pl_models_pltransportaddress", "label": "PlTransportAddress", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L117"}, {"id": "models_pl_models_plcert", "label": "PlCert", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L141"}, {"id": "models_pl_models_pldriver", "label": "PlDriver", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L167"}, {"id": "models_pl_models_plvehiclepersonnel", "label": "PlVehiclePersonnel", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L193"}, {"id": "models_pl_models_plvehicledriver", "label": "PlVehicleDriver", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L214"}, {"id": "models_pl_models_plplan", "label": "PlPlan", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L239"}, {"id": "models_pl_models_plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L338"}, {"id": "models_pl_models_plplanchange", "label": "PlPlanChange", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L394"}, {"id": "models_pl_models_pltransport", "label": "PlTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L421"}, {"id": "models_pl_models_pltransportdblink", "label": "PlTransportDbLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L483"}, {"id": "models_pl_models_plplantransport", "label": "PlPlanTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L506"}, {"id": "models_pl_models_pltransportsitelink", "label": "PlTransportSiteLink", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L525"}, {"id": "models_pl_models_pltransportapproval", "label": "PlTransportApproval", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L558"}, {"id": "models_pl_models_plfreightsettlement", "label": "PlFreightSettlement", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L587"}, {"id": "models_pl_models_plfreightsettlementapproval", "label": "PlFreightSettlementApproval", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L619"}, {"id": "models_pl_models_plfreightsettlementtransport", "label": "PlFreightSettlementTransport", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L646"}, {"id": "models_pl_models_plremittancevoucher", "label": "PlRemittanceVoucher", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L666"}, {"id": "models_pl_models_plinventory", "label": "PlInventory", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L693"}, {"id": "models_pl_models_plinventorytransaction", "label": "PlInventoryTransaction", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py", "source_location": "L725"}, {"id": "models_pl_models_plinventorywarning", "label": "PlInventoryWarning", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_zlwu_work_00code_shanxia2_0_backend_app_api_pl_base_data_py", "label": "base_data.py", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L1"}, {"id": "pl_base_data_list_carriers", "label": "list_carriers()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "str", "label": "str", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "int", "label": "int", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "carrierlistresponse", "label": "CarrierListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L74"}, {"id": "pl_base_data_create_carrier", "label": "create_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L119"}, {"id": "carriercreate", "label": "CarrierCreate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L119"}, {"id": "carrierread", "label": "CarrierRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L119"}, {"id": "pl_base_data_get_carrier", "label": "get_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L149"}, {"id": "pl_base_data_update_carrier", "label": "update_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L160"}, {"id": "carrierupdate", "label": "CarrierUpdate", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L160"}, {"id": "pl_base_data_delete_carrier", "label": "delete_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L194"}, {"id": "pl_base_data_approve_carrier", "label": "approve_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L208"}, {"id": "pl_base_data_reject_carrier", "label": "reject_carrier()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L232"}, {"id": "carrierauditreject", "label": "CarrierAuditReject", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L232"}, {"id": "plcarrier", "label": "PlCarrier", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L254"}, {"id": "pl_base_data_vehicle_to_read", "label": "_vehicle_to_read()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L275"}, {"id": "plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L275"}, {"id": "vehicleread", "label": "VehicleRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L275"}, {"id": "pl_base_data_vehicle_to_detail", "label": "_vehicle_to_detail()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L288"}, {"id": "vehicledetailread", "label": "VehicleDetailRead", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L288"}, {"id": "pl_base_data_list_vehicles", "label": "list_vehicles()", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L306"}, {"id": "bool", "label": "bool", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L306"}, {"id": "vehiclelistresponse", "label": "VehicleListResponse", "file_type": "code", "source_file": "/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py", "source_location": "L306"}, {"id": "pl_base_data_create_vehicle", "laLine truncated
+1 -1
View File
@@ -1 +1 @@
{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6716,"mtime_ns":1783761682571576393,"hash":"bbe64081c9790941eb2fa8f9c18ee39daf151068b3503f1fdb74c2cb9f7b5b45"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4Line truncated
{"/Users/zlwu/work/00code/shanxia2.0/backend/start-prod.sh":{"size":7357,"mtime_ns":1782639355476060453,"hash":"ca89869446be74a2bf2367b3b28f448580220859a39a84ad9613ac5fe35a8433"},"/Users/zlwu/work/00code/shanxia2.0/backend/start.sh":{"size":9548,"mtime_ns":1780114661446983244,"hash":"4d632863e8a0cd81194b491d83a0218a4157435ae812dec76a6f6197b4d48e57"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982281973984,"hash":"60a04ff08fd97084e83ea809fd13feda6d2848be20ebf309e31fc1dad7523230"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/main.py":{"size":6280,"mtime_ns":1782790852770448643,"hash":"8f9da799977bee7134d87a26ff8434ac5ef8eabb0b7368dd537eee49ad7fd00b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/tasks/__init__.py":{"size":2207,"mtime_ns":1783208067881286091,"hash":"313e6da0e91d743ce891dd283458c79b50b24c4d909dc96ca7b3fd9d2bfa5013"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/auth.py":{"size":7457,"mtime_ns":1782790841500516545,"hash":"34bac56e1c2cb6215f4b5fa2dc32d77d4b4abca51428819eebf4afd269cc51cf"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto.py":{"size":1870,"mtime_ns":1780108930724444498,"hash":"c04b748aea86890a5971f749523c7b17dbad1ddd6f9f7f07a5679d159c984e33"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/login_crypto.py":{"size":3858,"mtime_ns":1780108930725155710,"hash":"b82d51afabf2293a9cb1fb2e5f994c383948e7f47ea95fe674686b4f3f3342b1"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/config.py":{"size":6716,"mtime_ns":1783761682571576393,"hash":"bbe64081c9790941eb2fa8f9c18ee39daf151068b3503f1fdb74c2cb9f7b5b45"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_pay.py":{"size":23585,"mtime_ns":1780108930725768130,"hash":"3d52265ce65d21e7103f94afa1e8547aee0823b5ea6d65c50317436ac81fb4fc"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_request_log_middleware.py":{"size":11971,"mtime_ns":1782701669198208175,"hash":"0b84c32828939e6102bb3647691fa8f04b1f68e0e0e9f28127374cc44e19a11c"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/database.py":{"size":1510,"mtime_ns":1782790847265601586,"hash":"8bc9c8ae62e2be2c6920036582ba448dfcd1711c41cebb62274ff6bc6ee14b0b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/security.py":{"size":728,"mtime_ns":1780108930725470004,"hash":"df0f01115064f7b8de1c81dbe9a6db39360c236610b68c33b3234a51d7f0025b"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/request_payload.py":{"size":2842,"mtime_ns":1780108930725350836,"hash":"e9937151530ab1417ef297aef59111977725cf90be2a8fa76c00a0618789aba0"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/__init__.py":{"size":61,"mtime_ns":1780108930724312414,"hash":"3dd1842ee59b0c804176b03b4b8a323bd4a252b7feb1bbc32db64f3a8928570e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/response.py":{"size":1824,"mtime_ns":1780108930725412462,"hash":"f1739352d65ea8287d76e282a59553aa8d63c97e35764c64ac378aa5bd9c347e"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/menu_guard.py":{"size":1611,"mtime_ns":1780108930725223002,"hash":"3e26077288c3620a4780038b891a326206b45bffe00aca545bdae30172487235"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/db_collation.py":{"size":548,"mtime_ns":1780108930725025043,"hash":"0c680e25a18244ca839a7de6ed1f4313b33bc8da118d89c463aee1b6af00c6f9"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/context.py":{"size":421,"mtime_ns":1780108930724824458,"hash":"83a10aee67162374993c10a30bf8bb8bd9c4b35cf48f42c832415ccebd91660f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/utils.py":{"size":1498,"mtime_ns":1780108930725607296,"hash":"09323693661c1d4560458b86a954128ccff82bbaf6522da0b44f24da99ddf9ea"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/storage.py":{"size":14141,"mtime_ns":1782855136810943216,"hash":"3ae2c413c10898a8adb3d6773870bafbfbc18c739c10e0e8b4f765b4b5ca6c08"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/order_status_compat.py":{"size":2828,"mtime_ns":1780108930725284669,"hash":"5349e5a0745ee6734c4a995337779307adefc46f453906171988a77de2081540"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/exceptions.py":{"size":4946,"mtime_ns":1780108930725084335,"hash":"79c997d964bca1a4f12f2b92fb1f181fdb9e2b3cf72eae39f4cc164d23794fd7"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/data_scope.py":{"size":8043,"mtime_ns":1782648925694296304,"hash":"626dff86937e5cacec634ecbc61cd53d3763d760223a1d4ec7cd11f64e59a220"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/admin_request_json.py":{"size":2669,"mtime_ns":1780108930724378914,"hash":"4f68b3cf84291e7bddad6ecf4bb4d29ca5e1ec246a9f7f9c4cf5909d0ec1174a"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/api_crypto_middleware.py":{"size":10876,"mtime_ns":1782790001926627098,"hash":"b39f128b6612589ea6ac5babd30752e9585b3af07b82bb1ad53c9e3c0d90816f"},"/Users/zlwu/work/00code/shanxia2.0/backend/app/core/wechat_message.py":{"size":11194,"mtime_ns":1780108930725684255,"hash":"5086070deb14d99d1a69e419b2299ec4Line truncated
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,24 @@
-- pl_personnel 增加人员归属:self-山峡 carrier-非山峡
SET @col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'pl_personnel'
AND COLUMN_NAME = 'personnel_affiliation'
);
SET @ddl := IF(
@col_exists = 0,
'ALTER TABLE `pl_personnel`
ADD COLUMN `personnel_affiliation` varchar(20) NOT NULL DEFAULT ''self''
COMMENT ''人员归属 self-山峡 carrier-非山峡''
AFTER `personnel_type`',
'SELECT ''personnel_affiliation already exists'' AS msg'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE `pl_personnel`
SET `personnel_affiliation` = 'self'
WHERE `personnel_affiliation` IS NULL OR `personnel_affiliation` = '';
@@ -0,0 +1,52 @@
-- pl_personnel 增加人员类型;历史数据按车辆关联推断
-- 可重复执行(列已存在则跳过加列,UPDATE 可重复跑)
SET @col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'pl_personnel'
AND COLUMN_NAME = 'personnel_type'
);
SET @ddl := IF(
@col_exists = 0,
'ALTER TABLE `pl_personnel`
ADD COLUMN `personnel_type` varchar(20) NOT NULL DEFAULT ''both''
COMMENT ''人员类型 driver-驾驶员 escort-押运员 both-驾驶员兼押运员''
AFTER `id_card_no`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 仅有 driver 关联
UPDATE pl_personnel p
SET p.personnel_type = 'driver'
WHERE p.del_flag = 0
AND (p.personnel_type IS NULL OR p.personnel_type = '' OR p.personnel_type = 'both')
AND EXISTS (
SELECT 1 FROM pl_vehicle_personnel vp
WHERE vp.personnel_id = p.id AND vp.del_flag = 0 AND vp.role = 'driver'
)
AND NOT EXISTS (
SELECT 1 FROM pl_vehicle_personnel vp
WHERE vp.personnel_id = p.id AND vp.del_flag = 0 AND vp.role = 'escort'
);
-- 仅有 escort 关联
UPDATE pl_personnel p
SET p.personnel_type = 'escort'
WHERE p.del_flag = 0
AND (p.personnel_type IS NULL OR p.personnel_type = '' OR p.personnel_type = 'both')
AND EXISTS (
SELECT 1 FROM pl_vehicle_personnel vp
WHERE vp.personnel_id = p.id AND vp.del_flag = 0 AND vp.role = 'escort'
)
AND NOT EXISTS (
SELECT 1 FROM pl_vehicle_personnel vp
WHERE vp.personnel_id = p.id AND vp.del_flag = 0 AND vp.role = 'driver'
);
-- 同时有 driver + escort 或无关联保持 both(默认)
@@ -0,0 +1,24 @@
-- pl_personnel 增加人员归属:self-山峡 carrier-非山峡
SET @col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'pl_personnel'
AND COLUMN_NAME = 'personnel_affiliation'
);
SET @ddl := IF(
@col_exists = 0,
'ALTER TABLE `pl_personnel`
ADD COLUMN `personnel_affiliation` varchar(20) NOT NULL DEFAULT ''self''
COMMENT ''人员归属 self-山峡 carrier-非山峡''
AFTER `personnel_type`',
'SELECT ''personnel_affiliation already exists'' AS msg'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE `pl_personnel`
SET `personnel_affiliation` = 'self'
WHERE `personnel_affiliation` IS NULL OR `personnel_affiliation` = '';
@@ -0,0 +1,52 @@
-- pl_personnel 增加人员类型;历史数据按车辆关联推断
-- 可重复执行(列已存在则跳过加列,UPDATE 可重复跑)
SET @col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'pl_personnel'
AND COLUMN_NAME = 'personnel_type'
);
SET @ddl := IF(
@col_exists = 0,
'ALTER TABLE `pl_personnel`
ADD COLUMN `personnel_type` varchar(20) NOT NULL DEFAULT ''both''
COMMENT ''人员类型 driver-驾驶员 escort-押运员 both-驾驶员兼押运员''
AFTER `id_card_no`',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 仅有 driver 关联
UPDATE pl_personnel p
SET p.personnel_type = 'driver'
WHERE p.del_flag = 0
AND (p.personnel_type IS NULL OR p.personnel_type = '' OR p.personnel_type = 'both')
AND EXISTS (
SELECT 1 FROM pl_vehicle_personnel vp
WHERE vp.personnel_id = p.id AND vp.del_flag = 0 AND vp.role = 'driver'
)
AND NOT EXISTS (
SELECT 1 FROM pl_vehicle_personnel vp
WHERE vp.personnel_id = p.id AND vp.del_flag = 0 AND vp.role = 'escort'
);
-- 仅有 escort 关联
UPDATE pl_personnel p
SET p.personnel_type = 'escort'
WHERE p.del_flag = 0
AND (p.personnel_type IS NULL OR p.personnel_type = '' OR p.personnel_type = 'both')
AND EXISTS (
SELECT 1 FROM pl_vehicle_personnel vp
WHERE vp.personnel_id = p.id AND vp.del_flag = 0 AND vp.role = 'escort'
)
AND NOT EXISTS (
SELECT 1 FROM pl_vehicle_personnel vp
WHERE vp.personnel_id = p.id AND vp.del_flag = 0 AND vp.role = 'driver'
);
-- 同时有 driver + escort 或无关联保持 both(默认)
+2
View File
@@ -81,6 +81,8 @@ CREATE TABLE `pl_personnel` (
`phone_no` varchar(32) DEFAULT NULL COMMENT '手机号(非必填)',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`id_card_no` varchar(32) DEFAULT NULL COMMENT '身份证号',
`personnel_type` varchar(20) NOT NULL DEFAULT 'both' COMMENT '人员类型 driver-驾驶员 escort-押运员 both-驾驶员兼押运员',
`personnel_affiliation` varchar(20) NOT NULL DEFAULT 'self' COMMENT '人员归属 self-山峡 carrier-非山峡',
`status` char(1) NOT NULL DEFAULT '1' COMMENT '状态 1-在职 0-停用',
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
+5 -4
View File
@@ -2752,6 +2752,7 @@ CREATE TABLE `pl_personnel` (
`phone_no` varchar(32) DEFAULT NULL COMMENT '手机号(非必填)',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`id_card_no` varchar(32) DEFAULT NULL COMMENT '身份证号',
`personnel_type` varchar(20) NOT NULL DEFAULT 'both' COMMENT '人员类型 driver-驾驶员 escort-押运员 both-驾驶员兼押运员',
`status` char(1) NOT NULL DEFAULT '1' COMMENT '状态 1-在职 0-停用',
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
@@ -2772,10 +2773,10 @@ CREATE TABLE `pl_personnel` (
-- Records of pl_personnel
-- ----------------------------
BEGIN;
INSERT INTO `pl_personnel` (`id`, `personnel_name`, `phone_no`, `age`, `id_card_no`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('3C13B995C2B149E8AA50222609D030A1', '里斯', '13099922123', 32, '123123', '1', 'shanxia2.0', NULL, '123123', 'd3000000000000000000000000000001', 'b13c2be2-4e71-11f1-9f47-00163e3bdd96', '2026-06-20 07:45:10', 'd3000000000000000000000000000001', '2026-06-20 07:55:22', 0);
INSERT INTO `pl_personnel` (`id`, `personnel_name`, `phone_no`, `age`, `id_card_no`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('4B753C6E5CE446AB9208C449A76BEA0C', '张三', '13900022112', 35, '340829181823123123123', '1', 'shanxia2.0', NULL, '备注', 'd3000000000000000000000000000001', 'b13c2be2-4e71-11f1-9f47-00163e3bdd96', '2026-06-19 22:56:38', 'd3000000000000000000000000000001', '2026-06-20 07:44:42', 0);
INSERT INTO `pl_personnel` (`id`, `personnel_name`, `phone_no`, `age`, `id_card_no`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('9AB6B5474343428C80C24503EB7771DC', '刘道银', '130023312', 32, '34082219912312', '1', 'shanxia2.0', NULL, '司机', 'BFB4CBEAB7054F48BDE82B55A985798C', 'b13c2be2-4e71-11f1-9f47-00163e3bdd96', '2026-06-28 14:57:39', 'd3000000000000000000000000000001', '2026-06-28 16:11:58', 0);
INSERT INTO `pl_personnel` (`id`, `personnel_name`, `phone_no`, `age`, `id_card_no`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('BD11DEEA3C1E4C8EA75CDF18A6EED21F', '吴春生', '1390001231', 32, '340822121323', '1', 'shanxia2.0', NULL, '押运员', 'BFB4CBEAB7054F48BDE82B55A985798C', 'b13c2be2-4e71-11f1-9f47-00163e3bdd96', '2026-06-28 14:56:57', 'd3000000000000000000000000000001', '2026-06-28 16:11:58', 0);
INSERT INTO `pl_personnel` (`id`, `personnel_name`, `phone_no`, `age`, `id_card_no`, `personnel_type`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('3C13B995C2B149E8AA50222609D030A1', '里斯', '13099922123', 32, '123123', 'both', '1', 'shanxia2.0', NULL, '123123', 'd3000000000000000000000000000001', 'b13c2be2-4e71-11f1-9f47-00163e3bdd96', '2026-06-20 07:45:10', 'd3000000000000000000000000000001', '2026-06-20 07:55:22', 0);
INSERT INTO `pl_personnel` (`id`, `personnel_name`, `phone_no`, `age`, `id_card_no`, `personnel_type`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('4B753C6E5CE446AB9208C449A76BEA0C', '张三', '13900022112', 35, '340829181823123123123', 'both', '1', 'shanxia2.0', NULL, '备注', 'd3000000000000000000000000000001', 'b13c2be2-4e71-11f1-9f47-00163e3bdd96', '2026-06-19 22:56:38', 'd3000000000000000000000000000001', '2026-06-20 07:44:42', 0);
INSERT INTO `pl_personnel` (`id`, `personnel_name`, `phone_no`, `age`, `id_card_no`, `personnel_type`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('9AB6B5474343428C80C24503EB7771DC', '刘道银', '130023312', 32, '34082219912312', 'driver', '1', 'shanxia2.0', NULL, '司机', 'BFB4CBEAB7054F48BDE82B55A985798C', 'b13c2be2-4e71-11f1-9f47-00163e3bdd96', '2026-06-28 14:57:39', 'd3000000000000000000000000000001', '2026-06-28 16:11:58', 0);
INSERT INTO `pl_personnel` (`id`, `personnel_name`, `phone_no`, `age`, `id_card_no`, `personnel_type`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('BD11DEEA3C1E4C8EA75CDF18A6EED21F', '吴春生', '1390001231', 32, '340822121323', 'escort', '1', 'shanxia2.0', NULL, '押运员', 'BFB4CBEAB7054F48BDE82B55A985798C', 'b13c2be2-4e71-11f1-9f47-00163e3bdd96', '2026-06-28 14:56:57', 'd3000000000000000000000000000001', '2026-06-28 16:11:58', 0);
COMMIT;
-- ----------------------------
@@ -0,0 +1,340 @@
# 运输管理 · 固定必备证书槽位 — 设计文档
> **日期**: 2026-07-12
> **状态**: 已实现(2026-07-12)
> **前置**: `docs/superpowers/specs/2026-06-19-pl-vehicle-personnel-cert-design.md`(动态多证、首期不配置必检)
> **界面模式**: P01 标准列表 + 弹窗表单(`/#/pl/vehicle`、`/#/pl/self-vehicle`、`/#/pl/personnel`)
## 背景与问题
### 现状(2026-06-19 上线后)
| 环节 | 行为 |
|------|------|
| 证书录入 | 用户点击「添加证书」→ 手选/手输证书名称 → 填写证号、有效期 → 上传附件 |
| 前端校验 | 仅有证书行时「证书名称不能为空」;**不**校验必备证种齐全 |
| 必备常数 | `pl-vehicle-cert.ts` / `pl-personnel-cert.ts` 已定义必备名称,但 `validateVehicleRequiredCerts` 已 `@deprecated` |
| 缺失提示 | 查看态标题可显示「(证书缺失)」;**保存不阻断** |
| 人员类型 | `pl_personnel` **无**人员类型字段;司机/押运员角色仅在车辆关联子表 `pl_vehicle_personnel.role` |
| 承运商车辆 | 「全部车辆信息」含自有 + 承运商;必备缺失检查仅对 `vehicleType=self` |
### 业务诉求(2026-07-12 确认)
1. **固定必备槽位**:打开编辑弹窗时,系统预置必备证书行,名称锁定不可改、不可删。
2. **必备三项必填**:每个必备槽位的 **证号、有效期、附件** 均必填。
3. **可增补其他证件**:除必备行外,仍可「添加其他证书」(名称可选/可输,可删)。
4. **山峡自有车强制**:仅 `vehicleType=self` 的车辆强制主车/挂车必备;**承运商等非山峡车辆不强制**。
5. **人员类型**:人员主数据新增类型——驾驶员、押运员、驾驶员兼押运员;按类型预置必备证书。
6. **历史数据**:**一律阻断**——只要保存(含仅改备注),必备证书未补齐则拒绝。
### 与 2026-06-19 设计的关系
2026-06-19 明确「首期不配置必检规则、安排运输不硬性拦截证书」。本次变更:
- **升级** 车辆/人员主数据保存时的证书完整性要求(仅山峡自有车 + 全部人员主数据)。
- **不变更** 运输计划安排运输时对 `hazard_cert_ready` 的既有逻辑(危化品计划级证书另案)。
- **沿用** `pl_cert` 动态多证表结构,不新增证书实例表。
---
## 已确认设计
| # | 决策 | 说明 |
|---|------|------|
| D1 | **必备槽位预置 + 锁定** | 必备行 `certName` 只读、不可删除;额外行可自由增删 |
| D2 | **必备字段:证号 + 有效期 + 附件** | 提醒天数维持默认 30,不作为必备条件 |
| D3 | **仅山峡自有车强制车辆证** | `vehicleType=self` 时校验主车 3 证、有挂车号时挂车 4 证;`carrier` 不校验 |
| D4 | **人员类型三选一** | `driver` 驾驶员 / `escort` 押运员 / `both` 驾驶员兼押运员;人员主数据必填 |
| D5 | **人员必备按类型** | driver、both → 身份证+驾驶证+从业资格证;escort → 身份证+从业资格证 |
| D6 | **从业资格证别名** | 「危化品从业资格证」等等价匹配「从业资格证」(沿用 `matchRequiredCertName`) |
| D7 | **保存一律阻断** | 历史数据缺必备证时,任何字段保存均须先补齐 |
| D8 | **前后端双校验** | 前端 UX + 后端 API 400,防绕过 |
| D9 | **关联人员角色兼容** | 车辆子表 `role` 保留;保存山峡自有车时校验人员类型与 role 可匹配(见 §三) |
| D10 | **挂车动态** | 无 `trailerNo` 不要求挂车槽位;填写挂车号后立即要求 4 项必备 |
---
## 一、必备证书清单
### 1.1 山峡自有车辆(`vehicleType=self`)
| 部位 | 条件 | 必备证书(各需证号+有效期+附件) |
|------|------|----------------------------------|
| 主车 | 始终 | 行驶证、营运证、保险单 |
| 挂车 | `trailerNo` 非空 | 行驶证、营运证、保险单、罐检报告 |
常数来源:`PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES`、`PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES`(`web-admin/src/utils/pl-vehicle-cert.ts`)。
### 1.2 承运商 / 非自有车辆(`vehicleType=carrier`)
- 不预置必备行(或预置但不阻断,**推荐:不预置、不校验**,与现网自由录入一致)。
- 用户仍可手动添加任意证书。
### 1.3 人员(`pl_personnel.personnel_type`)
| personnel_type | 展示文案 | 必备证书 |
|----------------|---------|---------|
| `driver` | 驾驶员 | 身份证、驾驶证、从业资格证 |
| `escort` | 押运员 | 身份证、从业资格证 |
| `both` | 驾驶员兼押运员 | 身份证、驾驶证、从业资格证 |
常数来源:`PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES`、`PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES`。
### 1.4 「证书已填」判定
必备槽位 **同时满足**:
```text
certName 匹配目标名称(从业资格证含别名规则)
certNo 非空 trim
expireDate 非空
attachmentUrls 非空 trim(至少一个 URL)
```
---
## 二、数据模型
### 2.1 `pl_personnel` 新增字段
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `personnel_type` | varchar(20) | ✅(新数据;迁移见 §2.3) | `driver` / `escort` / `both` |
DDL 示例:
```sql
ALTER TABLE `pl_personnel`
ADD COLUMN `personnel_type` varchar(20) NOT NULL DEFAULT 'both'
COMMENT '人员类型 driver-驾驶员 escort-押运员 both-驾驶员兼押运员'
AFTER `id_card_no`;
```
同步:`docs/sql/pl_tables.sql`、`docs/sql/shanxia2.0.sql`;增量脚本 `backend/scripts/migration_20260712_pl_personnel_type.sql`。
### 2.2 字典(可选增强)
| dict_type | 值 |
|-----------|-----|
| `pl_personnel_type` | driver-驾驶员, escort-押运员, both-驾驶员兼押运员 |
列表 Tag、表单下拉可与字典对齐;**必备规则仍硬编码于前后端 util**,不依赖运行时字典配置(YAGNI)。
### 2.3 历史数据迁移 · 人员类型
按优先级推断 `personnel_type`:
1. 若 `pl_vehicle_personnel` 中该人仅有 `role=driver` → `driver`
2. 若仅有 `role=escort` → `escort`
3. 若同时存在 driver 与 escort 关联 → `both`
4. 若无关联记录 → 默认 `both`(必备要求最宽,避免误标 escort 导致缺驾驶证仍可通过)
迁移后运营需对人员主数据做一次补录/核对(D7 保存阻断会驱动补齐)。
### 2.4 证书表
`pl_cert` **无结构变更**。必备/额外均存同一表;必备行通过 `cert_name` +(车辆)`vehicle_part` 识别。
可选:必备行写入时设 `order_num` 靠前,额外行在后。
---
## 三、业务规则
### 3.1 车辆保存(POST/PUT `/transport/vehicle`)
```
if vehicle_type == 'self':
校验主车必备 3 证齐全(证号+有效期+附件)
if trailer_no 非空:
校验挂车必备 4 证齐全
校验关联人员(见 3.2)
else:
跳过车辆必备校验(仅校验 certName 非空等基础项,若有证书行)
```
「山峡自有车辆信息」页固定 `vehicleType=self`,与「全部车辆信息」中自有行逻辑一致。
### 3.2 山峡自有车 · 关联人员
保存时除人员主数据证书外,额外校验:
| 检查项 | 规则 |
|--------|------|
| 类型与 role 匹配 | `escort` 类型不可关联为 `driver`;`driver` 类型不可关联为 `escort`;`both` 均可 |
| 角色必备证书 | 关联行的 `role=driver` 时,该人员证书须满足 driver 必备;`role=escort` 时满足 escort 必备 |
说明:人员主数据按 `personnel_type` 校验一次;若 `both` 且仅关联为 escort,主数据仍须驾驶证(因类型为 both)。若未来需放宽,可另开变更。
### 3.3 人员保存(POST/PUT `/transport/personnel`)
- `personnel_type` 必填。
- 按类型校验必备证书(D5)。
- **任意**字段更新(含仅 `remark`)均执行完整必备校验(D7)。
### 3.4 挂车号动态
- 编辑中 `trailerNo` 从空变为非空:前端立即展示挂车 4 个必备空槽;保存时校验。
- 从非空变为空:挂车必备校验跳过;已有挂车 `vehicle_part=trailer` 证书行保存策略——**保留数据但不阻断**(挂车号清空后不再校验挂车必备;证书行仍随 `certList` 提交,后端按现有 upsert 逻辑处理)。
---
## 四、前端设计
### 4.1 组件策略
**方案**:扩展或替换 `PlCertFormList` → `PlCertSlotFormList`(推荐新组件,原组件供承运商等非强制场景复用)。
| 模式 | props | 行为 |
|------|-------|------|
| `required` | `requiredCertNames`, `vehiclePart?`, `strictFields=true` | 预置必备行、锁定名称、校验三字段 |
| `free` | 无 required | 等同现有 `PlCertFormList` |
`PlVehicleCertPartTabs`:
- `vehicleType=self` → 各 part 传入对应 `requiredCertNames`
- `vehicleType=carrier` → 子组件 `mode=free`
### 4.2 页面改动
| 页面 | 变更 |
|------|------|
| `pl/self-vehicle/index.vue` | 证书区 `required`;保存前 `validateSelfVehicleCerts` |
| `pl/vehicle/index.vue` | 依 `formData.vehicleType` 切换 required/free;查看态缺失 suffix 逻辑保持 |
| `pl/personnel/index.vue` | 表单加「人员类型」;证书区按类型 `required`;列表加类型列 Tag |
### 4.3 表单 · 人员类型
```text
人员类型* [ el-select ]
- 驾驶员 (driver)
- 押运员 (escort)
- 驾驶员兼押运员 (both)
```
- 新增默认:`both` 或空(须必选,无默认值更安全 → **无默认,必选**)。
- 修改类型:重新计算必备槽位;已有同名校验数据保留,新增必备行补空槽。
### 4.4 UI 分区
```
┌─ 人员证书 ──────────────────────────────────────┐
│ ▎必备证书 │
│ 身份证 证号* 有效期* 附件* 🔒 │
│ 驾驶证 ... │
│ ▎其他证书 │
│ [+ 添加其他证书] │
└─────────────────────────────────────────────────┘
```
必备区与额外区可用分组标题或表格分段区分;必备行删除按钮隐藏。
### 4.5 校验与错误提示
- 保存失败:`ElMessage.warning` + 可选 `ElMessageBox.alert` 列出缺失项。
- 文案示例:`主车「行驶证」缺少:证号、附件`。
- 长表单:`scroll-to-error` 滚动至证书区首个缺失槽。
---
## 五、后端设计
### 5.1 新增服务 · `pl_cert_required_service.py`(或并入 `pl_cert_service.py`)
```python
def get_required_vehicle_cert_names(vehicle_type, trailer_no, part) -> list[str]
def get_required_personnel_cert_names(personnel_type) -> list[str]
def validate_required_certs(cert_rows, required_names, *, part_label=None) -> None # raises HTTPException 400
def validate_personnel_type_role_compat(personnel_type, role) -> None
```
### 5.2 API 接入点
| 路由 | 校验时机 |
|------|---------|
| `POST/PUT .../vehicle` | `vehicle_type=self` 时校验 cert_list + 关联人员 |
| `POST/PUT .../personnel` | 始终校验 personnel_type + cert_list |
错误响应示例:
```json
{
"detail": "必备证书未齐全:主车「保险单」缺少有效期、附件;人员「张三」类型为驾驶员,缺少驾驶证"
}
```
### 5.3 Schema
- `PersonnelBase` / `PersonnelCreate` / `PersonnelUpdate` / `PersonnelRead` 增加 `personnel_type`(alias `personnelType`)。
- `VehicleCreate/Update` 无需新字段;沿用 `vehicle_type`。
---
## 六、OpenSpec / 工程规范
建议在 `openspec/changes/transport-plan-management/` 或新 change 中增补 Scenario:
- 山峡自有车保存 SHALL 阻断缺失必备车辆证。
- 承运商车辆保存 SHALL NOT 因缺失必备车辆证阻断。
- 人员保存 SHALL 按 personnel_type 阻断缺失必备人员证。
交叉核对:`openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md`(Tag 展示人员类型、按钮图标等)。
---
## 七、验收标准
1. 山峡自有车新增:打开弹窗即见主车 3 个必备空槽;填挂车号后出现挂车 4 槽。
2. 必备槽位:名称不可编辑、不可删除;证号/有效期/附件任一缺失则无法保存。
3. 可添加「道路运输证」等额外证书并正常保存。
4. 承运商车辆在「全部车辆信息」中无必备槽位(或有不阻断),缺证仍可保存。
5. 人员页必选类型;三类人员必备槽位正确;改类型后槽位同步。
6. 历史自有车/人员:仅改备注时若缺必备证,保存返回明确错误。
7. 山峡自有车:驾驶员类型不可选为押运员 role(及反向);both 均可。
8. 后端 duplicate 校验:直接调 API 缺必备证返回 400。
9. `python scripts/check_field_consistency.py` 通过(personnel_type 字段)。
10. 迁移脚本可重复执行;现有 `pl_cert` 数据不丢失。
---
## 八、风险与缓解
| 风险 | 缓解 |
|------|------|
| 历史大量缺证导致无法保存 | D7 为业务明确要求;上线前导出缺证清单,运营批量补录 |
| 人员类型迁移不准 | 迁移规则 §2.3 + 列表展示类型供人工修正 |
| 组件复杂度上升 | 新组件 `PlCertSlotFormList`,承运商继续用旧组件 |
| 车辆页切换类型 self↔carrier | 切换为 carrier 时隐藏必备槽位且不校验;切回 self 时重新注入必备槽并校验 |
| both 类型关联 escort 仍要驾驶证 | 设计已说明;若业务异议可后续改为「按 role 动态必备」 |
---
## 九、工作量估算
| 模块 | 预估 |
|------|------|
| DB + 迁移 + Schema | 0.5 天 |
| 后端必备校验服务 + API | 0.5 天 |
| `PlCertSlotFormList` + 车辆 tabs 接入 | 1 天 |
| 人员页类型字段 + 证书接入 | 0.5 天 |
| 三页联调 + 历史场景自测 | 0.5~1 天 |
| **合计** | **约 3~3.5 人天** |
---
## 十、实施任务清单(概要)
1. SQL:`personnel_type` 列 + 迁移推断脚本
2. Backend:Model / Schema / `_row_to_dict` / 校验服务 / vehicle & personnel API
3. Frontend utils:统一 `validateRequiredCertSlots`、`buildRequiredCertRows`
4. Component:`PlCertSlotFormList`
5. Pages:`self-vehicle`、`vehicle`、`personnel`
6. 查看态:人员类型 Tag;缺失 suffix 与必备校验口径一致
7. 自测 + `check_field_consistency.py`
8. OpenSpec scenario 增补(可选同 PR)
---
## 附录:与现有常数对照
需求与 `pl-vehicle-cert.ts` / `pl-personnel-cert.ts` **完全一致**,无需调整必备名称列表,仅需 **启用** 校验并改造 UI 为固定槽位。
+77 -57
View File
@@ -210,8 +210,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py": {
"mtime": 1783777743.341288,
"ast_hash": "c922ece8f7295870b9b64f61ee6ddae9",
"mtime": 1783845022.041298,
"ast_hash": "1e988545bba72069f18f5deaf74915e7",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/models.py": {
@@ -320,8 +320,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/base_data.py": {
"mtime": 1783176696.4542158,
"ast_hash": "6ef5cf534eb50f012ed9bda34df152d9",
"mtime": 1783845022.3061872,
"ast_hash": "378d20f7fbaca5f1c88ac1d322535994",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/vehicle_urea.py": {
@@ -445,7 +445,7 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract.py": {
"mtime": 1783823649.766528,
"mtime": 1783845455.3648205,
"ast_hash": "6553f54ca1fd19086c170095f402a628",
"semantic_hash": ""
},
@@ -750,8 +750,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/crm_contract_routes.py": {
"mtime": 1783763319.0708206,
"ast_hash": "44223920a99b6b0796a49232e8c93a8e",
"mtime": 1783825526.6141698,
"ast_hash": "46a845a2d7ce98f2486ad45bde2d9612",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/workflow.py": {
@@ -765,8 +765,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py": {
"mtime": 1783760540.4064815,
"ast_hash": "e5c5c37075fe5e8adb90b6f1859071b7",
"mtime": 1783845558.7014904,
"ast_hash": "853850cff4f1ebd6ff197681e21e81f6",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py": {
@@ -800,8 +800,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/base_data.py": {
"mtime": 1782831664.104135,
"ast_hash": "705af7431e010950bd742926a8767600",
"mtime": 1783845063.8368685,
"ast_hash": "8529164d201cbaed199ea77d3a33fb71",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/vehicle_urea.py": {
@@ -965,8 +965,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_personnel_service.py": {
"mtime": 1783239403.8478649,
"ast_hash": "4d0591f2ccd5eed7b4949a11f838d95c",
"mtime": 1783845044.1152287,
"ast_hash": "94f2517f259023b46dfef7f0f74fede3",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/crm_quote_pdf.py": {
@@ -2530,8 +2530,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/pl-personnel-cert.ts": {
"mtime": 1783142758.6120825,
"ast_hash": "103281f7dcc60fb29211c534ed6e6744",
"mtime": 1783845064.4713657,
"ast_hash": "0e5d989ac54b364a5983a5f9af9fd6d6",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/patch-el-select-filterable.ts": {
@@ -2590,8 +2590,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/pl-display.ts": {
"mtime": 1783242794.3651183,
"ast_hash": "02e465faedad1f9a9ad7b42bdb4a60cd",
"mtime": 1783846376.254574,
"ast_hash": "e0e2f44cd091369aed67791e684b3c98",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/view-dialog-refresh.ts": {
@@ -2685,8 +2685,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/pl-vehicle-cert.ts": {
"mtime": 1783142762.1595736,
"ast_hash": "63cdbcaab03c15b70d8045462e56c93d",
"mtime": 1783844738.0471256,
"ast_hash": "873ca5c7a0f7afbfa44925292f3f68ff",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/protocol-payment-installment.ts": {
@@ -3575,8 +3575,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-vehicle-cert-personnel-tabs/index.vue": {
"mtime": 1783239419.8721654,
"ast_hash": "975163ea136bc1a966b166dc399f16b5",
"mtime": 1783844806.526282,
"ast_hash": "4661518a5a0ae9a2ad4384cfbeee5a42",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-list-cert-cell/index.vue": {
@@ -3585,8 +3585,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-cert-form-list/index.vue": {
"mtime": 1783204483.7404983,
"ast_hash": "ba0c4c8ae84a295116d6b6d48ed853cc",
"mtime": 1783844774.5544193,
"ast_hash": "675188fc7671fde3487a942e3b9af0d3",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/product-view-dialog/index.vue": {
@@ -3625,8 +3625,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-vehicle-cert-part-tabs/index.vue": {
"mtime": 1783203118.0167286,
"ast_hash": "ae5b4d29397ae52e65acd8308084ea92",
"mtime": 1783844759.0212061,
"ast_hash": "a8237cabea3fef3788d758e43fb360ec",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-product-name-hazard-cell/index.vue": {
@@ -3640,8 +3640,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-archive-dialog/index.vue": {
"mtime": 1783754641.7341132,
"ast_hash": "4ffb80920f360ffd3be7c5031656e6b7",
"mtime": 1783846283.221872,
"ast_hash": "a81c97a14c6f9aad011061c50194b296",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-view-tabs/index.vue": {
@@ -3715,8 +3715,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-vehicle-cert-part-view/index.vue": {
"mtime": 1783176286.6399972,
"ast_hash": "82094acfee492822cf747e537ef0e586",
"mtime": 1783844812.829093,
"ast_hash": "4cb51d89cbfacbc635f405599710ffa2",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-arrange-dialog/index.vue": {
@@ -3735,8 +3735,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-vehicle-view-dialog/index.vue": {
"mtime": 1783239462.7506347,
"ast_hash": "f4ba884a00eaba522145a7595a8568dc",
"mtime": 1783844817.3452597,
"ast_hash": "379405598f0f3c19e06d65bc1cbaed6b",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-inbound-slip-print-dialog/index.vue": {
@@ -3765,13 +3765,13 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/system-view-dialog/ViewKvTableCell.vue": {
"mtime": 1783203672.1663585,
"ast_hash": "40d984c0fe2ba989f54a3dd8a1b62ee0",
"mtime": 1783826496.3181424,
"ast_hash": "81ea08367bbe5878972f438eb3ec4b77",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/system-view-dialog/types.ts": {
"mtime": 1783203671.4708154,
"ast_hash": "6ebd9545f517277abc2e913e6b986a6c",
"mtime": 1783826501.0256994,
"ast_hash": "f9a7e100b725653c28513dd7e325c301",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/system-view-dialog/ViewHighlights.vue": {
@@ -3800,8 +3800,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-vehicle-personnel-view-list/PersonnelViewPanel.vue": {
"mtime": 1783753612.7487419,
"ast_hash": "eca43ee259a74b2b83da5f037efe6c3d",
"mtime": 1783844813.70631,
"ast_hash": "7a27cdd0fc0db3ab1de01a16174729e9",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/ListPagination/index.vue": {
@@ -3980,8 +3980,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/api/pl/index.ts": {
"mtime": 1783754611.920795,
"ast_hash": "8ca7f86a901ee6834fd39adeb9bcc473",
"mtime": 1783845066.0273972,
"ast_hash": "fa2ffcb5318c1ed1a6a06855422880d9",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/api/pl/public-transport.ts": {
@@ -4165,8 +4165,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/vehicle/index.vue": {
"mtime": 1783252180.3044844,
"ast_hash": "1c9691b756903ec22f8b302fc81bbe5d",
"mtime": 1783845088.2792692,
"ast_hash": "23f9cedc7e3b87b4d670e429cf736a55",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/vehicle-recharge-record/index.vue": {
@@ -4180,8 +4180,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/plan/index.vue": {
"mtime": 1783750515.7251935,
"ast_hash": "503f7ef7167e32d3bb0ebc78489bf973",
"mtime": 1783841453.747368,
"ast_hash": "9ae88b869dc2c937b027f7fdc6ac60aa",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/plan/arrange.vue": {
@@ -4285,23 +4285,23 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/plan-archive/index.vue": {
"mtime": 1783750518.4763997,
"ast_hash": "ba57b39a05f387fabae440c908c46985",
"mtime": 1783846246.026981,
"ast_hash": "8e2207eb52be4b0dca7811323f61aabf",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/self-vehicle/index.vue": {
"mtime": 1783252181.1993465,
"ast_hash": "ccda242ae74e22346703bcefaf58f903",
"mtime": 1783845096.14697,
"ast_hash": "25357bf2f887716989f8197285b5964e",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/carrier/index.vue": {
"mtime": 1783821190.6414018,
"ast_hash": "047b4cc96dc747d17f802d892ba4f424",
"mtime": 1783841744.362471,
"ast_hash": "1fcfd92cfd0d2b344d2110d7164b4b2a",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/personnel/index.vue": {
"mtime": 1783203297.0313385,
"ast_hash": "2ccaa7d6c84a1d2b660b75412ef816a3",
"mtime": 1783845137.1568706,
"ast_hash": "f12fbbc1e2a8e2e7cc1d3c6df3ceff6f",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/vehicle-maintenance-process/index.vue": {
@@ -4485,8 +4485,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/index.vue": {
"mtime": 1783749577.7739928,
"ast_hash": "ce91084329a7737179e85701ec10f737",
"mtime": 1783826680.883232,
"ast_hash": "d072970a9ba6eb25f2a738cb3f5f653b",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/detail.vue": {
@@ -4520,8 +4520,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractDetailDialog.vue": {
"mtime": 1783753625.445439,
"ast_hash": "ea172441d0463ff69fcbb57cc998a1e5",
"mtime": 1783826496.7785032,
"ast_hash": "24d49c1edca30ad1243380953b540b2f",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ContractAttachmentPanel.vue": {
@@ -4540,8 +4540,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/ApprovalDialog.vue": {
"mtime": 1783753615.3856215,
"ast_hash": "0722dd2ab21a746ddd7e2ecdbc5d0e4f",
"mtime": 1783826784.9388635,
"ast_hash": "c2a695de1bbd282d616715c5bde1d414",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/crm/contract/components/RenewDialog.vue": {
@@ -7398,5 +7398,25 @@
"mtime": 1783777759.8294604,
"ast_hash": "73869416708f10036beec5769af334bb",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_cert_required_service.py": {
"mtime": 1783845035.534594,
"ast_hash": "279f299a4969244bad7f866ae9ee97fb",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260712_pl_personnel_type.sql": {
"mtime": 1783843576.7474706,
"ast_hash": "82dcd1f60a2b486cc94f34bfb2228bd6",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/pl-cert-slots.ts": {
"mtime": 1783844749.9400146,
"ast_hash": "33ea084eaaba3ec98c170d8651ca83f9",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/scripts/migration_20260712_pl_personnel_affiliation.sql": {
"mtime": 1783845019.9129975,
"ast_hash": "f97c57b5fac34dc5d47491e9de4c1769",
"semantic_hash": ""
}
}
+5 -11
View File
@@ -267,14 +267,12 @@
"265": "Community 265",
"266": "Community 266",
"267": "Community 267",
"268": "Community 268",
"269": "Community 269",
"270": "Community 270",
"271": "Community 271",
"272": "Community 272",
"273": "Community 273",
"274": "Community 274",
"275": "Community 275",
"276": "Community 276",
"277": "Community 277",
"278": "Community 278",
@@ -618,7 +616,6 @@
"616": "Community 616",
"617": "Community 617",
"618": "Community 618",
"619": "Community 619",
"620": "Community 620",
"621": "Community 621",
"622": "Community 622",
@@ -682,13 +679,13 @@
"680": "Community 680",
"681": "Community 681",
"682": "Community 682",
"683": "Community 683",
"684": "Community 684",
"685": "Community 685",
"686": "Community 686",
"687": "Community 687",
"688": "Community 688",
"689": "Community 689",
"690": "Community 690",
"691": "Community 691",
"692": "Community 692",
"693": "Community 693",
@@ -713,28 +710,25 @@
"713": "Community 713",
"714": "Community 714",
"715": "Community 715",
"716": "Community 716",
"717": "Community 717",
"718": "Community 718",
"719": "Community 719",
"720": "Community 720",
"721": "Community 721",
"722": "Community 722",
"723": "Community 723",
"724": "Community 724",
"725": "Community 725",
"726": "Community 726",
"727": "Community 727",
"728": "Community 728",
"729": "Community 729",
"730": "Community 730",
"731": "Community 731",
"732": "Community 732",
"733": "Community 733",
"734": "Community 734",
"735": "Community 735",
"736": "Community 736",
"737": "Community 737",
"738": "Community 738",
"739": "Community 739",
"740": "Community 740",
"741": "Community 741",
"743": "Community 743"
"742": "Community 742"
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+5
View File
@@ -147,6 +147,8 @@ export interface Personnel {
phoneNo?: string
age?: number
idCardNo?: string
personnelType: string
personnelAffiliation: string
status: string
remark?: string
nearestExpireDate?: string
@@ -162,6 +164,8 @@ export interface PersonnelPayload {
phoneNo?: string
age?: number | null
idCardNo?: string
personnelType: string
personnelAffiliation: string
status: string
remark?: string
certList?: CertRow[]
@@ -170,6 +174,7 @@ export interface PersonnelPayload {
export interface PersonnelQuery {
q?: string
status?: string
personnelAffiliation?: string
page?: number
pageSize?: number
}
@@ -3,11 +3,11 @@
<div class="cert-form-header">
<span class="cert-form-title">{{ title }}</span>
<el-button type="primary" size="small" @click="handleAddCert">
<el-icon><Plus /></el-icon>添加证书
<el-icon><Plus /></el-icon>{{ addButtonLabel }}
</el-button>
</div>
<div v-if="!certList.length" class="cert-empty">暂无证书,点击「添加证书」</div>
<div v-if="!certList.length" class="cert-empty">{{ emptyHint }}</div>
<el-table v-else-if="layout === 'table'" :data="certList" stripe class="list-table cert-form-table">
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
@@ -20,7 +20,7 @@
>
<template #default="{ row }">
<el-select
v-if="hasTrailerPlateOption"
v-if="hasTrailerPlateOption && !isRowRequired(row)"
:model-value="normalizeVehicleCertPart(row.vehiclePart)"
placeholder="请选择车牌号"
style="width: 100%"
@@ -33,12 +33,17 @@
:value="option.value"
/>
</el-select>
<span v-else>{{ headPlateLabel }}</span>
<span v-else>{{ resolvePlateLabel(row) }}</span>
</template>
</el-table-column>
<el-table-column label="证书名称" min-width="160">
<template #default="{ row }">
<span v-if="isRowRequired(row)" class="cert-name-locked">
{{ row.certName }}
<el-tag type="info" size="small">必备</el-tag>
</span>
<el-select
v-else
v-model="row.certName"
placeholder="请选择或输入"
filterable
@@ -87,8 +92,8 @@
</template>
</el-table-column>
<el-table-column label="操作" min-width="60" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" @click="handleRemoveCert($index)">
<template #default="{ row, $index }">
<el-button v-if="!isRowRequired(row)" link type="danger" @click="handleRemoveCert($index)">
<el-icon><Delete /></el-icon>
</el-button>
</template>
@@ -98,15 +103,19 @@
<div v-else class="cert-card-list">
<div v-for="(cert, index) in certList" :key="cert._key" class="cert-card">
<div class="cert-card-head">
<span class="cert-card-index">证书 {{ index + 1 }}</span>
<el-button link type="danger" @click="handleRemoveCert(index)">
<span class="cert-card-index">
{{ isRowRequired(cert) ? `必备 · ${cert.certName}` : `证书 ${index + 1}` }}
</span>
<el-button v-if="!isRowRequired(cert)" link type="danger" @click="handleRemoveCert(index)">
<el-icon><Delete /></el-icon>删除
</el-button>
</div>
<div class="form-grid-4 cert-card-grid">
<el-form-item label="证书名称" required>
<span v-if="isRowRequired(cert)" class="cert-name-locked">{{ cert.certName }}</span>
<el-select
v-else
v-model="cert.certName"
placeholder="请选择或输入证书名称"
filterable
@@ -155,12 +164,23 @@ import { Delete, Plus } from '@element-plus/icons-vue'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import type { CertRow } from '@/api/pl'
import {
ensureCarrierVehicleCertSlots,
ensureSelfVehicleCertSlots,
mergeCertListWithRequiredSlots,
type CertRowWithMeta
} from '@/utils/pl-cert-slots'
import { matchRequiredCertName } from '@/utils/pl-personnel-cert'
import {
PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES,
PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES,
PL_VEHICLE_CERT_PART_HEAD,
PL_VEHICLE_CERT_PART_TRAILER,
normalizeVehicleCertPart
} from '@/utils/pl-vehicle-cert'
type CertRowWithKey = CertRow & { _key: string }
type CertRowWithKey = CertRowWithMeta & { _key: string }
type CertUploadRef = InstanceType<typeof AttachmentUploadField>
const props = withDefaults(
@@ -169,10 +189,16 @@ const props = withDefaults(
uploadFolder: string
certNameOptions?: string[]
layout?: 'card' | 'table'
/** 列表模式:展示车牌号列,可从主车/挂车车牌中选择 */
showVehiclePartColumn?: boolean
licensePlate?: string
trailerNo?: string
/** 人员/单段车辆:固定必备证书名称 */
requiredCertNames?: readonly string[]
requiredVehiclePart?: string
/** 山峡自有车统一表格:按主车/挂车自动注入必备槽位 */
enforceSelfVehicleRequired?: boolean
/** 承运商车辆:按主车/挂车注入必备槽位(行驶证、营运证) */
enforceCarrierVehicleRequired?: boolean
}>(),
{
title: '证书',
@@ -180,10 +206,27 @@ const props = withDefaults(
layout: 'card',
showVehiclePartColumn: false,
licensePlate: '',
trailerNo: ''
trailerNo: '',
requiredCertNames: () => [],
requiredVehiclePart: PL_VEHICLE_CERT_PART_HEAD,
enforceSelfVehicleRequired: false,
enforceCarrierVehicleRequired: false
}
)
const hasRequiredMode = computed(
() =>
props.enforceSelfVehicleRequired ||
props.enforceCarrierVehicleRequired ||
(props.requiredCertNames?.length ?? 0) > 0
)
const addButtonLabel = computed(() => (hasRequiredMode.value ? '添加其他证书' : '添加证书'))
const emptyHint = computed(() =>
hasRequiredMode.value ? '暂无证书' : '暂无证书,点击「添加证书」'
)
const headPlateLabel = computed(() => props.licensePlate?.trim() || '—')
const hasTrailerPlateOption = computed(() => Boolean(props.trailerNo?.trim()))
@@ -203,23 +246,88 @@ const vehiclePlateOptions = computed(() => {
return options
})
function resolvePlateLabel(row: CertRowWithKey) {
const part = normalizeVehicleCertPart(row.vehiclePart)
if (part === PL_VEHICLE_CERT_PART_TRAILER) {
return props.trailerNo?.trim() || '挂车'
}
return headPlateLabel.value
}
function requiredNamesForRow(row: CertRowWithKey): readonly string[] {
if (props.requiredCertNames?.length) {
return props.requiredCertNames
}
if (!props.enforceSelfVehicleRequired && !props.enforceCarrierVehicleRequired) return []
const part = normalizeVehicleCertPart(row.vehiclePart)
if (props.enforceCarrierVehicleRequired) {
return part === PL_VEHICLE_CERT_PART_TRAILER
? PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
: PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES
}
return part === PL_VEHICLE_CERT_PART_TRAILER
? PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
: PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES
}
function isRowRequired(row: CertRowWithKey) {
if (row._required) return true
const requiredNames = requiredNamesForRow(row)
if (!requiredNames.length) return false
return requiredNames.some(name => matchRequiredCertName(row.certName, name))
}
function syncRequiredSlots() {
if (props.enforceSelfVehicleRequired) {
certList.value = ensureSelfVehicleCertSlots(certList.value, props.trailerNo)
return
}
if (props.enforceCarrierVehicleRequired) {
certList.value = ensureCarrierVehicleCertSlots(certList.value, props.trailerNo)
return
}
if (props.requiredCertNames?.length) {
certList.value = mergeCertListWithRequiredSlots(certList.value, props.requiredCertNames, {
vehiclePart: props.requiredVehiclePart
})
}
}
function handleVehiclePartChange(row: CertRowWithKey, value: string) {
if (isRowRequired(row)) return
row.vehiclePart = normalizeVehicleCertPart(value)
}
const certList = defineModel<CertRowWithKey[]>('modelValue', { default: () => [] })
watch(
() =>
[
props.enforceSelfVehicleRequired,
props.enforceCarrierVehicleRequired,
props.requiredCertNames,
props.trailerNo
] as const,
() => {
if (hasRequiredMode.value) {
syncRequiredSlots()
}
},
{ immediate: true }
)
const certUploadRefs = ref<Record<number, CertUploadRef | null>>({})
function createEmptyCert(): CertRowWithKey {
return {
_key: `cert-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
vehiclePart: PL_VEHICLE_CERT_PART_HEAD,
vehiclePart: props.requiredVehiclePart || PL_VEHICLE_CERT_PART_HEAD,
certName: '',
certNo: '',
expireDate: '',
remindDays: 30,
attachmentUrls: ''
attachmentUrls: '',
_required: false
}
}
@@ -228,6 +336,8 @@ function handleAddCert() {
}
function handleRemoveCert(index: number) {
const row = certList.value[index]
if (row && isRowRequired(row)) return
const next = [...certList.value]
next.splice(index, 1)
certList.value = next
@@ -250,7 +360,11 @@ function clearUploadRefs() {
certUploadRefs.value = {}
}
defineExpose({ hasUploading, clearUploadRefs })
function refreshRequiredSlots() {
syncRequiredSlots()
}
defineExpose({ hasUploading, clearUploadRefs, refreshRequiredSlots })
</script>
<style scoped lang="scss">
@@ -282,6 +396,14 @@ defineExpose({ hasUploading, clearUploadRefs })
background: #f8fafc;
}
.cert-name-locked {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 500;
color: var(--text, #1e293b);
}
.cert-form-table {
border-top: 1px solid var(--mint-border, #d9e8e2);
@@ -26,61 +26,67 @@
</section>
<section class="pl-plan-form-section pl-plan-freight-section">
<h4 class="pl-plan-subtitle">运费信息</h4>
<div class="pl-plan-freight-section-head">
<h4 class="pl-plan-subtitle">运费信息</h4>
<label class="pl-plan-freight-switch">
<span class="pl-plan-freight-switch-label">是否有运输费用</span>
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
</label>
</div>
<el-form
v-if="formData.hasFreight === 1"
:model="formData"
label-width="140px"
label-position="right"
class="pl-plan-freight-form"
>
<div class="form-grid-4">
<el-form-item label="是否有运输费用">
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
<div class="form-grid-4 pl-plan-archive-freight-grid">
<el-form-item label="运费计算方式">
<el-select v-model="formData.freightType">
<el-option label="按重量" value="by_weight" />
<el-option label="按趟" value="by_trip" />
</el-select>
</el-form-item>
<el-form-item label="运输公里数">
<el-input-number
v-model="formData.distanceKm"
:min="0"
:precision="2"
/>
</el-form-item>
<el-form-item
v-if="formData.freightType === 'by_weight'"
:label="`运输量(${freightUnitLabel})`"
>
<el-input-number
v-model="freightCalc.transportQty.value"
:min="0"
:precision="3"
@change="freightCalc.markTransportQtyTouched()"
/>
</el-form-item>
<div v-else class="pl-plan-archive-freight-grid-cell" aria-hidden="true" />
<div class="pl-plan-archive-freight-grid-cell" aria-hidden="true" />
<el-form-item
v-if="formData.freightType === 'by_weight'"
:label="`运输单价(元/${freightUnitLabel})`"
>
<el-input-number
v-model="freightCalc.freightPrice.value"
:min="0"
:precision="2"
/>
</el-form-item>
<div v-else class="pl-plan-archive-freight-grid-cell" aria-hidden="true" />
<el-form-item label="运费金额(元)">
<el-input-number
v-model="freightCalc.freightAmount.value"
:min="0"
:precision="2"
@change="freightCalc.markFreightAmountTouched()"
/>
</el-form-item>
<template v-if="formData.hasFreight === 1">
<el-form-item label="运输公里数">
<el-input-number
v-model="formData.distanceKm"
:min="0"
:precision="2"
/>
</el-form-item>
<el-form-item label="运费计算方式">
<el-select v-model="formData.freightType">
<el-option label="按重量" value="by_weight" />
<el-option label="按趟" value="by_trip" />
</el-select>
</el-form-item>
<el-form-item
v-if="formData.freightType === 'by_weight'"
:label="`运输量(${freightUnitLabel})`"
>
<el-input-number
v-model="freightCalc.transportQty.value"
:min="0"
:precision="3"
@change="freightCalc.markTransportQtyTouched()"
/>
</el-form-item>
<el-form-item
v-if="formData.freightType === 'by_weight'"
:label="`运输单价(元/${freightUnitLabel})`"
>
<el-input-number
v-model="freightCalc.freightPrice.value"
:min="0"
:precision="2"
/>
</el-form-item>
<el-form-item label="运费金额(元)">
<el-input-number
v-model="freightCalc.freightAmount.value"
:min="0"
:precision="2"
@change="freightCalc.markFreightAmountTouched()"
/>
</el-form-item>
</template>
</div>
</el-form>
</section>
@@ -119,7 +125,10 @@ import {
import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
import { enrichPlanProductsHazardFromCrm } from '@/composables/usePlToxicProductCertCheck'
import {
buildMissingHazardCertReadyMessage,
enrichPlanProductsHazardFromCrm
} from '@/composables/usePlToxicProductCertCheck'
import { usePlFreightCalc, resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
import { useDictStore } from '@/store/modules/dict'
import { pickBxData } from '@/utils/http/bx'
@@ -302,6 +311,16 @@ async function handleSubmit() {
return
}
if (!isAdjustMode.value) {
const certMessage = buildMissingHazardCertReadyMessage(plan.value, productRows.value)
if (certMessage) {
await ElMessageBox.alert(`${certMessage},请至【危化品证上传】页面操作。`, '无法归档', {
type: 'warning'
})
return
}
}
submitLoading.value = true
try {
const body: PlanArchiveBody = {
@@ -392,6 +411,7 @@ async function handleSubmit() {
.pl-plan-freight-section {
.pl-plan-subtitle {
margin-top: 4px;
margin-bottom: 0;
}
:deep(.el-form-item__label) {
@@ -400,6 +420,28 @@ async function handleSubmit() {
}
}
.pl-plan-freight-section-head {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 16px;
margin-bottom: 10px;
}
.pl-plan-freight-switch {
display: inline-flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
cursor: pointer;
}
.pl-plan-freight-switch-label {
font-size: 13px;
color: var(--mint-text-muted, #64748b);
white-space: nowrap;
}
.pl-plan-freight-form {
:deep(.el-form-item) {
margin-bottom: 0;
@@ -418,8 +460,13 @@ async function handleSubmit() {
min-width: 0;
}
.form-grid-4 {
.pl-plan-archive-freight-grid {
align-items: center;
row-gap: 12px;
}
.pl-plan-archive-freight-grid-cell {
min-height: 1px;
}
:deep(.el-select),
@@ -11,6 +11,8 @@
show-vehicle-part-column
:license-plate="licensePlate"
:trailer-no="trailerNo"
:enforce-self-vehicle-required="enforceSelfVehicleRequired"
:enforce-carrier-vehicle-required="enforceCarrierVehicleRequired"
/>
<el-tabs
@@ -73,13 +75,19 @@ const props = withDefaults(
certNameOptions?: string[]
title?: string
layout?: 'card' | 'table'
/** 山峡自有车:注入必备证书槽位并校验 */
enforceSelfVehicleRequired?: boolean
/** 承运商车辆:注入必备证书槽位并校验 */
enforceCarrierVehicleRequired?: boolean
}>(),
{
licensePlate: '',
trailerNo: '',
certNameOptions: () => [],
title: '车辆证书',
layout: 'card'
layout: 'card',
enforceSelfVehicleRequired: false,
enforceCarrierVehicleRequired: false
}
)
@@ -149,9 +157,16 @@ function clearUploadRefs() {
trailerCertFormRef.value?.clearUploadRefs()
}
function refreshRequiredSlots() {
unifiedCertFormRef.value?.refreshRequiredSlots?.()
headCertFormRef.value?.refreshRequiredSlots?.()
trailerCertFormRef.value?.refreshRequiredSlots?.()
}
defineExpose({
hasUploading,
clearUploadRefs
clearUploadRefs,
refreshRequiredSlots
})
</script>
@@ -46,7 +46,8 @@ import type { ViewTableMode } from '@/components/business/system-view-dialog/typ
import {
PL_VEHICLE_CERT_PART_HEAD,
PL_VEHICLE_CERT_PART_TRAILER,
getSelfVehiclePartCertMissingSuffix,
getVehiclePartCertMissingSuffix,
type PlVehicleCertCheckType,
splitVehicleCertsByPart
} from '@/utils/pl-vehicle-cert'
@@ -56,6 +57,8 @@ const props = withDefaults(
licensePlate?: string
trailerNo?: string
vehicleId?: string
vehicleCertCheckType?: PlVehicleCertCheckType
/** @deprecated 使用 vehicleCertCheckType */
checkSelfVehicleCerts?: boolean
tableMode?: ViewTableMode
certLayout?: 'card' | 'table'
@@ -65,6 +68,7 @@ const props = withDefaults(
licensePlate: '',
trailerNo: '',
vehicleId: '',
vehicleCertCheckType: undefined,
checkSelfVehicleCerts: false,
tableMode: 'triple',
certLayout: 'card'
@@ -98,16 +102,26 @@ const scopedCertList = computed(() => {
const headCertList = computed(() => splitVehicleCertsByPart(scopedCertList.value).headCerts)
const trailerCertList = computed(() => splitVehicleCertsByPart(scopedCertList.value).trailerCerts)
const resolvedVehicleCertCheckType = computed<PlVehicleCertCheckType | undefined>(() => {
if (props.vehicleCertCheckType) return props.vehicleCertCheckType
return props.checkSelfVehicleCerts ? 'self' : undefined
})
const headTabLabelWithHint = computed(() => {
if (!props.checkSelfVehicleCerts) return headTabLabel.value
return headTabLabel.value + getSelfVehiclePartCertMissingSuffix(headCertList.value, PL_VEHICLE_CERT_PART_HEAD)
const checkType = resolvedVehicleCertCheckType.value
if (!checkType) return headTabLabel.value
return (
headTabLabel.value +
getVehiclePartCertMissingSuffix(headCertList.value, PL_VEHICLE_CERT_PART_HEAD, checkType)
)
})
const trailerTabLabelWithHint = computed(() => {
if (!props.checkSelfVehicleCerts) return trailerTabLabel.value
const checkType = resolvedVehicleCertCheckType.value
if (!checkType) return trailerTabLabel.value
return (
trailerTabLabel.value +
getSelfVehiclePartCertMissingSuffix(trailerCertList.value, PL_VEHICLE_CERT_PART_TRAILER)
getVehiclePartCertMissingSuffix(trailerCertList.value, PL_VEHICLE_CERT_PART_TRAILER, checkType)
)
})
@@ -7,7 +7,7 @@
:license-plate="licensePlate"
:trailer-no="trailerNo"
:vehicle-id="vehicleId"
:check-self-vehicle-certs="checkSelfVehicleCerts"
:vehicle-cert-check-type="vehicleCertCheckType"
:table-mode="tableMode"
:cert-layout="certLayout"
/>
@@ -20,7 +20,7 @@
>
<PersonnelViewPanel
:person="person"
:check-self-vehicle-certs="checkSelfVehicleCerts"
:check-linked-personnel-certs="showLinkedPersonnelCertCheck"
:table-mode="tableMode"
:cert-layout="certLayout"
/>
@@ -34,7 +34,7 @@ import PersonnelViewPanel from '@/components/business/pl-vehicle-personnel-view-
import type { CertRead } from '@/api/pl'
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
import type { ViewTableMode } from '@/components/business/system-view-dialog/types'
import { getSelfVehicleCertMissingSuffix } from '@/utils/pl-vehicle-cert'
import { getVehicleCertMissingSuffix, type PlVehicleCertCheckType } from '@/utils/pl-vehicle-cert'
import { getPersonnelCertMissingSuffix } from '@/utils/pl-personnel-cert'
import { formatPlPersonnelNameWithRole } from '@/utils/pl-display'
@@ -45,6 +45,10 @@ const props = withDefaults(
trailerNo?: string
vehicleId?: string
personnelList?: VehiclePersonnelViewRow[]
vehicleCertCheckType?: PlVehicleCertCheckType
/** 山峡自有车:关联人员证书缺失提示 */
checkLinkedPersonnelCerts?: boolean
/** @deprecated 使用 vehicleCertCheckType + checkLinkedPersonnelCerts */
checkSelfVehicleCerts?: boolean
tableMode?: ViewTableMode
certLayout?: 'card' | 'table'
@@ -55,6 +59,8 @@ const props = withDefaults(
trailerNo: '',
vehicleId: '',
personnelList: () => [],
vehicleCertCheckType: undefined,
checkLinkedPersonnelCerts: false,
checkSelfVehicleCerts: false,
tableMode: 'triple',
certLayout: 'card'
@@ -73,23 +79,29 @@ const tabsInstanceKey = computed(
const vehicleCertTabLabel = computed(() => {
const label = '车辆证书'
if (!props.checkSelfVehicleCerts) return label
const checkType = props.vehicleCertCheckType
if (!checkType) return label
return (
label +
getSelfVehicleCertMissingSuffix({
getVehicleCertMissingSuffix({
vehicleType: checkType,
certList: props.certList,
trailerNo: props.trailerNo
})
)
})
const showLinkedPersonnelCertCheck = computed(
() => props.checkLinkedPersonnelCerts || props.checkSelfVehicleCerts
)
function getPersonKey(person: VehiclePersonnelViewRow, index: number) {
return `person-${props.vehicleId || props.licensePlate || 'vehicle'}-${person.personnelId || `${person.personnelName}-${index}`}`
}
function getPersonTabLabel(person: VehiclePersonnelViewRow) {
const suffix =
props.checkSelfVehicleCerts && person.role
showLinkedPersonnelCertCheck.value && person.role
? getPersonnelCertMissingSuffix(person.certList || [], person.role as 'driver' | 'escort')
: ''
return `${formatPlPersonnelNameWithRole(person.personnelName, person.role)}${suffix}`
@@ -38,19 +38,26 @@ const props = withDefaults(
person: VehiclePersonnelViewRow
showTitle?: boolean
checkSelfVehicleCerts?: boolean
/** @deprecated 使用 checkLinkedPersonnelCerts */
checkLinkedPersonnelCerts?: boolean
tableMode?: ViewTableMode
certLayout?: 'card' | 'table'
}>(),
{
showTitle: false,
checkSelfVehicleCerts: false,
checkLinkedPersonnelCerts: false,
tableMode: 'triple',
certLayout: 'card'
}
)
const showPersonnelCertCheck = computed(
() => props.checkLinkedPersonnelCerts || props.checkSelfVehicleCerts
)
const personnelCertMissingSuffix = computed(() => {
if (!props.checkSelfVehicleCerts || !props.person.role) return ''
if (!showPersonnelCertCheck.value || !props.person.role) return ''
return getPersonnelCertMissingSuffix(
props.person.certList || [],
props.person.role as 'driver' | 'escort'
@@ -18,7 +18,8 @@
:trailer-no="detail?.trailerNo"
:vehicle-id="detail?.id"
:personnel-list="personnelList"
:check-self-vehicle-certs="detail?.vehicleType === 'self'"
:vehicle-cert-check-type="detail?.vehicleType === 'self' || detail?.vehicleType === 'carrier' ? detail.vehicleType : undefined"
:check-linked-personnel-certs="detail?.vehicleType === 'self'"
cert-layout="table"
/>
</template>
+241
View File
@@ -0,0 +1,241 @@
import type { CertRow } from '@/api/pl'
import { matchRequiredCertName } from '@/utils/pl-personnel-cert'
import {
PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES,
PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES,
PL_VEHICLE_CERT_PART_HEAD,
PL_VEHICLE_CERT_PART_TRAILER,
type PlVehicleCertCheckType,
getVehicleHeadRequiredCertNames,
getVehicleTrailerRequiredCertNames,
normalizeVehicleCertPart
} from '@/utils/pl-vehicle-cert'
export type CertRowWithMeta = CertRow & { _key?: string; _required?: boolean }
export function isRequiredCertSlotComplete(
cert: CertRow | undefined,
requiredName: string
): boolean {
if (!cert) return false
if (!matchRequiredCertName(cert.certName, requiredName)) return false
if (!cert.certNo?.trim()) return false
if (!cert.expireDate) return false
if (!cert.attachmentUrls?.trim()) return false
return true
}
export function getMissingRequiredCertFieldLabels(
cert: CertRow | undefined,
requiredName: string
): string[] {
if (!cert || !matchRequiredCertName(cert.certName, requiredName)) {
return ['证号', '有效期', '附件']
}
const missing: string[] = []
if (!cert.certNo?.trim()) missing.push('证号')
if (!cert.expireDate) missing.push('有效期')
if (!cert.attachmentUrls?.trim()) missing.push('附件')
return missing
}
export function collectRequiredCertSlotErrors(
certList: CertRow[],
requiredNames: readonly string[],
partLabel = ''
): string[] {
const errors: string[] = []
for (const requiredName of requiredNames) {
const matched = certList.find(c => matchRequiredCertName(c.certName, requiredName))
const missing = getMissingRequiredCertFieldLabels(matched, requiredName)
if (missing.length) {
const prefix = partLabel ? `${partLabel}「${requiredName}」` : `「${requiredName}」`
errors.push(`${prefix}缺少:${missing.join('、')}`)
}
}
return errors
}
export function collectSelfVehicleRequiredCertErrors(
certList: CertRow[],
trailerNo?: string | null
): string[] {
return collectVehicleRequiredCertErrors(certList, trailerNo, 'self')
}
export function collectCarrierVehicleRequiredCertErrors(
certList: CertRow[],
trailerNo?: string | null
): string[] {
return collectVehicleRequiredCertErrors(certList, trailerNo, 'carrier')
}
export function collectVehicleRequiredCertErrors(
certList: CertRow[],
trailerNo: string | null | undefined,
vehicleType: PlVehicleCertCheckType
): string[] {
const headCerts = certList.filter(
c => normalizeVehicleCertPart(c.vehiclePart) === PL_VEHICLE_CERT_PART_HEAD
)
const trailerCerts = certList.filter(
c => normalizeVehicleCertPart(c.vehiclePart) === PL_VEHICLE_CERT_PART_TRAILER
)
const errors = [
...collectRequiredCertSlotErrors(
headCerts,
getVehicleHeadRequiredCertNames(vehicleType),
'主车'
),
...(trailerNo?.trim()
? collectRequiredCertSlotErrors(
trailerCerts,
getVehicleTrailerRequiredCertNames(vehicleType),
'挂车'
)
: [])
]
return errors
}
function createEmptyCertRow(
certName: string,
vehiclePart?: string,
required = false
): CertRowWithMeta {
return {
_key: `cert-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
vehiclePart,
certName,
certNo: '',
expireDate: '',
remindDays: 30,
attachmentUrls: '',
_required: required
}
}
function findCertIndexForRequired(
certs: CertRowWithMeta[],
requiredName: string,
vehiclePart?: string
): number {
return certs.findIndex(cert => {
if (vehiclePart && normalizeVehicleCertPart(cert.vehiclePart) !== vehiclePart) {
return false
}
return matchRequiredCertName(cert.certName, requiredName)
})
}
export function mergeCertListWithRequiredSlots(
existing: CertRowWithMeta[],
requiredNames: readonly string[],
options?: { vehiclePart?: string }
): CertRowWithMeta[] {
const vehiclePart = options?.vehiclePart
const next = existing.map(cert => ({ ...cert }))
const normalizedPart = vehiclePart ? normalizeVehicleCertPart(vehiclePart) : null
const extras = next.filter(cert => {
if (normalizedPart && normalizeVehicleCertPart(cert.vehiclePart) !== normalizedPart) {
return false
}
return !requiredNames.some(name => matchRequiredCertName(cert.certName, name))
})
const requiredRows: CertRowWithMeta[] = requiredNames.map(requiredName => {
const idx = findCertIndexForRequired(next, requiredName, normalizedPart || undefined)
if (idx >= 0) {
return {
...next[idx],
certName: requiredName,
vehiclePart: normalizedPart || next[idx].vehiclePart || PL_VEHICLE_CERT_PART_HEAD,
_required: true
}
}
return createEmptyCertRow(
requiredName,
normalizedPart || PL_VEHICLE_CERT_PART_HEAD,
true
)
})
const otherPartRows = normalizedPart
? next.filter(cert => normalizeVehicleCertPart(cert.vehiclePart) !== normalizedPart)
: []
return [...otherPartRows, ...requiredRows, ...extras.map(c => ({ ...c, _required: false }))]
}
function ensureVehicleCertSlotsByNames(
certList: CertRowWithMeta[],
trailerNo: string | null | undefined,
headRequired: readonly string[],
trailerRequired: readonly string[]
): CertRowWithMeta[] {
let merged = mergeCertListWithRequiredSlots(certList, headRequired, {
vehiclePart: PL_VEHICLE_CERT_PART_HEAD
})
if (trailerNo?.trim()) {
merged = mergeCertListWithRequiredSlots(merged, trailerRequired, {
vehiclePart: PL_VEHICLE_CERT_PART_TRAILER
})
} else {
merged = stripTrailerRequiredSlots(merged, trailerRequired)
}
return merged
}
export function ensureSelfVehicleCertSlots(
certList: CertRowWithMeta[],
trailerNo?: string | null
): CertRowWithMeta[] {
return ensureVehicleCertSlotsByNames(
certList,
trailerNo,
PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
)
}
export function ensureCarrierVehicleCertSlots(
certList: CertRowWithMeta[],
trailerNo?: string | null
): CertRowWithMeta[] {
return ensureVehicleCertSlotsByNames(
certList,
trailerNo,
PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
)
}
export function ensureVehicleCertSlots(
certList: CertRowWithMeta[],
trailerNo: string | null | undefined,
vehicleType: PlVehicleCertCheckType
): CertRowWithMeta[] {
return vehicleType === 'carrier'
? ensureCarrierVehicleCertSlots(certList, trailerNo)
: ensureSelfVehicleCertSlots(certList, trailerNo)
}
export function stripTrailerRequiredSlots(
certList: CertRowWithMeta[],
trailerRequiredNames: readonly string[] = PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
): CertRowWithMeta[] {
return certList.filter(cert => {
if (normalizeVehicleCertPart(cert.vehiclePart) !== PL_VEHICLE_CERT_PART_TRAILER) {
return true
}
return !trailerRequiredNames.some(name => matchRequiredCertName(cert.certName, name))
})
}
export function formatRequiredCertErrors(errors: string[]): string {
if (!errors.length) return ''
return `必备证书未齐全:${errors.join(';')}`
}
+1 -1
View File
@@ -225,7 +225,7 @@ export function getPlPlanFollowStatusLabel(status?: string) {
}
export function getPlPlanArchiveStatusTagType(status?: string) {
return status === 'archived' ? 'info' : 'success'
return status === 'archived' ? 'info' : 'danger'
}
export function getPlPlanArchiveStatusLabel(status?: string) {
+113 -6
View File
@@ -11,6 +11,95 @@ export const PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES = [
export const PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES = ['身份证', '从业资格证'] as const
export type PersonnelCertRole = 'driver' | 'escort'
export type PersonnelType = 'driver' | 'escort' | 'both'
export const PL_PERSONNEL_TYPE_OPTIONS = [
{ value: 'driver', label: '驾驶员' },
{ value: 'escort', label: '押运员' },
{ value: 'both', label: '驾驶员兼押运员' }
] as const
export type PersonnelAffiliation = 'self' | 'carrier'
export const PL_PERSONNEL_AFFILIATION_OPTIONS = [
{ value: 'self', label: '山峡' },
{ value: 'carrier', label: '非山峡' }
] as const
export function getPersonnelAffiliationLabel(affiliation?: string | null): string {
const found = PL_PERSONNEL_AFFILIATION_OPTIONS.find(item => item.value === affiliation)
return found?.label || affiliation || '—'
}
export function getPersonnelAffiliationTagType(
affiliation?: string | null
): 'success' | 'warning' | 'info' {
if (affiliation === 'self') return 'success'
if (affiliation === 'carrier') return 'warning'
return 'info'
}
export function validatePersonnelAffiliationVehicleCompat(
personnelAffiliation: string | undefined,
vehicleType: string | undefined,
onError: (message: string) => void
): boolean {
if (!personnelAffiliation || !vehicleType) return true
if (personnelAffiliation === vehicleType) return true
const vehicleLabel = vehicleType === 'self' ? '山峡' : '非山峡'
onError(`${vehicleLabel}车辆仅可关联${vehicleLabel}人员`)
return false
}
export function getPersonnelTypeLabel(type?: string | null): string {
const found = PL_PERSONNEL_TYPE_OPTIONS.find(item => item.value === type)
return found?.label || type || '—'
}
export function getPersonnelTypeTagType(type?: string | null): 'success' | 'warning' | 'primary' | 'info' {
if (type === 'driver') return 'primary'
if (type === 'escort') return 'warning'
if (type === 'both') return 'success'
return 'info'
}
export function getRequiredPersonnelCertNamesByType(
personnelType?: string | null
): readonly string[] {
if (personnelType === 'escort') {
return PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES
}
return PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES
}
export function validatePersonnelTypeRoleCompat(
personnelType: string | undefined,
role: string | undefined,
onError: (message: string) => void
): boolean {
if (!personnelType || !role) return true
if (personnelType === 'driver' && role === 'escort') {
onError('驾驶员类型人员不可关联为押运员')
return false
}
if (personnelType === 'escort' && role === 'driver') {
onError('押运员类型人员不可关联为驾驶员')
return false
}
return true
}
export function isPersonnelCertStrictComplete(
cert: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'> | undefined,
requiredName: string
): boolean {
if (!cert?.certName?.trim()) return false
if (!matchRequiredCertName(cert.certName, requiredName)) return false
if (!cert.certNo?.trim()) return false
if (!cert.expireDate) return false
if (!cert.attachmentUrls?.trim()) return false
return true
}
export function matchRequiredCertName(
actualName: string | undefined,
@@ -28,8 +117,13 @@ export function matchRequiredCertName(
return name === requiredName
}
export function isPersonnelCertUploaded(cert?: Pick<CertRow, 'certName' | 'attachmentUrls'>) {
return Boolean(cert?.certName?.trim() && cert?.attachmentUrls?.trim())
export function isPersonnelCertUploaded(cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'>) {
return Boolean(
cert?.certName?.trim() &&
cert?.certNo?.trim() &&
cert?.expireDate &&
cert?.attachmentUrls?.trim()
)
}
export function getRequiredPersonnelCertNames(role: PersonnelCertRole): readonly string[] {
@@ -55,15 +149,28 @@ export function hasPersonnelCertMissing(
return getMissingPersonnelCertNames(role, certList).length > 0
}
/** 人员主数据页:至少满足押运员必备证书(身份证 + 从业资格证) */
export function hasPersonnelMasterCertMissing(certList: CertRow[] = []): boolean {
return hasPersonnelCertMissing('escort', certList)
/** 人员主数据页:按人员类型检查必备证书 */
export function hasPersonnelMasterCertMissing(
certList: CertRow[] = [],
personnelType?: string | null
): boolean {
const required = getRequiredPersonnelCertNamesByType(personnelType || 'both')
return required.some(requiredName => {
const matched = certList.find(cert => matchRequiredCertName(cert.certName, requiredName))
return !isPersonnelCertStrictComplete(matched, requiredName)
})
}
export function getPersonnelCertMissingSuffix(
certList: CertRow[] = [],
role?: PersonnelCertRole
role?: PersonnelCertRole,
personnelType?: string | null
): string {
if (personnelType) {
return hasPersonnelMasterCertMissing(certList, personnelType)
? PL_PERSONNEL_CERT_MISSING_SUFFIX
: ''
}
if (role) {
return hasPersonnelCertMissing(role, certList) ? PL_PERSONNEL_CERT_MISSING_SUFFIX : ''
}
+80 -14
View File
@@ -21,6 +21,12 @@ export const PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES = [
'罐检报告'
] as const
export const PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
export const PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
export type PlVehicleCertCheckType = 'self' | 'carrier'
export const PL_VEHICLE_CERT_NAME_OPTIONS = [
'行驶证',
'营运证',
@@ -70,18 +76,40 @@ export function mergeVehicleCertsByPart<T extends CertRow>(
return [...normalizedHead, ...normalizedTrailer]
}
export function isVehicleCertUploaded(cert?: Pick<CertRow, 'certName' | 'attachmentUrls'>) {
return Boolean(cert?.certName?.trim() && cert?.attachmentUrls?.trim())
export function isVehicleCertUploaded(cert?: Pick<CertRow, 'certName' | 'certNo' | 'expireDate' | 'attachmentUrls'>) {
return Boolean(
cert?.certName?.trim() &&
cert?.certNo?.trim() &&
cert?.expireDate &&
cert?.attachmentUrls?.trim()
)
}
export function getMissingSelfVehiclePartCertNames(
export function getVehicleHeadRequiredCertNames(
vehicleType: PlVehicleCertCheckType
): readonly string[] {
return vehicleType === 'carrier'
? PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES
: PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES
}
export function getVehicleTrailerRequiredCertNames(
vehicleType: PlVehicleCertCheckType
): readonly string[] {
return vehicleType === 'carrier'
? PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
: PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
}
export function getMissingVehiclePartCertNames(
certList: CertRow[] = [],
part: VehicleCertPart
part: VehicleCertPart,
vehicleType: PlVehicleCertCheckType
): string[] {
const requiredNames =
part === PL_VEHICLE_CERT_PART_TRAILER
? PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
: PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES
? getVehicleTrailerRequiredCertNames(vehicleType)
: getVehicleHeadRequiredCertNames(vehicleType)
return requiredNames.filter(requiredName => {
const matched = certList.find(cert => cert.certName?.trim() === requiredName)
@@ -89,39 +117,77 @@ export function getMissingSelfVehiclePartCertNames(
})
}
export function hasSelfVehicleCertMissing(options: {
export function getMissingSelfVehiclePartCertNames(
certList: CertRow[] = [],
part: VehicleCertPart
): string[] {
return getMissingVehiclePartCertNames(certList, part, 'self')
}
export function hasVehicleCertMissing(options: {
vehicleType: PlVehicleCertCheckType
certList?: CertRow[]
trailerNo?: string | null
}): boolean {
const { certList = [], trailerNo } = options
const { vehicleType, certList = [], trailerNo } = options
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList)
const hasTrailer = Boolean(trailerNo?.trim())
if (getMissingSelfVehiclePartCertNames(headCerts, PL_VEHICLE_CERT_PART_HEAD).length > 0) {
if (
getMissingVehiclePartCertNames(headCerts, PL_VEHICLE_CERT_PART_HEAD, vehicleType).length > 0
) {
return true
}
if (hasTrailer && getMissingSelfVehiclePartCertNames(trailerCerts, PL_VEHICLE_CERT_PART_TRAILER).length > 0) {
if (
hasTrailer &&
getMissingVehiclePartCertNames(trailerCerts, PL_VEHICLE_CERT_PART_TRAILER, vehicleType)
.length > 0
) {
return true
}
return false
}
export function hasSelfVehicleCertMissing(options: {
certList?: CertRow[]
trailerNo?: string | null
}): boolean {
return hasVehicleCertMissing({ ...options, vehicleType: 'self' })
}
export function getVehicleCertMissingSuffix(options: {
vehicleType: PlVehicleCertCheckType
certList?: CertRow[]
trailerNo?: string | null
}): string {
return hasVehicleCertMissing(options) ? PL_VEHICLE_CERT_MISSING_SUFFIX : ''
}
export function getSelfVehicleCertMissingSuffix(options: {
certList?: CertRow[]
trailerNo?: string | null
}): string {
return hasSelfVehicleCertMissing(options) ? PL_VEHICLE_CERT_MISSING_SUFFIX : ''
return getVehicleCertMissingSuffix({ ...options, vehicleType: 'self' })
}
export function getVehiclePartCertMissingSuffix(
certList: CertRow[] = [],
part: VehicleCertPart,
vehicleType?: PlVehicleCertCheckType | null
): string {
if (!vehicleType) return ''
return getMissingVehiclePartCertNames(certList, part, vehicleType).length > 0
? PL_VEHICLE_CERT_MISSING_SUFFIX
: ''
}
export function getSelfVehiclePartCertMissingSuffix(
certList: CertRow[] = [],
part: VehicleCertPart
): string {
return getMissingSelfVehiclePartCertNames(certList, part).length > 0
? PL_VEHICLE_CERT_MISSING_SUFFIX
: ''
return getVehiclePartCertMissingSuffix(certList, part, 'self')
}
/** @deprecated 保存不再阻断证书完整性,仅校验证书名称非空等基础项 */
+14 -26
View File
@@ -426,6 +426,7 @@ const formData = reactive({
const createQualificationRules = {
businessLicenseUrls: [
{
required: true,
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
if (!value || !String(value).trim()) {
callback(new Error('请上传营业执照'))
@@ -438,6 +439,7 @@ const createQualificationRules = {
],
roadTransportLicenseUrls: [
{
required: true,
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
if (!value || !String(value).trim()) {
callback(new Error('请上传道路经营许可证'))
@@ -450,6 +452,7 @@ const createQualificationRules = {
],
transportAgreementUrls: [
{
required: true,
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
if (!value || !String(value).trim()) {
callback(new Error('请上传运输协议'))
@@ -462,10 +465,12 @@ const createQualificationRules = {
]
}
const formRules = computed(() => ({
const formRules = {
carrierName: [{ required: true, message: '请输入承运商名称', trigger: 'blur' }],
...(isEdit.value ? {} : createQualificationRules)
}))
contactName: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
...createQualificationRules
}
function handleCertFieldChange(
field: 'businessLicenseUrls' | 'roadTransportLicenseUrls' | 'transportAgreementUrls'
@@ -658,29 +663,12 @@ async function handleSubmit() {
contactPhone: formData.contactPhone,
status: formData.status,
remark: formData.remark,
...(isEdit.value
? {
...(formData.businessLicenseUrls
? { businessLicenseUrls: formData.businessLicenseUrls }
: {}),
...(formData.roadTransportLicenseUrls
? { roadTransportLicenseUrls: formData.roadTransportLicenseUrls }
: {}),
...(formData.transportAgreementUrls
? { transportAgreementUrls: formData.transportAgreementUrls }
: {}),
...(formData.transportAgreementExpireDate
? { transportAgreementExpireDate: formData.transportAgreementExpireDate }
: {})
}
: {
businessLicenseUrls: formData.businessLicenseUrls,
roadTransportLicenseUrls: formData.roadTransportLicenseUrls,
transportAgreementUrls: formData.transportAgreementUrls,
...(formData.transportAgreementExpireDate
? { transportAgreementExpireDate: formData.transportAgreementExpireDate }
: {})
})
businessLicenseUrls: formData.businessLicenseUrls,
roadTransportLicenseUrls: formData.roadTransportLicenseUrls,
transportAgreementUrls: formData.transportAgreementUrls,
...(formData.transportAgreementExpireDate
? { transportAgreementExpireDate: formData.transportAgreementExpireDate }
: {})
}
if (isEdit.value) {
await carrierApi.update(formData.id, payload)
+145 -17
View File
@@ -24,6 +24,16 @@
<el-option label="停用" value="0" />
</el-select>
</el-form-item>
<el-form-item label="人员归属">
<el-select v-model="searchForm.personnelAffiliation" placeholder="全部" clearable>
<el-option
v-for="option in PL_PERSONNEL_AFFILIATION_OPTIONS"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-end">
@@ -112,6 +122,32 @@
<el-link type="primary" @click="handleView(row)">{{ row.personnelName }}</el-link>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('personnelAffiliation')"
label="人员归属"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="getPersonnelAffiliationTagType(row.personnelAffiliation)" size="small">
{{ getPersonnelAffiliationLabel(row.personnelAffiliation) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('personnelType')"
label="人员类型"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="getPersonnelTypeTagType(row.personnelType)" size="small">
{{ getPersonnelTypeLabel(row.personnelType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('phoneNo')"
prop="phoneNo"
@@ -224,6 +260,26 @@
<el-form-item label="身份证号" prop="idCardNo">
<el-input v-model="formData.idCardNo" placeholder="请输入身份证号" maxlength="32" />
</el-form-item>
<el-form-item label="人员归属" prop="personnelAffiliation">
<el-select v-model="formData.personnelAffiliation" placeholder="请选择人员归属">
<el-option
v-for="option in PL_PERSONNEL_AFFILIATION_OPTIONS"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item label="人员类型" prop="personnelType">
<el-select v-model="formData.personnelType" placeholder="请选择人员类型">
<el-option
v-for="option in PL_PERSONNEL_TYPE_OPTIONS"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择">
<el-option label="在职" value="1" />
@@ -248,6 +304,7 @@
title="人员证书"
upload-folder="pl/personnel-cert"
:cert-name-options="certNameOptions"
:required-cert-names="requiredPersonnelCertNames"
layout="table"
/>
</el-form>
@@ -311,7 +368,21 @@ import { pickBxPageResult } from '@/utils/http/bx'
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
import { formatDateTime } from '@/utils/format'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import { getPersonnelCertMissingSuffix } from '@/utils/pl-personnel-cert'
import {
collectRequiredCertSlotErrors,
formatRequiredCertErrors,
mergeCertListWithRequiredSlots
} from '@/utils/pl-cert-slots'
import {
getPersonnelCertMissingSuffix,
getRequiredPersonnelCertNamesByType,
getPersonnelTypeLabel,
getPersonnelTypeTagType,
getPersonnelAffiliationLabel,
getPersonnelAffiliationTagType,
PL_PERSONNEL_AFFILIATION_OPTIONS,
PL_PERSONNEL_TYPE_OPTIONS
} from '@/utils/pl-personnel-cert'
type CertListItem = CertRow & { _key?: string }
type CertFormListRef = InstanceType<typeof PlCertFormList>
@@ -319,6 +390,8 @@ type CertFormListRef = InstanceType<typeof PlCertFormList>
const loading = ref(false)
const listColumnDefs = [
{ prop: 'personnelAffiliation', label: '人员归属' },
{ prop: 'personnelType', label: '人员类型' },
{ prop: 'phoneNo', label: '手机号' },
{ prop: 'age', label: '年龄' },
{ prop: 'idCardNo', label: '身份证号' },
@@ -359,7 +432,8 @@ const certNameOptions = [
const searchForm = reactive({
q: '',
status: ''
status: '',
personnelAffiliation: ''
})
const pagination = reactive({
@@ -374,12 +448,20 @@ const formData = reactive({
phoneNo: '',
age: null as number | null,
idCardNo: '',
personnelType: '',
personnelAffiliation: 'self',
status: '1',
remark: ''
})
const requiredPersonnelCertNames = computed(() =>
getRequiredPersonnelCertNamesByType(formData.personnelType)
)
const formRules = {
personnelName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
personnelAffiliation: [{ required: true, message: '请选择人员归属', trigger: 'change' }],
personnelType: [{ required: true, message: '请选择人员类型', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
@@ -410,7 +492,7 @@ const viewHeroMeta = computed(() => {
const viewDialogTitle = computed(() => {
const name = viewDetail.value?.personnelName || '人员详情'
if (!viewDetail.value) return name
return name + getPersonnelCertMissingSuffix(viewDetail.value.certList || [])
return name + getPersonnelCertMissingSuffix(viewDetail.value.certList || [], undefined, viewDetail.value.personnelType)
})
const viewSections = computed<ViewSection[]>(() => {
@@ -423,6 +505,16 @@ const viewSections = computed<ViewSection[]>(() => {
fields: [
{ label: '姓名', value: d.personnelName },
{ label: '手机号', value: d.phoneNo || '—' },
{
label: '人员归属',
value: getPersonnelAffiliationLabel(d.personnelAffiliation),
tagType: getPersonnelAffiliationTagType(d.personnelAffiliation)
},
{
label: '人员类型',
value: getPersonnelTypeLabel(d.personnelType),
tagType: getPersonnelTypeTagType(d.personnelType)
},
{ label: '年龄', value: d.age ?? '—' },
{ label: '身份证号', value: maskIdCard(d.idCardNo) },
{
@@ -443,7 +535,7 @@ const viewSections = computed<ViewSection[]>(() => {
tableMode: 'triple'
},
{
title: `人员证书${viewDetail.value ? getPersonnelCertMissingSuffix(viewDetail.value.certList || []) : ''}`,
title: `人员证书${viewDetail.value ? getPersonnelCertMissingSuffix(viewDetail.value.certList || [], undefined, viewDetail.value.personnelType) : ''}`,
fields: [],
slot: 'certs'
}
@@ -451,15 +543,39 @@ const viewSections = computed<ViewSection[]>(() => {
})
function validateCerts(): boolean {
if (!formData.personnelType) {
ElMessage.warning('请选择人员类型')
return false
}
const errors = collectRequiredCertSlotErrors(
certList.value,
requiredPersonnelCertNames.value
)
if (errors.length) {
ElMessage.warning(formatRequiredCertErrors(errors))
return false
}
for (const cert of certList.value) {
if (!cert.certName?.trim()) {
ElMessage.warning('证书名称不能为空')
if (!cert._required && !cert.certName?.trim()) {
ElMessage.warning('其他证书名称不能为空')
return false
}
}
return true
}
watch(
() => formData.personnelType,
() => {
if (!formData.personnelType) return
certList.value = mergeCertListWithRequiredSlots(
certList.value,
getRequiredPersonnelCertNamesByType(formData.personnelType)
)
certFormListRef.value?.refreshRequiredSlots?.()
}
)
function resetCertList() {
certList.value = []
certFormListRef.value?.clearUploadRefs()
@@ -471,6 +587,7 @@ async function fetchData() {
const res = await personnelApi.list({
q: searchForm.q || undefined,
status: searchForm.status || undefined,
personnelAffiliation: searchForm.personnelAffiliation || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
@@ -492,6 +609,7 @@ function handleSearch() {
function handleReset() {
searchForm.q = ''
searchForm.status = ''
searchForm.personnelAffiliation = ''
selectedRows.value = []
pagination.page = 1
fetchData()
@@ -548,6 +666,8 @@ function handleAdd() {
phoneNo: '',
age: null,
idCardNo: '',
personnelType: '',
personnelAffiliation: 'self',
status: '1',
remark: ''
})
@@ -583,19 +703,24 @@ async function handleEdit(row: Personnel) {
phoneNo: detail.phoneNo || '',
age: detail.age ?? null,
idCardNo: detail.idCardNo || '',
personnelType: detail.personnelType || 'both',
personnelAffiliation: detail.personnelAffiliation || 'self',
status: detail.status,
remark: detail.remark || ''
})
certList.value = (detail.certList || []).map(c => ({
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
certId: c.id || c.certId,
certName: c.certName,
certNo: c.certNo,
expireDate: c.expireDate,
remindDays: c.remindDays ?? 30,
attachmentUrls: c.attachmentUrls || ''
}))
certFormListRef.value?.clearUploadRefs()
certList.value = mergeCertListWithRequiredSlots(
(detail.certList || []).map(c => ({
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
certId: c.id || c.certId,
certName: c.certName,
certNo: c.certNo,
expireDate: c.expireDate,
remindDays: c.remindDays ?? 30,
attachmentUrls: c.attachmentUrls || ''
})),
getRequiredPersonnelCertNamesByType(detail.personnelType || 'both')
)
certFormListRef.value?.refreshRequiredSlots?.()
} catch {
ElMessage.error('加载人员详情失败')
}
@@ -616,6 +741,8 @@ async function handleSubmit() {
phoneNo: formData.phoneNo || undefined,
age: formData.age ?? undefined,
idCardNo: formData.idCardNo || undefined,
personnelType: formData.personnelType,
personnelAffiliation: formData.personnelAffiliation,
status: formData.status,
remark: formData.remark || undefined,
certList: certList.value.map(c => ({
@@ -664,7 +791,8 @@ async function handleDelete(row: Personnel) {
useDbInstantSearch(
() => ({
q: searchForm.q,
status: searchForm.status
status: searchForm.status,
personnelAffiliation: searchForm.personnelAffiliation
}),
handleSearch
)
+66 -34
View File
@@ -148,36 +148,6 @@
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('planType')"
prop="planType"
label="单据类型"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getPlanTypeTagType(row.planType)" size="small">
{{ getPlanTypeLabel(row.planType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('customerName')"
prop="customerName"
label="客户名称"
min-width="180"
show-overflow-tooltip
/>
<el-table-column v-if="isColumnVisible('products')" label="产品信息" min-width="240">
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" />
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('planAttachments')" label="计划附件" min-width="200">
<template #default="{ row }">
<PlPlanListCertCell :products="row.products" compact hide-product />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('archiveStatus')"
prop="archiveStatus"
@@ -217,6 +187,36 @@
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('planType')"
prop="planType"
label="单据类型"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getPlanTypeTagType(row.planType)" size="small">
{{ getPlanTypeLabel(row.planType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('customerName')"
prop="customerName"
label="客户名称"
min-width="180"
show-overflow-tooltip
/>
<el-table-column v-if="isColumnVisible('products')" label="产品信息" min-width="240">
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" />
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('planAttachments')" label="计划附件" min-width="200">
<template #default="{ row }">
<PlPlanListCertCell :products="row.products" compact hide-product />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
@@ -313,6 +313,11 @@ import {
Search
} from '@element-plus/icons-vue'
import { planApi, type Plan } from '@/api/pl'
import {
buildMissingHazardCertReadyMessage,
enrichPlanProductsHazardFromCrm
} from '@/composables/usePlToxicProductCertCheck'
import { pickBxData } from '@/utils/http/bx'
import PlPlanArchiveDialog from '@/components/business/pl-plan-archive-dialog/index.vue'
import PlPlanFormDialog from '@/components/business/pl-plan-form-dialog/index.vue'
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
@@ -347,13 +352,13 @@ const planList = ref<Plan[]>([])
const listColumnDefs = [
{ prop: 'planDate', label: '计划日期' },
{ prop: 'archiveStatus', label: '归档状态' },
{ prop: 'followStatus', label: '跟进状态' },
{ prop: 'transportStatus', label: '运输状态' },
{ prop: 'planType', label: '单据类型' },
{ prop: 'customerName', label: '客户名称' },
{ prop: 'products', label: '产品信息' },
{ prop: 'planAttachments', label: '计划附件' },
{ prop: 'archiveStatus', label: '归档状态' },
{ prop: 'followStatus', label: '跟进状态' },
{ prop: 'transportStatus', label: '运输状态' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'personnelSummary', label: '人员' },
{ prop: 'followUserName', label: '跟进人', defaultVisible: false }
@@ -530,7 +535,34 @@ function handleEdit(row: Plan) {
formDialogVisible.value = true
}
function handleArchive(row: Plan) {
async function handleArchive(row: Plan) {
if (row.archiveStatus === 'archived') {
archivePlanId.value = row.id
archiveDialogVisible.value = true
return
}
try {
const res = await planApi.get(row.id)
const data = pickBxData<Plan>(res)
if (!data?.id) {
throw new Error('运输计划数据无效')
}
const rawProducts = Array.isArray(data.products) ? data.products : []
const enrichedProducts = await enrichPlanProductsHazardFromCrm(rawProducts)
const certMessage = buildMissingHazardCertReadyMessage(data, enrichedProducts)
if (certMessage) {
await ElMessageBox.alert(`${certMessage},请至【危化品证上传】页面操作。`, '无法归档', {
type: 'warning'
})
return
}
} catch (error) {
const message = error instanceof Error ? error.message : '加载运输计划失败'
ElMessage.error(message.includes('加载') ? message : `加载运输计划失败:${message}`)
return
}
archivePlanId.value = row.id
archiveDialogVisible.value = true
}
+1 -1
View File
@@ -197,7 +197,7 @@
align="center"
>
<template #default="{ row }">
<el-tag :type="row.transportStatus === 'arranged' ? 'success' : 'info'" size="small">
<el-tag :type="row.transportStatus === 'arranged' ? 'success' : 'danger'" size="small">
{{ row.transportStatus === 'arranged' ? '已安排' : '待安排' }}
</el-tag>
</template>
+47 -22
View File
@@ -316,6 +316,7 @@
upload-folder="pl/vehicle-cert"
:cert-name-options="certNameOptions"
layout="table"
enforce-self-vehicle-required
/>
</el-form>
<template #footer>
@@ -362,7 +363,8 @@
:trailer-no="viewDetail?.trailerNo"
:vehicle-id="viewDetail?.id"
:personnel-list="viewPersonnelList"
check-self-vehicle-certs
vehicle-cert-check-type="self"
check-linked-personnel-certs
cert-layout="table"
/>
</template>
@@ -403,12 +405,16 @@ import {
getPlVehicleStatusTagType,
normalizePlPersonnelSummaryDisplay
} from '@/utils/pl-display'
import {
collectSelfVehicleRequiredCertErrors,
ensureSelfVehicleCertSlots,
formatRequiredCertErrors
} from '@/utils/pl-cert-slots'
import {
PL_VEHICLE_CERT_NAME_OPTIONS,
splitVehicleCertsByPart,
getSelfVehicleCertMissingSuffix,
validateVehicleCertNamesNotEmpty
getSelfVehicleCertMissingSuffix
} from '@/utils/pl-vehicle-cert'
import { validatePersonnelTypeRoleCompat, validatePersonnelAffiliationVehicleCompat } from '@/utils/pl-personnel-cert'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
const VEHICLE_TYPE_SELF = 'self'
@@ -628,7 +634,7 @@ function resolvePersonnelRows(rows: VehiclePersonnelRow[]) {
}
async function loadPersonnelOptions() {
const res = await personnelApi.list({ status: '1', pageSize: 100 })
const res = await personnelApi.list({ status: '1', pageSize: 100, personnelAffiliation: 'self' })
personnelOptions.value = pickBxList<Personnel>(res)
}
@@ -682,14 +688,29 @@ function validateSubTables(): boolean {
ElMessage.warning('请选择人员类型')
return false
}
const person = personnelOptions.value.find(p => p.id === row.personnelId)
if (
!validatePersonnelTypeRoleCompat(person?.personnelType, row.role, message =>
ElMessage.warning(message)
)
) {
return false
}
if (
!validatePersonnelAffiliationVehicleCompat(person?.personnelAffiliation, 'self', message =>
ElMessage.warning(message)
)
) {
return false
}
}
const hasTrailer = Boolean(formData.trailerNo?.trim())
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList.value)
return validateVehicleCertNamesNotEmpty(
[headCerts, ...(hasTrailer ? [trailerCerts] : [])],
message => ElMessage.warning(message)
)
const certErrors = collectSelfVehicleRequiredCertErrors(certList.value, formData.trailerNo)
if (certErrors.length) {
ElMessage.warning(formatRequiredCertErrors(certErrors))
return false
}
return true
}
async function fetchData() {
@@ -794,6 +815,7 @@ function handleAdd() {
remark: ''
})
resetSubTables()
certList.value = ensureSelfVehicleCertSlots([], formData.trailerNo)
loadPersonnelOptions()
dialogVisible.value = true
}
@@ -845,17 +867,20 @@ async function handleEdit(row: Vehicle) {
age: p.age ?? null
}))
)
certList.value = (detail.certList || []).map(c => ({
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
certId: c.id || c.certId,
vehiclePart: c.vehiclePart,
certName: c.certName,
certNo: c.certNo,
expireDate: c.expireDate,
remindDays: c.remindDays ?? 30,
attachmentUrls: c.attachmentUrls || ''
}))
certFormListRef.value?.clearUploadRefs()
certList.value = ensureSelfVehicleCertSlots(
(detail.certList || []).map(c => ({
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
certId: c.id || c.certId,
vehiclePart: c.vehiclePart,
certName: c.certName,
certNo: c.certNo,
expireDate: c.expireDate,
remindDays: c.remindDays ?? 30,
attachmentUrls: c.attachmentUrls || ''
})),
formData.trailerNo
)
certFormListRef.value?.refreshRequiredSlots?.()
} catch {
dialogVisible.value = false
ElMessage.error('加载车辆详情失败')
+91 -15
View File
@@ -335,6 +335,8 @@
upload-folder="pl/vehicle-cert"
:cert-name-options="certNameOptions"
layout="table"
:enforce-self-vehicle-required="formData.vehicleType === 'self'"
:enforce-carrier-vehicle-required="formData.vehicleType === 'carrier'"
/>
</el-form>
<template #footer>
@@ -381,7 +383,10 @@
:trailer-no="viewDetail?.trailerNo"
:vehicle-id="viewDetail?.id"
:personnel-list="viewPersonnelList"
:check-self-vehicle-certs="viewDetail?.vehicleType === 'self'"
:vehicle-cert-check-type="viewVehicleCertCheckType"
:check-linked-personnel-certs="
viewDetail?.vehicleType === 'self' || viewDetail?.vehicleType === 'carrier'
"
cert-layout="table"
/>
</template>
@@ -416,12 +421,18 @@ import { pickBxList, pickBxPageResult } from '@/utils/http/bx'
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
import { formatDateTime } from '@/utils/format'
import { formatPlVehiclePlateDisplay, normalizePlPersonnelSummaryDisplay } from '@/utils/pl-display'
import {
collectCarrierVehicleRequiredCertErrors,
collectSelfVehicleRequiredCertErrors,
ensureVehicleCertSlots,
formatRequiredCertErrors
} from '@/utils/pl-cert-slots'
import {
PL_VEHICLE_CERT_NAME_OPTIONS,
splitVehicleCertsByPart,
getSelfVehicleCertMissingSuffix,
validateVehicleCertNamesNotEmpty
getVehicleCertMissingSuffix,
type PlVehicleCertCheckType
} from '@/utils/pl-vehicle-cert'
import { validatePersonnelTypeRoleCompat, validatePersonnelAffiliationVehicleCompat } from '@/utils/pl-personnel-cert'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
type CertListItem = CertRow & { _key?: string }
@@ -534,12 +545,19 @@ const viewHeroMeta = computed(() => {
return parts.join(' · ')
})
const viewVehicleCertCheckType = computed<PlVehicleCertCheckType | undefined>(() => {
const type = viewDetail.value?.vehicleType
return type === 'self' || type === 'carrier' ? type : undefined
})
const viewDialogTitle = computed(() => {
const plate = viewDetail.value?.licensePlate || '车辆详情'
if (!viewDetail.value || viewDetail.value.vehicleType !== 'self') return plate
const checkType = viewVehicleCertCheckType.value
if (!viewDetail.value || !checkType) return plate
return (
plate +
getSelfVehicleCertMissingSuffix({
getVehicleCertMissingSuffix({
vehicleType: checkType,
certList: viewDetail.value.certList,
trailerNo: viewDetail.value.trailerNo
})
@@ -682,7 +700,13 @@ function resolvePersonnelRows(rows: VehiclePersonnelRow[]) {
}
async function loadPersonnelOptions() {
const res = await personnelApi.list({ status: '1', pageSize: 100 })
const affiliation =
formData.vehicleType === 'carrier' ? 'carrier' : formData.vehicleType === 'self' ? 'self' : undefined
const res = await personnelApi.list({
status: '1',
pageSize: 100,
personnelAffiliation: affiliation
})
personnelOptions.value = pickBxList<Personnel>(res)
}
@@ -736,14 +760,41 @@ function validateSubTables(): boolean {
ElMessage.warning('请选择人员类型')
return false
}
if (formData.vehicleType === 'self' || formData.vehicleType === 'carrier') {
const person = personnelOptions.value.find(p => p.id === row.personnelId)
if (
!validatePersonnelTypeRoleCompat(person?.personnelType, row.role, message =>
ElMessage.warning(message)
)
) {
return false
}
if (
!validatePersonnelAffiliationVehicleCompat(
person?.personnelAffiliation,
formData.vehicleType,
message => ElMessage.warning(message)
)
) {
return false
}
}
}
const hasTrailer = Boolean(formData.trailerNo?.trim())
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList.value)
return validateVehicleCertNamesNotEmpty(
[headCerts, ...(hasTrailer ? [trailerCerts] : [])],
message => ElMessage.warning(message)
)
if (formData.vehicleType === 'self') {
const certErrors = collectSelfVehicleRequiredCertErrors(certList.value, formData.trailerNo)
if (certErrors.length) {
ElMessage.warning(formatRequiredCertErrors(certErrors))
return false
}
} else if (formData.vehicleType === 'carrier') {
const certErrors = collectCarrierVehicleRequiredCertErrors(certList.value, formData.trailerNo)
if (certErrors.length) {
ElMessage.warning(formatRequiredCertErrors(certErrors))
return false
}
}
return true
}
async function fetchData() {
@@ -850,6 +901,7 @@ function handleAdd() {
remark: ''
})
resetSubTables()
certList.value = ensureVehicleCertSlots([], formData.trailerNo, formData.vehicleType as PlVehicleCertCheckType)
loadPersonnelOptions()
dialogVisible.value = true
}
@@ -898,7 +950,7 @@ async function handleEdit(row: Vehicle) {
age: p.age ?? null
}))
)
certList.value = (detail.certList || []).map(c => ({
const mappedCerts = (detail.certList || []).map(c => ({
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
certId: c.id || c.certId,
vehiclePart: c.vehiclePart,
@@ -908,7 +960,15 @@ async function handleEdit(row: Vehicle) {
remindDays: c.remindDays ?? 30,
attachmentUrls: c.attachmentUrls || ''
}))
certFormListRef.value?.clearUploadRefs()
certList.value =
detail.vehicleType === 'self' || detail.vehicleType === 'carrier'
? ensureVehicleCertSlots(
mappedCerts,
formData.trailerNo,
detail.vehicleType as PlVehicleCertCheckType
)
: mappedCerts
certFormListRef.value?.refreshRequiredSlots?.()
} finally {
dialogLoading.value = false
}
@@ -993,6 +1053,22 @@ async function handleDelete(row: Vehicle) {
fetchData()
}
watch(
() => formData.vehicleType,
async type => {
if (!dialogVisible.value) return
await loadPersonnelOptions()
if (type === 'self' || type === 'carrier') {
certList.value = ensureVehicleCertSlots(
certList.value,
formData.trailerNo,
type as PlVehicleCertCheckType
)
certFormListRef.value?.refreshRequiredSlots?.()
}
}
)
useDbInstantSearch(
() => ({
q: searchForm.q,