style(backend): apply ruff format

This commit is contained in:
2026-05-03 18:14:23 +08:00
parent 738217d9c9
commit ab68f019c5
41 changed files with 960 additions and 970 deletions
+13 -18
View File
@@ -14,10 +14,12 @@ class AdminService:
@staticmethod
def get_pending_users(db: Session) -> List[User]:
"""获取待审批用户列表"""
users = db.query(User).filter(
User.is_approved == False,
User.role == "user"
).order_by(User.created_at.desc()).all()
users = (
db.query(User)
.filter(User.is_approved == False, User.role == "user")
.order_by(User.created_at.desc())
.all()
)
return users
@@ -38,11 +40,7 @@ class AdminService:
logger.info(f"管理员审批通过用户: {user.alias} (ID: {user.id})")
return {
"success": True,
"message": "审批成功",
"user_id": user.id
}
return {"success": True, "message": "审批成功", "user_id": user.id}
@staticmethod
def reject_user(user_id: int, db: Session) -> Dict[str, Any]:
@@ -58,21 +56,18 @@ class AdminService:
logger.info(f"管理员拒绝用户: {alias} (ID: {user_id})")
return {
"success": True,
"message": "已拒绝并删除用户"
}
return {"success": True, "message": "已拒绝并删除用户"}
@staticmethod
def delete_expired_pending_users(db: Session) -> int:
"""删除24小时未审批的用户"""
cutoff_time = datetime.now() - timedelta(hours=24)
expired_users = db.query(User).filter(
User.is_approved == False,
User.role == "user",
User.created_at < cutoff_time
).all()
expired_users = (
db.query(User)
.filter(User.is_approved == False, User.role == "user", User.created_at < cutoff_time)
.all()
)
count = len(expired_users)
+75 -132
View File
@@ -45,10 +45,7 @@ class AuthService:
# 检查是否为空 jwt_sub(测试账号)
if not existing_user.jwt_sub:
logger.warning(f"用户 {alias} 是测试账号(未绑定 QQ),禁止扫码登录")
return {
"status": "error",
"message": "此账户为测试账号,暂未绑定 QQ,无法扫码登录"
}
return {"status": "error", "message": "此账户为测试账号,暂未绑定 QQ,无法扫码登录"}
# 老用户:刷新 Token
logger.info(f"老用户 {alias} 请求刷新 Token,会话: {session_id}")
@@ -57,7 +54,7 @@ class AuthService:
thread = threading.Thread(
target=get_token_headless,
args=(session_id, existing_user.jwt_sub, alias, client_ip),
daemon=True
daemon=True,
)
thread.start()
@@ -67,16 +64,14 @@ class AuthService:
logger.warning(f"用户名 {alias} 已被预占")
return {
"status": "error",
"message": "该用户名正在被其他人注册,请稍后再试或更换用户名"
"message": "该用户名正在被其他人注册,请稍后再试或更换用户名",
}
logger.info(f"新用户 {alias} 请求注册,会话: {session_id},已预占用户名")
# 在后台线程启动 Selenium,不传入 jwt_sub(新用户)
thread = threading.Thread(
target=get_token_headless,
args=(session_id, None, alias, client_ip),
daemon=True
target=get_token_headless, args=(session_id, None, alias, client_ip), daemon=True
)
thread.start()
@@ -96,29 +91,20 @@ class AuthService:
qr_image_data = session_data.get("qr_image_data")
if qr_image_data:
logger.info(f"会话 {session_id} 的二维码已生成")
return {
"session_id": session_id,
"qrcode_base64": qr_image_data
}
return {"session_id": session_id, "qrcode_base64": qr_image_data}
# 如果已经失败,直接返回错误
elif status == "failed":
error_msg = session_data.get("message", "生成二维码失败")
logger.error(f"会话 {session_id} 生成二维码失败: {error_msg}")
return {
"status": "error",
"message": error_msg
}
return {"status": "error", "message": error_msg}
# 每 0.5 秒检查一次
time.sleep(0.5)
# 超时
logger.error(f"会话 {session_id} 等待二维码生成超时({max_wait_time}秒)")
return {
"status": "error",
"message": f"生成二维码超时,请重试"
}
return {"status": "error", "message": f"生成二维码超时,请重试"}
@staticmethod
def get_qrcode_status(session_id: str, db: Session) -> Dict[str, Any]:
@@ -135,10 +121,7 @@ class AuthService:
session_data = get_session_data(session_id)
if not session_data:
return {
"status": "pending",
"message": "会话不存在或正在初始化"
}
return {"status": "pending", "message": "会话不存在或正在初始化"}
status = session_data.get("status")
jwt_sub = session_data.get("jwt_sub") # 使用 jwt_sub 而非 signature
@@ -147,7 +130,7 @@ class AuthService:
return {
"status": "waiting_scan",
"message": "请使用手机 QQ 扫描二维码",
"qrcode_image": session_data.get("qr_image_data")
"qrcode_image": session_data.get("qr_image_data"),
}
elif status == "success":
@@ -160,15 +143,12 @@ class AuthService:
if not token:
logger.error("Token 为空")
return {
"status": "error",
"message": "Token 为空"
}
return {"status": "error", "message": "Token 为空"}
try:
# 清洗 TokenURL 解码 + 去除 Bearer 前缀(参考 v1 实现)
pure_token = unquote(token) # URL 解码
if pure_token.lower().startswith('bearer '):
if pure_token.lower().startswith("bearer "):
pure_token = pure_token[7:] # 去除 "Bearer " 前缀
decoded = jwt.decode(pure_token, options={"verify_signature": False})
@@ -177,10 +157,7 @@ class AuthService:
logger.info(f"成功解析 JWT for sub={jwt_sub}, exp={jwt_exp}")
except Exception as e:
logger.error(f"解析 JWT Token 失败: {e}")
return {
"status": "error",
"message": f"Token 解析失败: {str(e)}"
}
return {"status": "error", "message": f"Token 解析失败: {str(e)}"}
# 查找用户(通过 jwt_sub
user = db.query(User).filter(User.jwt_sub == jwt_sub).first()
@@ -191,12 +168,18 @@ class AuthService:
if alias and alias == user.alias:
# 用户使用别名登录,验证 jwt_sub 是否一致
# 如果用户之前的 jwt_sub 不为空且与当前不一致,说明QQ号被换绑了
existing_jwt_sub = getattr(user, 'jwt_sub', '')
if isinstance(existing_jwt_sub, str) and existing_jwt_sub.strip() and existing_jwt_sub != jwt_sub:
logger.warning(f"⚠️ 用户 {user.alias} 的 jwt_sub 不匹配!数据库: {existing_jwt_sub}, 当前: {jwt_sub}")
existing_jwt_sub = getattr(user, "jwt_sub", "")
if (
isinstance(existing_jwt_sub, str)
and existing_jwt_sub.strip()
and existing_jwt_sub != jwt_sub
):
logger.warning(
f"⚠️ 用户 {user.alias} 的 jwt_sub 不匹配!数据库: {existing_jwt_sub}, 当前: {jwt_sub}"
)
return {
"status": "error",
"message": "QQ账号不匹配,请使用正确的QQ号扫码登录"
"message": "QQ账号不匹配,请使用正确的QQ号扫码登录",
}
user.authorization = pure_token # 存储清理后的 token
@@ -221,9 +204,9 @@ class AuthService:
"alias": user.alias,
"role": user.role,
"is_approved": user.is_approved,
"jwt_sub": user.jwt_sub
"jwt_sub": user.jwt_sub,
},
"is_new_user": False
"is_new_user": False,
}
else:
@@ -233,20 +216,14 @@ class AuthService:
# 验证用户名是否被预占
if not alias or not registration_manager.is_alias_reserved(alias):
logger.error(f"新用户注册失败:用户名 {alias} 未预占或已过期")
return {
"status": "error",
"message": "注册失败:会话已过期,请重新扫码"
}
return {"status": "error", "message": "注册失败:会话已过期,请重新扫码"}
# 检查用户名是否已被其他人注册(防止竞态)
existing_user_by_alias = db.query(User).filter(User.alias == alias).first()
if existing_user_by_alias:
registration_manager.release_alias(alias)
logger.error(f"新用户注册失败:用户名 {alias} 已被占用")
return {
"status": "error",
"message": "注册失败:用户名已被占用,请更换用户名"
}
return {"status": "error", "message": "注册失败:用户名已被占用,请更换用户名"}
# 创建新用户(待审批状态)
new_user = User(
@@ -270,6 +247,7 @@ class AuthService:
# 发送邮件通知管理员
try:
from backend.services.email_service import EmailService
EmailService.notify_new_user_registration(new_user, db)
except Exception as e:
logger.error(f"发送注册通知邮件失败: {e}")
@@ -286,22 +264,16 @@ class AuthService:
"alias": new_user.alias,
"role": new_user.role,
"is_approved": new_user.is_approved,
"jwt_sub": new_user.jwt_sub
"jwt_sub": new_user.jwt_sub,
},
"is_new_user": True
"is_new_user": True,
}
elif status == "error":
return {
"status": "error",
"message": session_data.get("message", "未知错误")
}
return {"status": "error", "message": session_data.get("message", "未知错误")}
else:
return {
"status": "pending",
"message": "正在初始化..."
}
return {"status": "pending", "message": "正在初始化..."}
@staticmethod
def verify_token(authorization: str, db: Session) -> Dict[str, Any]:
@@ -318,7 +290,11 @@ class AuthService:
from backend.utils.jwt import JWTManager
# 移除 "Bearer " 前缀
token = authorization.replace("Bearer ", "") if authorization.startswith("Bearer ") else authorization
token = (
authorization.replace("Bearer ", "")
if authorization.startswith("Bearer ")
else authorization
)
try:
# 验证 JWT token
@@ -326,19 +302,13 @@ class AuthService:
user_id = payload.get("user_id")
if not user_id:
return {
"is_valid": False,
"message": "Token 格式错误"
}
return {"is_valid": False, "message": "Token 格式错误"}
# 从数据库获取用户
user = db.query(User).filter(User.id == user_id).first()
if not user:
return {
"is_valid": False,
"message": "用户不存在"
}
return {"is_valid": False, "message": "用户不存在"}
return {
"is_valid": True,
@@ -346,25 +316,16 @@ class AuthService:
"user_id": user.id,
"alias": user.alias,
"role": user.role,
"is_approved": user.is_approved
"is_approved": user.is_approved,
}
except jwt.ExpiredSignatureError:
return {
"is_valid": False,
"message": "JWT Token 已过期"
}
return {"is_valid": False, "message": "JWT Token 已过期"}
except jwt.InvalidTokenError:
return {
"is_valid": False,
"message": "JWT Token 无效"
}
return {"is_valid": False, "message": "JWT Token 无效"}
except Exception as e:
logger.error(f"验证 JWT Token 失败: {str(e)}")
return {
"is_valid": False,
"message": "Token 验证失败"
}
return {"is_valid": False, "message": "Token 验证失败"}
@staticmethod
def verify_checkin_authorization(user: User) -> Dict[str, Any]:
@@ -386,25 +347,17 @@ class AuthService:
is_timestamp_expired,
days_until_expiry,
minutes_until_expiry,
seconds_until_expiry
seconds_until_expiry,
)
# 检查是否有 authorization token
if not user.authorization or user.authorization == "":
return {
"is_valid": False,
"message": "未设置打卡凭证",
"reason": "no_token"
}
return {"is_valid": False, "message": "未设置打卡凭证", "reason": "no_token"}
# 解析 jwt_exp
exp_timestamp = parse_jwt_exp(user.jwt_exp)
if not exp_timestamp:
return {
"is_valid": False,
"message": "打卡凭证无效",
"reason": "invalid_expiry"
}
return {"is_valid": False, "message": "打卡凭证无效", "reason": "invalid_expiry"}
# 检查是否过期
if is_timestamp_expired(exp_timestamp):
@@ -413,7 +366,7 @@ class AuthService:
"is_valid": False,
"message": f"打卡凭证已过期 {days_expired}",
"reason": "expired",
"days_expired": days_expired
"days_expired": days_expired,
}
# Token 有效,计算剩余时间
@@ -430,7 +383,7 @@ class AuthService:
"days_remaining": days_remaining,
"minutes_remaining": minutes_remaining,
"expiring_soon": expiring_soon,
"expires_at": exp_timestamp
"expires_at": exp_timestamp,
}
@staticmethod
@@ -451,10 +404,7 @@ class AuthService:
if not user:
logger.warning(f"别名登录失败:用户 {alias} 不存在")
return {
"success": False,
"message": "用户名或密码错误"
}
return {"success": False, "message": "用户名或密码错误"}
# 检查账户是否被锁定
if user.locked_until:
@@ -462,10 +412,12 @@ class AuthService:
if datetime.now() < user.locked_until:
remaining_seconds = (user.locked_until - datetime.now()).total_seconds()
remaining_minutes = int(remaining_seconds / 60) + 1
logger.warning(f"别名登录失败:用户 {alias} 账户已锁定,剩余 {remaining_minutes} 分钟")
logger.warning(
f"别名登录失败:用户 {alias} 账户已锁定,剩余 {remaining_minutes} 分钟"
)
return {
"success": False,
"message": f"账户已锁定,请 {remaining_minutes} 分钟后再试"
"message": f"账户已锁定,请 {remaining_minutes} 分钟后再试",
}
else:
# 锁定时间已过,重置锁定状态
@@ -477,15 +429,12 @@ class AuthService:
# 检查用户是否设置了密码
if not user.password_hash:
logger.warning(f"别名登录失败:用户 {alias} 未设置密码")
return {
"success": False,
"message": "该用户未设置密码,请使用扫码登录"
}
return {"success": False, "message": "该用户未设置密码,请使用扫码登录"}
# 验证密码
try:
password_bytes = password.encode('utf-8')
hash_bytes = user.password_hash.encode('utf-8')
password_bytes = password.encode("utf-8")
hash_bytes = user.password_hash.encode("utf-8")
if not bcrypt.checkpw(password_bytes, hash_bytes):
# 密码错误,增加失败次数
@@ -497,24 +446,20 @@ class AuthService:
user.locked_until = datetime.now() + timedelta(minutes=15)
db.commit()
logger.warning(f"别名登录失败:用户 {alias} 密码错误次数过多,账户已锁定15分钟")
return {
"success": False,
"message": "密码错误次数过多,账户已锁定15分钟"
}
return {"success": False, "message": "密码错误次数过多,账户已锁定15分钟"}
db.commit()
remaining_attempts = 5 - user.failed_login_attempts
logger.warning(f"别名登录失败:用户 {alias} 密码错误,剩余尝试次数: {remaining_attempts}")
logger.warning(
f"别名登录失败:用户 {alias} 密码错误,剩余尝试次数: {remaining_attempts}"
)
return {
"success": False,
"message": f"用户名或密码错误,剩余尝试次数: {remaining_attempts}"
"message": f"用户名或密码错误,剩余尝试次数: {remaining_attempts}",
}
except Exception as e:
logger.error(f"密码验证异常:{e}")
return {
"success": False,
"message": "登录失败,请稍后重试"
}
return {"success": False, "message": "登录失败,请稍后重试"}
# 密码正确,重置失败次数
user.failed_login_attempts = 0
@@ -551,17 +496,21 @@ class AuthService:
"id": user.id,
"alias": user.alias,
"role": user.role,
"is_approved": user.is_approved
}
"is_approved": user.is_approved,
},
}
# 如果打卡 Token 有问题,添加警告信息(不影响网站使用)
if token_warning:
result["token_warning"] = token_warning
if token_warning == "token_invalid":
result["warning_message"] = "登录成功,但检测到打卡凭证无效,无法自动打卡,建议扫码更新"
result["warning_message"] = (
"登录成功,但检测到打卡凭证无效,无法自动打卡,建议扫码更新"
)
elif token_warning == "token_expired":
result["warning_message"] = "登录成功,但检测到打卡凭证已过期,无法自动打卡,建议扫码更新"
result["warning_message"] = (
"登录成功,但检测到打卡凭证已过期,无法自动打卡,建议扫码更新"
)
return result
@@ -576,10 +525,10 @@ class AuthService:
Returns:
加密后的密码哈希
"""
password_bytes = password.encode('utf-8')
password_bytes = password.encode("utf-8")
salt = bcrypt.gensalt()
hash_bytes = bcrypt.hashpw(password_bytes, salt)
return hash_bytes.decode('utf-8')
return hash_bytes.decode("utf-8")
@staticmethod
def verify_password(password: str, password_hash: str) -> bool:
@@ -594,8 +543,8 @@ class AuthService:
密码是否正确
"""
try:
password_bytes = password.encode('utf-8')
hash_bytes = password_hash.encode('utf-8')
password_bytes = password.encode("utf-8")
hash_bytes = password_hash.encode("utf-8")
return bcrypt.checkpw(password_bytes, hash_bytes)
except Exception as e:
logger.error(f"密码验证异常:{e}")
@@ -617,12 +566,6 @@ class AuthService:
success = cancel_session(session_id)
if success:
return {
"success": True,
"message": "会话已取消"
}
return {"success": True, "message": "会话已取消"}
else:
return {
"success": False,
"message": "取消失败或会话不存在"
}
return {"success": False, "message": "取消失败或会话不存在"}
+92 -91
View File
@@ -39,7 +39,9 @@ class CheckInService:
task_info = build_task_info(task)
# 发送打卡失败通知(内容包含 Token 失效说明和刷新指引)
EmailService.notify_check_in_result(user, task_info, False, "Token 已失效,需要重新授权")
EmailService.notify_check_in_result(
user, task_info, False, "Token 已失效,需要重新授权"
)
logger.info(f"已发送 Token 过期邮件到 {user.email}")
# 标记已发送 Token 过期通知
@@ -63,7 +65,9 @@ class CheckInService:
Returns:
打卡记录 ID
"""
logger.info(f"🎯 创建待处理打卡记录 - 任务: {task.name or f'Task-{task.id}'} (ID: {task.id})")
logger.info(
f"🎯 创建待处理打卡记录 - 任务: {task.name or f'Task-{task.id}'} (ID: {task.id})"
)
# 创建一个 pending 状态的记录
record = CheckInRecord(
@@ -72,7 +76,7 @@ class CheckInService:
response_text="",
error_message="",
location="{}",
trigger_type=trigger_type
trigger_type=trigger_type,
)
db.add(record)
db.commit()
@@ -106,10 +110,9 @@ class CheckInService:
# 更新记录状态为失败
record = db.query(CheckInRecord).filter(CheckInRecord.id == record_id).first()
if record:
db.query(CheckInRecord).filter(CheckInRecord.id == record_id).update({
"status": "failure",
"error_message": "任务不存在"
})
db.query(CheckInRecord).filter(CheckInRecord.id == record_id).update(
{"status": "failure", "error_message": "任务不存在"}
)
db.commit()
return
@@ -121,26 +124,31 @@ class CheckInService:
CheckInService.handle_token_expired(task.user, task, db)
# 更新记录
db.query(CheckInRecord).filter(CheckInRecord.id == record_id).update({
"status": result["status"],
"response_text": result["response_text"],
"error_message": result["error_message"]
})
db.query(CheckInRecord).filter(CheckInRecord.id == record_id).update(
{
"status": result["status"],
"response_text": result["response_text"],
"error_message": result["error_message"],
}
)
db.commit()
if result["success"]:
logger.info(f"✅ 后台打卡成功 - Record ID: {record_id}")
else:
logger.error(f"❌ 后台打卡失败 - Record ID: {record_id}, 错误: {result['error_message']}")
logger.error(
f"❌ 后台打卡失败 - Record ID: {record_id}, 错误: {result['error_message']}"
)
except Exception as e:
logger.error(f"💥 后台打卡异常 - Task ID: {task_id}, Record ID: {record_id}, 错误: {str(e)}")
logger.error(
f"💥 后台打卡异常 - Task ID: {task_id}, Record ID: {record_id}, 错误: {str(e)}"
)
# 更新记录状态
try:
db.query(CheckInRecord).filter(CheckInRecord.id == record_id).update({
"status": "failure",
"error_message": f"后台执行异常: {str(e)}"
})
db.query(CheckInRecord).filter(CheckInRecord.id == record_id).update(
{"status": "failure", "error_message": f"后台执行异常: {str(e)}"}
)
db.commit()
except Exception as inner_e:
logger.error(f"💥 更新记录失败: {str(inner_e)}")
@@ -175,17 +183,13 @@ class CheckInService:
response_text="",
error_message=error_msg,
location="{}",
trigger_type=trigger_type
trigger_type=trigger_type,
)
db.add(record)
db.commit()
db.refresh(record)
return {
"record_id": record.id,
"status": "failure",
"message": error_msg
}
return {"record_id": record.id, "status": "failure", "message": error_msg}
# 不再提前验证 Token,交给统一的打卡逻辑处理
# 这样可以确保所有错误(包括 Token 过期)都通过统一的流程处理
@@ -195,10 +199,11 @@ class CheckInService:
# 在后台线程中执行打卡
import threading
thread = threading.Thread(
target=CheckInService.execute_check_in_async,
args=(task.id, record_id, user.authorization),
daemon=True
daemon=True,
)
thread.start()
@@ -207,7 +212,7 @@ class CheckInService:
return {
"record_id": record_id,
"status": "pending",
"message": "打卡任务已启动,正在后台处理"
"message": "打卡任务已启动,正在后台处理",
}
@staticmethod
@@ -223,13 +228,17 @@ class CheckInService:
Returns:
打卡结果字典
"""
logger.info(f"🎯 开始打卡 - 任务: {task.name or f'Task-{task.id}'} (ID: {task.id}), 触发: {trigger_type}")
logger.info(
f"🎯 开始打卡 - 任务: {task.name or f'Task-{task.id}'} (ID: {task.id}), 触发: {trigger_type}"
)
# 获取用户的打卡 Token
user = task.user
if not user or not user.authorization:
error_msg = f"用户没有有效的打卡 Token"
logger.error(f"{error_msg} - Task ID: {task.id}, User ID: {user.id if user else 'None'}")
logger.error(
f"{error_msg} - Task ID: {task.id}, User ID: {user.id if user else 'None'}"
)
# 记录失败
record = CheckInRecord(
@@ -238,20 +247,17 @@ class CheckInService:
response_text="",
error_message=error_msg,
location="{}",
trigger_type=trigger_type
trigger_type=trigger_type,
)
db.add(record)
db.commit()
db.refresh(record)
return {
"success": False,
"message": error_msg,
"record_id": record.id
}
return {"success": False, "message": error_msg, "record_id": record.id}
# 使用统一的打卡 Token 验证方法
from backend.services.auth_service import AuthService
token_result = AuthService.verify_checkin_authorization(user)
if not token_result["is_valid"]:
@@ -268,7 +274,7 @@ class CheckInService:
response_text="",
error_message=error_msg,
location="{}",
trigger_type=trigger_type
trigger_type=trigger_type,
)
db.add(record)
db.commit()
@@ -277,7 +283,7 @@ class CheckInService:
return {
"success": False,
"message": f"{error_msg},请重新扫码登录",
"record_id": record.id
"record_id": record.id,
}
# 执行打卡(传递 task 对象和用户 token)
@@ -295,7 +301,7 @@ class CheckInService:
response_text=result["response_text"],
error_message=result["error_message"],
location="{}",
trigger_type=trigger_type
trigger_type=trigger_type,
)
db.add(record)
db.commit()
@@ -309,7 +315,7 @@ class CheckInService:
return {
"success": result["success"],
"message": "打卡成功" if result["success"] else f"打卡失败: {result['error_message']}",
"record_id": record.id
"record_id": record.id,
}
@staticmethod
@@ -326,13 +332,7 @@ class CheckInService:
"""
logger.info(f"🚀 开始批量打卡,任务数量: {len(task_ids)}")
results = {
"total": len(task_ids),
"success": 0,
"failure": 0,
"skipped": 0,
"details": []
}
results = {"total": len(task_ids), "success": 0, "failure": 0, "skipped": 0, "details": []}
# 优化:一次性查询所有任务,避免 N+1 查询
tasks = db.query(CheckInTask).filter(CheckInTask.id.in_(task_ids)).all()
@@ -344,11 +344,9 @@ class CheckInService:
if not task:
logger.warning(f"⚠️ 任务 ID {task_id} 不存在,跳过")
results["skipped"] += 1
results["details"].append({
"task_id": task_id,
"success": False,
"message": "任务不存在"
})
results["details"].append(
{"task_id": task_id, "success": False, "message": "任务不存在"}
)
continue
# 执行打卡(移除 is_active 检查,允许手动打卡)
@@ -361,24 +359,26 @@ class CheckInService:
results["failure"] += 1
logger.error(f"❌ 任务 {task_id} 批量打卡失败: {result['message']}")
results["details"].append({
"task_id": task_id,
"task_name": task.name or f'Task-{task.id}',
"success": result["success"],
"message": result["message"],
"record_id": result.get("record_id")
})
results["details"].append(
{
"task_id": task_id,
"task_name": task.name or f"Task-{task.id}",
"success": result["success"],
"message": result["message"],
"record_id": result.get("record_id"),
}
)
except Exception as e:
logger.error(f"💥 任务 {task_id} 处理异常: {str(e)}")
results["failure"] += 1
results["details"].append({
"task_id": task_id,
"success": False,
"message": f"异常: {str(e)}"
})
results["details"].append(
{"task_id": task_id, "success": False, "message": f"异常: {str(e)}"}
)
logger.info(f"📊 批量打卡完成 - 成功: {results['success']}, 失败: {results['failure']}, 跳过: {results['skipped']}")
logger.info(
f"📊 批量打卡完成 - 成功: {results['success']}, 失败: {results['failure']}, 跳过: {results['skipped']}"
)
return results
@staticmethod
@@ -388,7 +388,7 @@ class CheckInService:
skip: int = 0,
limit: int = 100,
status: Optional[str] = None,
trigger_type: Optional[str] = None
trigger_type: Optional[str] = None,
) -> tuple[List[CheckInRecord], int]:
"""
获取任务的打卡记录
@@ -416,9 +416,7 @@ class CheckInService:
total = query.count()
# 获取分页数据
records = query.order_by(
CheckInRecord.check_in_time.desc()
).offset(skip).limit(limit).all()
records = query.order_by(CheckInRecord.check_in_time.desc()).offset(skip).limit(limit).all()
return records, total
@@ -429,7 +427,7 @@ class CheckInService:
skip: int = 0,
limit: int = 100,
status: Optional[str] = None,
trigger_type: Optional[str] = None
trigger_type: Optional[str] = None,
) -> tuple[List[CheckInRecord], int]:
"""
获取用户的所有打卡记录
@@ -462,9 +460,7 @@ class CheckInService:
total = query.count()
# 获取分页数据
records = query.order_by(
CheckInRecord.check_in_time.desc()
).offset(skip).limit(limit).all()
records = query.order_by(CheckInRecord.check_in_time.desc()).offset(skip).limit(limit).all()
return records, total
@@ -474,7 +470,7 @@ class CheckInService:
skip: int = 0,
limit: int = 100,
task_id: Optional[int] = None,
status: Optional[str] = None
status: Optional[str] = None,
) -> tuple[List[CheckInRecord], int]:
"""
获取所有打卡记录(管理员)- 使用联表查询优化性能
@@ -506,9 +502,7 @@ class CheckInService:
total = query.count()
# 获取分页数据
records = query.order_by(
CheckInRecord.check_in_time.desc()
).offset(skip).limit(limit).all()
records = query.order_by(CheckInRecord.check_in_time.desc()).offset(skip).limit(limit).all()
return records, total
@@ -527,8 +521,11 @@ class CheckInService:
包含额外信息的记录字典
"""
# 尝试使用已加载的关联对象,如果没有则查询
task = record.task if hasattr(record, 'task') and record.task else \
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
@@ -537,28 +534,32 @@ class CheckInService:
if task:
# 尝试使用已加载的 user,否则查询
user = task.user if hasattr(task, 'user') and task.user else \
db.query(User).filter(User.id == task.user_id).first()
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
from backend.utils.json_helpers import extract_thread_id
thread_id = extract_thread_id(task.payload_config) # type: ignore
# 转换为字典并添加额外字段
record_dict = {
'id': record.id,
'task_id': record.task_id,
'status': record.status,
'response_text': record.response_text,
'error_message': record.error_message,
'location': record.location,
'trigger_type': record.trigger_type,
'check_in_time': record.check_in_time,
'user_id': user.id if user else None,
'user_email': user.email if user else None,
'task_name': task_name,
'thread_id': thread_id,
"id": record.id,
"task_id": record.task_id,
"status": record.status,
"response_text": record.response_text,
"error_message": record.error_message,
"location": record.location,
"trigger_type": record.trigger_type,
"check_in_time": record.check_in_time,
"user_id": user.id if user else None,
"user_email": user.email if user else None,
"task_name": task_name,
"thread_id": thread_id,
}
return record_dict
+27 -13
View File
@@ -69,7 +69,11 @@ class EmailService:
# 安全获取创建时间
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 '未知'
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>
@@ -191,7 +195,9 @@ class EmailService:
# 安全获取创建时间
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 '未知'
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>
@@ -270,7 +276,7 @@ class EmailService:
<div class="success-box">
<strong>✅ 审批结果:</strong> 已通过
<br>
<strong>审批时间:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
<strong>审批时间:</strong> {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
</div>
<table class="info-table">
@@ -391,7 +397,7 @@ class EmailService:
<div class="error-box">
<strong>❌ 审批结果:</strong> 未通过
<br>
<strong>处理时间:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
<strong>处理时间:</strong> {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
</div>
{reason_html}
@@ -409,7 +415,6 @@ class EmailService:
return EmailService.send_email([str(user_email)], subject, body_html)
@staticmethod
def notify_token_expiring(user: User, jwt_exp: str) -> bool:
"""
@@ -640,7 +645,9 @@ class EmailService:
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:
def notify_check_in_result(
user: User, task_info: dict, success: bool, message: str = ""
) -> bool:
"""
通知用户打卡结果
@@ -665,9 +672,16 @@ class EmailService:
subject = f"【接龙自动打卡】打卡{status_text} - {user.alias}"
# 判断是否是 Token 失效导致的失败
is_token_error = not success and message and (
"Token" in message or "token" in message or
"失效" in message or "授权" in message or "登录" in message
is_token_error = (
not success
and message
and (
"Token" in message
or "token" in message
or "失效" in message
or "授权" in message
or "登录" in message
)
)
# Token 失效时的额外提示内容
@@ -768,20 +782,20 @@ class EmailService:
<table class="info-table">
<tr>
<td>执行时间</td>
<td>{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</td>
<td>{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</td>
</tr>
<tr>
<td>任务 ID</td>
<td>{task_info.get('thread_id', '未知')}</td>
<td>{task_info.get("thread_id", "未知")}</td>
</tr>
<tr>
<td>打卡状态</td>
<td><strong style="color: {status_color};">{status_text}</strong></td>
</tr>
{f'<tr><td>失败原因</td><td>{message}</td></tr>' if message else ''}
{f"<tr><td>失败原因</td><td>{message}</td></tr>" if message else ""}
</table>
{token_error_section if is_token_error else '<p>如有问题,请及时检查您的打卡配置。</p>'}
{token_error_section if is_token_error else "<p>如有问题,请及时检查您的打卡配置。</p>"}
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿直接回复。</p>
+25 -18
View File
@@ -1,6 +1,7 @@
"""
用户名预占和注册限流管理器
"""
import time
import threading
import logging
@@ -47,23 +48,22 @@ class RegistrationManager:
reservation = self._reserved_aliases[alias]
# 检查是否过期
if reservation['expire_time'] > current_time:
if reservation["expire_time"] > current_time:
# 未过期,检查是否是同一个 session
if reservation['session_id'] == session_id:
if reservation["session_id"] == session_id:
# 同一个 session,更新过期时间
reservation['expire_time'] = expire_time
reservation["expire_time"] = expire_time
logger.info(f"用户名 {alias} 预占时间已更新(session: {session_id}")
return True
else:
# 不同 session,预占失败
logger.warning(f"用户名 {alias} 已被占用(session: {reservation['session_id']}")
logger.warning(
f"用户名 {alias} 已被占用(session: {reservation['session_id']}"
)
return False
# 预占用户名
self._reserved_aliases[alias] = {
'session_id': session_id,
'expire_time': expire_time
}
self._reserved_aliases[alias] = {"session_id": session_id, "expire_time": expire_time}
logger.info(f"用户名 {alias} 已预占(session: {session_id}, 超时: {timeout_seconds}s")
return True
@@ -85,7 +85,7 @@ class RegistrationManager:
reservation = self._reserved_aliases[alias]
# 如果指定了 session_id,则只释放匹配的
if session_id and reservation['session_id'] != session_id:
if session_id and reservation["session_id"] != session_id:
logger.warning(f"尝试释放用户名 {alias},但 session 不匹配")
return False
@@ -111,7 +111,7 @@ class RegistrationManager:
current_time = time.time()
# 检查是否过期
if reservation['expire_time'] <= current_time:
if reservation["expire_time"] <= current_time:
# 已过期,自动释放
del self._reserved_aliases[alias]
return False
@@ -138,7 +138,9 @@ class RegistrationManager:
# 检查是否过期
if expire_time > current_time:
remaining = int(expire_time - current_time)
logger.warning(f"Cookie {cookie_value[:8]}... 在限流期内(剩余 {remaining} 秒)")
logger.warning(
f"Cookie {cookie_value[:8]}... 在限流期内(剩余 {remaining} 秒)"
)
return False
else:
# 已过期,移除记录
@@ -168,8 +170,9 @@ class RegistrationManager:
# 清理过期的用户名预占
expired_aliases = [
alias for alias, reservation in self._reserved_aliases.items()
if reservation['expire_time'] <= current_time
alias
for alias, reservation in self._reserved_aliases.items()
if reservation["expire_time"] <= current_time
]
for alias in expired_aliases:
@@ -178,7 +181,8 @@ class RegistrationManager:
# 清理过期的注册限流记录
expired_cookies = [
cookie for cookie, expire_time in self._registration_cookies.items()
cookie
for cookie, expire_time in self._registration_cookies.items()
if expire_time <= current_time
]
@@ -187,10 +191,13 @@ class RegistrationManager:
logger.debug(f"Cookie {cookie[:8]}... 限流记录已过期,自动清理")
if expired_aliases or expired_cookies:
logger.info(f"清理完成:{len(expired_aliases)} 个用户名,{len(expired_cookies)} 个 Cookie")
logger.info(
f"清理完成:{len(expired_aliases)} 个用户名,{len(expired_cookies)} 个 Cookie"
)
def _start_cleanup_thread(self) -> None:
"""启动定期清理线程"""
def cleanup_loop():
while True:
try:
@@ -207,9 +214,9 @@ class RegistrationManager:
"""获取当前状态统计"""
with self._lock:
return {
'reserved_aliases_count': len(self._reserved_aliases),
'rate_limited_cookies_count': len(self._registration_cookies),
'reserved_aliases': list(self._reserved_aliases.keys()),
"reserved_aliases_count": len(self._reserved_aliases),
"rate_limited_cookies_count": len(self._registration_cookies),
"reserved_aliases": list(self._reserved_aliases.keys()),
}
+25 -20
View File
@@ -39,14 +39,15 @@ def load_scheduled_tasks(db: Session, scheduler_instance):
# 移除所有现有的动态任务(保留系统任务)
for job in scheduler_instance.get_jobs():
if job.id.startswith('task_'):
if job.id.startswith("task_"):
scheduler_instance.remove_job(job.id)
# 查询所有启用且有 cron 表达式的任务
tasks = db.query(CheckInTask).filter(
CheckInTask.is_active == True,
CheckInTask.cron_expression.isnot(None)
).all()
tasks = (
db.query(CheckInTask)
.filter(CheckInTask.is_active == True, CheckInTask.cron_expression.isnot(None))
.all()
)
loaded_count = 0
skipped_count = 0
@@ -76,7 +77,7 @@ def load_scheduled_tasks(db: Session, scheduler_instance):
id=job_id,
name=f"CheckIn-Task-{task.id}",
args=[task.id],
replace_existing=True
replace_existing=True,
)
logger.info(f"✅ 加载任务 {task.id}: {task.name} (Cron: {task.cron_expression})")
@@ -90,7 +91,7 @@ def load_scheduled_tasks(db: Session, scheduler_instance):
"loaded": loaded_count,
"skipped": skipped_count,
"errors": error_count,
"total": len(tasks)
"total": len(tasks),
}
logger.info(f"任务加载完成: {result}")
@@ -114,7 +115,9 @@ def scheduled_check_in_task(task_id: int):
return
if not task.is_scheduled_enabled:
logger.info(f"任务 {task_id} 未启用定时打卡 (is_active={task.is_active}, cron={task.cron_expression})")
logger.info(
f"任务 {task_id} 未启用定时打卡 (is_active={task.is_active}, cron={task.cron_expression})"
)
return
logger.info(f"🤖 执行定时打卡任务 {task_id}")
@@ -184,14 +187,18 @@ def check_token_expiration():
# 计算剩余时间
time_until_expiry = seconds_until_expiry(exp_timestamp)
logger.debug(f"用户 {user.alias}: 剩余 {time_until_expiry} 秒 (即将过期标志={user.token_expiring_notified}, 已过期标志={user.token_expired_notified})")
logger.debug(
f"用户 {user.alias}: 剩余 {time_until_expiry} 秒 (即将过期标志={user.token_expiring_notified}, 已过期标志={user.token_expired_notified})"
)
# 情况1:Token 即将过期(过期前 30 分钟内,且还未过期)
if 0 < time_until_expiry < 1800: # 30分钟 = 1800秒
# 检查是否已发送过提醒
expiring_notified = bool(user.token_expiring_notified)
if not expiring_notified:
logger.info(f"用户 {user.alias} 的打卡 Token 即将过期,发送邮件提醒到 {user_email}...")
logger.info(
f"用户 {user.alias} 的打卡 Token 即将过期,发送邮件提醒到 {user_email}..."
)
from backend.services.email_service import EmailService
# 发送"即将过期"邮件
@@ -212,7 +219,9 @@ def check_token_expiration():
# 检查是否已发送过提醒
expired_notified = bool(user.token_expired_notified)
if not expired_notified:
logger.info(f"用户 {user.alias} 的打卡 Token 已过期,发送邮件提醒到 {user_email}...")
logger.info(
f"用户 {user.alias} 的打卡 Token 已过期,发送邮件提醒到 {user_email}..."
)
from backend.services.email_service import EmailService
# 发送"已过期"邮件
@@ -320,11 +329,9 @@ def start_scheduler():
minutes=settings.TOKEN_CHECK_INTERVAL_MINUTES,
id="check_token_expiration",
name="Token 过期检查任务",
replace_existing=True
)
logger.info(
f"已添加 Token 过期检查任务: 每 {settings.TOKEN_CHECK_INTERVAL_MINUTES} 分钟"
replace_existing=True,
)
logger.info(f"已添加 Token 过期检查任务: 每 {settings.TOKEN_CHECK_INTERVAL_MINUTES} 分钟")
# 添加会话文件清理任务(每隔指定小时)
scheduler.add_job(
@@ -333,11 +340,9 @@ def start_scheduler():
hours=settings.SESSION_CLEANUP_INTERVAL_HOURS,
id="cleanup_old_sessions",
name="清理旧会话文件任务",
replace_existing=True
)
logger.info(
f"已添加会话清理任务: 每 {settings.SESSION_CLEANUP_INTERVAL_HOURS} 小时"
replace_existing=True,
)
logger.info(f"已添加会话清理任务: 每 {settings.SESSION_CLEANUP_INTERVAL_HOURS} 小时")
# 添加清理过期未审批用户任务(每小时执行一次)
scheduler.add_job(
@@ -346,7 +351,7 @@ def start_scheduler():
hours=1,
id="cleanup_expired_pending_users",
name="清理过期未审批用户任务",
replace_existing=True
replace_existing=True,
)
logger.info("已添加清理过期未审批用户任务: 每 1 小时")
+40 -32
View File
@@ -36,22 +36,22 @@ class TaskService:
from backend.utils.json_helpers import safe_parse_payload, extract_thread_id
payload = safe_parse_payload(task_data.payload_config)
thread_id = payload.get('ThreadId')
thread_id = payload.get("ThreadId")
if not thread_id:
raise ValueError("payload_config 中缺少 ThreadId")
# 3. 验证唯一性:同一用户在同一个接龙中不能有重复的任务
existing_tasks = db.query(
CheckInTask.payload_config
).filter(
CheckInTask.user_id == user_id
).all()
existing_tasks = (
db.query(CheckInTask.payload_config).filter(CheckInTask.user_id == user_id).all()
)
for (payload_config,) in existing_tasks:
existing_thread_id = extract_thread_id(payload_config)
# extract_thread_id 已处理异常,失败时返回 None
if existing_thread_id and existing_thread_id == thread_id:
logger.warning(f"⚠️ 任务创建冲突 - User: {user.alias}({user_id}), ThreadId: {thread_id}")
logger.warning(
f"⚠️ 任务创建冲突 - User: {user.alias}({user_id}), ThreadId: {thread_id}"
)
raise ValueError(f"该接龙中已存在任务。ThreadId: {thread_id}")
# 4. 记录日志
@@ -63,14 +63,16 @@ class TaskService:
user_id=user_id,
payload_config=task_data.payload_config,
name=task_data.name or task_name,
is_active=task_data.is_active if task_data.is_active is not None else True
is_active=task_data.is_active if task_data.is_active is not None else True,
)
try:
db.add(task)
db.commit()
db.refresh(task)
logger.info(f"✅ 任务创建成功 - ID: {task.id}, Name: {task.name}, ThreadId: {thread_id}")
logger.info(
f"✅ 任务创建成功 - ID: {task.id}, Name: {task.name}, ThreadId: {thread_id}"
)
# 如果任务启用且包含 cron_expression,立即添加到调度器
if task.is_scheduled_enabled:
@@ -111,33 +113,38 @@ class TaskService:
from backend.utils.json_helpers import extract_thread_id
# 获取最后一次打卡记录
last_record = db.query(CheckInRecord).filter(
CheckInRecord.task_id == task.id
).order_by(desc(CheckInRecord.check_in_time)).first()
last_record = (
db.query(CheckInRecord)
.filter(CheckInRecord.task_id == task.id)
.order_by(desc(CheckInRecord.check_in_time))
.first()
)
# 从 payload_config 提取 ThreadId
thread_id = extract_thread_id(task.payload_config) # type: ignore
# 转换为字典并添加额外字段
task_dict = {
'id': task.id,
'user_id': task.user_id,
'payload_config': task.payload_config,
'name': task.name,
'is_active': task.is_active,
'cron_expression': task.cron_expression,
'is_scheduled_enabled': task.is_scheduled_enabled,
'created_at': task.created_at,
'updated_at': task.updated_at,
'thread_id': thread_id,
'last_check_in_time': last_record.check_in_time if last_record else None,
'last_check_in_status': last_record.status if last_record else None,
"id": task.id,
"user_id": task.user_id,
"payload_config": task.payload_config,
"name": task.name,
"is_active": task.is_active,
"cron_expression": task.cron_expression,
"is_scheduled_enabled": task.is_scheduled_enabled,
"created_at": task.created_at,
"updated_at": task.updated_at,
"thread_id": thread_id,
"last_check_in_time": last_record.check_in_time if last_record else None,
"last_check_in_status": last_record.status if last_record else None,
}
return task_dict
@staticmethod
def get_user_tasks(user_id: int, db: Session, include_inactive: bool = True) -> List[CheckInTask]:
def get_user_tasks(
user_id: int, db: Session, include_inactive: bool = True
) -> List[CheckInTask]:
"""
获取用户的所有任务
@@ -191,8 +198,8 @@ class TaskService:
update_data = task_data.model_dump(exclude_unset=True)
# 检查是否更新了 cron_expression 或 is_active
cron_changed = 'cron_expression' in update_data
active_changed = 'is_active' in update_data
cron_changed = "cron_expression" in update_data
active_changed = "is_active" in update_data
for field, value in update_data.items():
setattr(task, field, value)
@@ -297,10 +304,11 @@ class TaskService:
Returns:
是否属于该用户
"""
task = db.query(CheckInTask).filter(
CheckInTask.id == task_id,
CheckInTask.user_id == user_id
).first()
task = (
db.query(CheckInTask)
.filter(CheckInTask.id == task_id, CheckInTask.user_id == user_id)
.first()
)
return task is not None
@@ -342,7 +350,7 @@ class TaskService:
id=job_id,
name=f"CheckIn-Task-{task.id}",
args=[task.id],
replace_existing=True
replace_existing=True,
)
logger.info(f"✅ 任务 {task.id} 已重新加载到调度器: {cron_str}")
else:
+52 -78
View File
@@ -132,15 +132,13 @@ class TemplateService:
except json.JSONDecodeError as e:
logger.error(f"模板字段配置 JSON 格式错误: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"字段配置 JSON 格式错误: {str(e)}"
status_code=status.HTTP_400_BAD_REQUEST, detail=f"字段配置 JSON 格式错误: {str(e)}"
)
except Exception as e:
logger.error(f"创建模板失败: {str(e)}")
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"创建模板失败: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"创建模板失败: {str(e)}"
)
@staticmethod
@@ -159,10 +157,7 @@ class TemplateService:
@staticmethod
def get_all_templates(
db: Session,
skip: int = 0,
limit: int = 100,
is_active: Optional[bool] = None
db: Session, skip: int = 0, limit: int = 100, is_active: Optional[bool] = None
) -> List[TaskTemplate]:
"""
获取所有模板列表
@@ -185,9 +180,7 @@ class TemplateService:
@staticmethod
def update_template(
template_id: int,
template_data: TemplateUpdate,
db: Session
template_id: int, template_data: TemplateUpdate, db: Session
) -> TaskTemplate:
"""
更新模板
@@ -202,18 +195,15 @@ class TemplateService:
"""
template = TemplateService.get_template(template_id, db)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="模板不存在"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模板不存在")
try:
# 更新字段
update_data = template_data.model_dump(exclude_unset=True)
# 验证 field_config 如果有更新
if 'field_config' in update_data and update_data['field_config']:
json.loads(update_data['field_config'])
if "field_config" in update_data and update_data["field_config"]:
json.loads(update_data["field_config"])
for field, value in update_data.items():
setattr(template, field, value)
@@ -227,15 +217,13 @@ class TemplateService:
except json.JSONDecodeError as e:
logger.error(f"模板字段配置 JSON 格式错误: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"字段配置 JSON 格式错误: {str(e)}"
status_code=status.HTTP_400_BAD_REQUEST, detail=f"字段配置 JSON 格式错误: {str(e)}"
)
except Exception as e:
logger.error(f"更新模板失败: {str(e)}")
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"更新模板失败: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"更新模板失败: {str(e)}"
)
@staticmethod
@@ -252,10 +240,7 @@ class TemplateService:
"""
template = TemplateService.get_template(template_id, db)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="模板不存在"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模板不存在")
try:
db.delete(template)
@@ -266,28 +251,26 @@ class TemplateService:
logger.error(f"删除模板失败: {str(e)}")
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"删除模板失败: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"删除模板失败: {str(e)}"
)
@staticmethod
def _is_field_config(obj: Any) -> bool:
"""判断是否为字段配置对象"""
return isinstance(obj, dict) and 'display_name' in obj
return isinstance(obj, dict) and "display_name" in obj
@staticmethod
def _is_object_field(obj: Any) -> bool:
"""判断是否为对象字段(包含多个子字段配置)"""
if not isinstance(obj, dict):
return False
if 'display_name' in obj:
if "display_name" in obj:
return False
# 检查所有值是否都是字段配置对象
return all(
TemplateService._is_field_config(v)
for v in obj.values()
if isinstance(v, dict)
) and len(obj) > 0
return (
all(TemplateService._is_field_config(v) for v in obj.values() if isinstance(v, dict))
and len(obj) > 0
)
@staticmethod
def _process_field_value(key: str, config: Any, field_values: Dict[str, Any]) -> Any:
@@ -304,12 +287,12 @@ class TemplateService:
"""
# 1. 普通字段配置
if TemplateService._is_field_config(config):
if config.get('hidden', False):
value = config.get('default_value', '')
if config.get("hidden", False):
value = config.get("default_value", "")
else:
value = field_values.get(key, config.get('default_value', ''))
value = field_values.get(key, config.get("default_value", ""))
value_type = config.get('value_type', 'string')
value_type = config.get("value_type", "string")
return TemplateService._validate_and_convert_value(value, value_type, key)
# 2. 数组字段
@@ -319,10 +302,10 @@ class TemplateService:
# 检查数组元素是否是字段配置对象
if TemplateService._is_field_config(item_config):
# 数组元素是字段配置对象,需要序列化为 JSON 字符串
value = item_config.get('default_value', '')
value_type = item_config.get('value_type', 'string')
value = item_config.get("default_value", "")
value_type = item_config.get("value_type", "string")
# 将对象序列化为 JSON 字符串
if value_type == 'json':
if value_type == "json":
if isinstance(value, str):
# 如果是字符串,验证 JSON 格式
try:
@@ -333,15 +316,16 @@ class TemplateService:
error_detail += f"JSON 解析错误: {str(e)}\n"
error_detail += "常见问题: 数字不能有前导零(如 00.00 应改为 0.0)"
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=error_detail
status_code=status.HTTP_400_BAD_REQUEST, detail=error_detail
)
result.append(value)
else:
# 如果是对象,序列化为 JSON 字符串
result.append(json.dumps(value, ensure_ascii=False))
else:
result.append(TemplateService._validate_and_convert_value(value, value_type, key))
result.append(
TemplateService._validate_and_convert_value(value, value_type, key)
)
elif isinstance(item_config, dict):
# 数组元素是普通对象,递归处理
item = {}
@@ -388,9 +372,7 @@ class TemplateService:
field_config = TemplateService.merge_parent_config(template, db)
# 初始化 payload,只包含 ThreadId(唯一必需,不在模板中配置)
payload = {
"ThreadId": "<接龙项目ID>"
}
payload = {"ThreadId": "<接龙项目ID>"}
# 递归处理所有字段,保持键名原样
for key, config in field_config.items():
@@ -402,15 +384,12 @@ class TemplateService:
logger.error(f"解析模板配置失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"解析模板配置失败: {str(e)}"
detail=f"解析模板配置失败: {str(e)}",
)
@staticmethod
def assemble_payload_from_template(
template: TaskTemplate,
thread_id: str,
field_values: Dict[str, Any],
db: Session
template: TaskTemplate, thread_id: str, field_values: Dict[str, Any], db: Session
) -> Dict[str, Any]:
"""
根据模板和用户输入组装完整的 payload
@@ -432,9 +411,7 @@ class TemplateService:
field_config = TemplateService.merge_parent_config(template, db)
# 初始化 payload,只包含 ThreadId(唯一必需)
payload = {
"ThreadId": thread_id
}
payload = {"ThreadId": thread_id}
# 递归处理所有字段,保持键名原样
for key, config in field_config.items():
@@ -445,14 +422,13 @@ class TemplateService:
except json.JSONDecodeError as e:
logger.error(f"解析模板配置失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"解析模板配置失败"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"解析模板配置失败"
)
except Exception as e:
logger.error(f"组装 payload 失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"组装 payload 失败: {str(e)}"
detail=f"组装 payload 失败: {str(e)}",
)
@staticmethod
@@ -469,17 +445,17 @@ class TemplateService:
转换后的值
"""
try:
if value_type == 'int':
return int(value) if value != '' else 0
elif value_type == 'double':
return float(value) if value != '' else 0.0
elif value_type == 'bool':
if value_type == "int":
return int(value) if value != "" else 0
elif value_type == "double":
return float(value) if value != "" else 0.0
elif value_type == "bool":
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in ('true', '1', 'yes')
return value.lower() in ("true", "1", "yes")
return bool(value)
elif value_type == 'json':
elif value_type == "json":
# JSON 类型:如果是字符串,尝试解析后再序列化;如果是对象,直接序列化
if isinstance(value, str):
# 验证是否为有效 JSON
@@ -493,7 +469,7 @@ class TemplateService:
except (ValueError, TypeError, json.JSONDecodeError) as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"字段 '{field_name}' 类型错误:期望 {value_type},实际值为 '{value}',错误: {str(e)}"
detail=f"字段 '{field_name}' 类型错误:期望 {value_type},实际值为 '{value}',错误: {str(e)}",
)
@staticmethod
@@ -504,7 +480,7 @@ class TemplateService:
user_id: int,
task_name: Optional[str],
db: Session,
cron_expression: Optional[str] = "0 20 * * *"
cron_expression: Optional[str] = "0 20 * * *",
) -> CheckInTask:
"""
从模板创建打卡任务
@@ -524,16 +500,12 @@ class TemplateService:
# 获取模板
template = TemplateService.get_template(template_id, db)
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="模板不存在"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模板不存在")
# 检查模板是否启用
if template.is_active is not True:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="该模板未启用,无法创建任务"
status_code=status.HTTP_403_FORBIDDEN, detail="该模板未启用,无法创建任务"
)
# 组装 payload
@@ -543,7 +515,7 @@ class TemplateService:
# 生成任务名称
if not task_name:
signature = payload.get('Signature', 'Unknown')
signature = payload.get("Signature", "Unknown")
task_name = f"{template.name} - {signature}"
# 创建任务(包含 cron_expression
@@ -553,17 +525,20 @@ class TemplateService:
payload_config=json.dumps(payload, ensure_ascii=False),
name=task_name,
is_active=True,
cron_expression=cron_expression or "0 20 * * *"
cron_expression=cron_expression or "0 20 * * *",
)
db.add(task)
db.commit()
db.refresh(task)
logger.info(f"从模板创建任务成功: {task.name} (ID: {task.id}, 模板: {template.name}, ThreadId: {thread_id})")
logger.info(
f"从模板创建任务成功: {task.name} (ID: {task.id}, 模板: {template.name}, ThreadId: {thread_id})"
)
# 如果任务启用且包含 cron_expression,立即添加到调度器
if task.is_scheduled_enabled:
from backend.services.task_service import TaskService
TaskService._reload_scheduler_for_task(task, db)
return task
@@ -572,6 +547,5 @@ class TemplateService:
logger.error(f"从模板创建任务失败: {str(e)}")
db.rollback()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"创建任务失败: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"创建任务失败: {str(e)}"
)
+14 -7
View File
@@ -20,7 +20,7 @@ def escape_like_pattern(text: str) -> str:
Returns:
转义后的文本
"""
return text.replace('%', r'\%').replace('_', r'\_')
return text.replace("%", r"\%").replace("_", r"\_")
class UserService:
@@ -49,7 +49,9 @@ class UserService:
alias=user_data.alias,
email=user_data.email,
role=user_data.role or "user",
is_approved=user_data.is_approved if user_data.is_approved is not None else True, # 使用请求中的值,默认已审批
is_approved=user_data.is_approved
if user_data.is_approved is not None
else True, # 使用请求中的值,默认已审批
jwt_exp="0",
authorization=None,
)
@@ -57,14 +59,17 @@ class UserService:
# 如果提供了密码,则设置密码
if user_data.password:
import bcrypt
password_hash = bcrypt.hashpw(user_data.password.encode('utf-8'), bcrypt.gensalt())
setattr(user, 'password_hash', password_hash.decode('utf-8'))
password_hash = bcrypt.hashpw(user_data.password.encode("utf-8"), bcrypt.gensalt())
setattr(user, "password_hash", password_hash.decode("utf-8"))
db.add(user)
db.commit()
db.refresh(user)
logger.info(f"管理员创建用户成功: {user.alias} (ID: {user.id}, 角色: {user.role}, 密码: {'已设置' if user_data.password else '未设置'})")
logger.info(
f"管理员创建用户成功: {user.alias} (ID: {user.id}, 角色: {user.role}, 密码: {'已设置' if user_data.password else '未设置'})"
)
return user
@staticmethod
@@ -115,7 +120,7 @@ class UserService:
skip: int = 0,
limit: int = 100,
search: Optional[str] = None,
role: Optional[str] = None
role: Optional[str] = None,
) -> List[User]:
"""
获取所有用户
@@ -241,7 +246,9 @@ class UserService:
raise ValueError("修改密码时必须提供当前密码")
# 验证当前密码
if not AuthService.verify_password(update_data["current_password"], user.password_hash):
if not AuthService.verify_password(
update_data["current_password"], user.password_hash
):
raise ValueError("当前密码错误")
# 设置新密码