""" 邮件业务服务 (高级) 职能:提供业务相关的邮件操作 - 新用户注册通知 - 用户审批通知 - 打卡结果通知 - Token 到期提醒 - 调用底层 EmailNotifier 发送邮件 """ import logging from datetime import datetime from html import escape from typing import Any, List from sqlalchemy.orm import Session from backend.config import settings from backend.models import User from backend.services.email_settings_service import EmailSettingsService from backend.services.email_templates import EmailTemplate, SafeHtml, render_email_template from backend.utils.time_helpers import minutes_until_expiry, parse_jwt_exp from backend.workers.email_notifier import EmailNotifier logger = logging.getLogger(__name__) def _format_datetime(value: Any) -> str: if value is None: return "未知" try: return value.strftime("%Y-%m-%d %H:%M:%S") except AttributeError: return str(value) def _now_text() -> str: return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def _frontend_path(path: str) -> str: return f"{settings.FRONTEND_URL}{path}" def _email_text(value: object) -> str: return escape(str(value), quote=True) def _optional_reason_section(reason: str) -> SafeHtml: if not reason: return SafeHtml("") return SafeHtml( '
拒绝原因
' f"{_email_text(reason)}
" "如有问题,请及时检查您的打卡配置。
") dashboard_url = _email_text(_frontend_path("/dashboard")) if is_token_error: status_html = ( '打卡凭证已过期
' "打卡凭证已过期,无法自动打卡。所有自动打卡任务已暂停,请进入网页处理。
" "请进入网页检查任务配置和最近记录。
" return SafeHtml( status_html + '您正在验证接龙自动打卡系统账号邮箱。
" f"验证码:{_email_text(code)}
" "验证码 10 分钟内有效。如非本人操作,请忽略本邮件。
" ) return EmailService.send_email([to_email], subject, body_html) @staticmethod def notify_user_approved(user: User) -> bool: """ 通知用户审批已通过 Args: user: 已通过审批的用户 Returns: 是否发送成功 """ user_email = user.email if user_email is None: logger.info(f"用户 {user.alias} 未设置邮箱,跳过审批通知") return False if not user.email_verified_at: logger.info(f"用户 {user.alias} 邮箱未验证,跳过审批通知") return False subject = f"【接龙自动打卡系统】账户审批通过 - {user.alias}" body_html = render_email_template( EmailTemplate.USER_APPROVED, { "user_alias": user.alias, "user_role": user.role, "created_time": _format_datetime(user.created_at), "approved_time": _now_text(), "login_url": _frontend_path("/login"), }, ) return EmailService.send_email([str(user_email)], subject, body_html) @staticmethod def notify_user_rejected(user: User, reason: str = "") -> bool: """ 通知用户审批被拒绝 Args: user: 被拒绝的用户 reason: 拒绝原因(可选) Returns: 是否发送成功 """ user_email = user.email if user_email is None: logger.info(f"用户 {user.alias} 未设置邮箱,跳过拒绝通知") return False subject = f"【接龙自动打卡系统】账户审批结果 - {user.alias}" body_html = render_email_template( EmailTemplate.USER_REJECTED, { "user_alias": user.alias, "processed_time": _now_text(), "reason_section": _optional_reason_section(reason), }, ) return EmailService.send_email([str(user_email)], subject, body_html) @staticmethod def notify_token_expiring(user: User, jwt_exp: str) -> bool: """ 通知用户 Token 即将过期 Args: user: 用户对象 jwt_exp: Token 过期时间戳 Returns: 是否发送成功 """ if not EmailSettingsService.is_token_expiring_notification_enabled(): logger.info("Token 即将过期通知已关闭,跳过发送") return False user_email = user.email if user_email is None: logger.info(f"用户 {user.alias} 未设置邮箱,跳过 Token 过期通知") return False exp_timestamp = parse_jwt_exp(jwt_exp) minutes_left = minutes_until_expiry(exp_timestamp) if exp_timestamp else 0 subject = f"【接龙自动打卡系统】登录凭证即将过期 - {user.alias}" body_html = render_email_template( EmailTemplate.TOKEN_EXPIRING, { "user_alias": user.alias, "minutes_left": minutes_left, }, ) return EmailService.send_email([str(user_email)], subject, body_html) @staticmethod def notify_token_expired(user: User) -> bool: """ 通知用户 Token 已过期 Args: user: 用户对象 Returns: 是否发送成功 """ user_email = user.email if user_email is None: logger.info(f"用户 {user.alias} 未设置邮箱,跳过 Token 已过期通知") return False subject = f"【接龙自动打卡系统】登录凭证已过期 - {user.alias}" body_html = render_email_template( EmailTemplate.TOKEN_EXPIRED, { "user_alias": user.alias, "login_url": _frontend_path("/login"), }, ) return EmailService.send_email([str(user_email)], subject, body_html) @staticmethod def notify_check_in_result( user: User, task_info: dict, success: bool, message: str = "" ) -> bool: """ 通知用户打卡结果 Args: user: 用户对象 task_info: 打卡任务信息(包含 thread_id, texts, values 等) success: 打卡是否成功 message: 额外消息 Returns: 是否发送成功 """ if success and not EmailSettingsService.is_check_in_success_notification_enabled(): logger.info("打卡成功通知已关闭,跳过发送") return False user_email = user.email if user_email is None: logger.info(f"用户 {user.alias} 未设置邮箱,跳过打卡通知") return False status_text = "成功" if success else "失败" status_color = "#15803d" if success else "#b91c1c" subject = f"【接龙自动打卡】打卡{'✅ 成功' if success else '❌ 失败'} - {user.alias}" token_error = _is_token_error(success, message) body_html = render_email_template( EmailTemplate.CHECK_IN_RESULT, { "user_alias": user.alias, "executed_time": _now_text(), "thread_id": task_info.get("thread_id", "未知"), "status_text": status_text, "status_color": status_color, "message_row": _message_row(message), "guidance_section": _check_in_guidance_section(success, token_error), }, ) return EmailService.send_email([str(user_email)], subject, body_html)