Files
shanxia2.0/backend/app/api/pl/public_site.py
T
2026-08-06 09:58:20 +08:00

279 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""运输计划 · 工厂现场人员免登录公共 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)}")