| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- """基础实验交付日前一天:服务号模板提醒。"""
- from __future__ import annotations
- import logging
- import uuid
- from datetime import date, datetime, timedelta
- from typing import Any, Dict, List, Optional
- from sqlalchemy.orm import Session
- from app.core.config import settings
- from app.models.models import (
- BizPilotApplication,
- BizPilotInfusionPlan,
- BizPilotInfusionSchedule,
- BizPilotInfusionScheduleProduct,
- BizWxNotifyLog,
- SysUser,
- )
- logger = logging.getLogger(__name__)
- NOTIFY_BIZ_TYPE = "basic_experiment_delivery_remind"
- CANCELLED_SCHEDULE_STATUS = "yqx"
- def format_delivery_product_name(
- *,
- project_name: str = "",
- spec_text: str = "",
- package_text: str = "",
- ) -> str:
- """产品名称拼接:名称(规格、包装);空段省略。"""
- name = (project_name or "").strip()
- spec = (spec_text or "").strip()
- package = (package_text or "").strip()
- parts = [p for p in (spec, package) if p]
- if not name and not parts:
- return ""
- if not parts:
- return name
- suffix = "、".join(parts)
- if name:
- return f"{name}({suffix})"
- return f"({suffix})"
- def _notify_enabled() -> bool:
- if not bool(getattr(settings, "WX_MP_NOTIFY_ENABLED", False)):
- return False
- tid = (getattr(settings, "WX_MP_TEMPLATE_BASIC_EXPERIMENT_DELIVERY", "") or "").strip()
- return bool(tid)
- def _already_success(db: Session, *, tenant_no: str, schedule_id: str, notify_date: str) -> bool:
- row = (
- db.query(BizWxNotifyLog)
- .filter(
- BizWxNotifyLog.tenant_no == tenant_no,
- BizWxNotifyLog.biz_type == NOTIFY_BIZ_TYPE,
- BizWxNotifyLog.biz_id == schedule_id,
- BizWxNotifyLog.notify_date == notify_date,
- BizWxNotifyLog.status == "success",
- BizWxNotifyLog.del_flag == 0,
- )
- .first()
- )
- return row is not None
- def _write_log(
- db: Session,
- *,
- tenant_no: str,
- schedule_id: str,
- notify_date: str,
- user_id: str,
- status: str,
- errcode: str = "",
- errmsg: str = "",
- ) -> None:
- now = datetime.now()
- db.add(
- BizWxNotifyLog(
- id=str(uuid.uuid4()).replace("-", "").upper(),
- biz_type=NOTIFY_BIZ_TYPE,
- biz_id=schedule_id,
- notify_date=notify_date,
- user_id=user_id or "",
- status=status,
- errcode=(errcode or "")[:64] or None,
- errmsg=(errmsg or "")[:512] or None,
- tenant_no=tenant_no,
- order_num=0,
- remark="",
- create_by=user_id or "",
- create_dept_id="",
- create_time=now,
- update_by=user_id or "",
- update_time=now,
- del_flag=0,
- )
- )
- def _iter_candidate_schedules(db: Session, *, delivery_day: date) -> List[Any]:
- """返回 (schedule, application, product) 元组列表。"""
- day_start = datetime.combine(delivery_day, datetime.min.time())
- day_end = datetime.combine(delivery_day, datetime.max.time()).replace(microsecond=0)
- rows = (
- db.query(BizPilotInfusionSchedule, BizPilotApplication, BizPilotInfusionScheduleProduct)
- .join(
- BizPilotInfusionPlan,
- BizPilotInfusionPlan.id == BizPilotInfusionSchedule.infusion_plan_id,
- )
- .join(
- BizPilotApplication,
- BizPilotApplication.id == BizPilotInfusionPlan.pilot_application_id,
- )
- .outerjoin(
- BizPilotInfusionScheduleProduct,
- (
- BizPilotInfusionScheduleProduct.infusion_schedule_id
- == BizPilotInfusionSchedule.id
- )
- & (BizPilotInfusionScheduleProduct.del_flag == 0),
- )
- .filter(
- BizPilotApplication.biz_type == "basic_experiment",
- BizPilotApplication.del_flag == 0,
- BizPilotInfusionPlan.del_flag == 0,
- BizPilotInfusionSchedule.del_flag == 0,
- BizPilotInfusionSchedule.infusion_time >= day_start,
- BizPilotInfusionSchedule.infusion_time <= day_end,
- BizPilotInfusionSchedule.schedule_status != CANCELLED_SCHEDULE_STATUS,
- )
- .order_by(BizPilotInfusionSchedule.infusion_time.asc())
- .all()
- )
- # 一计划多产品时取首条产品行(基础实验通常一行)
- seen: set = set()
- out = []
- for sched, app, prod in rows:
- if sched.id in seen:
- continue
- seen.add(sched.id)
- out.append((sched, app, prod))
- return out
- def run_delivery_remind_for_date(
- db: Session,
- *,
- today: Optional[date] = None,
- ) -> Dict[str, int]:
- """扫描「明天」交付计划并发送提醒。返回计数统计。"""
- stats = {"candidates": 0, "sent": 0, "skipped": 0, "failed": 0}
- if not _notify_enabled():
- logger.info("basic experiment delivery remind skipped: notify disabled or no template")
- return stats
- base = today or date.today()
- delivery_day = base + timedelta(days=1)
- notify_date = base.strftime("%Y-%m-%d")
- from app.services.wechat_mp_message import send_basic_experiment_delivery_remind
- candidates = _iter_candidate_schedules(db, delivery_day=delivery_day)
- stats["candidates"] = len(candidates)
- for sched, app, prod in candidates:
- tenant_no = (app.tenant_no or sched.tenant_no or "").strip() or "zkxw"
- schedule_id = sched.id
- if _already_success(db, tenant_no=tenant_no, schedule_id=schedule_id, notify_date=notify_date):
- stats["skipped"] += 1
- continue
- creator_id = (app.create_by or "").strip()
- if not creator_id:
- logger.info(
- "delivery remind skip: no create_by schedule=%s app=%s",
- schedule_id,
- app.id,
- )
- stats["skipped"] += 1
- continue
- user = (
- db.query(SysUser)
- .filter(SysUser.id == creator_id, SysUser.del_flag == 0)
- .first()
- )
- mp = (getattr(user, "wx_mp_openid", None) or "").strip() if user else ""
- if not mp:
- logger.info(
- "delivery remind skip: no mp_openid user=%s schedule=%s",
- creator_id,
- schedule_id,
- )
- stats["skipped"] += 1
- continue
- product_name = format_delivery_product_name(
- project_name=(prod.project_name if prod else "") or "",
- spec_text=(prod.spec_text if prod else "") or (sched.spec_text or ""),
- package_text=(prod.package_text if prod else "") or (sched.package_text or ""),
- )
- qty = 1
- if prod is not None and prod.quantity is not None:
- qty = int(prod.quantity)
- time_text = ""
- if sched.infusion_time:
- time_text = sched.infusion_time.strftime("%Y-%m-%d %H:%M")
- result = send_basic_experiment_delivery_remind(
- mp_openid=mp,
- customer_name=app.customer_name or "",
- product_name=product_name,
- quantity=str(qty),
- contact_name=app.customer_contact or "",
- time_text=time_text,
- )
- if result.get("success"):
- _write_log(
- db,
- tenant_no=tenant_no,
- schedule_id=schedule_id,
- notify_date=notify_date,
- user_id=creator_id,
- status="success",
- )
- stats["sent"] += 1
- else:
- logger.error(
- "delivery remind send failed schedule=%s user=%s err=%s",
- schedule_id,
- creator_id,
- result,
- )
- stats["failed"] += 1
- try:
- db.commit()
- except Exception:
- db.rollback()
- logger.exception("delivery remind commit failed")
- raise
- return stats
- def run_delivery_remind_job(get_db_session) -> None:
- """APScheduler 入口。"""
- db = get_db_session()
- try:
- stats = run_delivery_remind_for_date(db)
- logger.info("basic experiment delivery remind done: %s", stats)
- except Exception:
- logger.exception("basic experiment delivery remind job failed")
- finally:
- db.close()
|