162 lines
6.1 KiB
Python
162 lines
6.1 KiB
Python
"""证件到期周一、周三、周五汇总:扫描后入队,发送前复核真实证件与收件权限。"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import date, datetime
|
||
import json
|
||
from zoneinfo import ZoneInfo
|
||
|
||
from sqlalchemy import and_
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.config import settings
|
||
from app.core.db_auth import DbUserContext
|
||
from app.models.models import SysRole, SysUser, SysUserRole
|
||
from app.models.pl_models import PlCert, PlDocumentReminder
|
||
from app.models.wechat_notice import WechatNotice
|
||
from app.services.pl_data_scope import apply_pl_scope
|
||
from app.services.web_auth_roles import resolve_web_role_for_user
|
||
from app.services.wechat_notice_events import enqueue_event
|
||
|
||
|
||
SHANGHAI = ZoneInfo("Asia/Shanghai")
|
||
|
||
|
||
def _local_time(now: datetime | None) -> datetime:
|
||
# 无时区输入遵循业务本地时间;生产默认始终取上海时区。
|
||
value = now or datetime.now(SHANGHAI)
|
||
return value.replace(tzinfo=SHANGHAI) if value.tzinfo is None else value.astimezone(SHANGHAI)
|
||
|
||
|
||
def _recipients(db: Session):
|
||
return (
|
||
db.query(SysUser)
|
||
.join(SysUserRole, and_(
|
||
SysUserRole.user_id == SysUser.id,
|
||
SysUserRole.tenant_no == SysUser.tenant_no,
|
||
SysUserRole.del_flag == 0,
|
||
))
|
||
.join(SysRole, and_(
|
||
SysRole.id == SysUserRole.role_id,
|
||
SysRole.tenant_no == SysUser.tenant_no,
|
||
SysRole.del_flag == 0,
|
||
SysRole.status == "1",
|
||
))
|
||
.filter(
|
||
SysUser.status == "1", SysUser.del_flag == 0,
|
||
SysRole.role_name == settings.WECHAT_OA_CERT_NOTICE_ROLE_NAME,
|
||
)
|
||
.distinct()
|
||
.populate_existing()
|
||
)
|
||
|
||
|
||
def _is_notice_time(now: datetime) -> bool:
|
||
# 门户预警仍每日扫描,微信只在上海时间周一、周三、周五10点(默认)之后发送。
|
||
return now.weekday() in (0, 2, 4) and (now.hour, now.minute) >= (
|
||
settings.WECHAT_OA_CERT_REMINDER_HOUR, settings.WECHAT_OA_CERT_REMINDER_MINUTE,
|
||
)
|
||
|
||
|
||
def _active_cert_counts(db: Session, user: SysUser, today: date) -> tuple[int, int]:
|
||
query = db.query(PlCert.id, PlCert.expire_date, PlCert.remind_days).join(
|
||
PlDocumentReminder, and_(
|
||
PlDocumentReminder.cert_id == PlCert.id,
|
||
PlDocumentReminder.tenant_no == PlCert.tenant_no,
|
||
),
|
||
).filter(
|
||
PlCert.tenant_no == user.tenant_no,
|
||
PlCert.del_flag == 0,
|
||
PlCert.status == "valid",
|
||
PlCert.expire_date.isnot(None),
|
||
PlDocumentReminder.tenant_no == user.tenant_no,
|
||
PlDocumentReminder.del_flag == 0,
|
||
PlDocumentReminder.status == "pending",
|
||
)
|
||
context = DbUserContext(
|
||
id=user.id, name=user.nick_name or user.user_no, dept_id=user.dept_id or "",
|
||
# 已校验本租户接收角色,因此不会走角色解析的默认租户回退。
|
||
role=resolve_web_role_for_user(db, user.id, user.tenant_no), tenant_no=user.tenant_no,
|
||
)
|
||
query = apply_pl_scope(query, PlDocumentReminder, context, db, user.tenant_no)
|
||
# 不使用历史 days_remaining;重复 pending 按证件去重,包含仍未处理的已过期证件。
|
||
upcoming, expired = 0, 0
|
||
for _, expiry, remind_days in query.distinct().all():
|
||
days = (expiry - today).days
|
||
if days > remind_days:
|
||
continue
|
||
# 与提醒列表统计一致:当天到期也纳入已到期数量。
|
||
if days <= 0:
|
||
expired += 1
|
||
else:
|
||
upcoming += 1
|
||
return upcoming, expired
|
||
|
||
|
||
def _payload(now: datetime, upcoming: int, expired: int) -> dict:
|
||
parts = []
|
||
if upcoming:
|
||
parts.append(f"到期{upcoming}项")
|
||
if expired:
|
||
parts.append(f"已到期{expired}项")
|
||
title = "证件" + ",".join(parts)
|
||
if len(title) > 20:
|
||
title = f"{upcoming + expired}项证件到期提醒"
|
||
return {
|
||
"thing5": {"value": title},
|
||
"time6": {"value": now.strftime("%Y-%m-%d %H:%M:%S")},
|
||
"thing3": {"value": "系统默认发出"},
|
||
}
|
||
|
||
|
||
def enqueue_daily_cert_notices(db: Session, *, now: datetime | None = None) -> int:
|
||
"""每日扫描后调用,仅通知日入队;事务由调用者提交,按租户/收件人/上海日期幂等。"""
|
||
if not settings.WECHAT_OA_ENABLED or not settings.WECHAT_OA_CERT_REMINDER_ENABLED:
|
||
return 0
|
||
local_now = _local_time(now)
|
||
if not _is_notice_time(local_now):
|
||
return 0
|
||
db.flush()
|
||
created = 0
|
||
for recipient in _recipients(db).all():
|
||
upcoming, expired = _active_cert_counts(db, recipient, local_now.date())
|
||
if not upcoming and not expired:
|
||
continue
|
||
notice = enqueue_event(
|
||
db, tenant_no=recipient.tenant_no, business_type="pl_cert_expiry",
|
||
business_id=recipient.id, event_id=local_now.date().isoformat(),
|
||
kind="cert_daily", recipient_id=recipient.id,
|
||
template_id=settings.WECHAT_OA_TEMPLATE_CERT_REMINDER,
|
||
payload=_payload(local_now, upcoming, expired),
|
||
)
|
||
if notice is not None:
|
||
created += 1
|
||
return created
|
||
|
||
|
||
def refresh_cert_notice(
|
||
db: Session, notice: WechatNotice, *, now: datetime | None = None,
|
||
) -> bool:
|
||
"""发送前复核当日、收件人、角色及证件状态,time6 使用实际发送时间。"""
|
||
if not settings.WECHAT_OA_ENABLED or not settings.WECHAT_OA_CERT_REMINDER_ENABLED:
|
||
return False
|
||
local_now = _local_time(now)
|
||
if not _is_notice_time(local_now):
|
||
return False
|
||
if (
|
||
notice.kind != "cert_daily" or notice.business_type != "pl_cert_expiry"
|
||
or notice.business_id != notice.recipient_id
|
||
or notice.cycle_id != local_now.date().isoformat()
|
||
):
|
||
return False
|
||
db.flush()
|
||
recipient = _recipients(db).filter(
|
||
SysUser.id == notice.recipient_id, SysUser.tenant_no == notice.tenant_no,
|
||
).first()
|
||
if recipient is None:
|
||
return False
|
||
upcoming, expired = _active_cert_counts(db, recipient, local_now.date())
|
||
if not upcoming and not expired:
|
||
return False
|
||
notice.payload = json.dumps(_payload(local_now, upcoming, expired), ensure_ascii=False)
|
||
return True
|