167 lines
5.7 KiB
Python
167 lines
5.7 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, PlTransportSiteLink
|
||
from app.schemas.db.public_inventory import PublicInventoryFeedback, PublicInventoryPageRead
|
||
from app.services.db.mappers import inventory_date_to_api
|
||
|
||
router = APIRouter(prefix="/public/pl/site", tags=["运输计划-现场公共"])
|
||
|
||
|
||
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 _build_public_page(db: Session, link: PlTransportSiteLink) -> PublicInventoryPageRead:
|
||
voided = _resolve_plan_voided(db, link)
|
||
return PublicInventoryPageRead(
|
||
batch_no=None,
|
||
business_type=link.business_type,
|
||
date=inventory_date_to_api(link.plan_date) or None,
|
||
partner_company=link.partner_company or "",
|
||
product=link.product or "",
|
||
specification=link.specification,
|
||
quantity=float(link.quantity or 0),
|
||
vehicle_no=link.vehicle_no or "",
|
||
plan_no=link.plan_no,
|
||
status=None,
|
||
remark=link.remark,
|
||
site_feedback_remark=link.site_feedback_remark,
|
||
site_feedback_images=_parse_feedback_images(link.site_feedback_images),
|
||
site_feedback_time=link.site_feedback_time,
|
||
read_only=False,
|
||
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")
|
||
async 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 = 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=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)}")
|