dingdanquanliucheng/backend/app/repositories/logistics_repository.py
taiyi a5edb8be4d feat: 司机端拍照识别物流运单号并支持一对多录入
新增 logistics_waybill 表存储一个任务关联的多个运单号,
司机在揽货时可通过拍照调用快递100 OCR 自动识别运单号,
支持连续拍多张统一提交、手动输入兜底、编辑删除,
揽货时必填至少一个运单号并自动同步到 logistics_task。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 14:03:36 +08:00

179 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
物流数据访问层。
负责封装物流任务LogisticsTask和物流跟踪记录LogisticsTrace的数据库查询操作
提供物流任务的条件筛选、创建、状态更新,以及物流节点跟踪记录的查询和创建等功能。
被 OrderService、LogisticsService 调用。
"""
from sqlalchemy import select
from sqlalchemy.orm import Session
from backend.app.models.business import LogisticsTask, LogisticsTrace, LogisticsWaybill
class LogisticsRepository:
"""物流数据访问层,封装物流任务和物流跟踪表的数据库操作。
被 OrderService、LogisticsService 调用。
"""
def list_tasks_by_filters(self, session: Session, filters: dict) -> list[LogisticsTask]:
"""根据筛选条件查询物流任务列表,支持按订单 ID、任务号、状态、司机、工厂筛选。
:param session: 数据库会话
:param filters: 筛选条件字典,可包含 order_id、task_no模糊
status、driver_id、factory_id 等键
:return: 符合条件的物流任务列表,按 id 降序排列
被 LogisticsService.list_tasks 调用。
"""
stmt = select(LogisticsTask).where(LogisticsTask.deleted == 0)
if filters.get("order_id") is not None:
stmt = stmt.where(LogisticsTask.order_id == filters["order_id"])
if filters.get("task_no"):
stmt = stmt.where(LogisticsTask.task_no.contains(filters["task_no"]))
if filters.get("status"):
stmt = stmt.where(LogisticsTask.status == filters["status"])
if filters.get("driver_id") is not None:
stmt = stmt.where(LogisticsTask.driver_id == filters["driver_id"])
if filters.get("factory_id") is not None:
stmt = stmt.where(LogisticsTask.factory_id == filters["factory_id"])
stmt = stmt.order_by(LogisticsTask.id.desc())
return list(session.execute(stmt).scalars())
def get_task(self, session: Session, task_id: int) -> LogisticsTask | None:
"""根据 ID 获取单个物流任务详情。
:param session: 数据库会话
:param task_id: 物流任务主键 ID
:return: 物流任务对象,不存在则返回 None
被 LogisticsService.get_task 调用。
"""
stmt = select(LogisticsTask).where(LogisticsTask.id == task_id, LogisticsTask.deleted == 0)
return session.execute(stmt).scalar_one_or_none()
def get_task_by_order_id(self, session: Session, order_id: int) -> LogisticsTask | None:
"""根据订单 ID 获取关联的最新物流任务(按 id 降序取第一条)。
:param session: 数据库会话
:param order_id: 订单主键 ID
:return: 最新的物流任务对象,不存在则返回 None
被 OrderService 在查询订单详情时关联获取物流信息调用。
"""
stmt = (
select(LogisticsTask)
.where(LogisticsTask.order_id == order_id, LogisticsTask.deleted == 0)
.order_by(LogisticsTask.id.desc())
)
return session.execute(stmt).scalar_one_or_none()
def create_task(self, session: Session, payload: dict) -> LogisticsTask:
"""创建新的物流任务记录。
:param session: 数据库会话
:param payload: 物流任务字段字典
:return: 新创建的物流任务对象(含自增 ID
被 LogisticsService.create_task 调用。
"""
task = LogisticsTask(**payload)
session.add(task)
session.flush()
return task
def update_task_status(self, session: Session, task: LogisticsTask, status: str) -> LogisticsTask:
"""更新物流任务的状态。
:param session: 数据库会话
:param task: 物流任务对象
:param status: 目标状态值
:return: 更新后的物流任务对象
被 LogisticsService 中的状态流转方法调用。
"""
task.status = status
session.add(task)
session.flush()
return task
def list_traces_by_order_id(self, session: Session, order_id: int) -> list[LogisticsTrace]:
"""根据订单 ID 查询该订单的所有物流跟踪节点记录,按节点时间和 id 升序排列。
:param session: 数据库会话
:param order_id: 订单主键 ID
:return: 物流跟踪记录列表
被 LogisticsService.list_traces、OrderService.get_order 调用,
用于展示物流轨迹时间线。
"""
stmt = (
select(LogisticsTrace)
.where(LogisticsTrace.order_id == order_id)
.order_by(LogisticsTrace.node_time.asc(), LogisticsTrace.id.asc())
)
return list(session.execute(stmt).scalars())
def create_trace(self, session: Session, payload: dict) -> LogisticsTrace:
"""创建一条物流跟踪节点记录。
:param session: 数据库会话
:param payload: 物流跟踪记录字段字典
:return: 新创建的跟踪记录对象
被 LogisticsService.create_trace 调用,用于记录物流各节点信息。
"""
trace = LogisticsTrace(**payload)
session.add(trace)
session.flush()
return trace
def list_waybills_by_task_id(self, session: Session, task_id: int) -> list[LogisticsWaybill]:
"""查询指定任务的所有运单号记录。
:param session: 数据库会话
:param task_id: 物流任务 ID
:return: 运单号列表,按 id 升序排列
被 LogisticsService.list_waybills 调用。
"""
stmt = (
select(LogisticsWaybill)
.where(LogisticsWaybill.task_id == task_id, LogisticsWaybill.deleted == 0)
.order_by(LogisticsWaybill.id.asc())
)
return list(session.execute(stmt).scalars())
def create_waybill(self, session: Session, payload: dict) -> LogisticsWaybill:
"""创建运单号记录。
:param session: 数据库会话
:param payload: 运单号字段字典
:return: 新创建的运单号对象
被 LogisticsService.submit_waybills 调用。
"""
waybill = LogisticsWaybill(**payload)
session.add(waybill)
session.flush()
return waybill
def delete_waybill(self, session: Session, waybill_id: int, task_id: int) -> bool:
"""软删除单条运单号记录。
校验 task_id 防止越权删除其他任务的运单号。
:param session: 数据库会话
:param waybill_id: 运单号记录 ID
:param task_id: 物流任务 ID用于校验归属
:return: 删除成功返回 True
被 LogisticsService.delete_waybill 调用。
"""
stmt = select(LogisticsWaybill).where(
LogisticsWaybill.id == waybill_id,
LogisticsWaybill.task_id == task_id,
LogisticsWaybill.deleted == 0,
)
waybill = session.execute(stmt).scalar_one_or_none()
if waybill is None:
return False
waybill.deleted = 1
session.add(waybill)
session.flush()
return True