public_site.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. """运输计划 · 工厂现场人员免登录公共 API(pl_transport_site_link)"""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. from datetime import datetime
  6. from typing import Optional
  7. from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
  8. from sqlalchemy.orm import Session
  9. from app.core.attachment_biz import resolve_upload_folder
  10. from app.core.config import settings
  11. from app.core.database import get_db
  12. from app.core.response import error_response, success_response
  13. from app.core.storage import storage
  14. from app.crud import oss as oss_svc
  15. from app.models.pl_models import PlPlan, PlPlanProduct, PlTransportSiteLink
  16. from app.schemas.db.public_inventory import PublicInventoryFeedback
  17. from app.schemas.pl.transport import PublicTransportPlanProductRead, PublicTransportSitePageRead
  18. from app.services.db.mappers import inventory_date_to_api
  19. from app.utils.excel_export import load_dict_label_maps
  20. router = APIRouter(prefix="/public/pl/site", tags=["运输计划-现场公共"])
  21. CRM_PRODUCT_UNIT = "crm_product_unit"
  22. def _parse_feedback_images(raw: Optional[str]) -> list[str]:
  23. if not raw:
  24. return []
  25. try:
  26. parsed = json.loads(raw)
  27. if isinstance(parsed, list):
  28. return [str(item) for item in parsed if item]
  29. except (TypeError, json.JSONDecodeError):
  30. pass
  31. return [part.strip() for part in raw.split(",") if part.strip()]
  32. def _get_site_link_by_token(db: Session, token: str) -> PlTransportSiteLink:
  33. row = (
  34. db.query(PlTransportSiteLink)
  35. .filter(
  36. PlTransportSiteLink.site_access_token == token,
  37. PlTransportSiteLink.del_flag == 0,
  38. )
  39. .first()
  40. )
  41. if not row:
  42. raise HTTPException(status_code=404, detail="链接无效或已失效")
  43. return row
  44. def _resolve_plan_voided(db: Session, link: PlTransportSiteLink) -> bool:
  45. plan = db.query(PlPlan).filter(PlPlan.id == link.plan_id, PlPlan.del_flag == 0).first()
  46. return bool(plan and plan.follow_status == "cancelled")
  47. def _resolve_unit_label(unit_map: dict[str, str], unit_code: Optional[str]) -> str:
  48. code = (unit_code or "").strip()
  49. if not code:
  50. return "吨"
  51. return unit_map.get(code, code)
  52. def _resolve_plan_product(db: Session, link: PlTransportSiteLink) -> Optional[PlPlanProduct]:
  53. if not link.plan_product_id:
  54. return None
  55. return (
  56. db.query(PlPlanProduct)
  57. .filter(
  58. PlPlanProduct.id == link.plan_product_id,
  59. PlPlanProduct.del_flag == 0,
  60. )
  61. .first()
  62. )
  63. def _build_site_product_read(
  64. db: Session,
  65. link: PlTransportSiteLink,
  66. plan_product: Optional[PlPlanProduct],
  67. ) -> PublicTransportPlanProductRead:
  68. unit_map = load_dict_label_maps(
  69. db, [CRM_PRODUCT_UNIT], tenant_no=link.tenant_no
  70. ).get(CRM_PRODUCT_UNIT, {})
  71. quantity = link.quantity
  72. unit = "吨"
  73. remark = None
  74. if plan_product:
  75. quantity = (
  76. plan_product.quantity
  77. or plan_product.inbound_estimate_qty
  78. or plan_product.outbound_estimate_qty
  79. or link.quantity
  80. )
  81. unit = _resolve_unit_label(unit_map, plan_product.unit)
  82. remark = (plan_product.remark or "").strip() or None
  83. return PublicTransportPlanProductRead(
  84. product_name=link.product or (plan_product.product_name if plan_product else "") or "",
  85. model=link.specification or (plan_product.model if plan_product else None),
  86. package_spec=plan_product.package_spec if plan_product else None,
  87. unit=unit,
  88. quantity=quantity,
  89. remark=remark,
  90. )
  91. def _build_public_page(db: Session, link: PlTransportSiteLink) -> PublicTransportSitePageRead:
  92. voided = _resolve_plan_voided(db, link)
  93. plan = (
  94. db.query(PlPlan)
  95. .filter(PlPlan.id == link.plan_id, PlPlan.del_flag == 0)
  96. .first()
  97. )
  98. plan_product = _resolve_plan_product(db, link)
  99. product_read = _build_site_product_read(db, link, plan_product)
  100. return PublicTransportSitePageRead(
  101. business_type=link.business_type,
  102. date=inventory_date_to_api(link.plan_date) or None,
  103. partner_company=link.partner_company or "",
  104. vehicle_no=link.vehicle_no or "",
  105. plan_no=link.plan_no,
  106. remark=link.remark,
  107. pickup_address=plan.pickup_address if plan else None,
  108. pickup_lng=plan.pickup_lng if plan else None,
  109. pickup_lat=plan.pickup_lat if plan else None,
  110. pickup_time=plan.pickup_time if plan else None,
  111. pickup_notes=plan.pickup_notes if plan else None,
  112. destination_name=plan.destination_name if plan else None,
  113. destination_address=(
  114. (plan.destination_address or plan.destination_name) if plan else None
  115. ),
  116. destination_lng=plan.destination_lng if plan else None,
  117. destination_lat=plan.destination_lat if plan else None,
  118. delivery_time=plan.delivery_time if plan else None,
  119. destination_notes=plan.destination_notes if plan else None,
  120. products=[product_read],
  121. site_feedback_remark=link.site_feedback_remark,
  122. site_feedback_images=_parse_feedback_images(link.site_feedback_images),
  123. site_feedback_time=link.site_feedback_time,
  124. voided=voided,
  125. )
  126. @router.get("/{token}")
  127. def get_public_site(token: str, db: Session = Depends(get_db)):
  128. link = _get_site_link_by_token(db, token)
  129. page = _build_public_page(db, link)
  130. return success_response(page.model_dump(by_alias=True))
  131. @router.post("/{token}/feedback")
  132. def save_public_site_feedback(
  133. token: str,
  134. payload: PublicInventoryFeedback,
  135. db: Session = Depends(get_db),
  136. ):
  137. link = _get_site_link_by_token(db, token)
  138. page = _build_public_page(db, link)
  139. if page.voided:
  140. return error_response(400, "运输计划已作废,无法提交回执")
  141. if payload.site_feedback_images is not None:
  142. link.site_feedback_images = json.dumps(payload.site_feedback_images)
  143. if payload.site_feedback_remark is not None:
  144. link.site_feedback_remark = payload.site_feedback_remark
  145. link.site_feedback_time = datetime.now()
  146. db.commit()
  147. return success_response({"success": True, "message": "保存成功"})
  148. def _format_file_size(size_bytes: int) -> str:
  149. if size_bytes < 1024:
  150. return f"{size_bytes} B"
  151. if size_bytes < 1024 * 1024:
  152. return f"{size_bytes / 1024:.1f} KB"
  153. return f"{size_bytes / (1024 * 1024):.1f} MB"
  154. @router.post("/{token}/attachments")
  155. def upload_public_site_attachment(
  156. token: str,
  157. file: UploadFile = File(...),
  158. folder: Optional[str] = Form(""),
  159. db: Session = Depends(get_db),
  160. ):
  161. link = _get_site_link_by_token(db, token)
  162. page = _build_public_page(db, link)
  163. if page.voided:
  164. return error_response(400, "运输计划已作废,无法上传附件")
  165. if not file or not file.filename:
  166. return error_response(400, "请选择文件")
  167. try:
  168. effective_folder = resolve_upload_folder(
  169. folder=folder or "", biz_type="pl_transport_site_feedback"
  170. )
  171. file_content = file.file.read()
  172. full_url = storage.upload_file(file_content, file.filename, effective_folder)
  173. base_url = settings.MINIO_BASE_URL.rstrip("/") if settings.MINIO_BASE_URL else ""
  174. if base_url and full_url.startswith(base_url + "/"):
  175. file_url = full_url[len(base_url) + 1 :]
  176. else:
  177. file_url = full_url
  178. record = oss_svc.create_oss(
  179. db,
  180. tenant_no=link.tenant_no,
  181. url=file_url,
  182. original_name=file.filename,
  183. file_suffix=os.path.splitext(file.filename)[1].lstrip(".") if file.filename else "",
  184. file_size=len(file_content),
  185. file_type=file.content_type or "",
  186. biz_type="pl_transport_site_feedback",
  187. user_id=None,
  188. ext1=effective_folder or None,
  189. )
  190. db.commit()
  191. return success_response(
  192. {
  193. "id": record.id,
  194. "fileName": record.original_name,
  195. "fileUrl": record.url,
  196. "fileSize": record.file_size,
  197. "fileSizeDisplay": _format_file_size(record.file_size or 0),
  198. "fileType": record.file_type,
  199. }
  200. )
  201. except Exception as exc:
  202. db.rollback()
  203. return error_response(500, f"上传失败: {str(exc)}")