""" 邮件业务服务 (高级) 职能:提供业务相关的邮件操作 - 新用户注册通知 - 用户审批通知 - 打卡结果通知 - 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)}

" "
" ) def _message_row(message: str) -> SafeHtml: if not message: return SafeHtml("") return SafeHtml(f'失败原因{_email_text(message)}') def _check_in_guidance_section(success: bool, is_token_error: bool) -> SafeHtml: if success: return SafeHtml("

如有问题,请及时检查您的打卡配置。

") dashboard_url = _email_text(_frontend_path("/dashboard")) if is_token_error: status_html = ( '
' '

打卡凭证已过期

' "

打卡凭证已过期,无法自动打卡。所有自动打卡任务已暂停,请进入网页处理。

" "
" ) else: status_html = "

请进入网页检查任务配置和最近记录。

" return SafeHtml( status_html + '
' f'立即登录刷新' "
" ) def _is_token_error(success: bool, message: str) -> bool: if success or not message: return False return any( marker in message for marker in ( "Token", "token", "失效", "授权", "登录", ) ) class EmailService: """邮件业务服务(高级服务)""" @staticmethod def send_email(to_emails: List[str], subject: str, body_html: str) -> bool: """ 发送邮件(业务层方法,调用底层 EmailNotifier) Args: to_emails: 收件人邮箱列表 subject: 邮件主题 body_html: 邮件正文(HTML 格式) Returns: 是否发送成功 """ return EmailNotifier.send_email(to_emails, subject, body_html) @staticmethod def notify_new_user_registration(user: User, db: Session) -> bool: """ 通知管理员有新用户注册 Args: user: 新注册的用户 db: 数据库会话 Returns: 是否发送成功 """ admins = db.query(User).filter(User.role == "admin", User.email.isnot(None)).all() admin_emails: List[str] = [] for admin in admins: email_value = admin.email if email_value is not None: admin_emails.append(str(email_value)) if not admin_emails: logger.warning("没有找到管理员邮箱,无法发送通知") return False subject = f"【接龙自动打卡系统】新用户注册通知 - {user.alias}" body_html = render_email_template( EmailTemplate.NEW_USER_REGISTRATION, { "user_alias": user.alias, "user_id": user.id, "created_time": _format_datetime(user.created_at), "admin_url": _frontend_path("/admin/users"), }, ) return EmailService.send_email(admin_emails, 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 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)