from datetime import datetime from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session from backend.app.core.error_codes import ErrorCode from backend.app.core.exceptions import AppException from backend.app.repositories.logistics_repository import LogisticsRepository from backend.app.repositories.order_repository import OrderRepository from backend.app.services.arrears_service import arrears_service from backend.app.services.demo_store import demo_store class LogisticsService: def __init__(self) -> None: self.logistics_repository = LogisticsRepository() self.order_repository = OrderRepository() self.trace_records: dict[int, list[dict]] = {} def list_tasks( self, session: Session | None = None, filters: dict | None = None, current_user: dict | None = None, ) -> dict: if session is not None: try: tasks = self.logistics_repository.list_tasks_by_filters( session, self.normalize_task_filters(filters, current_user), ) return { "total": len(tasks), "page_no": 1, "page_size": 20, "list": [self._map_task_summary(task, session) for task in tasks], } except SQLAlchemyError: pass return demo_store.list_logistics_tasks() def create_task( self, payload: dict, session: Session | None = None, current_user: dict | None = None, ) -> dict: if session is not None: try: order = self.order_repository.get_order(session, payload["order_id"]) if order is None: raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404) if order.order_status not in {"pending_factory", "approved"}: raise AppException(code=ErrorCode.INVALID_STATUS, message="当前状态不允许创建司机任务", status_code=400) existing_task = self.logistics_repository.get_task_by_order_id(session, order.id) if existing_task is not None and existing_task.status != "canceled": raise AppException(code=ErrorCode.PARAM_ERROR, message="该订单已存在有效司机任务", status_code=400) task = self.logistics_repository.create_task( session, { "task_no": f"LT{datetime.now().strftime('%Y%m%d%H%M%S')}", "order_id": order.id, "driver_id": payload["driver_id"], "factory_id": payload.get("factory_id") or order.factory_id, "pickup_address": payload["pickup_address"], "delivery_address": payload["delivery_address"], "pickup_content": payload["pickup_content"], "quantity": payload["quantity"], "status": "pending", "created_by": current_user.get("user_id") if current_user else None, "remark": payload.get("remark"), }, ) # 司机任务创建后,订单进入待司机接单阶段。 self.order_repository.update_order_status(session, order, "pending_driver") session.commit() return self._map_task_detail(task, session) except SQLAlchemyError: session.rollback() return demo_store.create_logistics_task(payload) def get_task( self, task_id: int, session: Session | None = None, current_user: dict | None = None, ) -> dict: if session is not None: try: task = self.logistics_repository.get_task(session, task_id) if task is None: raise AppException(code=ErrorCode.NOT_FOUND, message="司机任务不存在", status_code=404) self._ensure_task_access(task, current_user) return self._map_task_detail(task, session) except SQLAlchemyError: pass task = demo_store.get_logistics_task(task_id) if task is None: raise AppException(code=ErrorCode.NOT_FOUND, message="司机任务不存在", status_code=404) return task def list_driver_tasks(self, session: Session | None = None, current_user: dict | None = None) -> dict: return self.list_tasks(session=session, filters={}, current_user=current_user) def get_driver_task(self, task_id: int, session: Session | None = None, current_user: dict | None = None) -> dict: return self.get_task(task_id, session=session, current_user=current_user) def accept_task( self, task_id: int, payload: dict | None = None, session: Session | None = None, current_user: dict | None = None, ) -> dict: result = self._change_task_status(task_id, "pending", "accepted", "accepted", session, current_user) self._append_trace( result["order_id"], { "task_id": task_id, "node_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "node_desc": "司机已接单", "node_type": "accepted", "source_platform": "driver", "remark": (payload or {}).get("remark"), }, ) return result def pickup_task( self, task_id: int, payload: dict | None = None, session: Session | None = None, current_user: dict | None = None, ) -> dict: result = self._change_task_status(task_id, "accepted", "picked_up", "picked_up", session, current_user) self._append_trace( result["order_id"], { "task_id": task_id, "node_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "node_desc": "司机已揽货", "node_type": "picked_up", "source_platform": "driver", "remark": (payload or {}).get("remark"), "photo_files": (payload or {}).get("photo_files", []), "video_files": (payload or {}).get("video_files", []), }, ) return result def deliver_task( self, task_id: int, payload: dict | None = None, session: Session | None = None, current_user: dict | None = None, ) -> dict: result = self._change_task_status(task_id, "picked_up", "delivered", "delivered", session, current_user) self._append_trace(result["order_id"], self._build_trace_payload(task_id, "司机已送达", "delivered", payload)) return result def get_trace( self, order_id: int, session: Session | None = None, current_user: dict | None = None, ) -> dict: if session is not None: try: self._ensure_trace_access(order_id, session, current_user) traces = self.logistics_repository.list_traces_by_order_id(session, order_id) if traces: return { "order_id": order_id, "trace_list": [ { "task_id": item.task_id, "node_time": item.node_time.strftime("%Y-%m-%d %H:%M:%S") if item.node_time else "", "node_desc": item.node_desc, "node_type": item.node_type, "source_platform": item.source_platform, "remark": item.remark, } for item in traces ], } except SQLAlchemyError: pass return { "order_id": order_id, "trace_list": self.trace_records.get( order_id, [ { "node_time": "2026-05-14 12:00:00", "node_desc": "司机已揽货", "node_type": "picked_up", "source_platform": "manual", } ], ), } def create_trace( self, order_id: int, payload: dict, session: Session | None = None, current_user: dict | None = None, ) -> dict: if session is not None: try: self._ensure_trace_access(order_id, session, current_user) node_time = datetime.strptime(payload["node_time"], "%Y-%m-%d %H:%M:%S") self.logistics_repository.create_trace( session, { "order_id": order_id, "task_id": payload.get("task_id"), "node_time": node_time, "node_desc": payload["node_desc"], "node_type": payload.get("node_type"), "source_platform": payload.get("source_platform"), "remark": payload.get("remark"), }, ) session.commit() return {"order_id": order_id, "created": True} except ValueError as exc: session.rollback() raise AppException(code=ErrorCode.PARAM_ERROR, message="物流节点时间格式错误", status_code=400) from exc except SQLAlchemyError: session.rollback() self._append_trace(order_id, payload) return {"order_id": order_id, "created": True} def _change_task_status( self, task_id: int, current_status: str, target_status: str, order_status: str, session: Session | None = None, current_user: dict | None = None, ) -> dict: if session is not None: try: task = self.logistics_repository.get_task(session, task_id) if task is None: raise AppException(code=ErrorCode.NOT_FOUND, message="司机任务不存在", status_code=404) self._ensure_task_access(task, current_user) if task.status != current_status: raise AppException(code=ErrorCode.INVALID_STATUS, message="当前任务状态不允许执行该操作", status_code=400) self.logistics_repository.update_task_status(session, task, target_status) order = self.order_repository.get_order(session, task.order_id) if order is not None: self.order_repository.update_order_status(session, order, order_status) if target_status in {"accepted", "picked_up", "delivered"}: self.logistics_repository.create_trace( session, { "order_id": task.order_id, "task_id": task.id, "node_time": datetime.now(), "node_desc": self._status_desc(target_status), "node_type": target_status, "source_platform": "driver", "remark": None, }, ) if target_status == "delivered": arrears_service.sync_order_arrears(session, order) session.commit() return {"task_id": task.id, "status": task.status, "order_id": task.order_id} except SQLAlchemyError: session.rollback() task = demo_store.get_logistics_task(task_id) return { "task_id": task_id, "status": target_status, "order_id": task["order_id"] if task else 0, } def normalize_task_filters(self, filters: dict | None, current_user: dict | None = None) -> dict: normalized = dict(filters or {}) if current_user and current_user.get("role_code") == "driver": normalized["driver_id"] = current_user.get("user_id") return normalized def _ensure_task_access(self, task: object, current_user: dict | None) -> None: if not current_user: return role_code = current_user.get("role_code") if role_code in {"admin", "manager"}: return if role_code == "driver": if task.driver_id != current_user.get("user_id"): raise AppException(code=ErrorCode.FORBIDDEN, message="仅可访问本人司机任务", status_code=403) return raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403) def _ensure_trace_access(self, order_id: int, session: Session, current_user: dict | None) -> None: if not current_user: return role_code = current_user.get("role_code") if role_code in {"admin", "manager"}: return if role_code == "driver": task = self.logistics_repository.get_task_by_order_id(session, order_id) if task is None or task.driver_id != current_user.get("user_id"): raise AppException(code=ErrorCode.FORBIDDEN, message="仅可访问本人订单轨迹", status_code=403) return raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403) def _map_task_summary(self, task, session: Session) -> dict: order = self.order_repository.get_order(session, task.order_id) supplier = self.order_repository.get_supplier(session, task.factory_id) return { "task_id": task.id, "task_no": task.task_no, "order_id": task.order_id, "order_no": order.order_no if order else "", "driver_id": task.driver_id, "driver_name": f"司机{task.driver_id}", "factory_id": task.factory_id, "factory_name": supplier.supplier_name if supplier else "", "pickup_address": task.pickup_address, "delivery_address": task.delivery_address, "pickup_content": task.pickup_content, "quantity": float(task.quantity or 0), "status": task.status, "remark": task.remark, "created_at": task.created_at.strftime("%Y-%m-%d %H:%M:%S") if task.created_at else "", } def _map_task_detail(self, task, session: Session) -> dict: summary = self._map_task_summary(task, session) order = self.order_repository.get_order(session, task.order_id) summary["salesman_name"] = "" summary["customer_name"] = order.customer_name if order else "" return summary def _append_trace(self, order_id: int, payload: dict) -> None: trace_list = self.trace_records.setdefault(order_id, []) trace_list.append(payload) def _status_desc(self, status: str) -> str: if status == "accepted": return "司机已接单" if status == "picked_up": return "司机已揽货" if status == "delivered": return "司机已送达" return "物流状态更新" def _build_trace_payload(self, task_id: int, node_desc: str, node_type: str, payload: dict | None) -> dict: return { "task_id": task_id, "node_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "node_desc": node_desc, "node_type": node_type, "source_platform": "driver", "remark": (payload or {}).get("remark"), "photo_files": (payload or {}).get("photo_files", []), "video_files": (payload or {}).get("video_files", []), } logistics_service = LogisticsService()