delivery_warning.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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. warning_type = None
  76. urgency_level = None
  77. message = None
  78. if remaining_minutes < 0:
  79. warning_type = "timeout"
  80. urgency_level = "已超时"
  81. message = f"已超过配送时效 {abs(remaining_minutes)} 分钟,仍未完成配送"
  82. elif remaining_minutes <= advance:
  83. warning_type = "advance_deadline"
  84. urgency_level = "时效预警"
  85. message = f"距配送截止不足 {advance} 分钟(剩余约 {remaining_minutes} 分钟),请督促配送"
  86. else:
  87. return warnings
  88. w = self._upsert_task_warning(
  89. task=task,
  90. warning_type=warning_type,
  91. urgency_level=urgency_level,
  92. remaining_minutes=remaining_minutes,
  93. message=message,
  94. )
  95. if w:
  96. warnings.append(w)
  97. return warnings
  98. def _upsert_task_warning(
  99. self,
  100. task: BizDeliveryTask,
  101. warning_type: str,
  102. urgency_level: str,
  103. remaining_minutes: int,
  104. message: str
  105. ) -> Optional[BizDeliveryWarning]:
  106. """单任务单预警:存在则更新,不存在则创建"""
  107. try:
  108. warning = self._get_latest_warning_by_task(task.id)
  109. now = datetime.now()
  110. if warning:
  111. warning.warning_type = warning_type
  112. warning.urgency_level = urgency_level
  113. warning.remaining_minutes = remaining_minutes
  114. warning.message = message
  115. warning.order_id = task.order_id
  116. warning.is_sent = 0
  117. # 未完成前统一保持未结案(若历史上被手工结案,则重新激活)
  118. warning.is_resolved = 0
  119. warning.resolved_time = None
  120. warning.resolved_by = None
  121. warning.update_at = now
  122. warning.update_by = "system"
  123. self.db.flush()
  124. return warning
  125. warning = BizDeliveryWarning(
  126. id=generate_id(),
  127. task_id=task.id,
  128. order_id=task.order_id,
  129. warning_type=warning_type,
  130. urgency_level=urgency_level,
  131. remaining_minutes=remaining_minutes,
  132. message=message,
  133. is_sent=0,
  134. is_resolved=0,
  135. create_by="system",
  136. create_name="系统",
  137. )
  138. self.db.add(warning)
  139. self.db.flush()
  140. return warning
  141. except Exception as e:
  142. logger.error(f"创建预警记录失败: {str(e)}")
  143. return None
  144. def _get_latest_warning_by_task(self, task_id: str) -> Optional[BizDeliveryWarning]:
  145. return self.db.query(BizDeliveryWarning).filter(
  146. BizDeliveryWarning.task_id == task_id,
  147. BizDeliveryWarning.is_del == 0,
  148. ).order_by(BizDeliveryWarning.update_at.desc(), BizDeliveryWarning.create_at.desc()).first()
  149. def _send_warning_notification(self, warning: BizDeliveryWarning) -> bool:
  150. """发送预警通知(可通过微信模板消息等)"""
  151. try:
  152. # 获取配送员信息
  153. task = self.db.query(BizDeliveryTask).filter(
  154. BizDeliveryTask.id == warning.task_id
  155. ).first()
  156. if not task or not task.deliver_id:
  157. logger.warning(f"任务 {warning.task_id} 未分配配送员,跳过通知")
  158. return False
  159. # TODO: 调用微信模板消息接口发送通知
  160. # from app.core.wechat_pay import send_delivery_warning_message
  161. # send_delivery_warning_message(
  162. # deliver_id=task.deliver_id,
  163. # task_no=task.task_no,
  164. # message=warning.message
  165. # )
  166. logger.info(f"已发送预警通知: {task.task_no} - {warning.message}")
  167. return True
  168. except Exception as e:
  169. logger.error(f"发送预警通知失败: {str(e)}")
  170. return False
  171. def check_delivery_warnings(db: Session) -> dict:
  172. """执行配送时效预警检查(供外部调用)"""
  173. task = DeliveryWarningTask(db)
  174. return task.check_delivery_warnings()
  175. def cleanup_expired_warnings(db: Session, days: int = 7) -> int:
  176. """清理过期的预警记录
  177. Args:
  178. db: 数据库会话
  179. days: 保留天数,默认7天
  180. Returns:
  181. int: 删除的记录数
  182. """
  183. from datetime import timedelta
  184. cutoff_date = datetime.now() - timedelta(days=days)
  185. count = db.query(BizDeliveryWarning).filter(
  186. BizDeliveryWarning.create_at < cutoff_date,
  187. BizDeliveryWarning.is_resolved == 1,
  188. BizDeliveryWarning.is_del == 0
  189. ).update({"is_del": 1})
  190. db.commit()
  191. logger.info(f"已清理 {count} 条过期预警记录")
  192. return count