"""基础实验交付日前一天:服务号模板提醒。""" 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()