| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- """运输计划 · 工厂现场人员免登录公共 API(pl_transport_site_link)"""
- from __future__ import annotations
- import json
- import os
- from datetime import datetime
- from typing import Optional
- from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
- from sqlalchemy.orm import Session
- 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.core.storage import storage
- from app.crud import oss as oss_svc
- from app.models.pl_models import PlPlan, PlPlanProduct, PlTransportSiteLink
- from app.schemas.db.public_inventory import PublicInventoryFeedback
- from app.schemas.pl.transport import PublicTransportPlanProductRead, PublicTransportSitePageRead
- from app.services.db.mappers import inventory_date_to_api
- from app.utils.excel_export import load_dict_label_maps
- router = APIRouter(prefix="/public/pl/site", tags=["运输计划-现场公共"])
- CRM_PRODUCT_UNIT = "crm_product_unit"
- 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 _get_site_link_by_token(db: Session, token: str) -> PlTransportSiteLink:
- row = (
- db.query(PlTransportSiteLink)
- .filter(
- PlTransportSiteLink.site_access_token == token,
- PlTransportSiteLink.del_flag == 0,
- )
- .first()
- )
- if not row:
- raise HTTPException(status_code=404, detail="链接无效或已失效")
- return row
- def _resolve_plan_voided(db: Session, link: PlTransportSiteLink) -> bool:
- plan = db.query(PlPlan).filter(PlPlan.id == link.plan_id, PlPlan.del_flag == 0).first()
- return bool(plan and plan.follow_status == "cancelled")
- 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 _resolve_plan_product(db: Session, link: PlTransportSiteLink) -> Optional[PlPlanProduct]:
- if not link.plan_product_id:
- return None
- return (
- db.query(PlPlanProduct)
- .filter(
- PlPlanProduct.id == link.plan_product_id,
- PlPlanProduct.del_flag == 0,
- )
- .first()
- )
- def _build_site_product_read(
- db: Session,
- link: PlTransportSiteLink,
- plan_product: Optional[PlPlanProduct],
- ) -> PublicTransportPlanProductRead:
- unit_map = load_dict_label_maps(
- db, [CRM_PRODUCT_UNIT], tenant_no=link.tenant_no
- ).get(CRM_PRODUCT_UNIT, {})
- quantity = link.quantity
- unit = "吨"
- remark = None
- if plan_product:
- quantity = (
- plan_product.quantity
- or plan_product.inbound_estimate_qty
- or plan_product.outbound_estimate_qty
- or link.quantity
- )
- unit = _resolve_unit_label(unit_map, plan_product.unit)
- remark = (plan_product.remark or "").strip() or None
- return PublicTransportPlanProductRead(
- product_name=link.product or (plan_product.product_name if plan_product else "") or "",
- model=link.specification or (plan_product.model if plan_product else None),
- package_spec=plan_product.package_spec if plan_product else None,
- unit=unit,
- quantity=quantity,
- remark=remark,
- )
- def _build_public_page(db: Session, link: PlTransportSiteLink) -> PublicTransportSitePageRead:
- voided = _resolve_plan_voided(db, link)
- plan = (
- db.query(PlPlan)
- .filter(PlPlan.id == link.plan_id, PlPlan.del_flag == 0)
- .first()
- )
- plan_product = _resolve_plan_product(db, link)
- product_read = _build_site_product_read(db, link, plan_product)
- return PublicTransportSitePageRead(
- business_type=link.business_type,
- date=inventory_date_to_api(link.plan_date) or None,
- partner_company=link.partner_company or "",
- vehicle_no=link.vehicle_no or "",
- plan_no=link.plan_no,
- remark=link.remark,
- pickup_address=plan.pickup_address if plan else None,
- 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_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 None
- ),
- 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,
- destination_notes=plan.destination_notes if plan else None,
- products=[product_read],
- site_feedback_remark=link.site_feedback_remark,
- site_feedback_images=_parse_feedback_images(link.site_feedback_images),
- site_feedback_time=link.site_feedback_time,
- voided=voided,
- )
- @router.get("/{token}")
- def get_public_site(token: str, db: Session = Depends(get_db)):
- link = _get_site_link_by_token(db, token)
- page = _build_public_page(db, link)
- return success_response(page.model_dump(by_alias=True))
- @router.post("/{token}/feedback")
- def save_public_site_feedback(
- token: str,
- payload: PublicInventoryFeedback,
- db: Session = Depends(get_db),
- ):
- link = _get_site_link_by_token(db, token)
- page = _build_public_page(db, link)
- if page.voided:
- return error_response(400, "运输计划已作废,无法提交回执")
- if payload.site_feedback_images is not None:
- link.site_feedback_images = json.dumps(payload.site_feedback_images)
- if payload.site_feedback_remark is not None:
- link.site_feedback_remark = payload.site_feedback_remark
- link.site_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")
- def upload_public_site_attachment(
- token: str,
- file: UploadFile = File(...),
- folder: Optional[str] = Form(""),
- db: Session = Depends(get_db),
- ):
- link = _get_site_link_by_token(db, token)
- page = _build_public_page(db, link)
- if page.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_site_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=link.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_site_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)}")
|