delivery_warning.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. """配送时效预警定时任务
  2. 依据订单分配时写入的配送截止时间(biz_delivery_task.effective_time)及区域表 sys_region.delivery_warn_advance_minutes(缺省 30 分钟)生成运营端预警:
  3. - 进入「截止时间前 N 分钟」窗口且仍未完成配送:生成一条 advance_deadline 预警;
  4. - 已超过截止时间仍未完成:生成 timeout 预警;
  5. - 配送员确认送达(任务状态 delivering)或任务取消/完成时,由业务侧自动结案预警。
  6. 建议:每 1~5 分钟执行一次 check_delivery_warnings(db)。
  7. """
  8. import logging
  9. from datetime import datetime
  10. from typing import Optional
  11. from sqlalchemy.orm import Session
  12. from app.models.models import BizDeliveryTask, BizDeliveryWarning, SysRegion
  13. from app.core.utils import generate_id
  14. from app.crud.delivery_warning_resolve import auto_resolve_open_warnings_for_task
  15. logger = logging.getLogger(__name__)
  16. DEFAULT_ADVANCE_MINUTES = 30
  17. class DeliveryWarningTask:
  18. """配送时效预警任务"""
  19. def __init__(self, db: Session):
  20. self.db = db
  21. def check_delivery_warnings(self) -> dict:
  22. """检查配送任务并生成预警"""
  23. result = {
  24. "checked": 0,
  25. "warnings_created": 0,
  26. "notifications_sent": 0,
  27. "errors": []
  28. }
  29. try:
  30. tasks = (
  31. self.db.query(BizDeliveryTask)
  32. .filter(BizDeliveryTask.is_del == 0)
  33. .all()
  34. )
  35. result["checked"] = len(tasks)
  36. for task in tasks:
  37. try:
  38. if task.status in ("delivering", "completed", "cancelled"):
  39. auto_resolve_open_warnings_for_task(self.db, task.id)
  40. continue
  41. if task.status not in ("pending", "assigned", "picking"):
  42. continue
  43. warnings = self._check_task_warnings(task)
  44. result["warnings_created"] += len(warnings)
  45. for warning in warnings:
  46. self._send_warning_notification(warning)
  47. result["notifications_sent"] += 1
  48. except Exception as e:
  49. logger.error(f"处理任务 {task.id} 时出错: {str(e)}")
  50. result["errors"].append({
  51. "task_id": task.id,
  52. "error": str(e)
  53. })
  54. self.db.commit()
  55. except Exception as e:
  56. logger.error(f"配送时效预警任务执行失败: {str(e)}")
  57. self.db.rollback()
  58. result["errors"].append({"task_id": "all", "error": str(e)})
  59. return result
  60. def _region_advance_minutes(self, region_id: str) -> int:
  61. reg = self.db.query(SysRegion).filter(SysRegion.id == region_id, SysRegion.is_del == 0).first()
  62. if not reg:
  63. return DEFAULT_ADVANCE_MINUTES
  64. m = getattr(reg, "delivery_warn_advance_minutes", None)
  65. if m is None or int(m) < 1:
  66. return DEFAULT_ADVANCE_MINUTES
  67. return int(m)
  68. def _check_task_warnings(self, task: BizDeliveryTask) -> list:
  69. warnings = []
  70. if not task.effective_time:
  71. return warnings
  72. now = datetime.now()
  73. remaining_minutes = int((task.effective_time - now).total_seconds() // 60)
  74. advance = self._region_advance_minutes(task.region_id)
  75. if remaining_minutes < 0:
  76. if not self._get_existing_unresolved_warning(task.id, "timeout"):
  77. w = self._create_warning(
  78. task,
  79. "timeout",
  80. "已超时",
  81. remaining_minutes,
  82. f"已超过配送时效 {abs(remaining_minutes)} 分钟,仍未完成配送",
  83. )
  84. if w:
  85. warnings.append(w)
  86. return warnings
  87. if remaining_minutes <= advance:
  88. if not self._get_existing_unresolved_warning(task.id, "advance_deadline"):
  89. w = self._create_warning(
  90. task,
  91. "advance_deadline",
  92. "时效预警",
  93. remaining_minutes,
  94. f"距配送截止不足 {advance} 分钟(剩余约 {remaining_minutes} 分钟),请督促配送",
  95. )
  96. if w:
  97. warnings.append(w)
  98. return warnings
  99. def _create_warning(
  100. self,
  101. task: BizDeliveryTask,
  102. warning_type: str,
  103. urgency_level: str,
  104. remaining_minutes: int,
  105. message: str
  106. ) -> Optional[BizDeliveryWarning]:
  107. """创建预警记录"""
  108. try:
  109. warning = BizDeliveryWarning(
  110. id=generate_id(),
  111. task_id=task.id,
  112. order_id=task.order_id,
  113. warning_type=warning_type,
  114. urgency_level=urgency_level,
  115. remaining_minutes=remaining_minutes,
  116. message=message,
  117. is_sent=0,
  118. is_resolved=0,
  119. create_by="system",
  120. create_name="系统"
  121. )
  122. self.db.add(warning)
  123. self.db.flush()
  124. return warning
  125. except Exception as e:
  126. logger.error(f"创建预警记录失败: {str(e)}")
  127. return None
  128. def _get_existing_unresolved_warning(self, task_id: str, warning_type: str) -> Optional[BizDeliveryWarning]:
  129. return self.db.query(BizDeliveryWarning).filter(
  130. BizDeliveryWarning.task_id == task_id,
  131. BizDeliveryWarning.warning_type == warning_type,
  132. BizDeliveryWarning.is_del == 0,
  133. BizDeliveryWarning.is_resolved == 0,
  134. ).first()
  135. def _send_warning_notification(self, warning: BizDeliveryWarning) -> bool:
  136. """发送预警通知(可通过微信模板消息等)"""
  137. try:
  138. # 获取配送员信息
  139. task = self.db.query(BizDeliveryTask).filter(
  140. BizDeliveryTask.id == warning.task_id
  141. ).first()
  142. if not task or not task.deliver_id:
  143. logger.warning(f"任务 {warning.task_id} 未分配配送员,跳过通知")
  144. return False
  145. # TODO: 调用微信模板消息接口发送通知
  146. # from app.core.wechat_pay import send_delivery_warning_message
  147. # send_delivery_warning_message(
  148. # deliver_id=task.deliver_id,
  149. # task_no=task.task_no,
  150. # message=warning.message
  151. # )
  152. logger.info(f"已发送预警通知: {task.task_no} - {warning.message}")
  153. return True
  154. except Exception as e:
  155. logger.error(f"发送预警通知失败: {str(e)}")
  156. return False
  157. def check_delivery_warnings(db: Session) -> dict:
  158. """执行配送时效预警检查(供外部调用)"""
  159. task = DeliveryWarningTask(db)
  160. return task.check_delivery_warnings()
  161. def cleanup_expired_warnings(db: Session, days: int = 7) -> int:
  162. """清理过期的预警记录
  163. Args:
  164. db: 数据库会话
  165. days: 保留天数,默认7天
  166. Returns:
  167. int: 删除的记录数
  168. """
  169. from datetime import timedelta
  170. cutoff_date = datetime.now() - timedelta(days=days)
  171. count = db.query(BizDeliveryWarning).filter(
  172. BizDeliveryWarning.create_at < cutoff_date,
  173. BizDeliveryWarning.is_resolved == 1,
  174. BizDeliveryWarning.is_del == 0
  175. ).update({"is_del": 1})
  176. db.commit()
  177. logger.info(f"已清理 {count} 条过期预警记录")
  178. return count