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:
2026-01-03 01:38:38 +08:00
parent 42a1046750
commit 827c9198ae
57 changed files with 5517 additions and 2982 deletions
+33 -7
View File
@@ -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": "取消失败或会话不存在"
}
+27 -13
View File
@@ -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:
"""
为打卡记录添加用户和任务信息
注意:如果使用了 joinedloadtask 和 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
# 转换为字典并添加额外字段
+392 -49
View File
@@ -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)
+4 -2
View File
@@ -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:
+7 -8
View File
@@ -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: