Expose a read-only payload for the shared weighbridge QR card without auth. Co-authored-by: Cursor <cursoragent@cursor.com>
207 lines
7.4 KiB
Python
207 lines
7.4 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.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.db_models import DbInboundOutbound
|
|
from app.models.pl_models import PlPlan, PlTransportDbLink
|
|
from app.schemas.db.public_inventory import PublicInventoryFeedback, PublicInventoryPageRead
|
|
from app.services.db.mappers import inventory_date_to_api
|
|
from app.services.db.pound_qr_public import build_public_pound_qr_read
|
|
|
|
router = APIRouter(prefix="/public/db/inventory", tags=["地磅-出入库现场公共"])
|
|
|
|
COMPLETED_STATUS = "已完成"
|
|
|
|
|
|
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_inventory_by_token(db: Session, token: str) -> DbInboundOutbound:
|
|
row = (
|
|
db.query(DbInboundOutbound)
|
|
.filter(
|
|
DbInboundOutbound.site_access_token == token,
|
|
DbInboundOutbound.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="链接无效或已失效")
|
|
return row
|
|
|
|
|
|
def _resolve_plan_voided(db: Session, inventory: DbInboundOutbound) -> bool:
|
|
link = (
|
|
db.query(PlTransportDbLink)
|
|
.filter(
|
|
PlTransportDbLink.db_inventory_id == inventory.id,
|
|
PlTransportDbLink.del_flag == 0,
|
|
)
|
|
.order_by(PlTransportDbLink.create_time.desc())
|
|
.first()
|
|
)
|
|
if not link:
|
|
return False
|
|
plan = db.query(PlPlan).filter(PlPlan.id == link.plan_id, PlPlan.del_flag == 0).first()
|
|
return bool(plan and plan.follow_status == "cancelled")
|
|
|
|
|
|
def _assert_editable(inventory: DbInboundOutbound, voided: bool) -> None:
|
|
if voided:
|
|
raise HTTPException(status_code=400, detail="运输计划已作废,无法提交回执")
|
|
if (inventory.status or "").strip() == COMPLETED_STATUS:
|
|
raise HTTPException(status_code=400, detail="单据已完成,无法修改回执")
|
|
|
|
|
|
def _build_public_page(db: Session, inventory: DbInboundOutbound) -> PublicInventoryPageRead:
|
|
voided = _resolve_plan_voided(db, inventory)
|
|
read_only = (inventory.status or "").strip() == COMPLETED_STATUS
|
|
return PublicInventoryPageRead(
|
|
batch_no=inventory.batch_no,
|
|
business_type=inventory.business_type,
|
|
date=inventory_date_to_api(inventory.date) or None,
|
|
partner_company=inventory.partner_company,
|
|
product=inventory.product,
|
|
specification=inventory.specification,
|
|
quantity=float(inventory.quantity or 0),
|
|
vehicle_no=inventory.vehicle_no or "",
|
|
plan_no=inventory.plan_no,
|
|
status=inventory.status,
|
|
remark=inventory.remark,
|
|
site_feedback_remark=inventory.site_feedback_remark,
|
|
site_feedback_images=_parse_feedback_images(inventory.site_feedback_images),
|
|
site_feedback_time=inventory.site_feedback_time,
|
|
read_only=read_only,
|
|
voided=voided,
|
|
)
|
|
|
|
|
|
@router.get("/{inventory_id}/pound-qr")
|
|
def get_public_pound_qr(inventory_id: str, db: Session = Depends(get_db)):
|
|
row = (
|
|
db.query(DbInboundOutbound)
|
|
.filter(DbInboundOutbound.id == inventory_id, DbInboundOutbound.del_flag == 0)
|
|
.first()
|
|
)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="链接无效或单据不存在")
|
|
read = build_public_pound_qr_read(row, quantity_unit="吨")
|
|
return success_response(read.model_dump(by_alias=True))
|
|
|
|
|
|
@router.get("/{token}")
|
|
def get_public_inventory(token: str, db: Session = Depends(get_db)):
|
|
inventory = _get_inventory_by_token(db, token)
|
|
page = _build_public_page(db, inventory)
|
|
return success_response(page.model_dump(by_alias=True))
|
|
|
|
|
|
@router.post("/{token}/feedback")
|
|
def save_public_feedback(
|
|
token: str,
|
|
payload: PublicInventoryFeedback,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
inventory = _get_inventory_by_token(db, token)
|
|
page = _build_public_page(db, inventory)
|
|
if page.voided:
|
|
return error_response(400, "运输计划已作废,无法提交回执")
|
|
if page.read_only:
|
|
return error_response(400, "单据已完成,无法修改回执")
|
|
|
|
if payload.site_feedback_images is not None:
|
|
inventory.site_feedback_images = json.dumps(payload.site_feedback_images)
|
|
|
|
if payload.site_feedback_remark is not None:
|
|
inventory.site_feedback_remark = payload.site_feedback_remark
|
|
|
|
inventory.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_attachment(
|
|
token: str,
|
|
file: UploadFile = File(...),
|
|
folder: Optional[str] = Form(""),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
inventory = _get_inventory_by_token(db, token)
|
|
page = _build_public_page(db, inventory)
|
|
if page.voided:
|
|
return error_response(400, "运输计划已作废,无法上传附件")
|
|
if page.read_only:
|
|
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="db_inventory_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=inventory.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="db_inventory_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)}")
|