| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- """地磅出入库单 · 工厂现场人员免登录公共 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)}")
|