| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907 |
- """运输计划司机免登录公共 API"""
- from __future__ import annotations
- import json
- import os
- from datetime import date, datetime
- from decimal import Decimal
- from typing import Optional
- from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
- from sqlalchemy import and_, func, or_
- from sqlalchemy.orm import Session
- from app.core.storage import storage
- from app.core.attachment_biz import resolve_upload_folder
- from app.core.config import settings
- from app.core.database import get_db
- from app.core.response import error_response, success_response
- from app.crud import oss as oss_svc
- from app.models.crm_models import CrmProductBrand
- from app.models.pl_models import (
- PlPlan,
- PlPlanDelivery,
- PlPlanLoad,
- PlPlanProduct,
- PlPlanTransport,
- PlTransport,
- PlTransportProductBrandQty,
- PlTransportProductFeedback,
- )
- from app.schemas.pl.io_order import BrandOption, BrandQtyItem
- from app.schemas.pl.resource_snapshot import parse_resource_snapshot
- from app.schemas.pl.transport import (
- PublicTransportDriverStopFeedback,
- PublicTransportFeedback,
- PublicTransportHistoryPageRead,
- PublicTransportHistoryProductRead,
- PublicTransportHistoryRecordRead,
- PublicTransportHistoryStopProductRead,
- PublicTransportHistoryStopRead,
- PublicTransportPageRead,
- PublicTransportPersonnelRead,
- PublicTransportPlanProductRead,
- PublicTransportStopProductRead,
- PublicTransportStopRead,
- )
- from app.services.pl_dibang_bridge_service import plan_has_load_stop
- from app.services.pl_feedback_brand_qty import (
- persist_transport_product_feedback,
- require_brand_qtys_if_needed,
- )
- from app.services.pl_transport_stop_feedback_service import (
- PublicStopProductRow,
- load_driver_stop_segments,
- save_driver_stop_feedback,
- )
- from app.services.pl_product_display import (
- load_crm_product_alias_maps,
- resolve_product_display_names,
- )
- from app.utils.excel_export import load_dict_label_maps
- router = APIRouter(prefix="/public/pl/transport", tags=["运输计划-司机公共"])
- CRM_PRODUCT_UNIT = "crm_product_unit"
- def _resolve_unit_label(unit_map: dict[str, str], unit_code: Optional[str]) -> str:
- code = (unit_code or "").strip()
- if not code:
- return "吨"
- return unit_map.get(code, code)
- def _build_stop_product_reads(
- *,
- stop_products: list[PublicStopProductRow],
- plan_products_by_id: dict[str, PlPlanProduct],
- unit_map: dict[str, str],
- brand_options_by_product: dict[str, list[BrandOption]],
- standard_by_id: dict[str, str],
- alias_options_by_id: dict[str, list[str]],
- ) -> list[PublicTransportStopProductRead]:
- reads: list[PublicTransportStopProductRead] = []
- for row in stop_products:
- plan_prod = plan_products_by_id.get(row.plan_product_id)
- product_id = (row.product_id or (plan_prod.product_id if plan_prod else "") or "").strip()
- display_name, alias_name = resolve_product_display_names(
- saved_product_name=row.product_name,
- product_id=product_id,
- standard_by_id=standard_by_id,
- alias_options_by_id=alias_options_by_id,
- )
- brand_qtys = None
- if row.brand_qtys:
- brand_qtys = [
- BrandQtyItem(
- brand_id=str(item.get("brandId") or item.get("brand_id") or ""),
- brand_name=item.get("brandName") or item.get("brand_name"),
- qty=item.get("qty") or 0,
- )
- for item in row.brand_qtys
- if isinstance(item, dict)
- ] or None
- reads.append(
- PublicTransportStopProductRead(
- stop_product_id=row.stop_product_id,
- plan_product_id=row.plan_product_id,
- product_name=display_name,
- alias_name=alias_name,
- model=row.model,
- package_spec=row.package_spec,
- unit=_resolve_unit_label(unit_map, row.unit),
- quantity=row.plan_qty,
- remark=row.remark,
- brand_mode=plan_prod.brand_mode if plan_prod else None,
- brand_id=plan_prod.brand_id if plan_prod else None,
- brand_name=plan_prod.brand_name if plan_prod else None,
- brand_options=brand_options_by_product.get(product_id) or None,
- actual_qty=row.actual_qty,
- brand_qtys=brand_qtys,
- )
- )
- return reads
- def _get_transport_by_token(db: Session, token: str) -> PlTransport:
- transport = (
- db.query(PlTransport)
- .filter(
- PlTransport.driver_access_token == token,
- PlTransport.del_flag == 0,
- )
- .first()
- )
- if not transport:
- raise HTTPException(status_code=404, detail="链接无效或已失效")
- return transport
- def _parse_feedback_images(raw: Optional[str]) -> list[str]:
- if not raw:
- return []
- try:
- parsed = json.loads(raw)
- if isinstance(parsed, list):
- return [str(item) for item in parsed if item]
- except (TypeError, json.JSONDecodeError):
- pass
- return [part.strip() for part in raw.split(",") if part.strip()]
- def _format_plan_date(value) -> Optional[str]:
- if value is None:
- return None
- if isinstance(value, datetime):
- return value.strftime("%Y-%m-%d")
- if isinstance(value, date):
- return value.isoformat()
- return str(value)
- def _normalize_main_plate(value: Optional[str]) -> str:
- return (value or "").strip()
- def _parse_history_date(raw: Optional[str]) -> Optional[date]:
- text = (raw or "").strip()
- if not text:
- return None
- try:
- return date.fromisoformat(text)
- except ValueError:
- return None
- def _as_date(value) -> Optional[date]:
- if value is None:
- return None
- if isinstance(value, datetime):
- return value.date()
- if isinstance(value, date):
- return value
- return None
- def _history_matches_date(pickup_time, delivery_time, day: date) -> bool:
- return _as_date(pickup_time) == day or _as_date(delivery_time) == day
- def _history_status_label(plan_voided: bool, plan_archived: bool) -> str:
- if plan_voided:
- return "已作废"
- if plan_archived:
- return "已归档"
- return "进行中"
- def _build_history_stop_reads(
- db: Session,
- *,
- tenant_no: str,
- plan: PlPlan,
- transport: PlTransport,
- ) -> list[PublicTransportHistoryStopRead]:
- if not plan_has_load_stop(db, tenant_no, plan.id):
- return []
- plan_products = (
- db.query(PlPlanProduct)
- .filter(
- PlPlanProduct.plan_id == plan.id,
- PlPlanProduct.tenant_no == tenant_no,
- PlPlanProduct.del_flag == 0,
- )
- .all()
- )
- plan_products_by_id = {row.id: row for row in plan_products}
- crm_product_ids = {
- (row.product_id or "").strip() for row in plan_products if (row.product_id or "").strip()
- }
- standard_by_id, alias_options_by_id = load_crm_product_alias_maps(
- db, tenant_no=tenant_no, product_ids=crm_product_ids
- )
- unit_map = load_dict_label_maps(db, [CRM_PRODUCT_UNIT], tenant_no=tenant_no).get(
- CRM_PRODUCT_UNIT, {}
- )
- load_row = (
- db.query(PlPlanLoad)
- .filter(
- PlPlanLoad.plan_id == plan.id,
- PlPlanLoad.tenant_no == tenant_no,
- PlPlanLoad.del_flag == 0,
- )
- .first()
- )
- delivery_rows = (
- db.query(PlPlanDelivery)
- .filter(
- PlPlanDelivery.plan_id == plan.id,
- PlPlanDelivery.tenant_no == tenant_no,
- PlPlanDelivery.del_flag == 0,
- )
- .all()
- )
- delivery_time_by_id = {row.id: row.delivery_time for row in delivery_rows}
- load_time = load_row.load_time if load_row else None
- segments = load_driver_stop_segments(
- db,
- tenant_no=tenant_no,
- plan_id=plan.id,
- transport_id=transport.id,
- )
- reads: list[PublicTransportHistoryStopRead] = []
- for segment in segments:
- stop_time = load_time if segment.stop_type == "load" else delivery_time_by_id.get(segment.stop_id)
- stop_products: list[PublicTransportHistoryStopProductRead] = []
- for row in segment.products:
- plan_prod = plan_products_by_id.get(row.plan_product_id)
- product_id = (row.product_id or (plan_prod.product_id if plan_prod else "") or "").strip()
- display_name, _alias_name = resolve_product_display_names(
- saved_product_name=row.product_name,
- product_id=product_id,
- standard_by_id=standard_by_id,
- alias_options_by_id=alias_options_by_id,
- )
- stop_products.append(
- PublicTransportHistoryStopProductRead(
- product_name=display_name or row.product_name or "—",
- model=row.model,
- package_spec=row.package_spec,
- quantity=row.plan_qty,
- unit=_resolve_unit_label(unit_map, row.unit),
- )
- )
- reads.append(
- PublicTransportHistoryStopRead(
- stop_type=segment.stop_type,
- seq=segment.seq,
- label=segment.label,
- customer_name=segment.customer_name,
- address=segment.address,
- stop_time=stop_time,
- products=stop_products,
- )
- )
- return reads
- def _build_driver_url(token: str) -> str:
- origin = (settings.PUBLIC_WEB_ORIGIN or "").rstrip("/")
- return f"{origin}/#/public/pl/transport/{token}"
- def _resolve_driver_link_flags(
- plan: Optional[PlPlan], transport: PlTransport
- ) -> tuple[bool, bool]:
- """返回 (作废, 归档)。"""
- voided = (
- (plan is not None and plan.follow_status == "cancelled")
- or transport.transport_status == "cancelled"
- )
- archived = bool(plan is not None and plan.archive_status == "archived")
- return voided, archived
- def _resolve_other_personnel(transport: PlTransport, plan: Optional[PlPlan]) -> list[PublicTransportPersonnelRead]:
- raw = transport.resource_snapshot or (plan.resource_snapshot if plan else None)
- snapshot = parse_resource_snapshot(raw)
- if not snapshot or not snapshot.personnel_list:
- return []
- selected_driver_id = (snapshot.selected_driver_id or transport.driver_id or "").strip()
- driver_name = (transport.driver_name or "").strip()
- rows: list[PublicTransportPersonnelRead] = []
- for person in snapshot.personnel_list:
- personnel_id = (person.personnel_id or "").strip()
- personnel_name = (person.personnel_name or "").strip()
- is_primary_driver = person.role == "driver" and (
- person.is_default == 1
- or (selected_driver_id and personnel_id == selected_driver_id)
- or (driver_name and personnel_name == driver_name)
- )
- if is_primary_driver:
- continue
- rows.append(
- PublicTransportPersonnelRead(
- personnel_name=personnel_name or "—",
- phone_no=person.phone_no,
- role=person.role,
- )
- )
- return rows
- def _build_public_page(db: Session, transport: PlTransport) -> PublicTransportPageRead:
- link = (
- db.query(PlPlanTransport)
- .filter(
- PlPlanTransport.transport_id == transport.id,
- PlPlanTransport.del_flag == 0,
- )
- .order_by(PlPlanTransport.create_time.desc())
- .first()
- )
- plan = None
- products: list[PlPlanProduct] = []
- if link:
- plan = (
- db.query(PlPlan)
- .filter(PlPlan.id == link.plan_id, PlPlan.del_flag == 0)
- .first()
- )
- if plan:
- products = (
- db.query(PlPlanProduct)
- .filter(PlPlanProduct.plan_id == plan.id, PlPlanProduct.del_flag == 0)
- .order_by(PlPlanProduct.order_num.asc(), PlPlanProduct.create_time.asc())
- .all()
- )
- crm_product_ids = {(p.product_id or "").strip() for p in products if (p.product_id or "").strip()}
- brand_options_by_product: dict[str, list[BrandOption]] = {}
- if crm_product_ids:
- brand_rows = (
- db.query(CrmProductBrand)
- .filter(
- CrmProductBrand.product_id.in_(list(crm_product_ids)),
- CrmProductBrand.tenant_no == transport.tenant_no,
- CrmProductBrand.status == "on",
- CrmProductBrand.del_flag == 0,
- )
- .order_by(CrmProductBrand.order_num.asc(), CrmProductBrand.create_time.asc())
- .all()
- )
- for row in brand_rows:
- brand_options_by_product.setdefault(row.product_id, []).append(
- BrandOption(brand_id=row.id, brand_name=row.brand_name or "")
- )
- feedback_by_plan_product: dict[str, PlTransportProductFeedback] = {}
- brand_qtys_by_plan_product: dict[str, list[BrandQtyItem]] = {}
- feedback_rows = (
- db.query(PlTransportProductFeedback)
- .filter(
- PlTransportProductFeedback.transport_id == transport.id,
- PlTransportProductFeedback.tenant_no == transport.tenant_no,
- PlTransportProductFeedback.del_flag == 0,
- )
- .order_by(PlTransportProductFeedback.order_num.asc())
- .all()
- )
- for fb in feedback_rows:
- feedback_by_plan_product[fb.plan_product_id] = fb
- if feedback_rows:
- feedback_ids = [fb.id for fb in feedback_rows]
- brand_qty_rows = (
- db.query(PlTransportProductBrandQty)
- .filter(
- PlTransportProductBrandQty.transport_id == transport.id,
- PlTransportProductBrandQty.tenant_no == transport.tenant_no,
- PlTransportProductBrandQty.del_flag == 0,
- PlTransportProductBrandQty.feedback_id.in_(feedback_ids),
- )
- .order_by(PlTransportProductBrandQty.order_num.asc())
- .all()
- )
- for row in brand_qty_rows:
- brand_qtys_by_plan_product.setdefault(row.plan_product_id, []).append(
- BrandQtyItem(
- brand_id=row.brand_id or "",
- brand_name=row.brand_name,
- qty=row.qty or 0,
- )
- )
- standard_by_id, alias_options_by_id = load_crm_product_alias_maps(
- db, tenant_no=transport.tenant_no, product_ids=crm_product_ids
- )
- product_reads = []
- unit_map = load_dict_label_maps(
- db, [CRM_PRODUCT_UNIT], tenant_no=transport.tenant_no
- ).get(CRM_PRODUCT_UNIT, {})
- for prod in products:
- product_id = (prod.product_id or "").strip()
- options = brand_options_by_product.get(product_id) or None
- fb = feedback_by_plan_product.get(prod.id)
- brand_qtys = brand_qtys_by_plan_product.get(prod.id) or None
- display_name, alias_name = resolve_product_display_names(
- saved_product_name=prod.product_name,
- product_id=product_id,
- standard_by_id=standard_by_id,
- alias_options_by_id=alias_options_by_id,
- )
- product_reads.append(
- PublicTransportPlanProductRead(
- plan_product_id=prod.id,
- product_name=display_name,
- alias_name=alias_name,
- model=prod.model,
- package_spec=prod.package_spec,
- unit=_resolve_unit_label(unit_map, prod.unit),
- quantity=prod.quantity or prod.inbound_estimate_qty or prod.outbound_estimate_qty,
- remark=(prod.remark or "").strip() or None,
- brand_mode=prod.brand_mode,
- brand_id=prod.brand_id,
- brand_name=prod.brand_name,
- brand_options=options,
- actual_qty=fb.actual_qty if fb else None,
- brand_qtys=brand_qtys,
- )
- )
- other_personnel = _resolve_other_personnel(transport, plan)
- plan_voided, plan_archived = _resolve_driver_link_flags(plan, transport)
- stop_reads: list[PublicTransportStopRead] = []
- if plan and plan_has_load_stop(db, transport.tenant_no, plan.id):
- plan_products_by_id = {p.id: p for p in products}
- stop_reads = [
- PublicTransportStopRead(
- stop_type=segment.stop_type,
- stop_id=segment.stop_id,
- seq=segment.seq,
- label=segment.label,
- customer_name=segment.customer_name,
- address=segment.address,
- contact=segment.contact,
- notes=segment.notes,
- lng=segment.lng,
- lat=segment.lat,
- business_type=segment.business_type,
- plan_qty=segment.plan_qty,
- products=_build_stop_product_reads(
- stop_products=segment.products,
- plan_products_by_id=plan_products_by_id,
- unit_map=unit_map,
- brand_options_by_product=brand_options_by_product,
- standard_by_id=standard_by_id,
- alias_options_by_id=alias_options_by_id,
- ),
- attachment_urls=segment.attachment_urls,
- remark=segment.remark,
- feedback_time=segment.feedback_time,
- )
- for segment in load_driver_stop_segments(
- db,
- tenant_no=transport.tenant_no,
- plan_id=plan.id,
- transport_id=transport.id,
- )
- ]
- return PublicTransportPageRead(
- plan_no=plan.plan_no if plan else transport.transport_no,
- 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,
- pickup_time=plan.pickup_time if plan else None,
- pickup_contact=plan.pickup_contact if plan else None,
- pickup_notes=plan.pickup_notes if plan else None,
- destination_name=plan.destination_name 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,
- delivery_time=plan.delivery_time if plan else None,
- delivery_contact=plan.delivery_contact if plan else None,
- destination_notes=plan.destination_notes if plan else None,
- license_plate=transport.license_plate,
- driver_name=transport.driver_name,
- driver_phone=transport.driver_phone,
- other_personnel=other_personnel,
- products=product_reads,
- stops=stop_reads,
- feedback_remark=transport.feedback_remark,
- feedback_images=_parse_feedback_images(transport.feedback_images),
- feedback_qty=transport.feedback_qty,
- feedback_time=transport.feedback_time,
- plan_voided=plan_voided,
- plan_archived=plan_archived,
- )
- @router.get("/{token}")
- def get_public_transport(token: str, db: Session = Depends(get_db)):
- transport = _get_transport_by_token(db, token)
- page = _build_public_page(db, transport)
- return success_response(page.model_dump(by_alias=True))
- @router.get("/{token}/history")
- def get_public_transport_history(
- token: str,
- date: str = Query(..., description="筛选日 YYYY-MM-DD"),
- db: Session = Depends(get_db),
- ):
- transport = _get_transport_by_token(db, token)
- day = _parse_history_date(date)
- if day is None:
- return error_response(400, "日期格式无效,请使用 YYYY-MM-DD")
- plate = _normalize_main_plate(transport.license_plate)
- if not plate:
- page = PublicTransportHistoryPageRead(
- license_plate=None, date=day.isoformat(), records=[]
- )
- return success_response(page.model_dump(by_alias=True))
- rows = (
- db.query(PlPlan, PlTransport)
- .join(
- PlPlanTransport,
- and_(
- PlPlanTransport.plan_id == PlPlan.id,
- PlPlanTransport.del_flag == 0,
- ),
- )
- .join(
- PlTransport,
- and_(
- PlTransport.id == PlPlanTransport.transport_id,
- PlTransport.del_flag == 0,
- ),
- )
- .filter(
- PlPlan.tenant_no == transport.tenant_no,
- PlPlan.del_flag == 0,
- PlPlan.transport_status == "arranged",
- PlTransport.tenant_no == transport.tenant_no,
- PlTransport.license_plate == plate,
- or_(
- and_(PlPlan.pickup_time.isnot(None), func.date(PlPlan.pickup_time) == day),
- and_(PlPlan.delivery_time.isnot(None), func.date(PlPlan.delivery_time) == day),
- ),
- )
- .order_by(
- PlPlan.pickup_time.desc(),
- PlPlan.plan_date.desc(),
- PlPlan.create_time.desc(),
- )
- .all()
- )
- seen_plan: set[str] = set()
- plan_rows: list[tuple[PlPlan, PlTransport]] = []
- for plan, tr in rows:
- if plan.id in seen_plan:
- continue
- seen_plan.add(plan.id)
- plan_rows.append((plan, tr))
- products_by_plan: dict[str, list[PublicTransportHistoryProductRead]] = {}
- if plan_rows:
- plan_ids = [plan.id for plan, _ in plan_rows]
- product_rows = (
- db.query(PlPlanProduct)
- .filter(
- PlPlanProduct.plan_id.in_(plan_ids),
- PlPlanProduct.del_flag == 0,
- )
- .order_by(PlPlanProduct.order_num.asc(), PlPlanProduct.create_time.asc())
- .all()
- )
- crm_product_ids = {
- (p.product_id or "").strip() for p in product_rows if (p.product_id or "").strip()
- }
- standard_by_id, alias_options_by_id = load_crm_product_alias_maps(
- db, tenant_no=transport.tenant_no, product_ids=crm_product_ids
- )
- unit_map = load_dict_label_maps(
- db, [CRM_PRODUCT_UNIT], tenant_no=transport.tenant_no
- ).get(CRM_PRODUCT_UNIT, {})
- for prod in product_rows:
- product_id = (prod.product_id or "").strip()
- display_name, _alias_name = resolve_product_display_names(
- saved_product_name=prod.product_name,
- product_id=product_id,
- standard_by_id=standard_by_id,
- alias_options_by_id=alias_options_by_id,
- )
- products_by_plan.setdefault(prod.plan_id, []).append(
- PublicTransportHistoryProductRead(
- product_name=display_name or prod.product_name or "—",
- model=prod.model,
- package_spec=prod.package_spec,
- quantity=prod.quantity or prod.inbound_estimate_qty or prod.outbound_estimate_qty,
- unit=_resolve_unit_label(unit_map, prod.unit),
- )
- )
- records: list[PublicTransportHistoryRecordRead] = []
- for plan, tr in plan_rows:
- voided, archived = _resolve_driver_link_flags(plan, tr)
- tok = (tr.driver_access_token or "").strip()
- records.append(
- PublicTransportHistoryRecordRead(
- plan_id=plan.id,
- plan_no=plan.plan_no or "",
- transport_id=tr.id,
- driver_access_token=tok or None,
- driver_url=_build_driver_url(tok) if tok else None,
- license_plate=tr.license_plate or plate,
- pickup_time=plan.pickup_time,
- delivery_time=plan.delivery_time,
- pickup_address=plan.pickup_address,
- destination_address=plan.destination_address or plan.destination_name,
- products=products_by_plan.get(plan.id) or [],
- stops=_build_history_stop_reads(
- db, tenant_no=transport.tenant_no, plan=plan, transport=tr
- ),
- plan_voided=voided,
- plan_archived=archived,
- status_label=_history_status_label(voided, archived),
- )
- )
- page = PublicTransportHistoryPageRead(
- license_plate=plate,
- date=day.isoformat(),
- records=records,
- )
- return success_response(page.model_dump(by_alias=True))
- @router.post("/{token}/feedback")
- def save_public_feedback(
- token: str,
- payload: PublicTransportFeedback,
- db: Session = Depends(get_db),
- ):
- transport = _get_transport_by_token(db, token)
- page = _build_public_page(db, transport)
- if page.plan_voided:
- return error_response(400, "运输计划已作废,无法提交回执")
- if page.plan_archived:
- return error_response(400, "运输计划已归档,无法提交回执")
- if payload.feedback_images is not None:
- transport.feedback_images = json.dumps(payload.feedback_images)
- if payload.feedback_remark is not None:
- transport.feedback_remark = payload.feedback_remark
- if payload.products:
- link = (
- db.query(PlPlanTransport)
- .filter(
- PlPlanTransport.transport_id == transport.id,
- PlPlanTransport.del_flag == 0,
- )
- .order_by(PlPlanTransport.create_time.desc())
- .first()
- )
- if not link or not link.plan_id:
- return error_response(400, "未找到关联计划,无法保存产品回传")
- products_payload = [
- {
- "planProductId": item.plan_product_id,
- "actualQty": item.actual_qty,
- "brandQtys": [b.model_dump(by_alias=True) for b in (item.brand_qtys or [])],
- }
- for item in payload.products
- ]
- try:
- persist_transport_product_feedback(
- db,
- tenant_no=transport.tenant_no,
- transport=transport,
- plan_id=link.plan_id,
- products=products_payload,
- user_id=None,
- enforce_brand=True,
- )
- except ValueError as exc:
- return error_response(400, str(exc))
- elif payload.feedback_qty is not None:
- transport.feedback_qty = payload.feedback_qty
- transport.feedback_time = datetime.now()
- db.commit()
- return success_response({"success": True, "message": "保存成功"})
- @router.post("/{token}/driver-feedback")
- def save_public_driver_stop_feedback(
- token: str,
- payload: PublicTransportDriverStopFeedback,
- db: Session = Depends(get_db),
- ):
- transport = _get_transport_by_token(db, token)
- page = _build_public_page(db, transport)
- if page.plan_voided:
- return error_response(400, "运输计划已作废,无法提交回执")
- if page.plan_archived:
- return error_response(400, "运输计划已归档,无法提交回执")
- link = (
- db.query(PlPlanTransport)
- .filter(
- PlPlanTransport.transport_id == transport.id,
- PlPlanTransport.del_flag == 0,
- )
- .order_by(PlPlanTransport.create_time.desc())
- .first()
- )
- if not link or not link.plan_id:
- return error_response(400, "未找到关联计划,无法保存段附件")
- stop_type = (payload.stop_type or "").strip()
- stop_id = (payload.stop_id or "").strip()
- if stop_type not in ("load", "delivery") or not stop_id:
- return error_response(400, "停靠段参数无效")
- allowed = {
- (segment.stop_type, segment.stop_id)
- for segment in load_driver_stop_segments(
- db,
- tenant_no=transport.tenant_no,
- plan_id=link.plan_id,
- transport_id=transport.id,
- )
- }
- if (stop_type, stop_id) not in allowed:
- return error_response(400, "停靠段不存在或不可回传")
- segment = next(
- (
- row
- for row in load_driver_stop_segments(
- db,
- tenant_no=transport.tenant_no,
- plan_id=link.plan_id,
- transport_id=transport.id,
- )
- if row.stop_type == stop_type and row.stop_id == stop_id
- ),
- None,
- )
- if not segment:
- return error_response(400, "停靠段不存在或不可回传")
- products_payload: Optional[list[dict]] = None
- if payload.products is not None:
- allowed_products = {row.stop_product_id: row for row in segment.products}
- plan_products = (
- db.query(PlPlanProduct)
- .filter(
- PlPlanProduct.plan_id == link.plan_id,
- PlPlanProduct.tenant_no == transport.tenant_no,
- PlPlanProduct.del_flag == 0,
- )
- .all()
- )
- plan_products_by_id = {row.id: row for row in plan_products}
- products_payload = []
- for item in payload.products:
- stop_product_id = (item.stop_product_id or "").strip()
- stop_row = allowed_products.get(stop_product_id)
- if not stop_row:
- return error_response(400, "产品不属于当前停靠段")
- actual_qty = Decimal(str(item.actual_qty or 0))
- brand_qtys = [
- b.model_dump(by_alias=True) for b in (item.brand_qtys or [])
- ]
- plan_product = plan_products_by_id.get(stop_row.plan_product_id)
- try:
- require_brand_qtys_if_needed(
- brand_mode=plan_product.brand_mode if plan_product else None,
- required_brand_id=plan_product.brand_id if plan_product else None,
- actual_qty=actual_qty,
- rows=brand_qtys,
- enforce=True,
- )
- except ValueError as exc:
- return error_response(400, str(exc))
- row_payload: dict = {
- "stopProductId": stop_product_id,
- "actualQty": float(actual_qty),
- }
- if brand_qtys:
- row_payload["brandQtys"] = brand_qtys
- products_payload.append(row_payload)
- save_driver_stop_feedback(
- db,
- tenant_no=transport.tenant_no,
- transport_id=transport.id,
- plan_id=link.plan_id,
- stop_type=stop_type,
- stop_id=stop_id,
- attachment_urls=payload.attachment_urls or [],
- products=products_payload,
- remark=payload.remark,
- )
- db.commit()
- return success_response({"success": True, "message": "保存成功"})
- def _format_file_size(size_bytes: int) -> str:
- if size_bytes < 1024:
- return f"{size_bytes} B"
- if size_bytes < 1024 * 1024:
- return f"{size_bytes / 1024:.1f} KB"
- return f"{size_bytes / (1024 * 1024):.1f} MB"
- @router.post("/{token}/attachments")
- def upload_public_attachment(
- token: str,
- file: UploadFile = File(...),
- folder: Optional[str] = Form(""),
- db: Session = Depends(get_db),
- ):
- transport = _get_transport_by_token(db, token)
- page = _build_public_page(db, transport)
- if page.plan_voided:
- return error_response(400, "运输计划已作废,无法上传附件")
- if page.plan_archived:
- return error_response(400, "运输计划已归档,无法上传附件")
- if not file or not file.filename:
- return error_response(400, "请选择文件")
- try:
- effective_folder = resolve_upload_folder(folder=folder or "", biz_type="pl_transport_feedback")
- file_content = file.file.read()
- full_url = storage.upload_file(file_content, file.filename, effective_folder)
- base_url = settings.MINIO_BASE_URL.rstrip("/") if settings.MINIO_BASE_URL else ""
- if base_url and full_url.startswith(base_url + "/"):
- file_url = full_url[len(base_url) + 1 :]
- else:
- file_url = full_url
- record = oss_svc.create_oss(
- db,
- tenant_no=transport.tenant_no,
- url=file_url,
- original_name=file.filename,
- file_suffix=os.path.splitext(file.filename)[1].lstrip(".") if file.filename else "",
- file_size=len(file_content),
- file_type=file.content_type or "",
- biz_type="pl_transport_feedback",
- user_id=None,
- ext1=effective_folder or None,
- )
- db.commit()
- return success_response(
- {
- "id": record.id,
- "fileName": record.original_name,
- "fileUrl": record.url,
- "fileSize": record.file_size,
- "fileSizeDisplay": _format_file_size(record.file_size or 0),
- "fileType": record.file_type,
- }
- )
- except Exception as exc:
- db.rollback()
- return error_response(500, f"上传失败: {str(exc)}")
|