This commit is contained in:
zlwu committed 2026-07-04 08:21:06 +08:00
1 parent 8e70047f4c
commit b4a49183db
33 files changed
+11558 -5889

No files matched your search

+1 -1
View File
@@ -1 +1 @@
8708
8001
+4 -1
View File
@@ -2,13 +2,16 @@
from fastapi import APIRouter
from app.api.pl import plan, transport, freight, inventory
from app.api.pl import base_data, vehicle_recharge, vehicle_maintenance, vehicle_urea
from app.api.pl import base_data, vehicle_recharge, vehicle_maintenance, vehicle_urea, transport_address
pl_router = APIRouter(prefix="/transport", tags=["运输计划管理"])
# 基础数据(承运商、车辆、司机)
pl_router.include_router(base_data.router)
# 运输通用地址库
pl_router.include_router(transport_address.router)
# 计划管理
pl_router.include_router(plan.router)
+8
View File
@@ -303,9 +303,13 @@ def create_plan(
settlement_method=payload.settlement_method,
has_freight=payload.has_freight,
pickup_address=payload.pickup_address,
pickup_lng=payload.pickup_lng,
pickup_lat=payload.pickup_lat,
pickup_time=payload.pickup_time,
pickup_notes=payload.pickup_notes,
destination_address=payload.destination_address,
destination_lng=payload.destination_lng,
destination_lat=payload.destination_lat,
delivery_time=payload.delivery_time,
destination_notes=payload.destination_notes,
follow_user_id=payload.follow_user_id or user.id,
@@ -380,9 +384,13 @@ def create_plan_full(
settlement_method=payload.settlement_method,
has_freight=payload.has_freight,
pickup_address=payload.pickup_address,
pickup_lng=payload.pickup_lng,
pickup_lat=payload.pickup_lat,
pickup_time=payload.pickup_time,
pickup_notes=payload.pickup_notes,
destination_address=payload.destination_address,
destination_lng=payload.destination_lng,
destination_lat=payload.destination_lat,
delivery_time=payload.delivery_time,
destination_notes=payload.destination_notes,
total_amount=calc_plan_total_amount(payload.products or []),
+4
View File
@@ -113,9 +113,13 @@ def _build_public_page(db: Session, transport: PlTransport) -> PublicTransportPa
plan_date=_format_plan_date(plan.plan_date if plan else None),
plan_type=plan.plan_type if plan else "",
pickup_address=plan.pickup_address if plan else transport.start_point,
pickup_lng=plan.pickup_lng if plan else None,
pickup_lat=plan.pickup_lat if plan else None,
destination_address=(
(plan.destination_address or plan.destination_name) if plan else transport.end_point
),
destination_lng=plan.destination_lng if plan else None,
destination_lat=plan.destination_lat if plan else None,
license_plate=transport.license_plate,
driver_name=transport.driver_name,
driver_phone=transport.driver_phone,
+6
View File
@@ -246,6 +246,9 @@ def _apply_plan_logistics_from_payload(plan: PlPlan, payload: TransportArrange)
plan.pickup_address = payload.pickup_address
elif payload.start_point is not None:
plan.pickup_address = payload.start_point
if "pickup_lng" in payload.model_fields_set or "pickup_lat" in payload.model_fields_set:
plan.pickup_lng = payload.pickup_lng
plan.pickup_lat = payload.pickup_lat
if payload.pickup_time is not None:
plan.pickup_time = payload.pickup_time
if payload.pickup_notes is not None:
@@ -254,6 +257,9 @@ def _apply_plan_logistics_from_payload(plan: PlPlan, payload: TransportArrange)
plan.destination_address = payload.destination_address
elif payload.end_point is not None:
plan.destination_address = payload.end_point
if "destination_lng" in payload.model_fields_set or "destination_lat" in payload.model_fields_set:
plan.destination_lng = payload.destination_lng
plan.destination_lat = payload.destination_lat
if payload.delivery_time is not None:
plan.delivery_time = payload.delivery_time
if payload.destination_notes is not None:
+28
View File
@@ -102,6 +102,30 @@ class PlPersonnel(Base):
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
class PlTransportAddress(Base):
"""运输通用地址库 pl_transport_address"""
__tablename__ = "pl_transport_address"
id = Column(String(64), primary_key=True, comment="主键ID")
address_name = Column(String(200), nullable=False, comment="地址名称")
full_address = Column(String(500), nullable=False, comment="详细地址")
lng = Column(DECIMAL(10, 6), nullable=False, comment="经度")
lat = Column(DECIMAL(10, 6), nullable=False, comment="纬度")
contact_name = Column(String(100), nullable=True, comment="联系人")
contact_phone = Column(String(32), nullable=True, comment="联系电话")
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="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记:0-正常 1-已删除")
class PlCert(Base):
"""证书表 pl_cert"""
@@ -248,9 +272,13 @@ class PlPlan(Base):
# 运输信息
has_freight = Column(SmallInteger, nullable=False, default=0, comment="是否有运输费用 0-否 1-是")
pickup_address = Column(String(500), nullable=True, comment="取货地址")
pickup_lng = Column(DECIMAL(10, 6), nullable=True, comment="取货经度")
pickup_lat = Column(DECIMAL(10, 6), nullable=True, comment="取货纬度")
pickup_time = Column(DateTime, nullable=True, comment="取货时间")
pickup_notes = Column(String(500), nullable=True, comment="取货注意事项")
destination_address = Column(String(500), nullable=True, comment="目的地地址")
destination_lng = Column(DECIMAL(10, 6), nullable=True, comment="送货经度")
destination_lat = Column(DECIMAL(10, 6), nullable=True, comment="送货纬度")
delivery_time = Column(DateTime, nullable=True, comment="送达时间")
destination_notes = Column(String(500), nullable=True, comment="目的地注意事项")
transport_id = Column(String(64), nullable=True, comment="运输ID")
+13 -4
View File
@@ -5,6 +5,7 @@ from typing import Optional
from pydantic import BaseModel, Field
from app.schemas.pl.datetime_parse import OptionalPlanDateTime
from app.schemas.pl.resource_snapshot import PlanResourceSnapshotMixin
# =============================================================================
@@ -105,10 +106,14 @@ class PlanBase(BaseModel):
settlement_method: Optional[str] = Field(default=None, max_length=50, alias="settlementMethod", description="结算方式")
has_freight: int = Field(default=0, alias="hasFreight", description="是否有运输费用")
pickup_address: Optional[str] = Field(default=None, max_length=500, alias="pickupAddress", description="取货地址")
pickup_time: Optional[datetime] = Field(default=None, alias="pickupTime", description="取货时间")
pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng", description="取货经度")
pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat", description="取货纬度")
pickup_time: OptionalPlanDateTime = Field(default=None, alias="pickupTime", description="取货时间")
pickup_notes: Optional[str] = Field(default=None, max_length=500, alias="pickupNotes", description="取货注意事项")
destination_address: Optional[str] = Field(default=None, max_length=500, alias="destinationAddress", description="目的地地址")
delivery_time: Optional[datetime] = Field(default=None, alias="deliveryTime", description="送达时间")
destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng", description="送货经度")
destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat", description="送货纬度")
delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime", description="送达时间")
destination_notes: Optional[str] = Field(default=None, max_length=500, alias="destinationNotes", description="目的地注意事项")
notify_message: Optional[str] = Field(default=None, alias="notifyMessage", description="通知信息")
follow_user_id: Optional[str] = Field(default=None, alias="followUserId", description="跟进人ID")
@@ -158,10 +163,14 @@ class PlanUpdate(BaseModel):
settlement_method: Optional[str] = Field(default=None, max_length=50, alias="settlementMethod")
has_freight: Optional[int] = Field(default=None, alias="hasFreight")
pickup_address: Optional[str] = Field(default=None, max_length=500, alias="pickupAddress")
pickup_time: Optional[datetime] = Field(default=None, alias="pickupTime")
pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng")
pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat")
pickup_time: OptionalPlanDateTime = Field(default=None, alias="pickupTime")
pickup_notes: Optional[str] = Field(default=None, max_length=500, alias="pickupNotes")
destination_address: Optional[str] = Field(default=None, max_length=500, alias="destinationAddress")
delivery_time: Optional[datetime] = Field(default=None, alias="deliveryTime")
destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng")
destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat")
delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime")
destination_notes: Optional[str] = Field(default=None, max_length=500, alias="destinationNotes")
notify_message: Optional[str] = Field(default=None, alias="notifyMessage")
follow_user_id: Optional[str] = Field(default=None, alias="followUserId")
+11 -2
View File
@@ -5,6 +5,7 @@ from typing import Optional
from pydantic import BaseModel, Field
from app.schemas.pl.datetime_parse import OptionalPlanDateTime
from app.schemas.pl.resource_snapshot import PlanResourceSnapshotMixin
@@ -48,10 +49,14 @@ class TransportArrange(BaseModel):
carrier_id: Optional[str] = Field(default=None, alias="carrierId", description="承运商ID")
transport_medium: Optional[str] = Field(default=None, alias="transportMedium", description="运输介质")
pickup_address: Optional[str] = Field(default=None, alias="pickupAddress", description="取货地址")
pickup_time: Optional[datetime] = Field(default=None, alias="pickupTime", description="取货时间")
pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng", description="取货经度")
pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat", description="取货纬度")
pickup_time: OptionalPlanDateTime = Field(default=None, alias="pickupTime", description="取货时间")
pickup_notes: Optional[str] = Field(default=None, alias="pickupNotes", description="取货注意事项")
destination_address: Optional[str] = Field(default=None, alias="destinationAddress", description="送货地址")
delivery_time: Optional[datetime] = Field(default=None, alias="deliveryTime", description="送达时间")
destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng", description="送货经度")
destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat", description="送货纬度")
delivery_time: OptionalPlanDateTime = Field(default=None, alias="deliveryTime", description="送达时间")
destination_notes: Optional[str] = Field(default=None, alias="destinationNotes", description="送货注意事项")
start_point: Optional[str] = Field(default=None, alias="startPoint", description="运输起点(兼容,等同取货地址)")
end_point: Optional[str] = Field(default=None, alias="endPoint", description="运输终点(兼容,等同送货地址)")
@@ -191,7 +196,11 @@ class PublicTransportPageRead(BaseModel):
plan_date: Optional[str] = Field(default=None, alias="planDate")
plan_type: str = Field(alias="planType")
pickup_address: Optional[str] = Field(default=None, alias="pickupAddress")
pickup_lng: Optional[Decimal] = Field(default=None, alias="pickupLng")
pickup_lat: Optional[Decimal] = Field(default=None, alias="pickupLat")
destination_address: Optional[str] = Field(default=None, alias="destinationAddress")
destination_lng: Optional[Decimal] = Field(default=None, alias="destinationLng")
destination_lat: Optional[Decimal] = Field(default=None, alias="destinationLat")
license_plate: Optional[str] = Field(default=None, alias="licensePlate")
driver_name: Optional[str] = Field(default=None, alias="driverName")
driver_phone: Optional[str] = Field(default=None, alias="driverPhone")
+3 -13
View File
@@ -108,10 +108,8 @@
"106": "Community 106",
"107": "Community 107",
"108": "Community 108",
"109": "Community 109",
"110": "Community 110",
"111": "Community 111",
"112": "Community 112",
"113": "Community 113",
"114": "Community 114",
"115": "Community 115",
@@ -121,32 +119,24 @@
"119": "Community 119",
"120": "Community 120",
"121": "Community 121",
"122": "Community 122",
"123": "Community 123",
"124": "Community 124",
"125": "Community 125",
"126": "Community 126",
"127": "Community 127",
"128": "Community 128",
"129": "Community 129",
"130": "Community 130",
"131": "Community 131",
"132": "Community 132",
"133": "Community 133",
"134": "Community 134",
"135": "Community 135",
"136": "Community 136",
"137": "Community 137",
"138": "Community 138",
"139": "Community 139",
"140": "Community 140",
"141": "Community 141",
"142": "Community 142",
"143": "Community 143",
"144": "Community 144",
"145": "Community 145",
"146": "Community 146",
"147": "Community 147",
"148": "Community 148",
"150": "Community 150"
"149": "Community 149",
"150": "Community 150",
"151": "Community 151"
}
+182 -232
View File
@@ -1,16 +1,16 @@
# Graph Report - backend (2026-07-01)
## Corpus Check
- 226 files · ~158,852 words
- 229 files · ~159,883 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 5033 nodes · 34891 edges · 150 communities (148 shown, 2 thin omitted)
- Extraction: 51% EXTRACTED · 49% INFERRED · 0% AMBIGUOUS · INFERRED: 17194 edges (avg confidence: 0.5)
- 5097 nodes · 35085 edges · 140 communities (138 shown, 2 thin omitted)
- Extraction: 51% EXTRACTED · 49% INFERRED · 0% AMBIGUOUS · INFERRED: 17242 edges (avg confidence: 0.5)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `7c838cfb`
- Built from commit: `8e70047f`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -124,46 +124,36 @@
- [[_COMMUNITY_Community 106|Community 106]]
- [[_COMMUNITY_Community 107|Community 107]]
- [[_COMMUNITY_Community 108|Community 108]]
- [[_COMMUNITY_Community 109|Community 109]]
- [[_COMMUNITY_Community 110|Community 110]]
- [[_COMMUNITY_Community 111|Community 111]]
- [[_COMMUNITY_Community 112|Community 112]]
- [[_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]]
- [[_COMMUNITY_Community 127|Community 127]]
- [[_COMMUNITY_Community 128|Community 128]]
- [[_COMMUNITY_Community 129|Community 129]]
- [[_COMMUNITY_Community 130|Community 130]]
- [[_COMMUNITY_Community 131|Community 131]]
- [[_COMMUNITY_Community 132|Community 132]]
- [[_COMMUNITY_Community 133|Community 133]]
- [[_COMMUNITY_Community 134|Community 134]]
- [[_COMMUNITY_Community 135|Community 135]]
- [[_COMMUNITY_Community 136|Community 136]]
- [[_COMMUNITY_Community 137|Community 137]]
- [[_COMMUNITY_Community 138|Community 138]]
- [[_COMMUNITY_Community 139|Community 139]]
- [[_COMMUNITY_Community 140|Community 140]]
- [[_COMMUNITY_Community 141|Community 141]]
- [[_COMMUNITY_Community 143|Community 143]]
- [[_COMMUNITY_Community 144|Community 144]]
- [[_COMMUNITY_Community 145|Community 145]]
- [[_COMMUNITY_Community 146|Community 146]]
- [[_COMMUNITY_Community 147|Community 147]]
- [[_COMMUNITY_Community 148|Community 148]]
- [[_COMMUNITY_Community 149|Community 149]]
- [[_COMMUNITY_Community 150|Community 150]]
- [[_COMMUNITY_Community 151|Community 151]]
## God Nodes (most connected - your core abstractions)
1. `Session` - 1123 edges
2. `BaseModel` - 420 edges
2. `BaseModel` - 423 edges
3. `int` - 336 edges
4. `Any` - 331 edges
5. `get_tenant_no()` - 327 edges
6. `SysUser` - 282 edges
7. `DbUserContext` - 272 edges
4. `get_tenant_no()` - 334 edges
5. `Any` - 331 edges
6. `DbUserContext` - 282 edges
7. `SysUser` - 282 edges
8. `success_response()` - 259 edges
9. `Session` - 257 edges
10. `bool` - 255 edges
@@ -180,135 +170,135 @@
- `int` --uses--> `LocalStorage` [INFERRED]
scripts/run_db_auto_generate_plans.py → app/core/storage.py
## Communities (150 total, 2 thin omitted)
## Communities (140 total, 2 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.14
Nodes (103): date, DbUserContext, int, Session, str, DocumentReminderRead, DocumentReminderUpdate, FreightSettlementUpdate (+95 more)
Nodes (105): date, DbUserContext, int, Session, str, DocumentReminderRead, DocumentReminderUpdate, FreightSettlementCreate (+97 more)
### Community 1 - "Community 1"
Cohesion: 0.04
Nodes (46): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll (+38 more)
Cohesion: 0.08
Nodes (26): 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo (+18 more)
### Community 2 - "Community 2"
Cohesion: 0.35
Nodes (219): Any, bool, str, UploadFile, CRM 线索管理 Pydantic Schemas, CRM 客户管理 Pydantic Schemas, CRM 商机管理 Pydantic Schemas, CRM 客户公海池规则 Pydantic Schemas (+211 more)
Nodes (220): Any, bool, str, UploadFile, CRM 线索管理 Pydantic Schemas, CRM 客户管理 Pydantic Schemas, CRM 商机管理 Pydantic Schemas, CRM 客户公海池规则 Pydantic Schemas (+212 more)
### Community 3 - "Community 3"
Cohesion: 0.07
Nodes (171): _apply_default_invoice_info_to_customers(), category_add(), category_delete(), category_tree(), category_update(), clue_assign(), clue_check_convert(), clue_check_duplicate() (+163 more)
Nodes (170): _apply_default_invoice_info_to_customers(), category_add(), category_delete(), category_tree(), category_update(), clue_assign(), clue_check_convert(), clue_check_duplicate() (+162 more)
### Community 4 - "Community 4"
Cohesion: 0.13
Nodes (29): Any, bool, CRM 供方信息 CRUD — 对照 CrmSupplierInfoServiceImpl.java, int, Session, str, _apply_list_filters(), create_supplier_info() (+21 more)
Cohesion: 0.08
Nodes (41): Any, bool, CRM 供方信息 CRUD — 对照 CrmSupplierInfoServiceImpl.java, int, Session, str, generate_contract_no(), Java: generateContractNo — {供方编码}/X-JT + yyyyMMdd + 3位流水 (+33 more)
### Community 5 - "Community 5"
Cohesion: 0.08
Nodes (39): _db_pool_keepalive_loop(), _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str (+31 more)
Cohesion: 0.07
Nodes (41): _db_pool_keepalive_loop(), _error_json(), lifespan(), _mask_secret(), Exception, int, Request, str (+33 more)
### Community 6 - "Community 6"
Cohesion: 0.14
Nodes (39): Any, bool, int, Session, str, SysUser, create_user(), get_user() (+31 more)
### Community 7 - "Community 7"
Cohesion: 0.06
Nodes (33): BaseModel, AttachmentOut, ReimburseApplyCreate, ReimburseApplyUpdate, ReimburseApprovalCreate, _coerce_optional_id(), WorkflowDefCreate, WorkflowDefUpdate (+25 more)
Cohesion: 0.33
Nodes (5): _coerce_optional_id(), WorkflowDefCreate, WorkflowDefUpdate, WorkflowNodeCreate, WorkflowNodeOut
### Community 8 - "Community 8"
Cohesion: 0.35
Nodes (5): Any, str, WfProcessInstance, WorkflowEngine, WfProcessInstance
### Community 9 - "Community 9"
Cohesion: 0.19
Nodes (38): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, StartInstanceBody, Any, bool, WfProcessInstance, bool (+30 more)
Cohesion: 0.14
Nodes (34): Session, str, Session, str, user_info_from_context(), parse_date_str(), plan_to_read(), plan_to_read_with_fill_link() (+26 more)
### Community 10 - "Community 10"
Cohesion: 0.03
Nodes (151): register_quote_routes(), Any, bool, CRM 报价单 CRUD — 对照 CrmQuoteServiceImpl.java(P2 全量), datetime, Decimal, int, Session (+143 more)
Nodes (185): register_quote_routes(), CrmContractProductPriceTier, Query, Any, bool, int, Session, str (+177 more)
### Community 11 - "Community 11"
Cohesion: 0.15
Nodes (38): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+30 more)
Cohesion: 0.16
Nodes (26): Any, bool, BxUserContext, BxWorkflowNode, float, int, ReimburseApplyOut, Session (+18 more)
### Community 12 - "Community 12"
Cohesion: 0.12
Nodes (84): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+76 more)
Cohesion: 0.10
Nodes (92): date, DbUserContext, int, Session, str, VehicleUreaApplyCreate, VehicleUreaApplyRead, VehicleUreaApplyUpdate (+84 more)
### Community 13 - "Community 13"
Cohesion: 0.05
Nodes (72): BxUserContext, Session, str, UploadFile, Any, bool, bytes, int (+64 more)
Cohesion: 0.13
Nodes (8): bool, Path, str, BaseStorage, LocalStorage, OSSStorage, QiniuStorage, Path
### Community 14 - "Community 14"
Cohesion: 0.09
Nodes (55): Any, bool, datetime, int, Session, str, _advance_contract_after_approval(), approve_contract() (+47 more)
Nodes (58): Any, bool, CrmContract, datetime, int, Query, Session, str (+50 more)
### Community 15 - "Community 15"
Cohesion: 0.14
Nodes (28): str, T, bool, date, datetime, int, Session, str (+20 more)
Nodes (29): str, T, bool, date, datetime, int, Session, str (+21 more)
### Community 16 - "Community 16"
Cohesion: 0.07
Nodes (103): Any, CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), Decimal, int, Session (+95 more)
Cohesion: 0.03
Nodes (201): register_contract_routes(), CRM 合同子模块路由 — 变更/模板/审批独立页, register_contract_sub_routes(), Any, bool, CRM 合同 CRUD — 对照 CrmContractServiceImpl.java(列表/详情 P1), CrmContract, date (+193 more)
### Community 17 - "Community 17"
Cohesion: 0.18
Nodes (50): Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal, int, Session (+42 more)
Nodes (55): Any, date, Any, CRM 商机 CRUD(主表 + 阶段/赢输单/统计/看板/漏斗), CrmOpportunity, date, datetime, Decimal (+47 more)
### Community 18 - "Community 18"
Cohesion: 0.18
Nodes (19): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+11 more)
Cohesion: 0.14
Nodes (31): Any, bool, BxUserContext, BxWorkflowNode, float, int, Session, str (+23 more)
### Community 19 - "Community 19"
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)
Cohesion: 0.23
Nodes (25): approve_task(), _assert_wf_instance_in_scope(), _business_type_label(), delete_process_def(), _enrich_task_list(), get_business_all_tasks(), _get_business_form_fields(), get_designer_data() (+17 more)
### Community 20 - "Community 20"
Cohesion: 0.22
Nodes (47): BxUserContext, int, Session, str, BxUserContext, int, Session, str (+39 more)
Cohesion: 0.28
Nodes (34): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+26 more)
### Community 21 - "Community 21"
Cohesion: 0.05
Nodes (162): bool, DbUserContext, int, 运输计划管理 - 基础数据 API(承运商、车辆、人员), PlCarrier, PlVehicle, Session, str (+154 more)
Nodes (114): DbUserContext, int, 运输计划管理 - 基础数据 API(承运商、车辆、人员), Session, str, date, 运输计划管理 - 基础数据 Schema(承运商、车辆、人员、证书), bool (+106 more)
### Community 22 - "Community 22"
Cohesion: 0.22
Nodes (29): Any, int, Session, str, create_instance(), create_process_def(), create_task(), get_all_tasks_for_business() (+21 more)
### Community 23 - "Community 23"
Cohesion: 0.23
Nodes (37): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, datetime, str (+29 more)
Cohesion: 0.24
Nodes (44): AdvanceIssueRequest, BxUserContext, int, ReimburseApplyOut, Session, str, int, str (+36 more)
### Community 24 - "Community 24"
Cohesion: 0.12
Nodes (89): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+81 more)
Nodes (92): date, DbUserContext, int, Session, str, VehicleMaintenanceApplyCreate, VehicleMaintenanceApplyRead, VehicleMaintenanceApplyReminder (+84 more)
### Community 25 - "Community 25"
Cohesion: 0.09
Nodes (29): 运输计划管理 - 基础数据 Schema(承运商、车辆、人员、证书), CarrierAuditReject, CarrierBase, CarrierCreate, CarrierListResponse, CarrierQualificationBase, CarrierRead, CarrierUpdate (+21 more)
Cohesion: 0.05
Nodes (126): date, DbUserContext, int, PlanCreateFull, PlPlan, Session, str, PlTransportSiteLink (+118 more)
### Community 26 - "Community 26"
Cohesion: 0.15
Nodes (23): logout(), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, bool, int, Redis, Request (+15 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 27 - "Community 27"
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 28 - "Community 28"
Cohesion: 0.33
Nodes (29): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, BxUserContext, int, Session, str (+21 more)
Cohesion: 0.14
Nodes (25): bool, BxUserContext, int, Session, str, Any, bool, int (+17 more)
### Community 29 - "Community 29"
Cohesion: 0.08
Nodes (26): 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo, 供方详情 — Java: CrmSupplierInfoController.getInfo (+18 more)
Cohesion: 0.28
Nodes (25): DbUserContext, int, Session, str, PlTransportAddress, 运输通用地址库 pl_transport_address, _address_to_read(), _build_address_query() (+17 more)
### Community 30 - "Community 30"
Cohesion: 0.07
Nodes (43): AttachmentUploadResponse, ChangePasswordRequest, Config, LoginRequest, LoginResponse, Pydantic schemas - 用户认证, ResetPasswordRequest, UserCreate (+35 more)
Nodes (41): AttachmentUploadResponse, ChangePasswordRequest, Config, LoginRequest, LoginResponse, Pydantic schemas - 用户认证, ResetPasswordRequest, UserCreate (+33 more)
### Community 31 - "Community 31"
Cohesion: 0.15
Nodes (30): Any, bool, int, Session, str, approve_quote(), can_user_approve(), create_approval_records() (+22 more)
Cohesion: 0.08
Nodes (26): 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+18 more)
### Community 32 - "Community 32"
Cohesion: 0.10
@@ -316,15 +306,15 @@ Nodes (99): date, DbUserContext, int, Session, str, VehicleRechargeApplyCreate,
### Community 33 - "Community 33"
Cohesion: 0.13
Nodes (56): Any, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作), CrmCustomer (+48 more)
Nodes (54): Any, int, Session, str, Any, bool, CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作), CrmCustomer (+46 more)
### Community 34 - "Community 34"
Cohesion: 0.19
Nodes (34): int, str, generate_random_password(), get_file_size_display(), Engine, int, Path, build_id_maps() (+26 more)
Cohesion: 0.23
Nodes (28): Engine, build_id_maps(), clip_text(), count_rows(), export_mapping(), fetch_rows(), legacy_id_to_str(), main() (+20 more)
### Community 35 - "Community 35"
Cohesion: 0.04
Nodes (182): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, Any (+174 more)
Cohesion: 0.03
Nodes (196): DbUserContext, FreightSettlementDisburseBody, FreightSettlementSubmitApprovalBody, FreightSettlementUpdate, int, Session, str, Any (+188 more)
### Community 36 - "Community 36"
Cohesion: 0.19
@@ -339,60 +329,60 @@ 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 39 - "Community 39"
Cohesion: 0.18
Nodes (22): datetime, date, datetime, str, 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, format_datetime(), get_current_time(), parse_datetime() (+14 more)
Cohesion: 0.16
Nodes (23): datetime, int, str, datetime, str, 审批轮次过滤 — 驳回后重新提交时,进度接口仅展示当前轮次。, format_datetime(), generate_random_password() (+15 more)
### Community 40 - "Community 40"
Cohesion: 0.13
Nodes (37): Any, bool, Decimal, int, Session, str, batch_insert_quote_products(), calculate_subtotal() (+29 more)
### Community 41 - "Community 41"
Cohesion: 0.12
Nodes (41): Any, bool, date, int, Session, str, Session, str (+33 more)
Cohesion: 0.13
Nodes (40): Any, bool, date, int, Session, str, Session, str (+32 more)
### Community 42 - "Community 42"
Cohesion: 0.08
Nodes (25): 技术栈, 1. 创建虚拟环境, 项目结构, 1. 创建虚拟环境, 2. 安装依赖, 2. 安装依赖, 3. 配置环境变量, 快速开始 (+17 more)
### Community 43 - "Community 43"
Cohesion: 0.21
Nodes (16): Any, int, str, BusinessException, InvalidStatusTransitionException, log_api_request(), log_delivery_operation(), log_operation() (+8 more)
Cohesion: 0.08
Nodes (48): Any, int, str, float, int, str, Any, int (+40 more)
### Community 44 - "Community 44"
Cohesion: 0.21
Nodes (36): DbInboundOutbound, DbUserContext, Session, str, CreateLinkForPlanPayload, _apply_inventory_product_update(), _apply_inventory_update(), _base_query() (+28 more)
Nodes (37): DbInboundOutbound, DbUserContext, Session, str, CreateLinkForPlanPayload, _apply_inventory_product_update(), _apply_inventory_update(), _base_query() (+29 more)
### Community 45 - "Community 45"
Cohesion: 0.36
Nodes (15): create_leave(), delete_leave(), get_leave(), _leave_local_callback(), leave_workflow_callback(), list_leaves(), submit_leave(), update_leave() (+7 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 46 - "Community 46"
Cohesion: 0.15
Nodes (22): clue_import_template(), customer_import_template(), product_import_template(), str, Any, bool, Session, str (+14 more)
### Community 47 - "Community 47"
Cohesion: 0.05
Nodes (82): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, register_contract_routes(), bool, bool, 运营端角色守卫:admin / super_admin 可访问。, require_admin_or_super_admin(), batch_assign_owner(), batch_submit_approval() (+74 more)
Cohesion: 0.24
Nodes (10): Exception, bytes, bytes, BytesIO, convert_heic_to_jpeg(), MinIOStorage, process_image_content(), 文件存储工具模块 支持多种存储方式: 1. 本地存储 (local) - 默认 2. 七牛云存储 (qiniu) - 推荐生产环境使用 3. 阿里云OSS ( (+2 more)
### Community 48 - "Community 48"
Cohesion: 0.39
Nodes (8): bool, BxUserContext, Session, str, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, WorkflowService, BxWorkflowDef
Cohesion: 0.18
Nodes (23): BxUserContext, int, Session, str, Any, str, create_workflow(), delete_workflow() (+15 more)
### Community 49 - "Community 49"
Cohesion: 0.30
Nodes (21): Any, bool, CrmOpportunity, datetime, int, Session, str, ensure_crm_create_by_row_in_scope() (+13 more)
Cohesion: 0.31
Nodes (20): Any, bool, CrmOpportunity, datetime, int, Session, str, create_opportunity_follow() (+12 more)
### Community 50 - "Community 50"
Cohesion: 0.12
Nodes (31): CRM 报价单 API 路由 — 挂接到 crm.router, CRM 报价单 Pydantic Schemas, QuoteAcceptBody, QuoteApprovalApproveBody, QuoteApprovalListBody, QuoteApprovalRefreshNamesBody, QuoteApprovalWithdrawBody, QuoteCreateBody (+23 more)
Cohesion: 0.11
Nodes (39): CRM 报价单 API 路由 — 挂接到 crm.router, str, CRM 报价单 Pydantic Schemas, AdvanceApprovalCreate, ContractApprovalPassBody, CustomerCheckDuplicateBody, CustomerPoolDetailBody, CustomerPoolListBody (+31 more)
### Community 51 - "Community 51"
Cohesion: 0.07
Nodes (55): int, PlTransport, Session, str, UploadFile, Any, Session, str (+47 more)
Cohesion: 0.22
Nodes (18): bytes, int, str, download_file_bytes(), file_stream(), get_access_url(), get_download_url(), get_preview_url() (+10 more)
### Community 52 - "Community 52"
Cohesion: 0.10
Nodes (63): CrmContract, Query, Any, bool, CrmContractProductPriceTier, date, Decimal, int (+55 more)
Cohesion: 0.16
Nodes (33): Any, bool, CrmContractProductPriceTier, date, Decimal, int, Session, str (+25 more)
### Community 53 - "Community 53"
Cohesion: 0.24
@@ -403,8 +393,8 @@ Cohesion: 0.22
Nodes (17): Any, bool, Session, str, list_share_logs(), _parse_user_agent(), CRM 报价单分享访问日志 CRUD — 对照 CrmQuoteShareLogServiceImpl.java, Java: statisticsVisits (+9 more)
### Community 55 - "Community 55"
Cohesion: 0.20
Nodes (30): Any, bool, CRM 产品模块 CRUD(产品分类 + 产品 + 附件), int, Session, str, barcode_exists(), category_code_exists() (+22 more)
Cohesion: 0.04
Nodes (114): Any, bool, CRM 产品模块 CRUD(产品分类 + 产品 + 附件), int, Session, str, SysOss, int (+106 more)
### Community 56 - "Community 56"
Cohesion: 0.26
@@ -423,28 +413,28 @@ Cohesion: 0.25
Nodes (23): Any, bool, int, Session, str, create_role(), get_role(), get_role_data_scope() (+15 more)
### Community 60 - "Community 60"
Cohesion: 0.28
Nodes (18): Any, int, Session, str, _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept() (+10 more)
Cohesion: 0.35
Nodes (17): Any, bool, int, Session, str, _build_tree(), _cascade_soft_delete(), create_menu() (+9 more)
### Community 61 - "Community 61"
Cohesion: 0.12
Nodes (46): Session, str, bool, date, DbInboundOutbound, DbPurchasePlan, float, PlPlan (+38 more)
Cohesion: 0.08
Nodes (85): int, PlTransport, Session, str, UploadFile, bool, date, DbInboundOutbound (+77 more)
### Community 62 - "Community 62"
Cohesion: 0.12
Nodes (45): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, Session, str, UploadFile (+37 more)
Cohesion: 0.10
Nodes (56): bool, DbInboundOutbound, int, PublicInventoryFeedback, PublicInventoryPageRead, Session, str, UploadFile (+48 more)
### Community 63 - "Community 63"
Cohesion: 0.09
Nodes (22): generate_auto_product_no(), Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配), Java: resolveProductIdForQuoteItem(CrmProduct 无 tax_rate,跳过税率匹配), Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符, Java: generateAutoProductNo — SXWL + 8位随机大写字符 (+14 more)
Cohesion: 0.14
Nodes (19): Session, str, Session, str, get_user_menu_codes(), 菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms), 读取用户授权的菜单权限标识集合(与前端 meta.menuCode / perms 对齐)。, 接口菜单权限守卫。super_admin 直接放行。 (+11 more)
### Community 64 - "Community 64"
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)
### Community 65 - "Community 65"
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.39
Nodes (8): bool, BxUserContext, Session, str, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, 按业务类型获取工作流;已知类型不存在时自动创建空定义(无节点)。, WorkflowService, BxWorkflowDef
### Community 66 - "Community 66"
Cohesion: 0.14
@@ -455,20 +445,20 @@ Cohesion: 0.16
Nodes (33): Any, bool, int, Session, str, Any, Session, str (+25 more)
### Community 68 - "Community 68"
Cohesion: 0.25
Nodes (11): check_customer_protect_time(), _execute_single_rule(), _get_all_tenant_nos(), pool_recovery_job(), 公海池自动回收任务(由 APScheduler 调度) 遍历所有租户的启用规则,执行自动回收。, 每天凌晨1点:检查保护期到期客户,跟进次数不足则退回公海, 每天早上9点:保护期到期提醒(3天内到期且跟进不足), remind_customer_recycle() (+3 more)
Cohesion: 0.07
Nodes (61): Any, CertRead, date, int, Session, str, CertRead, Session (+53 more)
### Community 69 - "Community 69"
Cohesion: 0.28
Nodes (25): DbUserContext, float, Session, str, Session, str, DashboardCargoStat, DashboardPartnerCargoStat (+17 more)
Cohesion: 0.33
Nodes (22): DbUserContext, float, Session, str, get_db_current_user(), DashboardCargoStat, DashboardPartnerCargoStat, DashboardStats (+14 more)
### Community 70 - "Community 70"
Cohesion: 0.29
Nodes (14): is_valid_dict_type(), load_portal_baseline(), load_sx(), load_zkxw(), main(), map_tag_type(), normalize_row(), parse_insert() (+6 more)
### Community 71 - "Community 71"
Cohesion: 0.08
Nodes (51): BxUserContext, Session, str, Any, str, bool, BxUserContext, int (+43 more)
Cohesion: 0.25
Nodes (18): BxUserContext, Session, str, bool, BxUserContext, Session, str, create_expense_item() (+10 more)
### Community 72 - "Community 72"
Cohesion: 0.34
@@ -479,36 +469,36 @@ Cohesion: 0.07
Nodes (100): attachment_download_url(), attachment_preview_url(), AttachmentDownloadUrlBody, create_dept(), create_dict_data(), create_dict_type(), create_menu(), create_post() (+92 more)
### Community 74 - "Community 74"
Cohesion: 0.08
Nodes (26): 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list, 分页查询供方列表 — Java: CrmSupplierInfoController.list (+18 more)
Cohesion: 0.04
Nodes (46): invoice_info_list(), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 按客户ID查询开票信息(与 by-customer 等价), 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll, 供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll (+38 more)
### Community 75 - "Community 75"
Cohesion: 0.05
Nodes (124): bool, Decimal, int, PlanCreateFull, PlPlan, PlPlanProduct, PlTransport, PlVehicle (+116 more)
Cohesion: 0.07
Nodes (95): bool, Decimal, int, PlanCreateFull, PlPlan, PlPlanProduct, PlTransport, PlVehicle (+87 more)
### Community 76 - "Community 76"
Cohesion: 0.18
Nodes (10): Any, Request, str, str, parse_admin_request_json(), 运营 Web(/web/admin)JSON 请求体解析。 与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `{, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。 Returns:, 统一解析接口请求载荷(明文 / 密文信封)。 (+2 more)
### Community 77 - "Community 77"
Cohesion: 0.34
Nodes (23): DbUserContext, DbWeighingSlip, Session, str, weighing_to_read(), apply_product_snapshot_to_weighing(), _apply_weighing_product_update(), _apply_weighing_update() (+15 more)
Cohesion: 0.23
Nodes (29): DbUserContext, DbWeighingSlip, Session, str, Session, str, weighing_to_read(), apply_product_snapshot_to_weighing() (+21 more)
### Community 78 - "Community 78"
Cohesion: 0.13
Nodes (20): bool, date, Session, str, get_last_month_range(), month_range(), 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。 - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收) - 遍历所有租户,按上月计划模板生 (+12 more)
Cohesion: 0.19
Nodes (14): bool, date, Session, str, get_last_month_range(), month_range(), 地磅计划按月自动生成 — 迁移自 simple-dibang-xiaoshou/auto-create-plan/auto_create_monthly_pla, 按月自动生成采购计划。 - 默认仅在每月 1 日执行(force=True 时跳过该限制,供手动触发验收) - 遍历所有租户,按上月计划模板生 (+6 more)
### Community 79 - "Community 79"
Cohesion: 0.36
Nodes (10): add_scope_before_order(), ensure_import(), patch_base_data(), patch_get_helper(), patch_inventory(), patch_plan(), patch_transport(), str (+2 more)
### Community 80 - "Community 80"
Cohesion: 0.05
Nodes (62): str, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody, ContractApprovalExportBody, ContractApprovalListBody, ContractApprovalListQuery (+54 more)
Cohesion: 0.07
Nodes (46): CRM 合同 API 路由 — GET/PUT/DELETE/POST 对齐原 Java CRM, CRM 合同 Pydantic Schemas — 对照 CrmContractBo / CrmContractVo, ContractApprovalActionBody, ContractApprovalApproveBody, ContractApprovalByContractBody, ContractApprovalExportBody, ContractApprovalListBody, ContractApprovalListQuery (+38 more)
### Community 81 - "Community 81"
Cohesion: 0.25
Nodes (8): 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位序号}
Cohesion: 0.41
Nodes (13): BxUserContext, Session, str, UploadFile, bind_biz(), delete_attachment(), download_file(), list_by_biz() (+5 more)
### Community 82 - "Community 82"
Cohesion: 0.29
@@ -523,28 +513,28 @@ Cohesion: 0.41
Nodes (11): Any, str, build_quote_html(), _default_value(), _format_date(), _format_discount(), _format_money(), generate_quote_pdf() (+3 more)
### Community 85 - "Community 85"
Cohesion: 0.26
Nodes (49): InboundOutboundRead, bool, DbPurchasePlan, DbUserContext, PurchasePlanRead, PurchasePlanWithFillLink, Session, str (+41 more)
Cohesion: 0.23
Nodes (47): InboundOutboundRead, bool, DbPurchasePlan, DbUserContext, PurchasePlanRead, PurchasePlanWithFillLink, WeighingSlipRead, bool (+39 more)
### Community 86 - "Community 86"
Cohesion: 0.18
Nodes (11): float, int, str, 发送订单状态通知 Args: openid: 用户openid order_n, 发送支付成功通知 Args: openid: 用户openid order_n, 发送配送通知 Args: openid: 配送员openid task_no:, 发送配送时效预警 Args: openid: 配送员openid task_n, 发送退款结果通知 Args: openid: 用户openid order_n (+3 more)
Cohesion: 0.30
Nodes (13): Any, bool, bytes, int, str, ApiCryptoError, _b64url_decode(), _b64url_encode() (+5 more)
### Community 87 - "Community 87"
Cohesion: 0.22
Nodes (12): Any, bool, str, WfProcessInstance, _LocalCallbackFn, _dispatch_local(), 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。 (+4 more)
Cohesion: 0.19
Nodes (38): ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, StartInstanceBody, Any, bool, WfProcessInstance, bool (+30 more)
### Community 88 - "Community 88"
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)
Cohesion: 0.25
Nodes (11): check_customer_protect_time(), _execute_single_rule(), _get_all_tenant_nos(), pool_recovery_job(), 公海池自动回收任务(由 APScheduler 调度) 遍历所有租户的启用规则,执行自动回收。, 每天凌晨1点:检查保护期到期客户,跟进次数不足则退回公海, 每天早上9点:保护期到期提醒(3天内到期且跟进不足), remind_customer_recycle() (+3 more)
### Community 89 - "Community 89"
Cohesion: 0.14
Nodes (14): _build_category_tree(), _collect_category_with_ancestors(), list_category_tree(), 获取产品分类树(嵌套 children,供树形表格/树选择器使用), 获取产品分类树(嵌套 children,供树形表格/树选择器使用), 获取产品分类树(嵌套 children,供树形表格/树选择器使用), 按 parentId 将扁平分类列表组装为嵌套树, 获取产品分类树(嵌套 children,供树形表格/树选择器使用) (+6 more)
Cohesion: 0.20
Nodes (6): int, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名下载 URL,携带原始文件名。, MinIO 预签名内联预览 URL(PDF 等 iframe 场景)。, 带下载文件名的访问 URL;默认实现为普通访问地址。, 浏览器内联预览 URL;默认实现为普通访问地址。
### Community 90 - "Community 90"
Cohesion: 0.22
Nodes (18): Any, bool, bytes, int, Request, str, _claims_from_auth(), _format_body_bytes() (+10 more)
Cohesion: 0.16
Nodes (26): Any, bool, bytes, int, Request, str, bool, int (+18 more)
### Community 91 - "Community 91"
Cohesion: 0.32
@@ -555,8 +545,8 @@ Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_dict_type(), dict_type_code_exists(), get_dict_type() (+8 more)
### Community 93 - "Community 93"
Cohesion: 0.15
Nodes (22): int, Session, str, bool, datetime, DbWeighingSlip, Session, str (+14 more)
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 94 - "Community 94"
Cohesion: 0.18
@@ -571,112 +561,80 @@ Cohesion: 0.34
Nodes (16): Any, bool, int, Session, str, create_tenant(), get_tenant(), list_tenants() (+8 more)
### Community 97 - "Community 97"
Cohesion: 0.24
Nodes (27): Any, bool, CRM 公海池规则 CRUD + 规则执行逻辑, int, Session, str, create_pool_rule(), create_rule() (+19 more)
Cohesion: 0.21
Nodes (30): pool_rule_toggle(), Any, bool, CRM 公海池规则 CRUD + 规则执行逻辑, int, Session, str, create_pool_rule() (+22 more)
### Community 98 - "Community 98"
Cohesion: 0.38
Nodes (15): Any, bool, int, Session, str, create_post(), get_post(), list_posts() (+7 more)
### Community 99 - "Community 99"
Cohesion: 0.21
Nodes (9): API 路由包 — 山峡化工生产一体化门户, get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单), operlog_detail(), operlog_list(), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info() (+1 more)
Cohesion: 0.07
Nodes (29): BaseModel, AdvanceApplyOut, AttachmentOut, ReimburseApplyCreate, Config, PageResult, ResponseModel, ContactByCustomerBody (+21 more)
### Community 100 - "Community 100"
Cohesion: 0.45
Nodes (12): bool, date, PlCarrier, str, assert_carrier_eligible_for_transport(), carrier_agreement_expired(), carrier_audit_approved(), carrier_eligible_for_transport() (+4 more)
Cohesion: 0.20
Nodes (10): create_from_quote(), _quote_products_to_contract_products(), Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id (+2 more)
### Community 101 - "Community 101"
Cohesion: 0.40
Nodes (3): BaseSettings, Config, Settings
### Community 102 - "Community 102"
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)
Cohesion: 0.53
Nodes (11): Any, Session, str, _apply_create_range(), _base_query(), get_clue_level_statistics(), get_clue_overview_statistics(), get_clue_owner_statistics() (+3 more)
### Community 103 - "Community 103"
Cohesion: 0.08
Nodes (158): date, DbUserContext, int, PlanCreateFull, PlPlan, Session, str, bool (+150 more)
Cohesion: 0.07
Nodes (141): bool, PlCarrier, PlVehicle, VehicleCreate, VehicleRead, VehicleUpdate, bool, DbUserContext (+133 more)
### Community 104 - "Community 104"
Cohesion: 0.13
Nodes (38): bool, Redis, Request, str, _T, bool, Redis, decode_token() (+30 more)
Cohesion: 0.11
Nodes (49): 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), refresh_token(), bool, Redis, Request, Session, str, _T (+41 more)
### Community 105 - "Community 105"
Cohesion: 0.38
Nodes (15): Any, bool, int, Session, str, create_site(), get_site(), list_sites() (+7 more)
### Community 106 - "Community 106"
Cohesion: 0.13
Nodes (15): patch_plan_product_certs(), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输), 更新计划危化品产品购买证/运输证(不触发安排运输) (+7 more)
### Community 107 - "Community 107"
Cohesion: 0.22
Nodes (9): 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 (+1 more)
### Community 108 - "Community 108"
Cohesion: 0.44
Nodes (8): bool, int, Session, str, ApiRequestLogCRUD, SysApiRequestLog, SysApiRequestLogHis, SysApiRequestLog
### Community 109 - "Community 109"
Cohesion: 0.22
Nodes (9): Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签, Java: renewContract — 基础续签 (+1 more)
### Community 111 - "Community 111"
Cohesion: 0.07
Nodes (27): Session, Any, Session, str, Session, str, get_current_user(), get_current_user_by_token() (+19 more)
### Community 112 - "Community 112"
Cohesion: 0.17
Nodes (12): generate_product_no(), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001), 生成产品编号(格式:CP + yyyyMMdd + 3位序号,如 CP20260530001) (+4 more)
### Community 122 - "Community 122"
Cohesion: 0.40
Nodes (10): SysOss, int, Session, str, SysOss, create_oss(), create_oss(), get_oss_by_url() (+2 more)
### Community 123 - "Community 123"
Cohesion: 0.33
Nodes (8): Decimal, str, derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断; 若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
### Community 124 - "Community 124"
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 分离。
### Community 107 - "Community 107"
Cohesion: 0.34
Nodes (25): BxUserContext, Session, str, ApiResponse, approve_reimburse(), approved_by_me(), _build_details_data(), create_reimburse() (+17 more)
### Community 108 - "Community 108"
Cohesion: 0.40
Nodes (3): API 路由包 — 山峡化工生产一体化门户, get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,14 个一级菜单)
### Community 111 - "Community 111"
Cohesion: 0.25
Nodes (7): Any, Session, str, get_db_current_user_optional(), 与门户 JWT / apply_data_scope 所需的 user_info 字段对齐。, 有 Bearer 且为 admin/super_admin 时返回上下文,否则 None(供三方 H5 无登录调用)。, 有 Bearer 且为 admin/super_admin 时返回上下文,否则 None(供三方 H5 无登录调用)。
### Community 125 - "Community 125"
Cohesion: 0.23
Nodes (10): Any, bool, int, Session, str, camel_to_snake(), CRUDBase, get_model_attr() (+2 more)
### Community 126 - "Community 126"
Cohesion: 0.22
Nodes (9): _category_row_to_dict(), list_categories(), 分类 ORM 对象转 dict(camelCase 键名), 分类 ORM 对象转 dict(camelCase 键名), 分类 ORM 对象转 dict(camelCase 键名), 分类 ORM 对象转 dict(camelCase 键名), 获取分类(扁平列表,支持名称模糊与状态筛选), 获取分类(扁平列表,支持名称模糊与状态筛选) (+1 more)
Nodes (11): Any, bool, int, Session, str, generate_id(), camel_to_snake(), CRUDBase (+3 more)
### Community 127 - "Community 127"
Cohesion: 0.29
Nodes (10): datetime, int, Session, str, BizNoKind, allocate_next_biz_no(), _format_biz_no(), _prefix_for() (+2 more)
### Community 128 - "Community 128"
Cohesion: 0.20
Nodes (10): create_from_quote(), _quote_products_to_contract_products(), Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id, Java: createFromQuote — 返回合同 id (+2 more)
### Community 129 - "Community 129"
Cohesion: 0.22
Nodes (9): generate_barcode(), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001), 生成唯一条码(纯数字:yyyyMMdd + 6位序号,如 20260606000001) (+1 more)
Nodes (12): Any, bool, str, WfProcessInstance, _LocalCallbackFn, _dispatch_local(), 工作流回调服务 —— 流程状态变更时通知业务系统, 注册本地回调函数。当 callback_url 以 path 开头时直接调用 fn。 (+4 more)
### Community 130 - "Community 130"
Cohesion: 0.10
Nodes (32): _avatar_url_field(), _build_user_info_payload(), get_current_user_profile(), get_web_auth_menu_codes(), _jwt_user_payload(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/shanxia2.0.sql 基线), 获取当前用户信息(与 sys_user 字段对应)。, 获取当前用户信息(与 sys_user 字段对应)。 (+24 more)
Cohesion: 0.08
Nodes (30): _avatar_url_field(), _build_user_info_payload(), get_current_user_profile(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。 (+22 more)
### Community 131 - "Community 131"
Cohesion: 0.11
Nodes (33): bool, Session, str, _append_user_scope_conditions(), apply_data_scope(), assert_row_in_data_scope(), _collect_scope_ids(), _get_child_dept_ids() (+25 more)
Cohesion: 0.08
Nodes (43): bool, Session, str, Any, int, Session, str, _append_user_scope_conditions() (+35 more)
### Community 132 - "Community 132"
Cohesion: 0.40
Nodes (5): bool, str, get_password_hash(), 验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错), verify_password()
### Community 133 - "Community 133"
Cohesion: 0.32
Nodes (5): AdvanceApplyCreate, AdvanceApplyOut, AdvanceApplyUpdate, AdvanceApprovalCreate, _coerce_optional_id()
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 分离。
### Community 134 - "Community 134"
Cohesion: 0.38
@@ -687,32 +645,24 @@ Cohesion: 0.25
Nodes (7): 1. 上传到服务器, 2. 服务器首次启动, 3. 日常运维, 4. 配置文件, 5. 与 Nginx 配合, 6. 常见问题, shanxia2.0 Backend 生产部署说明
### Community 136 - "Community 136"
Cohesion: 0.29
Nodes (7): _product_row_to_dict(), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名), 产品 ORM 对象转 dict(camelCase 键名)
### Community 137 - "Community 137"
Cohesion: 0.13
Nodes (15): _generate_plan_no(), 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD######, 生成计划编号 SXDDHYYYYMMDD###### (+7 more)
Cohesion: 0.50
Nodes (4): 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 138 - "Community 138"
Cohesion: 0.39
Nodes (6): _inject_user_info_after_tenant(), patch_contract_routes(), patch_quote_routes(), patch_route_file(), Path, str
### Community 139 - "Community 139"
Cohesion: 0.20
Nodes (10): 解析司机信息:优先 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。 (+2 more)
### Community 140 - "Community 140"
Cohesion: 0.83
Nodes (3): _add_user_info_kwarg(), patch_file(), str
### Community 141 - "Community 141"
Cohesion: 0.29
Nodes (7): get_third_party_plan_page(), 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。, 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。, 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。, 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。, 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。, 三方 H5 填写页初始化数据(无需门户登录,与原系统一致)。
Cohesion: 0.25
Nodes (8): 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位序号}
### Community 143 - "Community 143"
Cohesion: 0.32
Nodes (22): Any, CrmOpportunity, Decimal, int, Session, str, batch_add_opportunity_products(), _calc_subtotal() (+14 more)
Cohesion: 0.18
Nodes (31): Any, CrmOpportunity, Decimal, int, Session, str, create_from_opportunity(), _opportunity_products_to_contract_products() (+23 more)
### Community 144 - "Community 144"
Cohesion: 0.17
@@ -722,37 +672,37 @@ Nodes (20): Any, int, str, get_oper_log_meta(), OperLogMeta, 请求级操作日
Cohesion: 0.33
Nodes (3): 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。, 地磅(db)Pydantic schemas — 对齐原 simple-dibang-xiaoshou 契约。, 地磅 API 路由聚合 — REST 风格,协议对齐原 simple-dibang-xiaoshou。
### Community 146 - "Community 146"
Cohesion: 0.47
Nodes (5): OperLogDetailBody, OperLogListBody, 操作日志(兼容 RuoYi OperLog 视图), OperLogDetailBody, OperLogListBody
### Community 147 - "Community 147"
Cohesion: 0.18
Nodes (26): Any, bool, int, Session, str, Session, str, _build_tree() (+18 more)
### Community 149 - "Community 149"
Cohesion: 0.13
Nodes (19): operlog_detail(), operlog_list(), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), 操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构), _user_info(), Session, _auto_fill_create_dept_id(), get_db() (+11 more)
### Community 150 - "Community 150"
Cohesion: 0.33
Nodes (3): _coerce_id(), OffsetItem, ReimburseDetailCreate
### Community 151 - "Community 151"
Cohesion: 0.50
Nodes (4): datetime, object, parse_optional_plan_datetime(), 运输计划相关日期时间解析:前端 date-picker 仅传 YYYY-MM-DD。
## Knowledge Gaps
- **69 isolated node(s):** `Session`, `Request`, `bool`, `bool`, `Config` (+64 more)
- **70 isolated node(s):** `Session`, `Request`, `bool`, `bool`, `Config` (+65 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **2 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `Session` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 27`, `Community 28`, `Community 29`, `Community 31`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 40`, `Community 41`, `Community 44`, `Community 45`, `Community 46`, `Community 47`, `Community 48`, `Community 49`, `Community 52`, `Community 54`, `Community 55`, `Community 56`, `Community 57`, `Community 59`, `Community 60`, `Community 61`, `Community 63`, `Community 65`, `Community 66`, `Community 67`, `Community 69`, `Community 71`, `Community 73`, `Community 74`, `Community 75`, `Community 77`, `Community 78`, `Community 81`, `Community 85`, `Community 88`, `Community 89`, `Community 92`, `Community 93`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 99`, `Community 102`, `Community 103`, `Community 105`, `Community 107`, `Community 108`, `Community 109`, `Community 111`, `Community 112`, `Community 122`, `Community 125`, `Community 126`, `Community 127`, `Community 128`, `Community 129`, `Community 130`, `Community 131`, `Community 137`, `Community 141`, `Community 143`, `Community 146`, `Community 147`?**
_High betweenness centrality (0.204) - this node is a cross-community bridge._
- **Why does `BaseModel` connect `Community 80` to `Community 0`, `Community 2`, `Community 133`, `Community 134`, `Community 7`, `Community 9`, `Community 11`, `Community 12`, `Community 146`, `Community 19`, `Community 148`, `Community 21`, `Community 20`, `Community 23`, `Community 150`, `Community 25`, `Community 24`, `Community 28`, `Community 30`, `Community 32`, `Community 44`, `Community 45`, `Community 47`, `Community 50`, `Community 51`, `Community 62`, `Community 69`, `Community 71`, `Community 73`, `Community 75`, `Community 77`, `Community 85`, `Community 103`, `Community 124`?**
_High betweenness centrality (0.067) - this node is a cross-community bridge._
- **Why does `datetime` connect `Community 39` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 143`, `Community 16`, `Community 17`, `Community 15`, `Community 147`, `Community 148`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 27`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 38`, `Community 40`, `Community 41`, `Community 43`, `Community 44`, `Community 47`, `Community 49`, `Community 50`, `Community 51`, `Community 52`, `Community 54`, `Community 55`, `Community 56`, `Community 59`, `Community 60`, `Community 61`, `Community 62`, `Community 67`, `Community 68`, `Community 69`, `Community 75`, `Community 77`, `Community 78`, `Community 80`, `Community 84`, `Community 85`, `Community 92`, `Community 93`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 100`, `Community 102`, `Community 103`, `Community 104`, `Community 105`, `Community 108`, `Community 124`, `Community 127`?**
_High betweenness centrality (0.058) - this node is a cross-community bridge._
- **Why does `Session` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 6`, `Community 8`, `Community 9`, `Community 10`, `Community 11`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 22`, `Community 23`, `Community 25`, `Community 27`, `Community 28`, `Community 31`, `Community 33`, `Community 35`, `Community 36`, `Community 37`, `Community 40`, `Community 41`, `Community 43`, `Community 44`, `Community 45`, `Community 46`, `Community 48`, `Community 49`, `Community 52`, `Community 54`, `Community 55`, `Community 56`, `Community 57`, `Community 59`, `Community 60`, `Community 61`, `Community 63`, `Community 65`, `Community 66`, `Community 67`, `Community 69`, `Community 71`, `Community 73`, `Community 74`, `Community 77`, `Community 78`, `Community 81`, `Community 85`, `Community 87`, `Community 90`, `Community 92`, `Community 93`, `Community 95`, `Community 96`, `Community 97`, `Community 98`, `Community 99`, `Community 100`, `Community 102`, `Community 103`, `Community 104`, `Community 105`, `Community 107`, `Community 111`, `Community 125`, `Community 127`, `Community 130`, `Community 131`, `Community 141`, `Community 143`, `Community 149`?**
_High betweenness centrality (0.217) - this node is a cross-community bridge._
- **Why does `BaseModel` connect `Community 50` to `Community 0`, `Community 2`, `Community 132`, `Community 134`, `Community 7`, `Community 11`, `Community 12`, `Community 15`, `Community 19`, `Community 148`, `Community 20`, `Community 150`, `Community 23`, `Community 149`, `Community 21`, `Community 25`, `Community 24`, `Community 29`, `Community 30`, `Community 32`, `Community 35`, `Community 44`, `Community 45`, `Community 48`, `Community 61`, `Community 62`, `Community 68`, `Community 69`, `Community 71`, `Community 73`, `Community 75`, `Community 77`, `Community 80`, `Community 85`, `Community 87`, `Community 99`, `Community 103`, `Community 107`?**
_High betweenness centrality (0.074) - this node is a cross-community bridge._
- **Why does `SysUser` connect `Community 87` to `Community 130`, `Community 131`, `Community 2`, `Community 3`, `Community 6`, `Community 8`, `Community 10`, `Community 12`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 22`, `Community 23`, `Community 24`, `Community 27`, `Community 32`, `Community 33`, `Community 35`, `Community 36`, `Community 39`, `Community 43`, `Community 52`, `Community 56`, `Community 69`, `Community 73`, `Community 90`, `Community 103`, `Community 104`?**
_High betweenness centrality (0.057) - this node is a cross-community bridge._
- **Are the 251 inferred relationships involving `str` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
_`str` has 251 INFERRED edges - model-reasoned connections that need verification._
- **Are the 248 inferred relationships involving `Session` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
_`Session` has 248 INFERRED edges - model-reasoned connections that need verification._
- **Are the 140 inferred relationships involving `int` (e.g. with `AdvanceApplyCreate` and `AdvanceApplyOut`) actually correct?**
_`int` has 140 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?**
_1298 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Are the 28 inferred relationships involving `get_tenant_no()` (e.g. with `category_tree()` and `contact_create()`) actually correct?**
_`get_tenant_no()` has 28 INFERRED edges - model-reasoned connections that need verification._
+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":1953,"mtime_ns":1780805101147405418,"hash":"d2a416ff9daedad88780cbe3f14ea75e8299044f8787afb60e731f43963dc405"},"/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":6698,"mtime_ns":1782790876685112837,"hash":"9d955a6baa70f78ddfcf29407d9711915a04e59a7f616254d06d25d25e8d0327"},"/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":1953,"mtime_ns":1780805101147405418,"hash":"d2a416ff9daedad88780cbe3f14ea75e8299044f8787afb60e731f43963dc405"},"/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":6698,"mtime_ns":1782790876685112837,"hash":"9d955a6baa70f78ddfcf29407d9711915a04e59a7f616254d06d25d25e8d0327"},"/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
+36
View File
@@ -2766,9 +2766,13 @@ CREATE TABLE `pl_plan` (
`settlement_method` varchar(50) DEFAULT NULL COMMENT '结算方式',
`has_freight` smallint(6) NOT NULL DEFAULT '0' COMMENT '是否有运输费用 0-否 1-是',
`pickup_address` varchar(500) DEFAULT NULL COMMENT '取货地址',
`pickup_lng` decimal(10,6) DEFAULT NULL COMMENT '取货经度',
`pickup_lat` decimal(10,6) DEFAULT NULL COMMENT '取货纬度',
`pickup_time` datetime DEFAULT NULL COMMENT '取货时间',
`pickup_notes` varchar(500) DEFAULT NULL COMMENT '取货注意事项',
`destination_address` varchar(500) DEFAULT NULL COMMENT '目的地地址',
`destination_lng` decimal(10,6) DEFAULT NULL COMMENT '送货经度',
`destination_lat` decimal(10,6) DEFAULT NULL COMMENT '送货纬度',
`delivery_time` datetime DEFAULT NULL COMMENT '送达时间',
`destination_notes` varchar(500) DEFAULT NULL COMMENT '目的地注意事项',
`transport_id` varchar(64) DEFAULT NULL COMMENT '运输ID',
@@ -3041,6 +3045,38 @@ INSERT INTO `pl_remittance_voucher` (`id`, `settlement_id`, `voucher_no`, `remit
INSERT INTO `pl_remittance_voucher` (`id`, `settlement_id`, `voucher_no`, `remittance_date`, `remittance_amount`, `voucher_urls`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('8BCB0652B9414F7EAD6EA2A4E752ECF3', '73854CC0B7B64AFE8C983120551420BB', NULL, '2026-06-28', 306.00, '[\"pl/freight-voucher/202606/28/58a088fd1641474c856a3dc71c0397ae.jpeg\"]', 'shanxia2.0', NULL, '123', 'd3000000000000000000000000000001', 'b13c2be2-4e71-11f1-9f47-00163e3bdd96', '2026-06-28 17:04:52', NULL, '2026-06-28 17:04:52', 0);
COMMIT;
-- ----------------------------
-- Table structure for pl_transport_address
-- ----------------------------
DROP TABLE IF EXISTS `pl_transport_address`;
CREATE TABLE `pl_transport_address` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`address_name` varchar(200) NOT NULL COMMENT '地址名称',
`full_address` varchar(500) NOT NULL COMMENT '详细地址',
`lng` decimal(10,6) NOT NULL COMMENT '经度',
`lat` decimal(10,6) NOT NULL COMMENT '纬度',
`contact_name` varchar(100) DEFAULT NULL COMMENT '联系人',
`contact_phone` varchar(32) DEFAULT NULL COMMENT '联系电话',
`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 '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_pl_transport_address_tenant` (`tenant_no`,`del_flag`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运输通用地址库';
-- ----------------------------
-- Records of pl_transport_address
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for pl_transport
-- ----------------------------
+80 -35
View File
@@ -280,7 +280,7 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/crm/contract.py": {
"mtime": 1782857094.9578602,
"mtime": 1782906332.877103,
"ast_hash": "6553f54ca1fd19086c170095f402a628",
"semantic_hash": ""
},
@@ -1725,8 +1725,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/env.d.ts": {
"mtime": 1782858528.2320914,
"ast_hash": "c9f9ecf49f80dcac1e408db4c1892ef8",
"mtime": 1782903782.0621061,
"ast_hash": "7490c67ebd4a28d850f0085e15e4eb26",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/shims-loose.d.ts": {
@@ -5835,18 +5835,18 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/models/pl_models.py": {
"mtime": 1782793455.702941,
"ast_hash": "6b8c4660d7028d7186267699d5ca3aa1",
"mtime": 1782906535.7235444,
"ast_hash": "e78c2b5826952efd2979f1a5d1fd93bb",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/plan.py": {
"mtime": 1782793472.1020458,
"ast_hash": "84fc6e5bd61edae63a7e23577d32c20b",
"mtime": 1782905365.6902194,
"ast_hash": "0f6ab4b0bf6aae653e35c93be4a9bb1c",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport.py": {
"mtime": 1782793487.671617,
"ast_hash": "4cf6d81e08cca8d6e92c43856ecf52e2",
"mtime": 1782905367.5290751,
"ast_hash": "e9b1480cdd11a2adb78247bd3d5b3b80",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/__init__.py": {
@@ -5865,18 +5865,18 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/plan.py": {
"mtime": 1782793517.0180697,
"ast_hash": "9167954b7475bdb07dad255a9348e6b1",
"mtime": 1782904185.1767316,
"ast_hash": "0d840b3017a9357fa63428b838b9c5b5",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport.py": {
"mtime": 1782793483.046418,
"ast_hash": "229a992f46ae31178bb20aa0834b60d5",
"mtime": 1782904202.7464836,
"ast_hash": "e75ce6bfce6df56f49e2a0cb4b678217",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/__init__.py": {
"mtime": 1782696220.8612864,
"ast_hash": "b5a4c54d5c2a73740d5dc29a59c62a89",
"mtime": 1782906547.2093558,
"ast_hash": "28dc4010ec275301fcb4a013b6855a50",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/freight.py": {
@@ -5895,8 +5895,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/api/pl/index.ts": {
"mtime": 1782858537.3636208,
"ast_hash": "c3c649ae2c94a034a0c57ee85bf8e4ec",
"mtime": 1782906561.7571106,
"ast_hash": "0f2965eae3756fb9045da589c832df80",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/transport/index.vue": {
@@ -5930,8 +5930,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/router/modules/pl.ts": {
"mtime": 1782858544.556459,
"ast_hash": "7d1ddf0f841328644a2c3734e9ff50fe",
"mtime": 1782906603.1698864,
"ast_hash": "1c1a94c7fdd3e4b1f93bbd77fdcd39c8",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/plan/arrange.vue": {
@@ -6030,8 +6030,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-form-dialog/index.vue": {
"mtime": 1782902572.0918643,
"ast_hash": "591a913bc32fa5d6d35e938609158451",
"mtime": 1782907228.400472,
"ast_hash": "ffa41946bfeaf3cd8dff3abbf585078c",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py": {
@@ -6100,13 +6100,13 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-arrange-dialog/index.vue": {
"mtime": 1782902592.068708,
"ast_hash": "3818b8e76ca8c7227fa79f0bbcbaa011",
"mtime": 1782906602.6501355,
"ast_hash": "e26aedc11d6ef73dae6ea8251ff78cce",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/public_transport.py": {
"mtime": 1782790898.570381,
"ast_hash": "fbbba3979eed1c05b47d9acc45c0ecb1",
"mtime": 1782904189.9898672,
"ast_hash": "39ff399ebeb91882700686c192780bb7",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/services/pl_dibang_bridge_service.py": {
@@ -6125,8 +6125,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-view-tabs/index.vue": {
"mtime": 1782902595.1814783,
"ast_hash": "0d2b0fb0b941d63b1ec42c70b4f77483",
"mtime": 1783001126.5947132,
"ast_hash": "c940f2b9f6c1b2a1678083b65ae38bbc",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/db/DbPoundSlipQrcodeDialog.vue": {
@@ -6135,13 +6135,13 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/api/pl/public-transport.ts": {
"mtime": 1782858537.3784862,
"ast_hash": "677f5eaaa05adb17442119694cd3aefc",
"mtime": 1782904310.288007,
"ast_hash": "42a82e7cc5bf345b537d35f6095c5b34",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/public/pl/transport/index.vue": {
"mtime": 1782858542.845052,
"ast_hash": "e8f8114d2972c7b1a388c8dd1d7f56b1",
"mtime": 1782904322.2179258,
"ast_hash": "de73ab2289eba99e7ccaadbbb5ed9390",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/layouts/PublicMobileShell.vue": {
@@ -6150,8 +6150,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/composables/usePlPlanArrangeActions.ts": {
"mtime": 1782902595.352905,
"ast_hash": "7bc4755be0a912766098ae7bebbc0c8f",
"mtime": 1782904305.6774886,
"ast_hash": "a640aac6bd737470877b0449125f5812",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/router/portal-menu.ts": {
@@ -6290,8 +6290,8 @@
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-plan-archive-dialog/index.vue": {
"mtime": 1782858535.4483042,
"ast_hash": "b1015cda352bbe0dea3fa29faac65561",
"mtime": 1783001120.0693388,
"ast_hash": "b7fa59a57d7261011227b147c250a84a",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/plan-archive/index.vue": {
@@ -6673,5 +6673,50 @@
"mtime": 1782865843.497152,
"ast_hash": "f33b59225e189655c09853759400b048",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/amap/loader.ts": {
"mtime": 1782915097.1272,
"ast_hash": "2a88741c1b8e29efe4049d40d44db119",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/amap-address-picker/index.vue": {
"mtime": 1782904217.7621434,
"ast_hash": "06a6495e30b80e357a6a11c7b2b7cca6",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/utils/amap/navigation.ts": {
"mtime": 1782904191.971898,
"ast_hash": "a1beb46fb9a717580c5bccec73dde5d9",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/datetime_parse.py": {
"mtime": 1782905360.7460208,
"ast_hash": "999484f6738d95159b3771ac05d57b4f",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/schemas/pl/transport_address.py": {
"mtime": 1782906539.7314954,
"ast_hash": "9e795527053f9ac5d2f4c4161782000c",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/backend/app/api/pl/transport_address.py": {
"mtime": 1782906547.114098,
"ast_hash": "465480e4c04d089602bd09ee8618bcf3",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-transport-address-select-dialog/index.vue": {
"mtime": 1782907507.8495402,
"ast_hash": "0f140b53c99c240dc8665e249bba168a",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/components/business/pl-address-with-library/index.vue": {
"mtime": 1782907231.5989144,
"ast_hash": "e3c51808508324c011fe3e069edfc646",
"semantic_hash": ""
},
"/Users/zlwu/work/00code/shanxia2.0/web-admin/src/views/pl/transport-address/index.vue": {
"mtime": 1782906655.9155624,
"ast_hash": "4ba8d6395ad9952db3868ff6f6a5df10",
"semantic_hash": ""
}
}
+3 -8
View File
@@ -703,33 +703,28 @@
"701": "Community 701",
"702": "Community 702",
"703": "Community 703",
"704": "Community 704",
"705": "Community 705",
"706": "Community 706",
"707": "Community 707",
"708": "Community 708",
"709": "Community 709",
"710": "Community 710",
"711": "Community 711",
"712": "Community 712",
"713": "Community 713",
"714": "Community 714",
"715": "Community 715",
"717": "Community 717",
"718": "Community 718",
"719": "Community 719",
"720": "Community 720",
"722": "Community 722",
"723": "Community 723",
"724": "Community 724",
"727": "Community 727",
"728": "Community 728",
"729": "Community 729",
"730": "Community 730",
"731": "Community 731",
"733": "Community 733",
"734": "Community 734",
"735": "Community 735",
"736": "Community 736",
"737": "Community 737",
"738": "Community 738",
"739": "Community 739",
"741": "Community 741"
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+58
View File
@@ -184,6 +184,52 @@ export const personnelApi = {
delete: (id: string) => request.delete(`/transport/personnel/${id}`)
}
// 运输通用地址库
export interface TransportAddress {
id: string
addressName: string
fullAddress: string
lng: number
lat: number
contactName?: string
contactPhone?: string
status: string
remark?: string
createTime: string
updateTime: string
}
export interface TransportAddressQuery {
q?: string
status?: string
page?: number
pageSize?: number
}
export interface TransportAddressPayload {
addressName: string
fullAddress: string
lng: number
lat: number
contactName?: string
contactPhone?: string
status?: string
remark?: string
}
export const transportAddressApi = {
list: (params?: TransportAddressQuery) =>
request.get<PlListResponse<TransportAddress>>('/transport/address', { params }),
listAll: (params?: { q?: string }) =>
request.get<PlListResponse<TransportAddress>>('/transport/address/all', { params }),
get: (id: string) => request.get<TransportAddress>(`/transport/address/${id}`),
create: (data: TransportAddressPayload) =>
request.post<TransportAddress>('/transport/address', data),
update: (id: string, data: Partial<TransportAddressPayload>) =>
request.put<TransportAddress>(`/transport/address/${id}`, data),
delete: (id: string) => request.delete(`/transport/address/${id}`)
}
// 司机(兼容旧 API,映射至 personnel)
export interface Driver {
id: string
@@ -325,9 +371,13 @@ export interface Plan {
settlementMethod?: string
hasFreight: number
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
pickupTime?: string
pickupNotes?: string
destinationAddress?: string
destinationLng?: number | null
destinationLat?: number | null
deliveryTime?: string
destinationNotes?: string
vehicleId?: string
@@ -403,9 +453,13 @@ export interface PlanCreateFullPayload {
settlementMethod?: string
hasFreight: number
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
pickupTime?: string
pickupNotes?: string
destinationAddress?: string
destinationLng?: number | null
destinationLat?: number | null
deliveryTime?: string
destinationNotes?: string
followUserId?: string
@@ -521,9 +575,13 @@ export interface TransportArrange {
carrierId?: string
transportMedium?: string
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
pickupTime?: string
pickupNotes?: string
destinationAddress?: string
destinationLng?: number | null
destinationLat?: number | null
deliveryTime?: string
destinationNotes?: string
distanceKm?: number
+4
View File
@@ -13,7 +13,11 @@ export interface PublicTransportPage {
planDate?: string
planType: string
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
destinationAddress?: string
destinationLng?: number | null
destinationLat?: number | null
licensePlate?: string
driverName?: string
driverPhone?: string
@@ -0,0 +1,427 @@
<template>
<div class="amap-address-picker">
<el-input
:id="inputId"
v-model="innerAddress"
:placeholder="placeholder"
:disabled="disabled"
clearable
@input="handleAddressInput"
>
<template #append>
<el-button :disabled="disabled" :loading="mapOpening" @click="openMapDialog">
<el-icon><Location /></el-icon>地图选点
</el-button>
</template>
</el-input>
<el-dialog
v-model="mapVisible"
title="地图选点"
width="760px"
align-center
destroy-on-close
append-to-body
class="dialog-form-layout amap-address-picker-dialog"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
style="backdrop-filter: none"
@opened="handleMapDialogOpened"
@closed="handleMapDialogClosed"
>
<div class="amap-address-picker-body">
<div class="amap-address-picker-search">
<el-input
:id="searchInputId"
v-model="searchKeyword"
placeholder="搜索地点、POI"
clearable
@keyup.enter="handleSearch"
>
<template #append>
<el-button :loading="searchLoading" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</template>
</el-input>
</div>
<div ref="mapContainerRef" class="amap-address-picker-map" />
<p v-if="pickedAddress" class="amap-address-picker-result">
已选:<span>{{ pickedAddress }}</span>
</p>
<p v-else class="amap-address-picker-hint">点击地图选点,或搜索后选择结果;可拖拽标记微调位置</p>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="mapVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button type="primary" :disabled="!pickedAddress" @click="confirmPick">
<el-icon><Check /></el-icon>确定
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { Check, Close, Location, Search } from '@element-plus/icons-vue'
import {
formatPoiAddress,
isAmapConfigured,
loadAmap,
loadAmapPlugins,
resolveLngLat,
type AMapAutoComplete,
type AMapGeocoder,
type AMapGlobal,
type AMapLngLat,
type AMapMap,
type AMapMarker,
type AMapPlaceSearch,
type AMapPoi
} from '@/utils/amap/loader'
export interface AmapAddressPickResult {
address: string
lng?: number
lat?: number
}
const props = withDefaults(
defineProps<{
modelValue?: string
lng?: number | null
lat?: number | null
placeholder?: string
disabled?: boolean
city?: string
}>(),
{
modelValue: '',
lng: null,
lat: null,
placeholder: '请输入或地图选点',
disabled: false,
city: ''
}
)
const emit = defineEmits<{
'update:modelValue': [value: string]
'update:lng': [value: number | null]
'update:lat': [value: number | null]
change: [payload: AmapAddressPickResult]
}>()
const inputId = `amap-address-input-${useId()}`
const searchInputId = `amap-address-search-${useId()}`
const innerAddress = ref(props.modelValue || '')
const mapVisible = ref(false)
const mapOpening = ref(false)
const searchKeyword = ref('')
const searchLoading = ref(false)
const pickedAddress = ref('')
const pickedLngLat = ref<[number, number] | null>(null)
const mapContainerRef = ref<HTMLDivElement>()
let amapRef: AMapGlobal | null = null
let mapInstance: AMapMap | null = null
let markerInstance: AMapMarker | null = null
let geocoder: AMapGeocoder | null = null
let placeSearch: AMapPlaceSearch | null = null
let autoComplete: AMapAutoComplete | null = null
let mainAutoComplete: AMapAutoComplete | null = null
watch(
() => props.modelValue,
value => {
if (value !== innerAddress.value) {
innerAddress.value = value || ''
}
}
)
onMounted(() => {
initMainAutoComplete()
})
onBeforeUnmount(() => {
destroyMainAutoComplete()
destroyMapResources()
})
function setCoordinates(lng?: number | null, lat?: number | null) {
const nextLng = lng ?? null
const nextLat = lat ?? null
emit('update:lng', nextLng)
emit('update:lat', nextLat)
}
function handleAddressInput(value: string) {
emit('update:modelValue', value)
setCoordinates(null, null)
emit('change', { address: value })
}
async function ensureAmapReady() {
if (!isAmapConfigured()) {
ElMessage.warning('未配置高德地图 Key,请在 .env.development.local 中设置 VITE_AMAP_KEY')
return false
}
try {
amapRef = await loadAmap()
await loadAmapPlugins()
return true
} catch (error) {
const message = error instanceof Error ? error.message : '高德地图加载失败'
ElMessage.error(message)
return false
}
}
function bindAutoComplete(
target: AMapAutoComplete | null,
onSelect: (poi?: AMapPoi) => void
) {
if (!target) return
const handler = (event: { poi?: AMapPoi }) => onSelect(event.poi)
target.on('select', handler)
}
async function initMainAutoComplete() {
const ready = await ensureAmapReady()
if (!ready || !amapRef) return
amapRef.plugin(['AMap.AutoComplete'], () => {
if (mainAutoComplete) return
mainAutoComplete = new amapRef!.AutoComplete({
input: inputId,
city: props.city || '全国',
citylimit: !!props.city
})
bindAutoComplete(mainAutoComplete, poi => {
const address = formatPoiAddress(poi)
if (!address) return
innerAddress.value = address
emit('update:modelValue', address)
if (poi?.location) {
const [lng, lat] = resolveLngLat(poi.location)
setCoordinates(lng, lat)
emit('change', { address, lng, lat })
} else {
setCoordinates(null, null)
emit('change', { address })
}
})
})
}
function destroyMainAutoComplete() {
mainAutoComplete = null
}
async function openMapDialog() {
if (props.disabled) return
mapOpening.value = true
try {
const ready = await ensureAmapReady()
if (!ready) return
pickedAddress.value = innerAddress.value || ''
pickedLngLat.value =
props.lng != null && props.lat != null ? [Number(props.lng), Number(props.lat)] : null
searchKeyword.value = innerAddress.value || ''
mapVisible.value = true
} finally {
mapOpening.value = false
}
}
async function handleMapDialogOpened() {
await nextTick()
await initMapPicker()
if (pickedLngLat.value) {
applyLngLat({ lng: pickedLngLat.value[0], lat: pickedLngLat.value[1] }, pickedAddress.value)
} else if (searchKeyword.value.trim()) {
handleSearch()
}
}
function handleMapDialogClosed() {
destroyMapResources()
}
async function initMapPicker() {
if (!amapRef || !mapContainerRef.value) return
destroyMapResources()
mapInstance = new amapRef.Map(mapContainerRef.value, {
zoom: 12,
viewMode: '2D'
})
geocoder = new amapRef.Geocoder()
placeSearch = new amapRef.PlaceSearch({
map: mapInstance,
pageSize: 8,
city: props.city || '全国',
citylimit: !!props.city
})
autoComplete = new amapRef.AutoComplete({
input: searchInputId,
city: props.city || '全国',
citylimit: !!props.city
})
bindAutoComplete(autoComplete, poi => {
if (!poi?.location) return
applyPoiSelection(poi)
})
mapInstance.on('click', event => {
applyLngLat(event.lnglat)
})
window.setTimeout(() => {
mapInstance?.resize()
}, 120)
}
function applyPoiSelection(poi: AMapPoi) {
if (!poi.location) return
applyLngLat(poi.location, formatPoiAddress(poi))
}
function applyLngLat(lnglat: AMapLngLat, presetAddress?: string) {
if (!amapRef || !mapInstance) return
const position = resolveLngLat(lnglat)
pickedLngLat.value = position
if (!markerInstance) {
markerInstance = new amapRef.Marker({
position,
draggable: true
})
markerInstance.on('dragend', () => {
const pos = markerInstance?.getPosition()
if (pos) reverseGeocode(pos)
})
mapInstance.add(markerInstance)
} else {
markerInstance.setPosition(position)
}
mapInstance.setCenter(position)
mapInstance.setZoom(16)
if (presetAddress) {
pickedAddress.value = presetAddress
return
}
reverseGeocode(lnglat)
}
function reverseGeocode(lnglat: AMapLngLat) {
if (!geocoder) return
geocoder.getAddress(lnglat, (status, result) => {
if (status !== 'complete') return
const address = result.regeocode?.formattedAddress?.trim()
if (address) pickedAddress.value = address
})
}
function handleSearch() {
const keyword = searchKeyword.value.trim()
if (!keyword) {
ElMessage.warning('请输入搜索关键词')
return
}
if (!placeSearch) return
searchLoading.value = true
placeSearch.search(keyword, (status, result) => {
searchLoading.value = false
if (status !== 'complete') {
ElMessage.warning('未找到相关地点')
return
}
const poi = result.poiList?.pois?.[0]
if (!poi) {
ElMessage.warning('未找到相关地点')
return
}
applyPoiSelection(poi)
})
}
function confirmPick() {
if (!pickedAddress.value) return
innerAddress.value = pickedAddress.value
emit('update:modelValue', pickedAddress.value)
const payload: AmapAddressPickResult = { address: pickedAddress.value }
if (pickedLngLat.value) {
payload.lng = pickedLngLat.value[0]
payload.lat = pickedLngLat.value[1]
setCoordinates(payload.lng, payload.lat)
} else {
setCoordinates(null, null)
}
emit('change', payload)
mapVisible.value = false
}
function destroyMapResources() {
autoComplete = null
placeSearch = null
geocoder = null
markerInstance = null
if (mapInstance) {
mapInstance.destroy()
mapInstance = null
}
}
</script>
<style scoped lang="scss">
.amap-address-picker {
width: 100%;
}
.amap-address-picker-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.amap-address-picker-map {
width: 100%;
height: 420px;
border: 1px solid var(--border, #d9e8e2);
border-radius: 8px;
overflow: hidden;
}
.amap-address-picker-result,
.amap-address-picker-hint {
margin: 0;
font-size: 13px;
color: var(--text-muted, #64748b);
line-height: 1.5;
span {
color: var(--text, #1e293b);
}
}
</style>
<style lang="scss">
.el-dialog.amap-address-picker-dialog.dialog-form-layout {
width: min(760px, calc(100vw - 80px)) !important;
}
</style>
@@ -0,0 +1,89 @@
<template>
<div class="pl-address-with-library">
<div class="pl-address-with-library__row">
<AmapAddressPicker
v-model="innerAddress"
v-model:lng="innerLng"
v-model:lat="innerLat"
class="pl-address-with-library__picker"
:placeholder="placeholder"
:disabled="disabled"
/>
<el-button
class="pl-address-with-library__library-btn btn-ghost"
:disabled="disabled"
@click="libraryVisible = true"
>
<el-icon><FolderOpened /></el-icon>从地址库选择
</el-button>
</div>
<PlTransportAddressSelectDialog v-model="libraryVisible" @select="handleLibrarySelect" />
</div>
</template>
<script setup lang="ts">
import { FolderOpened } from '@element-plus/icons-vue'
import AmapAddressPicker from '@/components/business/amap-address-picker/index.vue'
import PlTransportAddressSelectDialog from '@/components/business/pl-transport-address-select-dialog/index.vue'
import type { TransportAddress } from '@/api/pl/index'
const props = defineProps<{
modelValue: string
lng?: number | null
lat?: number | null
placeholder?: string
disabled?: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
'update:lng': [value: number | null]
'update:lat': [value: number | null]
}>()
const libraryVisible = ref(false)
const innerAddress = computed({
get: () => props.modelValue,
set: (value: string) => emit('update:modelValue', value)
})
const innerLng = computed({
get: () => props.lng ?? null,
set: (value: number | null) => emit('update:lng', value)
})
const innerLat = computed({
get: () => props.lat ?? null,
set: (value: number | null) => emit('update:lat', value)
})
function handleLibrarySelect(row: TransportAddress) {
innerAddress.value = row.fullAddress
innerLng.value = row.lng != null ? Number(row.lng) : null
innerLat.value = row.lat != null ? Number(row.lat) : null
}
</script>
<style scoped lang="scss">
.pl-address-with-library {
width: 100%;
}
.pl-address-with-library__row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.pl-address-with-library__picker {
flex: 1 1 auto;
min-width: 0;
}
.pl-address-with-library__library-btn {
flex: 0 0 auto;
white-space: nowrap;
}
</style>
@@ -25,61 +25,64 @@
/>
</section>
<section class="pl-plan-form-section">
<section class="pl-plan-form-section pl-plan-freight-section">
<h4 class="pl-plan-subtitle">运费信息</h4>
<div class="form-grid-4">
<el-form-item label="是否有运输费用">
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
</el-form-item>
<template v-if="formData.hasFreight === 1">
<el-form-item label="运输公里数">
<el-input-number
v-model="formData.distanceKm"
:min="0"
:precision="2"
style="width: 100%"
/>
<el-form
:model="formData"
label-width="96px"
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" />
</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"
style="width: 100%"
@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"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="运费金额(元)">
<el-input-number
v-model="freightCalc.freightAmount.value"
:min="0"
:precision="2"
style="width: 100%"
@change="freightCalc.markFreightAmountTouched()"
/>
</el-form-item>
</template>
</div>
<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>
</div>
</section>
@@ -384,10 +387,32 @@ async function handleSubmit() {
margin-top: 0;
}
.pl-arrange-freight-muted {
margin-bottom: 12px;
font-size: 13px;
color: #64748b;
.pl-plan-freight-form {
:deep(.el-form-item) {
margin-bottom: 0;
align-items: center;
}
:deep(.el-form-item__label) {
height: auto !important;
line-height: 1.4 !important;
padding: 0 8px 0 0 !important;
justify-content: flex-end;
}
:deep(.el-form-item__content) {
flex: 1;
min-width: 0;
}
.form-grid-4 {
align-items: center;
}
:deep(.el-select),
:deep(.el-input-number) {
width: 100%;
}
}
.pl-arrange-footer {
@@ -396,6 +421,7 @@ async function handleSubmit() {
gap: 8px;
justify-content: flex-end;
}
</style>
<style lang="scss">
@@ -132,7 +132,12 @@
<h4 class="pl-plan-subtitle">取货信息</h4>
<div class="form-grid-3">
<el-form-item label="取货地址">
<el-input v-model="formData.pickupAddress" placeholder="取货地址" />
<PlAddressWithLibrary
v-model="formData.pickupAddress"
v-model:lng="formData.pickupLng"
v-model:lat="formData.pickupLat"
placeholder="取货地址"
/>
</el-form-item>
<el-form-item label="取货时间">
<el-date-picker
@@ -174,7 +179,12 @@
<h4 class="pl-plan-subtitle">送货信息</h4>
<div class="form-grid-3">
<el-form-item label="送货地址">
<el-input v-model="formData.destinationAddress" placeholder="送货地址" />
<PlAddressWithLibrary
v-model="formData.destinationAddress"
v-model:lng="formData.destinationLng"
v-model:lat="formData.destinationLat"
placeholder="送货地址"
/>
</el-form-item>
<el-form-item label="送达时间">
<el-date-picker
@@ -315,7 +325,11 @@
@click="
copyPlanSummary({
pickupAddress: formData.pickupAddress,
pickupLng: formData.pickupLng,
pickupLat: formData.pickupLat,
destinationAddress: formData.destinationAddress,
destinationLng: formData.destinationLng,
destinationLat: formData.destinationLat,
pickupNotes: formData.pickupNotes,
destinationNotes: formData.destinationNotes,
licensePlate: vehicleList.find(v => v.id === formData.vehicleId)?.licensePlate,
@@ -374,6 +388,7 @@ import {
type VehiclePersonnelRow
} from '@/api/pl'
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
import PlAddressWithLibrary from '@/components/business/pl-address-with-library/index.vue'
import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
@@ -449,9 +464,13 @@ const formData = reactive({
driverId: '',
carrierId: '',
pickupAddress: '',
pickupLng: null as number | null,
pickupLat: null as number | null,
pickupTime: '',
pickupNotes: '',
destinationAddress: '',
destinationLng: null as number | null,
destinationLat: null as number | null,
deliveryTime: '',
destinationNotes: '',
hasFreight: 0,
@@ -581,9 +600,13 @@ function applyVehiclePersonnel(personnelList: VehiclePersonnelRow[], preferredDr
async function applyPrefillFromPlan(data: Plan) {
formData.pickupAddress = data.pickupAddress || ''
formData.pickupLng = data.pickupLng != null ? Number(data.pickupLng) : null
formData.pickupLat = data.pickupLat != null ? Number(data.pickupLat) : null
formData.pickupTime = formatDate(data.pickupTime) || ''
formData.pickupNotes = data.pickupNotes || ''
formData.destinationAddress = data.destinationAddress || ''
formData.destinationLng = data.destinationLng != null ? Number(data.destinationLng) : null
formData.destinationLat = data.destinationLat != null ? Number(data.destinationLat) : null
formData.deliveryTime = formatDate(data.deliveryTime) || ''
formData.destinationNotes = data.destinationNotes || ''
formData.hasFreight = data.hasFreight ?? 0
@@ -657,9 +680,13 @@ function resetState() {
driverId: '',
carrierId: '',
pickupAddress: '',
pickupLng: null,
pickupLat: null,
pickupTime: '',
pickupNotes: '',
destinationAddress: '',
destinationLng: null,
destinationLat: null,
deliveryTime: '',
destinationNotes: '',
hasFreight: 0,
@@ -829,9 +856,13 @@ async function handleSubmit() {
driverId: formData.driverId,
carrierId: formData.carrierId || undefined,
pickupAddress: formData.pickupAddress || undefined,
pickupLng: formData.pickupLng,
pickupLat: formData.pickupLat,
pickupTime: formData.pickupTime || undefined,
pickupNotes: formData.pickupNotes || undefined,
destinationAddress: formData.destinationAddress || undefined,
destinationLng: formData.destinationLng,
destinationLat: formData.destinationLat,
deliveryTime: formData.deliveryTime || undefined,
destinationNotes: formData.destinationNotes || undefined,
transportMedium: transportMediumText.value || undefined,
@@ -122,9 +122,14 @@
<div class="pl-plan-logistics-row">
<div class="pl-plan-logistics-block">
<h4 class="pl-plan-subtitle">取货信息</h4>
<div class="form-grid-3">
<el-form-item label="取货地址">
<el-input v-model="formData.pickupAddress" placeholder="取货地址" />
<div class="form-grid-2">
<el-form-item label="取货地址" class="form-col-span-2">
<PlAddressWithLibrary
v-model="formData.pickupAddress"
v-model:lng="formData.pickupLng"
v-model:lat="formData.pickupLat"
placeholder="取货地址"
/>
</el-form-item>
<el-form-item label="取货时间">
<el-date-picker
@@ -164,9 +169,14 @@
<div class="pl-plan-logistics-block">
<h4 class="pl-plan-subtitle">送货信息</h4>
<div class="form-grid-3">
<el-form-item label="送货地址">
<el-input v-model="formData.destinationAddress" placeholder="送货地址" />
<div class="form-grid-2">
<el-form-item label="送货地址" class="form-col-span-2">
<PlAddressWithLibrary
v-model="formData.destinationAddress"
v-model:lng="formData.destinationLng"
v-model:lat="formData.destinationLat"
placeholder="送货地址"
/>
</el-form-item>
<el-form-item label="送达时间">
<el-date-picker
@@ -273,6 +283,7 @@ import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vu
import type { ViewField } from '@/components/business/system-view-dialog/types'
import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
import PlAddressWithLibrary from '@/components/business/pl-address-with-library/index.vue'
import PlPlanFormProductTable from './PlPlanFormProductTable.vue'
import {
applyContractToPlanForm,
@@ -373,9 +384,13 @@ const formData = reactive({
settlementMethod: '',
hasFreight: 0,
pickupAddress: '',
pickupLng: null as number | null,
pickupLat: null as number | null,
pickupTime: '',
pickupNotes: '',
destinationAddress: '',
destinationLng: null as number | null,
destinationLat: null as number | null,
deliveryTime: '',
destinationNotes: '',
vehicleId: '',
@@ -571,9 +586,13 @@ function buildPlanCommonPayload(options?: { allowClearTransport?: boolean }): Pl
settlementMethod: formData.settlementMethod || undefined,
hasFreight: formData.hasFreight,
pickupAddress: formData.pickupAddress || undefined,
pickupLng: formData.pickupLng,
pickupLat: formData.pickupLat,
pickupTime: formData.pickupTime || undefined,
pickupNotes: formData.pickupNotes || undefined,
destinationAddress: formData.destinationAddress || undefined,
destinationLng: formData.destinationLng,
destinationLat: formData.destinationLat,
deliveryTime: formData.deliveryTime || undefined,
destinationNotes: formData.destinationNotes || undefined,
followUserId: formData.followUserId || undefined,
@@ -650,9 +669,13 @@ function resetForm() {
settlementMethod: '',
hasFreight: 0,
pickupAddress: '',
pickupLng: null as number | null,
pickupLat: null as number | null,
pickupTime: '',
pickupNotes: '',
destinationAddress: '',
destinationLng: null as number | null,
destinationLat: null as number | null,
deliveryTime: '',
destinationNotes: '',
vehicleId: '',
@@ -705,9 +728,13 @@ async function loadPlanForEdit(planId: string) {
settlementMethod: data.settlementMethod || '',
hasFreight: data.hasFreight ?? 0,
pickupAddress: data.pickupAddress || '',
pickupLng: data.pickupLng != null ? Number(data.pickupLng) : null,
pickupLat: data.pickupLat != null ? Number(data.pickupLat) : null,
pickupTime: formatDate(data.pickupTime) || '',
pickupNotes: data.pickupNotes || '',
destinationAddress: data.destinationAddress || '',
destinationLng: data.destinationLng != null ? Number(data.destinationLng) : null,
destinationLat: data.destinationLat != null ? Number(data.destinationLat) : null,
deliveryTime: formatDate(data.deliveryTime) || '',
destinationNotes: data.destinationNotes || '',
vehicleId: data.vehicleId || '',
@@ -1084,7 +1111,7 @@ async function handleSubmit() {
margin-bottom: 0;
}
.form-grid-3 {
.form-grid-2 {
gap: 8px 12px;
}
}
@@ -77,7 +77,7 @@
<ViewKvTable :fields="pickupViewFields" mode="quad" />
</div>
<div class="party-flow-bridge" aria-hidden="true">
<div class="party-flow-bridge">
<span class="party-flow-bridge__label">运输</span>
<span class="party-flow-bridge__arrow">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
@@ -0,0 +1,238 @@
<template>
<el-dialog
v-model="innerVisible"
title="从地址库选择"
width="1000px"
align-center
destroy-on-close
append-to-body
class="dialog-form-layout pl-transport-address-select-dialog"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
style="backdrop-filter: none"
@open="handleOpen"
>
<div class="filter-block pl-transport-address-select-filter">
<el-form class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="名称/地址">
<el-input
v-model="keyword"
placeholder="搜索地址名称、详细地址、联系人或电话"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
class="list-table pl-transport-address-select-table"
:data="rows"
v-loading="loading"
stripe
row-key="id"
highlight-current-row
:max-height="480"
empty-text="暂无可用地址"
@row-dblclick="handleConfirm"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="addressName" label="地址名称" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleConfirm(row)">{{ row.addressName }}</el-link>
</template>
</el-table-column>
<el-table-column prop="fullAddress" label="详细地址" min-width="260" show-overflow-tooltip />
<el-table-column label="坐标" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.lng != null && row.lat != null">{{ row.lng }}, {{ row.lat }}</span>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
prop="contactName"
label="联系人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="contactPhone"
label="联系电话"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="100"
align="center"
header-align="center"
fixed="right"
class-name="col-actions"
>
<template #default="{ row }">
<el-button link type="primary" @click="handleConfirm(row)">
<el-icon><Select /></el-icon>选择
</el-button>
</template>
</el-table-column>
</el-table>
<ListPagination
v-show="pagination.total > 0"
:total="pagination.total"
v-model:page="pagination.page"
v-model:limit="pagination.pageSize"
@pagination="fetchList"
/>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="innerVisible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { Close, RefreshLeft, Search, Select } from '@element-plus/icons-vue'
import { transportAddressApi, type TransportAddress } from '@/api/pl/index'
import ListPagination from '@/components/ListPagination/index.vue'
import { pickBxPageResult } from '@/utils/http/bx'
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
select: [row: TransportAddress]
}>()
const innerVisible = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value)
})
const loading = ref(false)
const keyword = ref('')
const rows = ref<TransportAddress[]>([])
const pagination = reactive({
page: 1,
pageSize: DEFAULT_LIST_PAGE_SIZE,
total: 0
})
async function fetchList() {
loading.value = true
try {
const res = await transportAddressApi.list({
q: keyword.value.trim() || undefined,
status: '1',
page: pagination.page,
pageSize: pagination.pageSize
})
const { list, total } = pickBxPageResult<TransportAddress>(res)
rows.value = list
pagination.total = total
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
fetchList()
}
function handleReset() {
keyword.value = ''
pagination.page = 1
fetchList()
}
function handleOpen() {
keyword.value = ''
pagination.page = 1
pagination.pageSize = DEFAULT_LIST_PAGE_SIZE
pagination.total = 0
rows.value = []
fetchList()
}
function handleConfirm(row: TransportAddress) {
emit('select', row)
innerVisible.value = false
}
</script>
<style scoped lang="scss">
.pl-transport-address-select-filter {
padding: 12px 16px 10px;
background: var(--mint-surface, #fff);
border: 1px solid var(--mint-border, #d9e8e2);
border-radius: var(--mint-radius, 10px);
margin-bottom: 12px;
.filter-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 10px 14px;
max-width: 480px;
}
.filter-actions {
margin-top: 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.filter-actions-end {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.filter-actions-end .el-button + .el-button {
margin-left: 0 !important;
}
}
.pl-transport-address-select-dialog {
:deep(.el-dialog__body) {
min-height: 620px;
}
}
</style>
@@ -9,6 +9,7 @@ import { planApi } from '@/api/pl'
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
import { useDictStore } from '@/store/modules/dict'
import { formatDate } from '@/utils/format'
import { appendAmapNavLine, buildAmapNavigationUrl } from '@/utils/amap/navigation'
import { pickBxData } from '@/utils/http/bx'
import { formatPlanProductListText } from '@/utils/pl-display'
import { buildSiteSummaryFromInventoryLinks } from '@/utils/db-site-summary'
@@ -109,7 +110,11 @@ export function usePlPlanArrangeActions() {
startPoint?: string
endPoint?: string
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
destinationAddress?: string
destinationLng?: number | null
destinationLat?: number | null
pickupNotes?: string
destinationNotes?: string
licensePlate?: string
@@ -124,15 +129,37 @@ export function usePlPlanArrangeActions() {
.map(p => formatPlanProductListText(p, getUnitLabel, { usePlannedQuantity: true }))
.join(';')
lines.push(`产品:${productText || '—'}`)
lines.push(
`路线:${options?.pickupAddress || plan.value?.pickupAddress || options?.startPoint || '—'} → ${
options?.destinationAddress ||
plan.value?.destinationAddress ||
plan.value?.destinationName ||
options?.endPoint ||
'—'
}`
const pickupAddress =
options?.pickupAddress || plan.value?.pickupAddress || options?.startPoint || '—'
const destinationAddress =
options?.destinationAddress ||
plan.value?.destinationAddress ||
plan.value?.destinationName ||
options?.endPoint ||
'—'
lines.push(`取货地址:${pickupAddress}`)
lines.push(`送货地址:${destinationAddress}`)
const pickupLng = options?.pickupLng ?? plan.value?.pickupLng
const pickupLat = options?.pickupLat ?? plan.value?.pickupLat
const destinationLng = options?.destinationLng ?? plan.value?.destinationLng
const destinationLat = options?.destinationLat ?? plan.value?.destinationLat
appendAmapNavLine(
lines,
'取货导航',
buildAmapNavigationUrl(pickupLng, pickupLat, pickupAddress !== '—' ? pickupAddress : '取货点')
)
appendAmapNavLine(
lines,
'送货导航',
buildAmapNavigationUrl(
destinationLng,
destinationLat,
destinationAddress !== '—' ? destinationAddress : '送货点'
)
)
lines.push(`取货注意事项:${options?.pickupNotes || plan.value?.pickupNotes || '—'}`)
lines.push(`送货注意事项:${options?.destinationNotes || plan.value?.destinationNotes || '—'}`)
lines.push(`司机链接:${options?.driverUrl || driverUrl.value || '—'}`)
@@ -208,7 +235,11 @@ export function usePlPlanArrangeActions() {
startPoint?: string
endPoint?: string
pickupAddress?: string
pickupLng?: number | null
pickupLat?: number | null
destinationAddress?: string
destinationLng?: number | null
destinationLat?: number | null
pickupNotes?: string
destinationNotes?: string
licensePlate?: string
+9
View File
@@ -1,5 +1,14 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_AMAP_KEY?: string
readonly VITE_AMAP_SECURITY_CODE?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare module 'nprogress'
declare module 'crypto-js'
+7
View File
@@ -32,6 +32,13 @@ export const plRoutes: AppRouteRecord[] = [
component: '/pl/vehicle/index',
meta: { title: '车辆管理', keepAlive: true, menuCode: 'pl:vehicle:list' }
},
{
id: 'PL00000000000000000000000000004A',
name: 'PlTransportAddress',
path: 'transport-address',
component: '/pl/transport-address/index',
meta: { title: '地址库', keepAlive: true, menuCode: 'pl:transport-address:list' }
},
{
id: 'PL00000000000000000000000000004',
name: 'PlPersonnel',
+168
View File
@@ -0,0 +1,168 @@
declare global {
interface Window {
AMap?: AMapGlobal
_AMapSecurityConfig?: { securityJsCode: string }
}
}
export interface AMapLngLat {
lng: number
lat: number
getLng?: () => number
getLat?: () => number
}
export interface AMapPoi {
name?: string
address?: string
location?: AMapLngLat
district?: string
}
export interface AMapGlobal {
plugin: (names: string | string[], callback: () => void) => void
Map: new (container: HTMLElement | string, opts: Record<string, unknown>) => AMapMap
Marker: new (opts: Record<string, unknown>) => AMapMarker
AutoComplete: new (opts: Record<string, unknown>) => AMapAutoComplete
PlaceSearch: new (opts: Record<string, unknown>) => AMapPlaceSearch
Geocoder: new (opts?: Record<string, unknown>) => AMapGeocoder
}
export interface AMapMap {
on: (event: string, handler: (event: { lnglat: AMapLngLat }) => void) => void
destroy: () => void
resize: () => void
setCenter: (center: [number, number]) => void
setZoom: (zoom: number) => void
add: (overlay: unknown) => void
remove: (overlay: unknown) => void
}
export interface AMapMarker {
setPosition: (lnglat: [number, number] | AMapLngLat) => void
getPosition: () => AMapLngLat
on: (event: string, handler: () => void) => void
}
export interface AMapAutoComplete {
on: (event: string, handler: (event: { poi?: AMapPoi }) => void) => void
off: (event: string, handler: (event: { poi?: AMapPoi }) => void) => void
}
export interface AMapPlaceSearch {
search: (
keyword: string,
callback: (status: string, result: { poiList?: { pois?: AMapPoi[] } }) => void
) => void
}
export interface AMapGeocoder {
getAddress: (
lnglat: [number, number] | AMapLngLat,
callback: (status: string, result: { regeocode?: { formattedAddress?: string } }) => void
) => void
getLocation: (
address: string,
callback: (
status: string,
result: { geocodes?: Array<{ location?: { lng: number; lat: number } }> }
) => void
) => void
}
const AMAP_SCRIPT_ID = 'amap-js-sdk-v2'
const DEFAULT_PLUGINS = ['AMap.AutoComplete', 'AMap.PlaceSearch', 'AMap.Geocoder']
let loadPromise: Promise<AMapGlobal> | null = null
let pluginsReadyPromise: Promise<void> | null = null
export function getAmapKey() {
return String(import.meta.env.VITE_AMAP_KEY || '').trim()
}
export function getAmapSecurityCode() {
return String(import.meta.env.VITE_AMAP_SECURITY_CODE || '').trim()
}
export function isAmapConfigured() {
return !!getAmapKey()
}
function injectSecurityConfig() {
const securityJsCode = getAmapSecurityCode()
if (!securityJsCode) return
window._AMapSecurityConfig = { securityJsCode }
}
export function loadAmap(): Promise<AMapGlobal> {
const key = getAmapKey()
if (!key) {
return Promise.reject(new Error('未配置 VITE_AMAP_KEY'))
}
if (window.AMap) {
return Promise.resolve(window.AMap)
}
if (loadPromise) return loadPromise
injectSecurityConfig()
loadPromise = new Promise((resolve, reject) => {
const existing = document.getElementById(AMAP_SCRIPT_ID) as HTMLScriptElement | null
if (existing) {
existing.addEventListener('load', () => {
if (window.AMap) resolve(window.AMap)
else reject(new Error('高德地图 SDK 加载失败'))
})
existing.addEventListener('error', () => reject(new Error('高德地图 SDK 加载失败')))
return
}
const script = document.createElement('script')
script.id = AMAP_SCRIPT_ID
script.type = 'text/javascript'
script.async = true
script.src = `https://webapi.amap.com/maps?v=2.0&key=${encodeURIComponent(key)}`
script.onload = () => {
if (window.AMap) resolve(window.AMap)
else reject(new Error('高德地图 SDK 加载失败'))
}
script.onerror = () => reject(new Error('高德地图 SDK 加载失败'))
document.head.appendChild(script)
})
return loadPromise
}
export function loadAmapPlugins(plugins: string[] = DEFAULT_PLUGINS): Promise<void> {
if (pluginsReadyPromise) return pluginsReadyPromise
pluginsReadyPromise = loadAmap().then(
AMap =>
new Promise((resolve, reject) => {
const timer = window.setTimeout(() => {
pluginsReadyPromise = null
reject(new Error('高德地图插件加载超时'))
}, 15000)
AMap.plugin(plugins, () => {
window.clearTimeout(timer)
resolve()
})
})
)
return pluginsReadyPromise
}
export function resolveLngLat(lnglat: AMapLngLat): [number, number] {
const lng = typeof lnglat.getLng === 'function' ? lnglat.getLng() : lnglat.lng
const lat = typeof lnglat.getLat === 'function' ? lnglat.getLat() : lnglat.lat
return [lng, lat]
}
export function formatPoiAddress(poi?: AMapPoi) {
if (!poi) return ''
const parts = [poi.district, poi.address, poi.name].filter(Boolean)
return parts.join('').trim() || poi.name || poi.address || ''
}
+20
View File
@@ -0,0 +1,20 @@
/** 构建高德 URI 导航链接(手机点击可唤起高德 App) */
export function buildAmapNavigationUrl(
lng?: number | null,
lat?: number | null,
name?: string
): string | null {
if (lng == null || lat == null || Number.isNaN(Number(lng)) || Number.isNaN(Number(lat))) {
return null
}
const lngText = Number(lng).toFixed(6)
const latText = Number(lat).toFixed(6)
const toName = encodeURIComponent((name || '目的地').trim() || '目的地')
return `https://uri.amap.com/navigation?to=${lngText},${latText},${toName}&mode=car&coordinate=gaode&callnative=1`
}
export function appendAmapNavLine(lines: string[], label: string, url: string | null) {
if (url) {
lines.push(`${label}:${url}`)
}
}
@@ -0,0 +1,497 @@
<!-- 运输地址库 -->
<template>
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">地址库</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="名称/地址">
<el-input
v-model="searchForm.q"
placeholder="搜索名称或详细地址"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable>
<el-option label="启用" value="1" />
<el-option label="停用" value="0" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增地址
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length !== 1"
@click="handleToolbarEdit"
>
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button
class="btn-ghost"
type="danger"
:disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
:title="selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0]) ? CREATOR_DELETE_TIP : ''"
@click="handleToolbarDelete"
>
<el-icon><Delete /></el-icon>删除
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="addressRows"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="addressName" label="地址名称" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">{{ row.addressName }}</el-link>
</template>
</el-table-column>
<el-table-column prop="fullAddress" label="详细地址" min-width="240" show-overflow-tooltip />
<el-table-column label="坐标" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.lng != null && row.lat != null">{{ row.lng }}, {{ row.lat }}</span>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" min-width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" />
<el-table-column prop="status" label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
{{ row.status === '1' ? '启用' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</section>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="640px"
align-center
destroy-on-close
class="dialog-form-layout pl-transport-address-form-dialog"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
style="backdrop-filter: none"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="地址名称" prop="addressName">
<el-input v-model="formData.addressName" placeholder="如:安庆仓库、某某化工厂" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-option label="启用" value="1" />
<el-option label="停用" value="0" />
</el-select>
</el-form-item>
<el-form-item label="详细地址" prop="fullAddress" class="form-col-span-2">
<AmapAddressPicker
v-model="formData.fullAddress"
v-model:lng="formData.lng"
v-model:lat="formData.lat"
placeholder="请地图选点或搜索 POI"
/>
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="formData.contactName" placeholder="联系人(可选)" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="formData.contactPhone" placeholder="联系电话(可选)" />
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="备注(可选)" />
</el-form-item>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
<el-icon><Check /></el-icon>确定
</el-button>
</div>
</template>
</el-dialog>
<SystemViewDialog
v-model="viewVisible"
title="地址详情"
:loading="viewLoading"
:hero-title="viewDetail?.addressName || '—'"
:hero-meta="viewHeroMeta"
:sections="viewSections"
>
<template #hero-actions>
<el-button @click="handleEditFromView">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button
type="danger"
:disabled="!viewDetail || !canDeleteByCreator(viewDetail)"
@click="handleDeleteFromView"
>
<el-icon><Delete /></el-icon>删除
</el-button>
</template>
</SystemViewDialog>
</div>
</template>
<script setup lang="ts">
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
transportAddressApi,
type TransportAddress,
type TransportAddressPayload
} from '@/api/pl/index'
import AmapAddressPicker from '@/components/business/amap-address-picker/index.vue'
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { pickBxPageResult } from '@/utils/http/bx'
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
import { formatDateTime } from '@/utils/format'
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
const loading = ref(false)
const submitLoading = ref(false)
const addressRows = ref<TransportAddress[]>([])
const selectedRows = ref<TransportAddress[]>([])
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref()
const isEdit = ref(false)
const viewVisible = ref(false)
const viewLoading = ref(false)
const viewDetail = ref<TransportAddress | null>(null)
const searchForm = reactive({
q: '',
status: ''
})
const pagination = reactive({
page: 1,
pageSize: DEFAULT_LIST_PAGE_SIZE,
total: 0
})
const formData = reactive({
id: '',
addressName: '',
fullAddress: '',
lng: null as number | null,
lat: null as number | null,
contactName: '',
contactPhone: '',
status: '1',
remark: ''
})
const formRules = {
addressName: [{ required: true, message: '请输入地址名称', trigger: 'blur' }],
fullAddress: [{ required: true, message: '请填写详细地址', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
lng: [{ required: true, message: '请通过地图选点获取经度', trigger: 'change' }],
lat: [{ required: true, message: '请通过地图选点获取纬度', trigger: 'change' }]
}
const viewHeroMeta = computed(() => {
if (!viewDetail.value) return ''
return viewDetail.value.fullAddress || ''
})
const viewSections = computed<ViewSection[]>(() => {
const d = viewDetail.value
if (!d) return []
return [
{
title: '基本信息',
fields: [
{ label: '地址名称', value: d.addressName },
{ label: '详细地址', value: d.fullAddress, span: 'full' },
{
label: '坐标',
value: d.lng != null && d.lat != null ? `${d.lng}, ${d.lat}` : '—'
},
{ label: '联系人', value: d.contactName || '—' },
{ label: '联系电话', value: d.contactPhone || '—' },
{
label: '状态',
value: d.status === '1' ? '启用' : '停用',
tagType: d.status === '1' ? 'success' : 'info'
},
{ label: '创建时间', value: formatDateTime(d.createTime) },
{ label: '更新时间', value: formatDateTime(d.updateTime) },
{ label: '备注', value: d.remark || '—', span: 'full' }
]
}
]
})
async function fetchData() {
loading.value = true
try {
const res = await transportAddressApi.list({
q: searchForm.q || undefined,
status: searchForm.status || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
const { list, total } = pickBxPageResult<TransportAddress>(res)
addressRows.value = list
pagination.total = total
const idSet = new Set(addressRows.value.map(item => item.id))
selectedRows.value = selectedRows.value.filter(item => idSet.has(item.id))
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
fetchData()
}
function handleReset() {
searchForm.q = ''
searchForm.status = ''
selectedRows.value = []
pagination.page = 1
fetchData()
}
function handlePageChange() {
fetchData()
}
function handleSizeChange() {
pagination.page = 1
fetchData()
}
function handleSelectionChange(rows: TransportAddress[]) {
selectedRows.value = rows
}
function handleToolbarEdit() {
if (selectedRows.value.length !== 1) {
ElMessage.warning('请选择一条地址记录')
return
}
handleEdit(selectedRows.value[0])
}
function handleToolbarDelete() {
if (selectedRows.value.length !== 1) {
ElMessage.warning('请选择一条地址记录')
return
}
handleDelete(selectedRows.value[0])
}
function handleAdd() {
dialogTitle.value = '新增地址'
isEdit.value = false
Object.assign(formData, {
id: '',
addressName: '',
fullAddress: '',
lng: null,
lat: null,
contactName: '',
contactPhone: '',
status: '1',
remark: ''
})
dialogVisible.value = true
}
async function handleView(row: TransportAddress) {
viewVisible.value = true
viewLoading.value = true
viewDetail.value = null
try {
const res = await transportAddressApi.get(row.id)
viewDetail.value = (res.data ?? res) as TransportAddress
} catch {
viewVisible.value = false
ElMessage.error('加载地址详情失败')
} finally {
viewLoading.value = false
}
}
async function handleEdit(row: TransportAddress) {
dialogTitle.value = '编辑地址'
isEdit.value = true
dialogVisible.value = true
try {
const res = await transportAddressApi.get(row.id)
const detail = (res.data ?? res) as TransportAddress
Object.assign(formData, {
id: detail.id,
addressName: detail.addressName,
fullAddress: detail.fullAddress,
lng: detail.lng != null ? Number(detail.lng) : null,
lat: detail.lat != null ? Number(detail.lat) : null,
contactName: detail.contactName || '',
contactPhone: detail.contactPhone || '',
status: detail.status,
remark: detail.remark || ''
})
} catch {
ElMessage.error('加载地址详情失败')
}
}
function handleEditFromView() {
if (!viewDetail.value) return
viewVisible.value = false
handleEdit(viewDetail.value)
}
async function handleDeleteFromView() {
if (!viewDetail.value) return
await handleDelete(viewDetail.value)
viewVisible.value = false
viewDetail.value = null
}
function validateCoordinates(): boolean {
if (formData.lng == null || formData.lat == null) {
ElMessage.warning('请通过地图选点填写经纬度')
return false
}
return true
}
async function handleSubmit() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid || !validateCoordinates()) return
submitLoading.value = true
try {
const payload: TransportAddressPayload = {
addressName: formData.addressName.trim(),
fullAddress: formData.fullAddress.trim(),
lng: formData.lng as number,
lat: formData.lat as number,
contactName: formData.contactName.trim() || undefined,
contactPhone: formData.contactPhone.trim() || undefined,
status: formData.status,
remark: formData.remark.trim() || undefined
}
if (isEdit.value) {
await transportAddressApi.update(formData.id, payload)
} else {
await transportAddressApi.create(payload)
}
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
dialogVisible.value = false
fetchData()
} finally {
submitLoading.value = false
}
}
function handleDialogClose() {
formRef.value?.resetFields()
}
async function handleDelete(row: TransportAddress) {
if (!canDeleteByCreator(row)) {
ElMessage.warning(CREATOR_DELETE_TIP)
return
}
await ElMessageBox.confirm('确定删除该地址吗?', '提示', { type: 'warning' })
await transportAddressApi.delete(row.id)
ElMessage.success('删除成功')
if (viewDetail.value?.id === row.id) {
viewVisible.value = false
viewDetail.value = null
}
fetchData()
}
useDbInstantSearch(
() => ({
q: searchForm.q,
status: searchForm.status
}),
handleSearch
)
onMounted(() => {
fetchData()
})
</script>
@@ -15,6 +15,35 @@
<van-cell title="路线" :value="routeDisplay" class="route-cell" />
</van-cell-group>
<van-cell-group
v-if="pickupNavUrl || destinationNavUrl"
inset
class="section-card nav-card"
title="导航"
>
<div class="nav-actions">
<van-button
v-if="pickupNavUrl"
type="primary"
block
round
@click="openNavigation(pickupNavUrl)"
>
导航到取货点
</van-button>
<van-button
v-if="destinationNavUrl"
type="primary"
plain
block
round
@click="openNavigation(destinationNavUrl)"
>
导航到送货点
</van-button>
</div>
</van-cell-group>
<van-cell-group v-if="page.products.length" inset class="section-card" title="产品明细">
<van-cell
v-for="(item, idx) in page.products"
@@ -72,6 +101,7 @@
type PublicTransportProduct
} from '@/api/pl/public-transport'
import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
import { buildAmapNavigationUrl } from '@/utils/amap/navigation'
import 'vant/es/toast/style'
import '@/styles/public-mobile-vant.scss'
@@ -99,6 +129,28 @@
return `${page.value.pickupAddress || '—'} → ${page.value.destinationAddress || '—'}`
})
const pickupNavUrl = computed(() => {
if (!page.value) return null
return buildAmapNavigationUrl(
page.value.pickupLng,
page.value.pickupLat,
page.value.pickupAddress || '取货点'
)
})
const destinationNavUrl = computed(() => {
if (!page.value) return null
return buildAmapNavigationUrl(
page.value.destinationLng,
page.value.destinationLat,
page.value.destinationAddress || '送货点'
)
})
function openNavigation(url: string) {
window.location.href = url
}
function formatProductQuantity(value?: number) {
if (value == null || Number.isNaN(Number(value))) return '—'
const num = Number(value)
@@ -207,4 +259,11 @@
.uploader-wrap {
padding: 12px 16px 16px;
}
.nav-actions {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 16px 16px;
}
</style>