Files
shanxia2.0/backend/app/api/pl/public_transport.py
T
2026-06-21 09:19:07 +08:00

220 lines
7.5 KiB
Python

"""运输计划司机免登录公共 API"""
from __future__ import annotations
import json
import os
from datetime import date, datetime
from typing import Optional
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
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.pl_models import PlPlan, PlPlanProduct, PlPlanTransport, PlTransport
from app.schemas.pl.transport import PublicTransportFeedback, PublicTransportPageRead, PublicTransportPlanProductRead
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 _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 _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()
)
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_reads.append(
PublicTransportPlanProductRead(
product_name=prod.product_name or "",
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,
)
)
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,
destination_address=(
(plan.destination_address or plan.destination_name) if plan else transport.end_point
),
license_plate=transport.license_plate,
driver_name=transport.driver_name,
driver_phone=transport.driver_phone,
products=product_reads,
feedback_remark=transport.feedback_remark,
feedback_images=_parse_feedback_images(transport.feedback_images),
feedback_time=transport.feedback_time,
plan_voided=(
(plan is not None and plan.follow_status == "cancelled")
or transport.transport_status == "cancelled"
),
)
@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.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 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
transport.feedback_time = datetime.now()
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")
async 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 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 = await 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)}")