basic_experiment_delivery_remind.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. """基础实验交付日前一天:服务号模板提醒。"""
  2. from __future__ import annotations
  3. import logging
  4. import uuid
  5. from datetime import date, datetime, timedelta
  6. from typing import Any, Dict, List, Optional
  7. from sqlalchemy.orm import Session
  8. from app.core.config import settings
  9. from app.models.models import (
  10. BizPilotApplication,
  11. BizPilotInfusionPlan,
  12. BizPilotInfusionSchedule,
  13. BizPilotInfusionScheduleProduct,
  14. BizWxNotifyLog,
  15. SysUser,
  16. )
  17. logger = logging.getLogger(__name__)
  18. NOTIFY_BIZ_TYPE = "basic_experiment_delivery_remind"
  19. CANCELLED_SCHEDULE_STATUS = "yqx"
  20. def format_delivery_product_name(
  21. *,
  22. project_name: str = "",
  23. spec_text: str = "",
  24. package_text: str = "",
  25. ) -> str:
  26. """产品名称拼接:名称(规格、包装);空段省略。"""
  27. name = (project_name or "").strip()
  28. spec = (spec_text or "").strip()
  29. package = (package_text or "").strip()
  30. parts = [p for p in (spec, package) if p]
  31. if not name and not parts:
  32. return ""
  33. if not parts:
  34. return name
  35. suffix = "、".join(parts)
  36. if name:
  37. return f"{name}({suffix})"
  38. return f"({suffix})"
  39. def _notify_enabled() -> bool:
  40. if not bool(getattr(settings, "WX_MP_NOTIFY_ENABLED", False)):
  41. return False
  42. tid = (getattr(settings, "WX_MP_TEMPLATE_BASIC_EXPERIMENT_DELIVERY", "") or "").strip()
  43. return bool(tid)
  44. def _already_success(db: Session, *, tenant_no: str, schedule_id: str, notify_date: str) -> bool:
  45. row = (
  46. db.query(BizWxNotifyLog)
  47. .filter(
  48. BizWxNotifyLog.tenant_no == tenant_no,
  49. BizWxNotifyLog.biz_type == NOTIFY_BIZ_TYPE,
  50. BizWxNotifyLog.biz_id == schedule_id,
  51. BizWxNotifyLog.notify_date == notify_date,
  52. BizWxNotifyLog.status == "success",
  53. BizWxNotifyLog.del_flag == 0,
  54. )
  55. .first()
  56. )
  57. return row is not None
  58. def _write_log(
  59. db: Session,
  60. *,
  61. tenant_no: str,
  62. schedule_id: str,
  63. notify_date: str,
  64. user_id: str,
  65. status: str,
  66. errcode: str = "",
  67. errmsg: str = "",
  68. ) -> None:
  69. now = datetime.now()
  70. db.add(
  71. BizWxNotifyLog(
  72. id=str(uuid.uuid4()).replace("-", "").upper(),
  73. biz_type=NOTIFY_BIZ_TYPE,
  74. biz_id=schedule_id,
  75. notify_date=notify_date,
  76. user_id=user_id or "",
  77. status=status,
  78. errcode=(errcode or "")[:64] or None,
  79. errmsg=(errmsg or "")[:512] or None,
  80. tenant_no=tenant_no,
  81. order_num=0,
  82. remark="",
  83. create_by=user_id or "",
  84. create_dept_id="",
  85. create_time=now,
  86. update_by=user_id or "",
  87. update_time=now,
  88. del_flag=0,
  89. )
  90. )
  91. def _iter_candidate_schedules(db: Session, *, delivery_day: date) -> List[Any]:
  92. """返回 (schedule, application, product) 元组列表。"""
  93. day_start = datetime.combine(delivery_day, datetime.min.time())
  94. day_end = datetime.combine(delivery_day, datetime.max.time()).replace(microsecond=0)
  95. rows = (
  96. db.query(BizPilotInfusionSchedule, BizPilotApplication, BizPilotInfusionScheduleProduct)
  97. .join(
  98. BizPilotInfusionPlan,
  99. BizPilotInfusionPlan.id == BizPilotInfusionSchedule.infusion_plan_id,
  100. )
  101. .join(
  102. BizPilotApplication,
  103. BizPilotApplication.id == BizPilotInfusionPlan.pilot_application_id,
  104. )
  105. .outerjoin(
  106. BizPilotInfusionScheduleProduct,
  107. (
  108. BizPilotInfusionScheduleProduct.infusion_schedule_id
  109. == BizPilotInfusionSchedule.id
  110. )
  111. & (BizPilotInfusionScheduleProduct.del_flag == 0),
  112. )
  113. .filter(
  114. BizPilotApplication.biz_type == "basic_experiment",
  115. BizPilotApplication.del_flag == 0,
  116. BizPilotInfusionPlan.del_flag == 0,
  117. BizPilotInfusionSchedule.del_flag == 0,
  118. BizPilotInfusionSchedule.infusion_time >= day_start,
  119. BizPilotInfusionSchedule.infusion_time <= day_end,
  120. BizPilotInfusionSchedule.schedule_status != CANCELLED_SCHEDULE_STATUS,
  121. )
  122. .order_by(BizPilotInfusionSchedule.infusion_time.asc())
  123. .all()
  124. )
  125. # 一计划多产品时取首条产品行(基础实验通常一行)
  126. seen: set = set()
  127. out = []
  128. for sched, app, prod in rows:
  129. if sched.id in seen:
  130. continue
  131. seen.add(sched.id)
  132. out.append((sched, app, prod))
  133. return out
  134. def run_delivery_remind_for_date(
  135. db: Session,
  136. *,
  137. today: Optional[date] = None,
  138. ) -> Dict[str, int]:
  139. """扫描「明天」交付计划并发送提醒。返回计数统计。"""
  140. stats = {"candidates": 0, "sent": 0, "skipped": 0, "failed": 0}
  141. if not _notify_enabled():
  142. logger.info("basic experiment delivery remind skipped: notify disabled or no template")
  143. return stats
  144. base = today or date.today()
  145. delivery_day = base + timedelta(days=1)
  146. notify_date = base.strftime("%Y-%m-%d")
  147. from app.services.wechat_mp_message import send_basic_experiment_delivery_remind
  148. candidates = _iter_candidate_schedules(db, delivery_day=delivery_day)
  149. stats["candidates"] = len(candidates)
  150. for sched, app, prod in candidates:
  151. tenant_no = (app.tenant_no or sched.tenant_no or "").strip() or "zkxw"
  152. schedule_id = sched.id
  153. if _already_success(db, tenant_no=tenant_no, schedule_id=schedule_id, notify_date=notify_date):
  154. stats["skipped"] += 1
  155. continue
  156. creator_id = (app.create_by or "").strip()
  157. if not creator_id:
  158. logger.info(
  159. "delivery remind skip: no create_by schedule=%s app=%s",
  160. schedule_id,
  161. app.id,
  162. )
  163. stats["skipped"] += 1
  164. continue
  165. user = (
  166. db.query(SysUser)
  167. .filter(SysUser.id == creator_id, SysUser.del_flag == 0)
  168. .first()
  169. )
  170. mp = (getattr(user, "wx_mp_openid", None) or "").strip() if user else ""
  171. if not mp:
  172. logger.info(
  173. "delivery remind skip: no mp_openid user=%s schedule=%s",
  174. creator_id,
  175. schedule_id,
  176. )
  177. stats["skipped"] += 1
  178. continue
  179. product_name = format_delivery_product_name(
  180. project_name=(prod.project_name if prod else "") or "",
  181. spec_text=(prod.spec_text if prod else "") or (sched.spec_text or ""),
  182. package_text=(prod.package_text if prod else "") or (sched.package_text or ""),
  183. )
  184. qty = 1
  185. if prod is not None and prod.quantity is not None:
  186. qty = int(prod.quantity)
  187. time_text = ""
  188. if sched.infusion_time:
  189. time_text = sched.infusion_time.strftime("%Y-%m-%d %H:%M")
  190. result = send_basic_experiment_delivery_remind(
  191. mp_openid=mp,
  192. customer_name=app.customer_name or "",
  193. product_name=product_name,
  194. quantity=str(qty),
  195. contact_name=app.customer_contact or "",
  196. time_text=time_text,
  197. )
  198. if result.get("success"):
  199. _write_log(
  200. db,
  201. tenant_no=tenant_no,
  202. schedule_id=schedule_id,
  203. notify_date=notify_date,
  204. user_id=creator_id,
  205. status="success",
  206. )
  207. stats["sent"] += 1
  208. else:
  209. logger.error(
  210. "delivery remind send failed schedule=%s user=%s err=%s",
  211. schedule_id,
  212. creator_id,
  213. result,
  214. )
  215. stats["failed"] += 1
  216. try:
  217. db.commit()
  218. except Exception:
  219. db.rollback()
  220. logger.exception("delivery remind commit failed")
  221. raise
  222. return stats
  223. def run_delivery_remind_job(get_db_session) -> None:
  224. """APScheduler 入口。"""
  225. db = get_db_session()
  226. try:
  227. stats = run_delivery_remind_for_date(db)
  228. logger.info("basic experiment delivery remind done: %s", stats)
  229. except Exception:
  230. logger.exception("basic experiment delivery remind job failed")
  231. finally:
  232. db.close()