279 lines
9.8 KiB
Python
279 lines
9.8 KiB
Python
"""运输计划 · 工厂现场人员免登录公共 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.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/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_site_link_flags(plan: Optional[PlPlan]) -> tuple[bool, bool]:
|
||
"""返回 (作废, 归档)。"""
|
||
voided = bool(plan and plan.follow_status == "cancelled")
|
||
archived = bool(plan and plan.archive_status == "archived")
|
||
return voided, archived
|
||
|
||
|
||
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
|
||
product_id = ""
|
||
saved_name = (link.product or "").strip()
|
||
brand_mode = None
|
||
brand_id = None
|
||
brand_name = None
|
||
plan_product_id = ""
|
||
if plan_product:
|
||
plan_product_id = plan_product.id or ""
|
||
product_id = (plan_product.product_id or "").strip()
|
||
saved_name = (plan_product.product_name or saved_name or "").strip()
|
||
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
|
||
brand_mode = plan_product.brand_mode
|
||
brand_id = plan_product.brand_id
|
||
brand_name = plan_product.brand_name
|
||
|
||
standard_by_id, alias_options_by_id = load_crm_product_alias_maps(
|
||
db,
|
||
tenant_no=link.tenant_no,
|
||
product_ids={product_id} if product_id else set(),
|
||
)
|
||
display_name, alias_name = resolve_product_display_names(
|
||
saved_product_name=saved_name or link.product,
|
||
product_id=product_id,
|
||
standard_by_id=standard_by_id,
|
||
alias_options_by_id=alias_options_by_id,
|
||
)
|
||
return PublicTransportPlanProductRead(
|
||
plan_product_id=plan_product_id,
|
||
product_name=display_name or (link.product or ""),
|
||
alias_name=alias_name,
|
||
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,
|
||
brand_mode=brand_mode,
|
||
brand_id=brand_id,
|
||
brand_name=brand_name,
|
||
)
|
||
|
||
|
||
def _build_public_page(db: Session, link: PlTransportSiteLink) -> PublicTransportSitePageRead:
|
||
plan = (
|
||
db.query(PlPlan)
|
||
.filter(PlPlan.id == link.plan_id, PlPlan.del_flag == 0)
|
||
.first()
|
||
)
|
||
voided, archived = _resolve_site_link_flags(plan)
|
||
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_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 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,
|
||
delivery_contact=plan.delivery_contact 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,
|
||
archived=archived,
|
||
)
|
||
|
||
|
||
@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 page.archived:
|
||
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 page.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_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)}")
|