677 lines
22 KiB
Python
677 lines
22 KiB
Python
"""运输计划 · 现场按段页(history / page / feedback / upload)共享逻辑"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from typing import Any, Optional, Tuple
|
|
|
|
from fastapi import UploadFile
|
|
from sqlalchemy import and_, func, or_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.attachment_biz import resolve_upload_folder
|
|
from app.core.config import settings
|
|
from app.core.storage import storage
|
|
from app.crud import oss as oss_svc
|
|
from app.models.pl_models import (
|
|
PlPlan,
|
|
PlPlanDelivery,
|
|
PlPlanLoad,
|
|
PlPlanProduct,
|
|
PlPlanTransport,
|
|
PlTransport,
|
|
)
|
|
from app.schemas.pl.transport import (
|
|
PublicTransportHistoryPageRead,
|
|
PublicTransportHistoryProductRead,
|
|
PublicTransportHistoryRecordRead,
|
|
PublicTransportHistoryStopProductRead,
|
|
PublicTransportHistoryStopRead,
|
|
PublicTransportMultiStopSitePageRead,
|
|
)
|
|
from app.services.pl_product_display import (
|
|
load_crm_product_alias_maps,
|
|
resolve_product_display_names,
|
|
)
|
|
from app.services.pl_public_transport_helpers import (
|
|
CRM_PRODUCT_UNIT,
|
|
_accumulate_calendar_day_counts,
|
|
_history_status_label,
|
|
_normalize_main_plate,
|
|
_parse_calendar_month,
|
|
_parse_history_date,
|
|
_resolve_driver_link_flags,
|
|
_resolve_unit_label,
|
|
)
|
|
from app.services.pl_transport_stop_feedback_service import (
|
|
build_public_stop_reads_for_segments,
|
|
build_transport_site_url,
|
|
load_site_stop_segments,
|
|
save_site_stop_feedback,
|
|
)
|
|
from app.utils.excel_export import load_dict_label_maps
|
|
|
|
|
|
def plan_matches_history_day(plan: PlPlan, day: date) -> bool:
|
|
"""Mirrors SQL day filters in build_site_history_page for unit tests."""
|
|
if plan.plan_date == day:
|
|
return True
|
|
if plan.pickup_time is not None:
|
|
pt = plan.pickup_time
|
|
if (pt.date() if isinstance(pt, datetime) else pt) == day:
|
|
return True
|
|
if plan.delivery_time is not None:
|
|
dt = plan.delivery_time
|
|
if (dt.date() if isinstance(dt, datetime) else dt) == day:
|
|
return True
|
|
return False
|
|
|
|
|
|
def resolve_transport_for_plan(
|
|
db: Session, *, tenant_no: str, plan_id: str
|
|
) -> Tuple[Optional[PlPlan], Optional[PlTransport]]:
|
|
"""本租户计划 + 关联运输(须有有效 site_access_token)。找不到返回 (None, None)。"""
|
|
plan = (
|
|
db.query(PlPlan)
|
|
.filter(
|
|
PlPlan.id == plan_id,
|
|
PlPlan.tenant_no == tenant_no,
|
|
PlPlan.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if not plan:
|
|
return None, None
|
|
link = (
|
|
db.query(PlPlanTransport)
|
|
.filter(
|
|
PlPlanTransport.plan_id == plan.id,
|
|
PlPlanTransport.del_flag == 0,
|
|
)
|
|
.order_by(PlPlanTransport.create_time.desc())
|
|
.first()
|
|
)
|
|
if not link or not link.transport_id:
|
|
return plan, None
|
|
transport = (
|
|
db.query(PlTransport)
|
|
.filter(
|
|
PlTransport.id == link.transport_id,
|
|
PlTransport.tenant_no == tenant_no,
|
|
PlTransport.del_flag == 0,
|
|
PlTransport.site_access_token.isnot(None),
|
|
PlTransport.site_access_token != "",
|
|
)
|
|
.first()
|
|
)
|
|
return plan, transport
|
|
|
|
|
|
def _format_plan_date(value) -> Optional[str]:
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, datetime):
|
|
return value.strftime("%Y-%m-%d")
|
|
if isinstance(value, date):
|
|
return value.isoformat()
|
|
return str(value)
|
|
|
|
|
|
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"
|
|
|
|
|
|
def build_site_page_for_transport(
|
|
db: Session, transport: PlTransport
|
|
) -> PublicTransportMultiStopSitePageRead:
|
|
link = (
|
|
db.query(PlPlanTransport)
|
|
.filter(
|
|
PlPlanTransport.transport_id == transport.id,
|
|
PlPlanTransport.del_flag == 0,
|
|
)
|
|
.order_by(PlPlanTransport.create_time.desc())
|
|
.first()
|
|
)
|
|
plan = None
|
|
if link:
|
|
plan = (
|
|
db.query(PlPlan)
|
|
.filter(PlPlan.id == link.plan_id, PlPlan.del_flag == 0)
|
|
.first()
|
|
)
|
|
|
|
plan_voided, plan_archived = _resolve_driver_link_flags(plan, transport)
|
|
stop_reads: list[dict] = []
|
|
if plan:
|
|
segments = load_site_stop_segments(
|
|
db,
|
|
tenant_no=transport.tenant_no,
|
|
plan_id=plan.id,
|
|
transport_id=transport.id,
|
|
)
|
|
stop_reads = build_public_stop_reads_for_segments(
|
|
db,
|
|
tenant_no=transport.tenant_no,
|
|
plan_id=plan.id,
|
|
segments=segments,
|
|
)
|
|
|
|
return PublicTransportMultiStopSitePageRead(
|
|
plan_no=plan.plan_no if plan else transport.transport_no,
|
|
plan_date=_format_plan_date(plan.plan_date if plan else None),
|
|
license_plate=transport.license_plate,
|
|
vehicle_no=transport.license_plate,
|
|
plan_voided=plan_voided,
|
|
plan_archived=plan_archived,
|
|
stops=stop_reads,
|
|
)
|
|
|
|
|
|
def build_site_page_for_plan(
|
|
db: Session, *, tenant_no: str, plan_id: str
|
|
) -> tuple[Optional[PublicTransportMultiStopSitePageRead], Optional[str]]:
|
|
plan, transport = resolve_transport_for_plan(db, tenant_no=tenant_no, plan_id=plan_id)
|
|
if not plan:
|
|
return None, "计划不存在"
|
|
if not transport:
|
|
return None, "未找到关联运输或现场链接无效"
|
|
return build_site_page_for_transport(db, transport), None
|
|
|
|
|
|
def _build_site_history_stop_reads(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan: PlPlan,
|
|
transport: PlTransport,
|
|
) -> list[PublicTransportHistoryStopRead]:
|
|
plan_products = (
|
|
db.query(PlPlanProduct)
|
|
.filter(
|
|
PlPlanProduct.plan_id == plan.id,
|
|
PlPlanProduct.tenant_no == tenant_no,
|
|
PlPlanProduct.del_flag == 0,
|
|
)
|
|
.all()
|
|
)
|
|
plan_products_by_id = {row.id: row for row in plan_products}
|
|
crm_product_ids = {
|
|
(row.product_id or "").strip() for row in plan_products if (row.product_id or "").strip()
|
|
}
|
|
standard_by_id, alias_options_by_id = load_crm_product_alias_maps(
|
|
db, tenant_no=tenant_no, product_ids=crm_product_ids
|
|
)
|
|
unit_map = load_dict_label_maps(db, [CRM_PRODUCT_UNIT], tenant_no=tenant_no).get(
|
|
CRM_PRODUCT_UNIT, {}
|
|
)
|
|
|
|
load_row = (
|
|
db.query(PlPlanLoad)
|
|
.filter(
|
|
PlPlanLoad.plan_id == plan.id,
|
|
PlPlanLoad.tenant_no == tenant_no,
|
|
PlPlanLoad.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
delivery_rows = (
|
|
db.query(PlPlanDelivery)
|
|
.filter(
|
|
PlPlanDelivery.plan_id == plan.id,
|
|
PlPlanDelivery.tenant_no == tenant_no,
|
|
PlPlanDelivery.del_flag == 0,
|
|
)
|
|
.all()
|
|
)
|
|
delivery_time_by_id = {row.id: row.delivery_time for row in delivery_rows}
|
|
load_time = load_row.load_time if load_row else None
|
|
|
|
segments = load_site_stop_segments(
|
|
db,
|
|
tenant_no=tenant_no,
|
|
plan_id=plan.id,
|
|
transport_id=transport.id,
|
|
)
|
|
if not segments:
|
|
return []
|
|
|
|
reads: list[PublicTransportHistoryStopRead] = []
|
|
for segment in segments:
|
|
stop_time = load_time if segment.stop_type == "load" else delivery_time_by_id.get(segment.stop_id)
|
|
stop_products: list[PublicTransportHistoryStopProductRead] = []
|
|
for row in segment.products:
|
|
plan_prod = plan_products_by_id.get(row.plan_product_id)
|
|
product_id = (row.product_id or (plan_prod.product_id if plan_prod else "") or "").strip()
|
|
display_name, _alias_name = resolve_product_display_names(
|
|
saved_product_name=row.product_name,
|
|
product_id=product_id,
|
|
standard_by_id=standard_by_id,
|
|
alias_options_by_id=alias_options_by_id,
|
|
)
|
|
stop_products.append(
|
|
PublicTransportHistoryStopProductRead(
|
|
product_name=display_name or row.product_name or "—",
|
|
model=row.model,
|
|
package_spec=row.package_spec,
|
|
quantity=row.plan_qty,
|
|
unit=_resolve_unit_label(unit_map, row.unit),
|
|
)
|
|
)
|
|
reads.append(
|
|
PublicTransportHistoryStopRead(
|
|
stop_type=segment.stop_type,
|
|
seq=segment.seq,
|
|
label=segment.label,
|
|
customer_name=segment.customer_name,
|
|
address=segment.address,
|
|
stop_time=stop_time,
|
|
products=stop_products,
|
|
)
|
|
)
|
|
return reads
|
|
|
|
|
|
def build_site_history_page(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
date_str: str,
|
|
keyword: Optional[str] = None,
|
|
license_plate_hint: Optional[str] = None,
|
|
) -> tuple[Optional[PublicTransportHistoryPageRead], Optional[str]]:
|
|
day = _parse_history_date(date_str)
|
|
if day is None:
|
|
return None, "日期格式无效,请使用 YYYY-MM-DD"
|
|
|
|
kw = (keyword or "").strip()
|
|
query = (
|
|
db.query(PlPlan, PlTransport)
|
|
.join(
|
|
PlPlanTransport,
|
|
and_(
|
|
PlPlanTransport.plan_id == PlPlan.id,
|
|
PlPlanTransport.del_flag == 0,
|
|
),
|
|
)
|
|
.join(
|
|
PlTransport,
|
|
and_(
|
|
PlTransport.id == PlPlanTransport.transport_id,
|
|
PlTransport.del_flag == 0,
|
|
),
|
|
)
|
|
.filter(
|
|
PlPlan.tenant_no == tenant_no,
|
|
PlPlan.del_flag == 0,
|
|
PlPlan.transport_status == "arranged",
|
|
PlTransport.tenant_no == tenant_no,
|
|
PlTransport.site_access_token.isnot(None),
|
|
PlTransport.site_access_token != "",
|
|
or_(
|
|
PlPlan.plan_date == day,
|
|
and_(PlPlan.pickup_time.isnot(None), func.date(PlPlan.pickup_time) == day),
|
|
and_(PlPlan.delivery_time.isnot(None), func.date(PlPlan.delivery_time) == day),
|
|
),
|
|
)
|
|
)
|
|
if kw:
|
|
like = f"%{kw}%"
|
|
product_match = (
|
|
db.query(PlPlanProduct.id)
|
|
.filter(
|
|
PlPlanProduct.plan_id == PlPlan.id,
|
|
PlPlanProduct.del_flag == 0,
|
|
PlPlanProduct.product_name.like(like),
|
|
)
|
|
.exists()
|
|
)
|
|
query = query.filter(or_(PlTransport.license_plate.like(like), product_match))
|
|
|
|
rows = query.order_by(
|
|
PlPlan.pickup_time.desc(),
|
|
PlPlan.plan_date.desc(),
|
|
PlPlan.create_time.desc(),
|
|
).all()
|
|
|
|
seen_plan: set[str] = set()
|
|
plan_rows: list[tuple[PlPlan, PlTransport, list[PublicTransportHistoryStopRead]]] = []
|
|
for plan, tr in rows:
|
|
if plan.id in seen_plan:
|
|
continue
|
|
site_tok = (tr.site_access_token or "").strip()
|
|
if not site_tok:
|
|
continue
|
|
site_stops = _build_site_history_stop_reads(
|
|
db, tenant_no=tenant_no, plan=plan, transport=tr
|
|
)
|
|
if not site_stops:
|
|
continue
|
|
seen_plan.add(plan.id)
|
|
plan_rows.append((plan, tr, site_stops))
|
|
|
|
products_by_plan: dict[str, list[PublicTransportHistoryProductRead]] = {}
|
|
if plan_rows:
|
|
plan_ids = [plan.id for plan, _, _ in plan_rows]
|
|
product_rows = (
|
|
db.query(PlPlanProduct)
|
|
.filter(
|
|
PlPlanProduct.plan_id.in_(plan_ids),
|
|
PlPlanProduct.del_flag == 0,
|
|
)
|
|
.order_by(PlPlanProduct.order_num.asc(), PlPlanProduct.create_time.asc())
|
|
.all()
|
|
)
|
|
crm_product_ids = {
|
|
(p.product_id or "").strip() for p in product_rows if (p.product_id or "").strip()
|
|
}
|
|
standard_by_id, alias_options_by_id = load_crm_product_alias_maps(
|
|
db, tenant_no=tenant_no, product_ids=crm_product_ids
|
|
)
|
|
unit_map = load_dict_label_maps(db, [CRM_PRODUCT_UNIT], tenant_no=tenant_no).get(
|
|
CRM_PRODUCT_UNIT, {}
|
|
)
|
|
for prod in product_rows:
|
|
product_id = (prod.product_id or "").strip()
|
|
display_name, _alias_name = resolve_product_display_names(
|
|
saved_product_name=prod.product_name,
|
|
product_id=product_id,
|
|
standard_by_id=standard_by_id,
|
|
alias_options_by_id=alias_options_by_id,
|
|
)
|
|
products_by_plan.setdefault(prod.plan_id, []).append(
|
|
PublicTransportHistoryProductRead(
|
|
product_name=display_name or prod.product_name or "—",
|
|
model=prod.model,
|
|
package_spec=prod.package_spec,
|
|
quantity=prod.quantity or prod.inbound_estimate_qty or prod.outbound_estimate_qty,
|
|
unit=_resolve_unit_label(unit_map, prod.unit),
|
|
)
|
|
)
|
|
|
|
records: list[PublicTransportHistoryRecordRead] = []
|
|
for plan, tr, site_stops in plan_rows:
|
|
voided, archived = _resolve_driver_link_flags(plan, tr)
|
|
site_tok = (tr.site_access_token or "").strip()
|
|
records.append(
|
|
PublicTransportHistoryRecordRead(
|
|
plan_id=plan.id,
|
|
plan_no=plan.plan_no or "",
|
|
transport_id=tr.id,
|
|
site_access_token=site_tok or None,
|
|
site_url=build_transport_site_url(site_tok) if site_tok else None,
|
|
license_plate=tr.license_plate,
|
|
pickup_time=plan.pickup_time,
|
|
delivery_time=plan.delivery_time,
|
|
pickup_address=plan.pickup_address,
|
|
destination_address=plan.destination_address or plan.destination_name,
|
|
products=products_by_plan.get(plan.id) or [],
|
|
stops=site_stops,
|
|
plan_voided=voided,
|
|
plan_archived=archived,
|
|
status_label=_history_status_label(voided, archived),
|
|
)
|
|
)
|
|
|
|
page = PublicTransportHistoryPageRead(
|
|
license_plate=_normalize_main_plate(license_plate_hint) or None,
|
|
date=day.isoformat(),
|
|
keyword=kw or None,
|
|
records=records,
|
|
)
|
|
return page, None
|
|
|
|
|
|
def build_site_calendar_counts(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
month_str: str,
|
|
) -> tuple[Optional[dict[str, int]], Optional[str]]:
|
|
"""按月统计现场可见计划条数,键为 YYYY-MM-DD(不受列表关键字影响)。"""
|
|
bounds = _parse_calendar_month(month_str)
|
|
if bounds is None:
|
|
return None, "月份格式无效,请使用 YYYY-MM"
|
|
month_start, month_end = bounds
|
|
|
|
rows = (
|
|
db.query(PlPlan, PlTransport)
|
|
.join(
|
|
PlPlanTransport,
|
|
and_(
|
|
PlPlanTransport.plan_id == PlPlan.id,
|
|
PlPlanTransport.del_flag == 0,
|
|
),
|
|
)
|
|
.join(
|
|
PlTransport,
|
|
and_(
|
|
PlTransport.id == PlPlanTransport.transport_id,
|
|
PlTransport.del_flag == 0,
|
|
),
|
|
)
|
|
.filter(
|
|
PlPlan.tenant_no == tenant_no,
|
|
PlPlan.del_flag == 0,
|
|
PlPlan.transport_status == "arranged",
|
|
PlTransport.tenant_no == tenant_no,
|
|
PlTransport.site_access_token.isnot(None),
|
|
PlTransport.site_access_token != "",
|
|
or_(
|
|
and_(
|
|
PlPlan.plan_date.isnot(None),
|
|
PlPlan.plan_date >= month_start,
|
|
PlPlan.plan_date <= month_end,
|
|
),
|
|
and_(
|
|
PlPlan.pickup_time.isnot(None),
|
|
func.date(PlPlan.pickup_time) >= month_start,
|
|
func.date(PlPlan.pickup_time) <= month_end,
|
|
),
|
|
and_(
|
|
PlPlan.delivery_time.isnot(None),
|
|
func.date(PlPlan.delivery_time) >= month_start,
|
|
func.date(PlPlan.delivery_time) <= month_end,
|
|
),
|
|
),
|
|
)
|
|
.all()
|
|
)
|
|
|
|
seen: set[str] = set()
|
|
plans: list[PlPlan] = []
|
|
for plan, tr in rows:
|
|
if plan.id in seen:
|
|
continue
|
|
site_tok = (tr.site_access_token or "").strip()
|
|
if not site_tok:
|
|
continue
|
|
# 与 history 一致:无现场段则不计入
|
|
if not load_site_stop_segments(
|
|
db, tenant_no=tenant_no, plan_id=plan.id, transport_id=tr.id
|
|
):
|
|
continue
|
|
seen.add(plan.id)
|
|
plans.append(plan)
|
|
|
|
return (
|
|
_accumulate_calendar_day_counts(
|
|
plans,
|
|
month_start=month_start,
|
|
month_end=month_end,
|
|
include_plan_date=True,
|
|
),
|
|
None,
|
|
)
|
|
|
|
|
|
def save_site_feedback_for_transport(
|
|
db: Session,
|
|
transport: PlTransport,
|
|
*,
|
|
stop_type: str,
|
|
stop_id: str,
|
|
quantity: Optional[Decimal] = None,
|
|
attachment_urls: Optional[list[str]] = None,
|
|
remark: Optional[str] = None,
|
|
) -> tuple[bool, Optional[str]]:
|
|
"""保存段回传。成功返回 (True, None);失败返回 (False, msg)。不 commit。"""
|
|
page = build_site_page_for_transport(db, transport)
|
|
if page.plan_voided:
|
|
return False, "运输计划已作废,无法提交回执"
|
|
if page.plan_archived:
|
|
return False, "运输计划已归档,无法提交回执"
|
|
|
|
link = (
|
|
db.query(PlPlanTransport)
|
|
.filter(
|
|
PlPlanTransport.transport_id == transport.id,
|
|
PlPlanTransport.del_flag == 0,
|
|
)
|
|
.order_by(PlPlanTransport.create_time.desc())
|
|
.first()
|
|
)
|
|
if not link or not link.plan_id:
|
|
return False, "未找到关联计划,无法保存段回传"
|
|
|
|
stop_type_n = (stop_type or "").strip()
|
|
stop_id_n = (stop_id or "").strip()
|
|
if stop_type_n not in ("load", "delivery") or not stop_id_n:
|
|
return False, "停靠段参数无效"
|
|
|
|
segments = load_site_stop_segments(
|
|
db,
|
|
tenant_no=transport.tenant_no,
|
|
plan_id=link.plan_id,
|
|
transport_id=transport.id,
|
|
)
|
|
segment_map = {(item.stop_type, item.stop_id): item for item in segments}
|
|
segment = segment_map.get((stop_type_n, stop_id_n))
|
|
if not segment:
|
|
return False, "该段不在现场回传范围内"
|
|
|
|
try:
|
|
save_site_stop_feedback(
|
|
db,
|
|
tenant_no=transport.tenant_no,
|
|
transport_id=transport.id,
|
|
plan_id=link.plan_id,
|
|
stop_type=stop_type_n,
|
|
stop_id=stop_id_n,
|
|
business_type=segment.business_type or "入库",
|
|
quantity=quantity,
|
|
attachment_urls=attachment_urls,
|
|
remark=remark,
|
|
)
|
|
except ValueError as exc:
|
|
return False, str(exc)
|
|
|
|
return True, None
|
|
|
|
|
|
def save_site_feedback_for_plan(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan_id: str,
|
|
stop_type: str,
|
|
stop_id: str,
|
|
quantity: Optional[Decimal] = None,
|
|
attachment_urls: Optional[list[str]] = None,
|
|
remark: Optional[str] = None,
|
|
) -> tuple[bool, Optional[str]]:
|
|
plan, transport = resolve_transport_for_plan(db, tenant_no=tenant_no, plan_id=plan_id)
|
|
if not plan:
|
|
return False, "计划不存在"
|
|
if not transport:
|
|
return False, "未找到关联运输或现场链接无效"
|
|
return save_site_feedback_for_transport(
|
|
db,
|
|
transport,
|
|
stop_type=stop_type,
|
|
stop_id=stop_id,
|
|
quantity=quantity,
|
|
attachment_urls=attachment_urls,
|
|
remark=remark,
|
|
)
|
|
|
|
|
|
def upload_site_attachment_for_transport(
|
|
db: Session,
|
|
transport: PlTransport,
|
|
*,
|
|
file: UploadFile,
|
|
folder: Optional[str] = "",
|
|
) -> tuple[Optional[dict[str, Any]], Optional[tuple[int, str]]]:
|
|
"""上传附件。成功返回 (data, None) 并 commit;失败返回 (None, (code, msg))。"""
|
|
page = build_site_page_for_transport(db, transport)
|
|
if page.plan_voided:
|
|
return None, (400, "运输计划已作废,无法上传附件")
|
|
if page.plan_archived:
|
|
return None, (400, "运输计划已归档,无法上传附件")
|
|
if not file or not file.filename:
|
|
return None, (400, "请选择文件")
|
|
|
|
try:
|
|
effective_folder = resolve_upload_folder(
|
|
folder=folder or "", biz_type="pl_transport_site_stop_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=transport.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_stop_feedback",
|
|
user_id=None,
|
|
ext1=effective_folder or None,
|
|
)
|
|
db.commit()
|
|
return (
|
|
{
|
|
"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,
|
|
},
|
|
None,
|
|
)
|
|
except Exception as exc:
|
|
db.rollback()
|
|
return None, (500, f"上传失败: {str(exc)}")
|
|
|
|
|
|
def upload_site_attachment_for_plan(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
plan_id: str,
|
|
file: UploadFile,
|
|
folder: Optional[str] = "",
|
|
) -> tuple[Optional[dict[str, Any]], Optional[tuple[int, str]]]:
|
|
plan, transport = resolve_transport_for_plan(db, tenant_no=tenant_no, plan_id=plan_id)
|
|
if not plan:
|
|
return None, (400, "计划不存在")
|
|
if not transport:
|
|
return None, (400, "未找到关联运输或现场链接无效")
|
|
return upload_site_attachment_for_transport(
|
|
db, transport, file=file, folder=folder
|
|
)
|