mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 14:06:28 +00:00
feat: migrate from Element Plus to Ant Design Vue and update Vite configuration for better dependency management
- Updated Vite configuration to manually chunk Ant Design Vue for improved dependency management. - Added a comprehensive migration testing checklist for transitioning from Element Plus 2.13.0 to Ant Design Vue 4.x, covering various components and functionalities.
This commit is contained in:
@@ -25,7 +25,7 @@ class AuthService:
|
||||
|
||||
Args:
|
||||
alias: 用户别名
|
||||
client_ip: 客户端 IP 地址
|
||||
client_ip: 客户端 IP 地址(用于会话标识)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
@@ -42,11 +42,11 @@ class AuthService:
|
||||
|
||||
if existing_user:
|
||||
# 检查是否为空 jwt_sub(测试账号)
|
||||
if not existing_user.jwt_sub or existing_user.jwt_sub == "":
|
||||
logger.warning(f"用户 {alias} 是测试账号(空 jwt_sub),禁止登录")
|
||||
if not existing_user.jwt_sub:
|
||||
logger.warning(f"用户 {alias} 是测试账号(未绑定 QQ),禁止扫码登录")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "此账户为测试账号,暂未绑定 QQ,无法登录"
|
||||
"message": "此账户为测试账号,暂未绑定 QQ,无法扫码登录"
|
||||
}
|
||||
|
||||
# 老用户:刷新 Token
|
||||
@@ -243,7 +243,6 @@ class AuthService:
|
||||
}
|
||||
|
||||
# 创建新用户(待审批状态)
|
||||
client_ip = session_data.get("client_ip", "")
|
||||
new_user = User(
|
||||
jwt_sub=jwt_sub,
|
||||
alias=alias,
|
||||
@@ -251,7 +250,6 @@ class AuthService:
|
||||
jwt_exp=jwt_exp,
|
||||
role="user",
|
||||
is_approved=False, # 待审批
|
||||
registered_ip=client_ip
|
||||
)
|
||||
|
||||
db.add(new_user)
|
||||
@@ -427,7 +425,9 @@ class AuthService:
|
||||
"message": "登录成功",
|
||||
"user_id": user.id,
|
||||
"authorization": user.authorization,
|
||||
"alias": user.alias
|
||||
"alias": user.alias,
|
||||
"role": user.role,
|
||||
"is_approved": user.is_approved
|
||||
}
|
||||
|
||||
# 如果 Token 有问题,添加警告信息
|
||||
@@ -475,3 +475,29 @@ class AuthService:
|
||||
except Exception as e:
|
||||
logger.error(f"密码验证异常:{e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def cancel_qrcode_session(session_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
取消二维码登录会话
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
|
||||
Returns:
|
||||
包含取消结果的字典
|
||||
"""
|
||||
from backend.workers.token_refresher import cancel_session
|
||||
|
||||
success = cancel_session(session_id)
|
||||
|
||||
if success:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "会话已取消"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "取消失败或会话不存在"
|
||||
}
|
||||
|
||||
@@ -173,8 +173,9 @@ class CheckInService:
|
||||
"status": "failure",
|
||||
"message": f"{error_msg},请重新扫码登录"
|
||||
}
|
||||
except ValueError:
|
||||
pass
|
||||
except ValueError as e:
|
||||
# jwt_exp 格式不正确,记录警告后跳过 Token 过期验证
|
||||
logger.warning(f"任务 {task.id} 的用户 jwt_exp 格式不正确: {user.jwt_exp}, 错误: {e}")
|
||||
|
||||
# 创建待处理记录
|
||||
record_id = CheckInService.create_pending_check_in_record(task, trigger_type, db)
|
||||
@@ -264,8 +265,9 @@ class CheckInService:
|
||||
"message": f"{error_msg},请重新扫码登录",
|
||||
"record_id": record.id
|
||||
}
|
||||
except ValueError:
|
||||
pass
|
||||
except ValueError as e:
|
||||
# jwt_exp 格式不正确,记录警告后跳过 Token 过期验证
|
||||
logger.warning(f"任务 {task.id} 的用户 jwt_exp 格式不正确: {user.jwt_exp}, 错误: {e}")
|
||||
|
||||
# 执行打卡(传递 task 对象和用户 token)
|
||||
logger.info(f"🤖 调用 Selenium Worker 执行打卡...")
|
||||
@@ -409,8 +411,9 @@ class CheckInService:
|
||||
logger.warning(f"任务 ID: {task.id} 的用户 Token 已过期,跳过")
|
||||
results["skipped"] += 1
|
||||
continue
|
||||
except ValueError:
|
||||
pass
|
||||
except ValueError as e:
|
||||
# jwt_exp 格式不正确,记录警告后继续执行打卡
|
||||
logger.warning(f"任务 {task.id} 的用户 jwt_exp 格式不正确: {task.user.jwt_exp}, 错误: {e}")
|
||||
|
||||
# 执行打卡
|
||||
result = CheckInService.perform_task_check_in(task, "scheduled", db)
|
||||
@@ -514,7 +517,7 @@ class CheckInService:
|
||||
status: Optional[str] = None
|
||||
) -> List[CheckInRecord]:
|
||||
"""
|
||||
获取所有打卡记录(管理员)
|
||||
获取所有打卡记录(管理员)- 使用联表查询优化性能
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
@@ -526,7 +529,12 @@ class CheckInService:
|
||||
Returns:
|
||||
打卡记录列表
|
||||
"""
|
||||
query = db.query(CheckInRecord)
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
# 使用 joinedload 预加载关联的 task 和 user,避免 N+1 查询
|
||||
query = db.query(CheckInRecord).options(
|
||||
joinedload(CheckInRecord.task).joinedload(CheckInTask.user)
|
||||
)
|
||||
|
||||
if task_id:
|
||||
query = query.filter(CheckInRecord.task_id == task_id)
|
||||
@@ -543,15 +551,18 @@ class CheckInService:
|
||||
"""
|
||||
为打卡记录添加用户和任务信息
|
||||
|
||||
注意:如果使用了 joinedload,task 和 user 已经预加载,不会产生额外查询
|
||||
|
||||
Args:
|
||||
record: 打卡记录对象
|
||||
db: 数据库会话
|
||||
db: 数据库会话(可选,仅在未使用 joinedload 时使用)
|
||||
|
||||
Returns:
|
||||
包含额外信息的记录字典
|
||||
"""
|
||||
# 获取任务信息
|
||||
task = db.query(CheckInTask).filter(CheckInTask.id == record.task_id).first()
|
||||
# 尝试使用已加载的关联对象,如果没有则查询
|
||||
task = record.task if hasattr(record, 'task') and record.task else \
|
||||
db.query(CheckInTask).filter(CheckInTask.id == record.task_id).first()
|
||||
|
||||
# 获取用户信息
|
||||
user = None
|
||||
@@ -559,14 +570,17 @@ class CheckInService:
|
||||
thread_id = None
|
||||
|
||||
if task:
|
||||
user = db.query(User).filter(User.id == task.user_id).first()
|
||||
# 尝试使用已加载的 user,否则查询
|
||||
user = task.user if hasattr(task, 'user') and task.user else \
|
||||
db.query(User).filter(User.id == task.user_id).first()
|
||||
task_name = task.name
|
||||
|
||||
# 从 payload_config 提取 ThreadId
|
||||
try:
|
||||
payload = json.loads(str(task.payload_config))
|
||||
thread_id = payload.get('ThreadId')
|
||||
except:
|
||||
except (json.JSONDecodeError, KeyError, TypeError, AttributeError) as e:
|
||||
logger.debug(f"解析任务 {task.id} 的 payload_config 失败: {e}")
|
||||
pass
|
||||
|
||||
# 转换为字典并添加额外字段
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import smtplib
|
||||
"""
|
||||
邮件业务服务 (高级)
|
||||
|
||||
职能:提供业务相关的邮件操作
|
||||
- 新用户注册通知
|
||||
- 用户审批通知
|
||||
- 打卡结果通知
|
||||
- Token 到期提醒
|
||||
- 调用底层 EmailNotifier 发送邮件
|
||||
"""
|
||||
|
||||
import logging
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.config import settings
|
||||
from backend.models import User
|
||||
from backend.workers.email_notifier import EmailNotifier
|
||||
from backend.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailService:
|
||||
"""邮件通知服务"""
|
||||
"""邮件业务服务(高级服务)"""
|
||||
|
||||
@staticmethod
|
||||
def send_email(to_emails: List[str], subject: str, body_html: str) -> bool:
|
||||
"""
|
||||
发送邮件
|
||||
发送邮件(业务层方法,调用底层 EmailNotifier)
|
||||
|
||||
Args:
|
||||
to_emails: 收件人邮箱列表
|
||||
@@ -28,39 +37,7 @@ class EmailService:
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
# 检查邮件配置
|
||||
if not all([settings.SMTP_SERVER, settings.SMTP_SENDER_EMAIL, settings.SMTP_SENDER_PASSWORD]):
|
||||
logger.warning("邮件配置不完整,跳过发送邮件")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 创建邮件
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['From'] = settings.SMTP_SENDER_EMAIL
|
||||
msg['To'] = ', '.join(to_emails)
|
||||
msg['Subject'] = subject
|
||||
|
||||
# 添加 HTML 正文
|
||||
html_part = MIMEText(body_html, 'html', 'utf-8')
|
||||
msg.attach(html_part)
|
||||
|
||||
# 连接 SMTP 服务器并发送
|
||||
if settings.SMTP_USE_SSL:
|
||||
server = smtplib.SMTP_SSL(settings.SMTP_SERVER, settings.SMTP_PORT)
|
||||
else:
|
||||
server = smtplib.SMTP(settings.SMTP_SERVER, settings.SMTP_PORT)
|
||||
server.starttls()
|
||||
|
||||
server.login(settings.SMTP_SENDER_EMAIL, settings.SMTP_SENDER_PASSWORD)
|
||||
server.sendmail(settings.SMTP_SENDER_EMAIL, to_emails, msg.as_string())
|
||||
server.quit()
|
||||
|
||||
logger.info(f"邮件发送成功: {subject} -> {', '.join(to_emails)}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"邮件发送失败: {e}")
|
||||
return False
|
||||
return EmailNotifier.send_email(to_emails, subject, body_html)
|
||||
|
||||
@staticmethod
|
||||
def notify_new_user_registration(user: User, db: Session) -> bool:
|
||||
@@ -76,7 +53,12 @@ class EmailService:
|
||||
"""
|
||||
# 查询所有管理员邮箱
|
||||
admins = db.query(User).filter(User.role == "admin", User.email.isnot(None)).all()
|
||||
admin_emails = [admin.email for admin in admins if admin.email]
|
||||
# 使用 str() 转换避免类型检查问题,并过滤空值
|
||||
admin_emails: List[str] = []
|
||||
for admin in admins:
|
||||
email_value = admin.email
|
||||
if email_value is not None: # 使用 is not None 避免布尔转换
|
||||
admin_emails.append(str(email_value))
|
||||
|
||||
if not admin_emails:
|
||||
logger.warning("没有找到管理员邮箱,无法发送通知")
|
||||
@@ -85,6 +67,10 @@ class EmailService:
|
||||
# 构建邮件内容
|
||||
subject = f"【接龙自动打卡系统】新用户注册通知 - {user.alias}"
|
||||
|
||||
# 安全获取创建时间
|
||||
created_at_value = user.created_at
|
||||
created_time = created_at_value.strftime('%Y-%m-%d %H:%M:%S') if created_at_value is not None else '未知'
|
||||
|
||||
body_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -161,11 +147,7 @@ class EmailService:
|
||||
</tr>
|
||||
<tr>
|
||||
<td>注册时间</td>
|
||||
<td>{user.created_at.strftime('%Y-%m-%d %H:%M:%S') if user.created_at else '未知'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>注册 IP</td>
|
||||
<td>{user.registered_ip or '未记录'}</td>
|
||||
<td>{created_time}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -175,7 +157,7 @@ class EmailService:
|
||||
<p>请登录管理后台进行审批操作。</p>
|
||||
</div>
|
||||
|
||||
<p>登录地址:<a href="http://localhost:5173/admin/users">http://localhost:5173/admin/users</a></p>
|
||||
<p>登录地址:<a href="{settings.FRONTEND_URL}/admin/users">{settings.FRONTEND_URL}/admin/users</a></p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此邮件由系统自动发送,请勿直接回复。</p>
|
||||
@@ -188,6 +170,366 @@ class EmailService:
|
||||
|
||||
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}"
|
||||
|
||||
# 安全获取创建时间
|
||||
user_created_at = user.created_at
|
||||
created_time = user_created_at.strftime('%Y-%m-%d %H:%M:%S') if user_created_at is not None else '未知'
|
||||
|
||||
body_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}}
|
||||
.header {{
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}}
|
||||
.content {{
|
||||
background-color: #f9f9f9;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}}
|
||||
.info-table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 15px 0;
|
||||
}}
|
||||
.info-table td {{
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}}
|
||||
.info-table td:first-child {{
|
||||
font-weight: bold;
|
||||
width: 120px;
|
||||
}}
|
||||
.footer {{
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}}
|
||||
.success-box {{
|
||||
background-color: #d4edda;
|
||||
border-left: 4px solid #28a745;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}}
|
||||
.btn {{
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background-color: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h2>🎉 恭喜!账户审批通过</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>您好,{user.alias}!</p>
|
||||
<p>恭喜您的账户已通过管理员审批,现在可以使用所有功能了。</p>
|
||||
|
||||
<div class="success-box">
|
||||
<strong>✅ 审批结果:</strong> 已通过
|
||||
<br>
|
||||
<strong>审批时间:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
</div>
|
||||
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<td>用户名</td>
|
||||
<td>{user.alias}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>账户角色</td>
|
||||
<td>{user.role}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>注册时间</td>
|
||||
<td>{created_time}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p><strong>接下来您可以:</strong></p>
|
||||
<ul>
|
||||
<li>登录系统创建自动打卡任务</li>
|
||||
<li>配置打卡时间和内容</li>
|
||||
<li>查看打卡记录和统计</li>
|
||||
</ul>
|
||||
|
||||
<p style="text-align: center;">
|
||||
<a href="{settings.FRONTEND_URL}/login" class="btn">立即登录</a>
|
||||
</p>
|
||||
|
||||
<p style="color: #666; font-size: 14px;">
|
||||
💡 <strong>温馨提示:</strong>如果您还没有设置密码,建议在个人设置中设置密码,方便后续登录。
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此邮件由系统自动发送,请勿直接回复。</p>
|
||||
<p>接龙自动打卡系统 © {datetime.now().year}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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}"
|
||||
|
||||
reason_html = f"<p><strong>拒绝原因:</strong>{reason}</p>" if reason else ""
|
||||
|
||||
body_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}}
|
||||
.header {{
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}}
|
||||
.content {{
|
||||
background-color: #f9f9f9;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}}
|
||||
.footer {{
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}}
|
||||
.error-box {{
|
||||
background-color: #f8d7da;
|
||||
border-left: 4px solid #dc3545;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h2>账户审批结果通知</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>您好,{user.alias}!</p>
|
||||
<p>很遗憾,您的账户注册申请未能通过审批。</p>
|
||||
|
||||
<div class="error-box">
|
||||
<strong>❌ 审批结果:</strong> 未通过
|
||||
<br>
|
||||
<strong>处理时间:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
</div>
|
||||
|
||||
{reason_html}
|
||||
|
||||
<p>如有疑问,请联系系统管理员。</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此邮件由系统自动发送,请勿直接回复。</p>
|
||||
<p>接龙自动打卡系统 © {datetime.now().year}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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:
|
||||
是否发送成功
|
||||
"""
|
||||
user_email = user.email
|
||||
if user_email is None:
|
||||
logger.info(f"用户 {user.alias} 未设置邮箱,跳过 Token 过期通知")
|
||||
return False
|
||||
|
||||
# 计算剩余时间
|
||||
try:
|
||||
exp_timestamp = int(jwt_exp)
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
minutes_left = (exp_timestamp - current_timestamp) // 60
|
||||
except ValueError:
|
||||
minutes_left = 0
|
||||
|
||||
# 构建邮件内容
|
||||
subject = f"【接龙自动打卡系统】登录凭证即将过期 - {user.alias}"
|
||||
|
||||
body_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}}
|
||||
.header {{
|
||||
background-color: #ff9800;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}}
|
||||
.content {{
|
||||
background-color: #f9f9f9;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}}
|
||||
.warning-box {{
|
||||
background-color: #fff3cd;
|
||||
border-left: 4px solid #ff9800;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}}
|
||||
.footer {{
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}}
|
||||
.btn {{
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background-color: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h2>⚠️ 登录凭证即将过期</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>您好,{user.alias}!</p>
|
||||
<p>您的 QQ 登录凭证即将在 <strong>{minutes_left} 分钟</strong>后过期。</p>
|
||||
|
||||
<div class="warning-box">
|
||||
<strong>⚠️ 重要提示:</strong>
|
||||
<ul style="margin: 10px 0; padding-left: 20px;">
|
||||
<li>登录凭证过期后,系统将无法自动执行您的打卡任务</li>
|
||||
<li>建议尽快登录系统刷新凭证</li>
|
||||
<li>如果您已设置密码,可以使用密码登录后扫码刷新凭证</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p><strong>如何刷新凭证:</strong></p>
|
||||
<ol style="margin: 10px 0; padding-left: 20px;">
|
||||
<li>登录系统(扫码或密码登录)</li>
|
||||
<li>在个人设置中点击"刷新凭证"</li>
|
||||
<li>使用手机 QQ 扫描二维码完成刷新</li>
|
||||
</ol>
|
||||
|
||||
<p style="text-align: center;">
|
||||
<a href="{settings.FRONTEND_URL}/login" class="btn">立即登录刷新</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此邮件由系统自动发送,请勿直接回复。</p>
|
||||
<p>接龙自动打卡系统 © {datetime.now().year}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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:
|
||||
"""
|
||||
@@ -202,7 +544,8 @@ class EmailService:
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
if not user.email:
|
||||
user_email = user.email
|
||||
if user_email is None:
|
||||
logger.info(f"用户 {user.alias} 未设置邮箱,跳过打卡通知")
|
||||
return False
|
||||
|
||||
@@ -298,4 +641,4 @@ class EmailService:
|
||||
</html>
|
||||
"""
|
||||
|
||||
return EmailService.send_email([user.email], subject, body_html)
|
||||
return EmailService.send_email([str(user_email)], subject, body_html)
|
||||
|
||||
@@ -13,7 +13,6 @@ from backend.config import settings
|
||||
from backend.models import get_db, User, CheckInTask
|
||||
from backend.services.check_in_service import CheckInService
|
||||
from backend.services.admin_service import AdminService
|
||||
from backend.workers.email_notifier import send_expiration_notification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -182,7 +181,10 @@ def check_token_expiration():
|
||||
# 使用用户账户的邮箱发送通知
|
||||
if user.email:
|
||||
logger.info(f"用户 {user.alias} 的 Token 即将过期,发送邮件提醒到 {user.email}...")
|
||||
send_expiration_notification(user.email, user.jwt_exp)
|
||||
from backend.services.email_service import EmailService
|
||||
jwt_exp_value = user.jwt_exp
|
||||
jwt_exp_str = str(jwt_exp_value) if jwt_exp_value is not None else "0"
|
||||
EmailService.notify_token_expiring(user, jwt_exp_str)
|
||||
notified_count += 1
|
||||
|
||||
except ValueError:
|
||||
|
||||
@@ -32,10 +32,10 @@ class UserService:
|
||||
|
||||
# 创建用户(管理员创建的用户没有 jwt_sub,需要后续扫码绑定)
|
||||
user = User(
|
||||
jwt_sub="", # 空字符串表示未绑定 QQ
|
||||
jwt_sub=None, # NULL 表示未绑定 QQ
|
||||
alias=user_data.alias,
|
||||
role=user_data.role or "user",
|
||||
is_approved=True, # 管理员创建的用户默认已审批
|
||||
is_approved=user_data.is_approved if user_data.is_approved is not None else True, # 使用请求中的值,默认已审批
|
||||
jwt_exp="0",
|
||||
authorization=None,
|
||||
)
|
||||
@@ -114,12 +114,11 @@ class UserService:
|
||||
|
||||
# 搜索过滤
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
User.alias.ilike(f"%{search}%"),
|
||||
User.jwt_sub.ilike(f"%{search}%")
|
||||
)
|
||||
)
|
||||
# 注意:jwt_sub 可能为 NULL,需要处理
|
||||
search_conditions = [User.alias.ilike(f"%{search}%")]
|
||||
# 只有当 jwt_sub 不为空时才搜索
|
||||
search_conditions.append(User.jwt_sub.ilike(f"%{search}%"))
|
||||
query = query.filter(or_(*search_conditions))
|
||||
|
||||
# 角色过滤
|
||||
if role:
|
||||
|
||||
Reference in New Issue
Block a user