refactor: split some logic into util func

This commit is contained in:
2026-01-14 21:42:32 +08:00
parent 5a60e381d7
commit e842a9bc1d
14 changed files with 500 additions and 175 deletions
+3 -1
View File
@@ -189,7 +189,9 @@ async def get_system_stats(
# Token 即将过期的用户数(7天内) # Token 即将过期的用户数(7天内)
# 使用 SQL 直接查询,避免 N+1 问题 # 使用 SQL 直接查询,避免 N+1 问题
current_timestamp = int(datetime.now().timestamp()) from backend.utils.time_helpers import now_timestamp
current_timestamp = now_timestamp()
expiring_soon_timestamp = current_timestamp + (7 * 24 * 60 * 60) # 7天后 expiring_soon_timestamp = current_timestamp + (7 * 24 * 60 * 60) # 7天后
# 直接在数据库层面筛选即将过期的用户 # 直接在数据库层面筛选即将过期的用户
+3 -6
View File
@@ -66,13 +66,10 @@ async def get_check_in_record_status(
返回状态:pending(进行中)、success(成功)、failure(失败) 返回状态:pending(进行中)、success(成功)、failure(失败)
""" """
from backend.utils.db_helpers import get_or_404
# 获取打卡记录 # 获取打卡记录
record = db.query(CheckInRecord).filter(CheckInRecord.id == record_id).first() record = get_or_404(CheckInRecord, record_id, db, "打卡记录不存在")
if not record:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="打卡记录不存在"
)
# 验证记录归属(通过任务归属) # 验证记录归属(通过任务归属)
if not TaskService.verify_task_ownership(record.task_id, current_user.id, db): if not TaskService.verify_task_ownership(record.task_id, current_user.id, db):
+17 -29
View File
@@ -52,20 +52,11 @@ async def get_task(
需要验证任务属于当前用户 需要验证任务属于当前用户
""" """
# 验证任务归属 from backend.models import CheckInTask
if not TaskService.verify_task_ownership(task_id, current_user.id, db): from backend.utils.db_helpers import get_owned_or_403
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权访问此任务"
)
task = TaskService.get_task(task_id, db) # 获取任务并验证归属
task = get_owned_or_403(CheckInTask, task_id, current_user.id, db) # type: ignore
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在"
)
return task return task
@@ -82,12 +73,11 @@ async def update_task(
需要验证任务属于当前用户 需要验证任务属于当前用户
""" """
# 验证任务归属 from backend.models import CheckInTask
if not TaskService.verify_task_ownership(task_id, current_user.id, db): from backend.utils.db_helpers import get_owned_or_403
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, # 验证任务归属并获取任务
detail="无权访问此任务" get_owned_or_403(CheckInTask, task_id, current_user.id, db) # type: ignore
)
task = TaskService.update_task(task_id, task_data, db) task = TaskService.update_task(task_id, task_data, db)
@@ -111,12 +101,11 @@ async def delete_task(
需要验证任务属于当前用户,删除后会同时删除所有关联的打卡记录 需要验证任务属于当前用户,删除后会同时删除所有关联的打卡记录
""" """
from backend.models import CheckInTask
from backend.utils.db_helpers import get_owned_or_403
# 验证任务归属 # 验证任务归属
if not TaskService.verify_task_ownership(task_id, current_user.id, db): get_owned_or_403(CheckInTask, task_id, current_user.id, db) # type: ignore
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权访问此任务"
)
success = TaskService.delete_task(task_id, db) success = TaskService.delete_task(task_id, db)
@@ -138,12 +127,11 @@ async def toggle_task(
需要验证任务属于当前用户 需要验证任务属于当前用户
""" """
from backend.models import CheckInTask
from backend.utils.db_helpers import get_owned_or_403
# 验证任务归属 # 验证任务归属
if not TaskService.verify_task_ownership(task_id, current_user.id, db): get_owned_or_403(CheckInTask, task_id, current_user.id, db) # type: ignore
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权访问此任务"
)
task = TaskService.toggle_task(task_id, db) task = TaskService.toggle_task(task_id, db)
+14 -19
View File
@@ -1,6 +1,5 @@
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
import json
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
@@ -16,25 +15,23 @@ class TaskBase(BaseModel):
""" """
验证 payload_config 是否为有效的 JSON,并且包含必需的 ThreadId 字段 验证 payload_config 是否为有效的 JSON,并且包含必需的 ThreadId 字段
""" """
from backend.utils.json_helpers import safe_parse_json, extract_thread_id
if not v or not v.strip(): if not v or not v.strip():
raise ValueError("payload_config 不能为空") raise ValueError("payload_config 不能为空")
try: payload = safe_parse_json(v)
payload = json.loads(v) if payload is None:
except json.JSONDecodeError as e: raise ValueError("payload_config 必须是有效的 JSON 格式")
raise ValueError(f"payload_config 必须是有效的 JSON 格式: {str(e)}")
# 检查是否为字典类型 # 检查是否为字典类型
if not isinstance(payload, dict): if not isinstance(payload, dict):
raise ValueError("payload_config 必须是 JSON 对象(字典)") raise ValueError("payload_config 必须是 JSON 对象(字典)")
# 检查必需字段 ThreadId # 检查必需字段 ThreadId
if 'ThreadId' not in payload: thread_id = extract_thread_id(v)
raise ValueError("payload_config 必须包含 ThreadId 字段")
thread_id = payload.get('ThreadId')
if not thread_id or not str(thread_id).strip(): if not thread_id or not str(thread_id).strip():
raise ValueError("ThreadId 不能为空") raise ValueError("payload_config 必须包含有效的 ThreadId 字段")
return v return v
@@ -84,28 +81,26 @@ class TaskUpdate(BaseModel):
""" """
验证 payload_config 是否为有效的 JSON(如果提供的话) 验证 payload_config 是否为有效的 JSON(如果提供的话)
""" """
from backend.utils.json_helpers import safe_parse_json, extract_thread_id
if v is None: if v is None:
return v return v
if not v.strip(): if not v.strip():
raise ValueError("payload_config 不能为空字符串") raise ValueError("payload_config 不能为空字符串")
try: payload = safe_parse_json(v)
payload = json.loads(v) if payload is None:
except json.JSONDecodeError as e: raise ValueError("payload_config 必须是有效的 JSON 格式")
raise ValueError(f"payload_config 必须是有效的 JSON 格式: {str(e)}")
# 检查是否为字典类型 # 检查是否为字典类型
if not isinstance(payload, dict): if not isinstance(payload, dict):
raise ValueError("payload_config 必须是 JSON 对象(字典)") raise ValueError("payload_config 必须是 JSON 对象(字典)")
# 检查必需字段 ThreadId # 检查必需字段 ThreadId
if 'ThreadId' not in payload: thread_id = extract_thread_id(v)
raise ValueError("payload_config 必须包含 ThreadId 字段")
thread_id = payload.get('ThreadId')
if not thread_id or not str(thread_id).strip(): if not thread_id or not str(thread_id).strip():
raise ValueError("ThreadId 不能为空") raise ValueError("payload_config 必须包含有效的 ThreadId 字段")
return v return v
+21 -26
View File
@@ -381,6 +381,14 @@ class AuthService:
Returns: Returns:
包含打卡 token 验证结果的字典 包含打卡 token 验证结果的字典
""" """
from backend.utils.time_helpers import (
parse_jwt_exp,
is_timestamp_expired,
days_until_expiry,
minutes_until_expiry,
seconds_until_expiry
)
# 检查是否有 authorization token # 检查是否有 authorization token
if not user.authorization or user.authorization == "": if not user.authorization or user.authorization == "":
return { return {
@@ -389,20 +397,18 @@ class AuthService:
"reason": "no_token" "reason": "no_token"
} }
# 检查 Token 是否过期 # 解析 jwt_exp
if not user.jwt_exp or user.jwt_exp == "0": exp_timestamp = parse_jwt_exp(user.jwt_exp)
if not exp_timestamp:
return { return {
"is_valid": False, "is_valid": False,
"message": "打卡凭证无效", "message": "打卡凭证无效",
"reason": "invalid_expiry" "reason": "invalid_expiry"
} }
try: # 检查是否过期
exp_timestamp = int(user.jwt_exp) if is_timestamp_expired(exp_timestamp):
current_timestamp = int(datetime.now().timestamp()) days_expired = abs(days_until_expiry(exp_timestamp))
if current_timestamp > exp_timestamp:
days_expired = (current_timestamp - exp_timestamp) // 86400
return { return {
"is_valid": False, "is_valid": False,
"message": f"打卡凭证已过期 {days_expired}", "message": f"打卡凭证已过期 {days_expired}",
@@ -410,10 +416,10 @@ class AuthService:
"days_expired": days_expired "days_expired": days_expired
} }
# 计算剩余时间 # Token 有效,计算剩余时间
seconds_remaining = exp_timestamp - current_timestamp seconds_remaining = seconds_until_expiry(exp_timestamp)
days_remaining = seconds_remaining // 86400 days_remaining = days_until_expiry(exp_timestamp)
minutes_remaining = seconds_remaining // 60 minutes_remaining = minutes_until_expiry(exp_timestamp)
# 判断是否即将过期(30分钟内) # 判断是否即将过期(30分钟内)
expiring_soon = minutes_remaining <= 30 expiring_soon = minutes_remaining <= 30
@@ -427,14 +433,6 @@ class AuthService:
"expires_at": exp_timestamp "expires_at": exp_timestamp
} }
except ValueError:
logger.error(f"用户 {user.id} 的 jwt_exp 格式不正确: {user.jwt_exp}")
return {
"is_valid": False,
"message": "打卡凭证格式错误",
"reason": "invalid_format"
}
@staticmethod @staticmethod
def alias_login(alias: str, password: str, db: Session) -> Dict[str, Any]: def alias_login(alias: str, password: str, db: Session) -> Dict[str, Any]:
""" """
@@ -532,15 +530,12 @@ class AuthService:
token_warning = "token_invalid" token_warning = "token_invalid"
else: else:
# 检查 Token 是否过期 # 检查 Token 是否过期
try: from backend.utils.time_helpers import parse_jwt_exp, is_timestamp_expired
exp_timestamp = int(user.jwt_exp)
current_timestamp = int(datetime.now().timestamp())
if current_timestamp > exp_timestamp: exp_timestamp = parse_jwt_exp(user.jwt_exp)
if exp_timestamp and is_timestamp_expired(exp_timestamp):
logger.info(f"用户 {alias} Token 已过期,允许密码登录但需提示用户更新") logger.info(f"用户 {alias} Token 已过期,允许密码登录但需提示用户更新")
token_warning = "token_expired" token_warning = "token_expired"
except ValueError:
logger.error(f"用户 {user.id} 的 jwt_exp 格式不正确: {user.jwt_exp}")
# 登录成功 # 登录成功
logger.info(f"✅ 用户 {alias} (ID: {user.id}) 别名登录成功") logger.info(f"✅ 用户 {alias} (ID: {user.id}) 别名登录成功")
+5 -20
View File
@@ -2,7 +2,6 @@ import logging
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from datetime import datetime from datetime import datetime
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import json
import threading import threading
from backend.models import User, CheckInTask, CheckInRecord from backend.models import User, CheckInTask, CheckInRecord
@@ -34,20 +33,10 @@ class CheckInService:
try: try:
from backend.services.email_service import EmailService from backend.services.email_service import EmailService
from backend.utils.json_helpers import build_task_info
# 构建 task_info # 使用辅助函数构建 task_info
task_info = { task_info = build_task_info(task)
'thread_id': '未知',
'name': task.name or f'Task-{task.id}'
}
# 尝试从 payload_config 中获取 ThreadId
if task.payload_config:
try:
payload = json.loads(task.payload_config)
task_info['thread_id'] = payload.get('ThreadId', '未知')
except (json.JSONDecodeError, KeyError):
pass
# 发送打卡失败通知(内容包含 Token 失效说明和刷新指引) # 发送打卡失败通知(内容包含 Token 失效说明和刷新指引)
EmailService.notify_check_in_result(user, task_info, False, "Token 已失效,需要重新授权") EmailService.notify_check_in_result(user, task_info, False, "Token 已失效,需要重新授权")
@@ -553,12 +542,8 @@ class CheckInService:
task_name = task.name task_name = task.name
# 从 payload_config 提取 ThreadId # 从 payload_config 提取 ThreadId
try: from backend.utils.json_helpers import extract_thread_id
payload = json.loads(str(task.payload_config)) thread_id = extract_thread_id(task.payload_config) # type: ignore
thread_id = payload.get('ThreadId')
except (json.JSONDecodeError, KeyError, TypeError, AttributeError) as e:
logger.debug(f"解析任务 {task.id} 的 payload_config 失败: {e}")
pass
# 转换为字典并添加额外字段 # 转换为字典并添加额外字段
record_dict = { record_dict = {
+4 -6
View File
@@ -428,12 +428,10 @@ class EmailService:
return False return False
# 计算剩余时间 # 计算剩余时间
try: from backend.utils.time_helpers import parse_jwt_exp, minutes_until_expiry
exp_timestamp = int(jwt_exp)
current_timestamp = int(datetime.now().timestamp()) exp_timestamp = parse_jwt_exp(jwt_exp)
minutes_left = (exp_timestamp - current_timestamp) // 60 minutes_left = minutes_until_expiry(exp_timestamp) if exp_timestamp else 0
except ValueError:
minutes_left = 0
# 构建邮件内容 # 构建邮件内容
subject = f"【接龙自动打卡系统】登录凭证即将过期 - {user.alias}" subject = f"【接龙自动打卡系统】登录凭证即将过期 - {user.alias}"
+3 -4
View File
@@ -1,7 +1,6 @@
import logging import logging
import os import os
import time import time
from datetime import datetime
from pathlib import Path from pathlib import Path
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
@@ -154,6 +153,8 @@ def check_token_expiration():
检查所有用户的打卡 authorization token,如果在 30 分钟内过期,发送提醒邮件 检查所有用户的打卡 authorization token,如果在 30 分钟内过期,发送提醒邮件
注意:检查的是打卡业务 token,不是网站登录 JWT token 注意:检查的是打卡业务 token,不是网站登录 JWT token
""" """
from backend.utils.time_helpers import seconds_until_expiry
logger.info("Scheduler: 正在执行打卡 Token 过期检查...") logger.info("Scheduler: 正在执行打卡 Token 过期检查...")
try: try:
@@ -165,8 +166,6 @@ def check_token_expiration():
from backend.services.auth_service import AuthService from backend.services.auth_service import AuthService
users = db.query(User).all() users = db.query(User).all()
current_timestamp = int(datetime.now().timestamp())
notified_count = 0 notified_count = 0
for user in users: for user in users:
@@ -178,7 +177,7 @@ def check_token_expiration():
if not exp_timestamp: if not exp_timestamp:
continue continue
time_until_expiry = exp_timestamp - current_timestamp time_until_expiry = seconds_until_expiry(exp_timestamp)
# 情况1:Token 即将过期(过期前 30 分钟内,且还未过期) # 情况1:Token 即将过期(过期前 30 分钟内,且还未过期)
if 0 < time_until_expiry < 1800: # 30分钟 = 1800秒 if 0 < time_until_expiry < 1800: # 30分钟 = 1800秒
+11 -25
View File
@@ -2,7 +2,6 @@ import logging
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import desc from sqlalchemy import desc
import json
from backend.models import User, CheckInTask, CheckInRecord from backend.models import User, CheckInTask, CheckInRecord
from backend.schemas.task import TaskCreate, TaskUpdate from backend.schemas.task import TaskCreate, TaskUpdate
@@ -34,35 +33,26 @@ class TaskService:
raise ValueError(f"用户 ID {user_id} 不存在") raise ValueError(f"用户 ID {user_id} 不存在")
# 2. 从 payload_config 中提取 ThreadId 用于唯一性校验 # 2. 从 payload_config 中提取 ThreadId 用于唯一性校验
try: from backend.utils.json_helpers import safe_parse_payload, extract_thread_id
payload = json.loads(task_data.payload_config)
payload = safe_parse_payload(task_data.payload_config)
thread_id = payload.get('ThreadId') thread_id = payload.get('ThreadId')
if not thread_id: if not thread_id:
raise ValueError("payload_config 中缺少 ThreadId") raise ValueError("payload_config 中缺少 ThreadId")
except json.JSONDecodeError:
raise ValueError("payload_config 格式错误,必须是有效的 JSON")
# 3. 验证唯一性:同一用户在同一个接龙中不能有重复的任务 # 3. 验证唯一性:同一用户在同一个接龙中不能有重复的任务
# 优化:只查询必要的字段(id 和 payload_config),避免加载完整对象
existing_tasks = db.query( existing_tasks = db.query(
CheckInTask.id,
CheckInTask.payload_config CheckInTask.payload_config
).filter( ).filter(
CheckInTask.user_id == user_id CheckInTask.user_id == user_id
).all() ).all()
for task_id, payload_config in existing_tasks: for (payload_config,) in existing_tasks:
try: existing_thread_id = extract_thread_id(payload_config)
existing_payload = json.loads(payload_config) # extract_thread_id 已处理异常,失败时返回 None
if existing_payload.get('ThreadId') == thread_id: 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( raise ValueError(f"该接龙中已存在任务。ThreadId: {thread_id}")
f"该接龙中已存在任务。ThreadId: {thread_id}"
)
except (json.JSONDecodeError, AttributeError, TypeError):
# 跳过无法解析的 payload_config
logger.debug(f"跳过无法解析的任务配置 - Task ID: {task_id}")
continue
# 4. 记录日志 # 4. 记录日志
task_name = task_data.name or f"接龙任务 {thread_id}" task_name = task_data.name or f"接龙任务 {thread_id}"
@@ -118,19 +108,15 @@ class TaskService:
Returns: Returns:
包含额外信息的任务字典 包含额外信息的任务字典
""" """
from backend.utils.json_helpers import extract_thread_id
# 获取最后一次打卡记录 # 获取最后一次打卡记录
last_record = db.query(CheckInRecord).filter( last_record = db.query(CheckInRecord).filter(
CheckInRecord.task_id == task.id CheckInRecord.task_id == task.id
).order_by(desc(CheckInRecord.check_in_time)).first() ).order_by(desc(CheckInRecord.check_in_time)).first()
# 从 payload_config 提取 ThreadId # 从 payload_config 提取 ThreadId
thread_id = None thread_id = extract_thread_id(task.payload_config) # type: ignore
try:
payload = json.loads(str(task.payload_config))
thread_id = payload.get('ThreadId')
except (json.JSONDecodeError, AttributeError, TypeError):
logger.debug(f"无法从任务 {task.id} 的 payload_config 中提取 ThreadId")
pass
# 转换为字典并添加额外字段 # 转换为字典并添加额外字段
task_dict = { task_dict = {
+123
View File
@@ -0,0 +1,123 @@
"""
数据库操作辅助函数
提供统一的资源查询、权限验证等通用功能
"""
from typing import TypeVar, Type, Optional, Any
from sqlalchemy.orm import Session
from fastapi import HTTPException, status
T = TypeVar('T')
def get_or_404(
model: Type[T],
model_id: int,
db: Session,
error_message: Optional[str] = None
) -> T:
"""
查询资源,不存在则抛出 404
Args:
model: SQLAlchemy 模型类
model_id: 资源 ID
db: 数据库会话
error_message: 自定义错误消息
Returns:
查询到的资源对象
Raises:
HTTPException: 404 资源不存在
"""
obj = db.query(model).filter(model.id == model_id).first()
if not obj:
default_message = f"{model.__name__}不存在"
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=error_message or default_message
)
return obj
def get_owned_or_403(
model: Type[T],
model_id: int,
user_id: int,
db: Session,
error_message: Optional[str] = None
) -> T:
"""
查询资源并验证归属,否则抛出 403
Args:
model: SQLAlchemy 模型类(必须有 user_id 字段)
model_id: 资源 ID
user_id: 当前用户 ID
db: 数据库会话
error_message: 自定义错误消息
Returns:
查询到的资源对象
Raises:
HTTPException: 403 无权访问此资源
"""
obj = db.query(model).filter(
model.id == model_id,
model.user_id == user_id
).first()
if not obj:
# 先检查资源是否存在
exists = db.query(model).filter(model.id == model_id).first()
if not exists:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"{model.__name__}不存在"
)
# 资源存在但不属于当前用户
default_message = f"无权访问此{model.__name__}"
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=error_message or default_message
)
return obj
def get_by_field_or_404(
model: Type[T],
field_name: str,
field_value: Any,
db: Session,
error_message: Optional[str] = None
) -> T:
"""
根据字段查询资源,不存在则抛出 404
Args:
model: SQLAlchemy 模型类
field_name: 字段名
field_value: 字段值
db: 数据库会话
error_message: 自定义错误消息
Returns:
查询到的资源对象
Raises:
HTTPException: 404 资源不存在
"""
obj = db.query(model).filter(
getattr(model, field_name) == field_value
).first()
if not obj:
default_message = f"{model.__name__}不存在"
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=error_message or default_message
)
return obj
+103
View File
@@ -0,0 +1,103 @@
"""
JSON 处理辅助函数
提供安全的 JSON 解析和数据提取功能
"""
import json
import logging
from typing import Optional, Any, Dict
logger = logging.getLogger(__name__)
def safe_parse_json(
json_str: Optional[str],
default: Any = None,
log_error: bool = True
) -> Any:
"""
安全解析 JSON 字符串,失败时返回默认值
Args:
json_str: JSON 字符串
default: 解析失败时的默认值
log_error: 是否记录解析错误日志
Returns:
解析后的对象,失败时返回 default
"""
if not json_str:
return default
try:
return json.loads(str(json_str))
except (json.JSONDecodeError, AttributeError, TypeError) as e:
if log_error:
logger.debug(f"JSON 解析失败: {str(e)}, 原始数据: {json_str[:100]}...")
return default
def safe_parse_payload(
payload_config: Optional[str],
default: Optional[Dict] = None
) -> Dict:
"""
安全解析 payload_config,失败时返回默认字典
Args:
payload_config: payload 配置字符串
default: 解析失败时的默认值
Returns:
解析后的字典
"""
result = safe_parse_json(payload_config, default or {})
# 确保返回值是字典类型
if not isinstance(result, dict):
logger.warning(f"payload_config 不是字典类型: {type(result)}")
return default or {}
return result
def extract_thread_id(payload_config: Optional[str]) -> Optional[str]:
"""
从 payload_config 中提取 ThreadId
Args:
payload_config: payload 配置字符串
Returns:
ThreadId 或 None
"""
payload = safe_parse_payload(payload_config)
return payload.get('ThreadId')
def extract_signature(payload_config: Optional[str]) -> Optional[str]:
"""
从 payload_config 中提取 Signature
Args:
payload_config: payload 配置字符串
Returns:
Signature 或 None
"""
payload = safe_parse_payload(payload_config)
return payload.get('Signature')
def build_task_info(task) -> Dict[str, str]:
"""
从 task 对象构建 task_info 字典(用于邮件通知等场景)
Args:
task: CheckInTask 对象
Returns:
包含 thread_id 和 name 的字典
"""
return {
'thread_id': extract_thread_id(getattr(task, 'payload_config', None)) or '未知',
'name': getattr(task, 'name', None) or f'Task-{getattr(task, "id", "Unknown")}'
}
+148
View File
@@ -0,0 +1,148 @@
"""
时间处理辅助函数
提供统一的时间戳处理和格式化功能
"""
from datetime import datetime, timedelta
from typing import Optional
def now_timestamp() -> int:
"""
获取当前时间戳(秒)
Returns:
当前时间戳
"""
return int(datetime.now().timestamp())
def is_timestamp_expired(timestamp: int) -> bool:
"""
检查时间戳是否已过期
Args:
timestamp: 时间戳(秒)
Returns:
是否已过期
"""
return now_timestamp() > timestamp
def seconds_until_expiry(timestamp: int) -> int:
"""
计算距离过期的秒数(负数表示已过期)
Args:
timestamp: 时间戳(秒)
Returns:
距离过期的秒数
"""
return timestamp - now_timestamp()
def days_until_expiry(timestamp: int) -> int:
"""
计算距离过期的天数(负数表示已过期)
Args:
timestamp: 时间戳(秒)
Returns:
距离过期的天数
"""
seconds = seconds_until_expiry(timestamp)
return seconds // 86400
def hours_until_expiry(timestamp: int) -> int:
"""
计算距离过期的小时数(负数表示已过期)
Args:
timestamp: 时间戳(秒)
Returns:
距离过期的小时数
"""
seconds = seconds_until_expiry(timestamp)
return seconds // 3600
def minutes_until_expiry(timestamp: int) -> int:
"""
计算距离过期的分钟数(负数表示已过期)
Args:
timestamp: 时间戳(秒)
Returns:
距离过期的分钟数
"""
seconds = seconds_until_expiry(timestamp)
return seconds // 60
def format_timestamp(timestamp: int, format_str: str = '%Y-%m-%d %H:%M:%S') -> str:
"""
格式化时间戳为人类可读格式
Args:
timestamp: 时间戳(秒)
format_str: 时间格式字符串
Returns:
格式化后的时间字符串
"""
dt = datetime.fromtimestamp(timestamp)
return dt.strftime(format_str)
def format_expiry_time(timestamp: int) -> str:
"""
格式化过期时间为人类可读格式(带中文说明)
Args:
timestamp: 时间戳(秒)
Returns:
格式化后的时间字符串,如 "2024-01-01 12:00:00 (已过期 2 天)"
"""
formatted_time = format_timestamp(timestamp)
days = days_until_expiry(timestamp)
if days > 0:
return f"{formatted_time} (还剩 {days} 天)"
elif days == 0:
hours = hours_until_expiry(timestamp)
if hours > 0:
return f"{formatted_time} (还剩 {hours} 小时)"
else:
minutes = minutes_until_expiry(timestamp)
if minutes > 0:
return f"{formatted_time} (还剩 {minutes} 分钟)"
else:
return f"{formatted_time} (即将过期)"
else:
return f"{formatted_time} (已过期 {abs(days)} 天)"
def parse_jwt_exp(jwt_exp: Optional[str]) -> Optional[int]:
"""
解析 jwt_exp 字段为时间戳
Args:
jwt_exp: jwt_exp 字符串(可能是 "0" 或数字字符串)
Returns:
时间戳,无效时返回 None
"""
if not jwt_exp or jwt_exp == "0":
return None
try:
return int(jwt_exp)
except (ValueError, TypeError):
return None
+14 -12
View File
@@ -132,12 +132,10 @@ def perform_check_in(task, user_token: str) -> Dict[str, Any]:
- error_message: 错误信息 - error_message: 错误信息
""" """
# 从 payload_config 中提取 Signature 用于日志 # 从 payload_config 中提取 Signature 用于日志
try: from backend.utils.json_helpers import safe_parse_payload, extract_signature
payload_dict = json.loads(task.payload_config) if task.payload_config else {}
signature = payload_dict.get('Signature', 'Unknown') payload_dict = safe_parse_payload(task.payload_config)
except (json.JSONDecodeError, KeyError, TypeError, AttributeError) as e: signature = extract_signature(task.payload_config) or 'Unknown'
logger.debug(f"解析任务 {task.id} 的 payload_config 失败: {e}")
signature = 'Unknown'
logger.info(f"Selenium打卡: 正在为任务 ID: {task.id} (Signature: {signature}) 执行打卡...") logger.info(f"Selenium打卡: 正在为任务 ID: {task.id} (Signature: {signature}) 执行打卡...")
@@ -165,9 +163,12 @@ def perform_check_in(task, user_token: str) -> Dict[str, Any]:
try: try:
# 使用任务的 payload_config(从模板生成的完整配置,包含 ThreadId) # 使用任务的 payload_config(从模板生成的完整配置,包含 ThreadId)
payload = json.loads(task.payload_config) if task.payload_config else {} from backend.utils.json_helpers import safe_parse_payload, extract_thread_id
if not payload.get('ThreadId'): payload = safe_parse_payload(task.payload_config)
thread_id = extract_thread_id(task.payload_config)
if not thread_id:
error_msg = f"任务 ID: {task.id} 的 payload_config 缺少 ThreadId" error_msg = f"任务 ID: {task.id} 的 payload_config 缺少 ThreadId"
logger.error(error_msg) logger.error(error_msg)
return { return {
@@ -261,10 +262,11 @@ def perform_check_in(task, user_token: str) -> Dict[str, Any]:
if task.user and task.user.email: if task.user and task.user.email:
try: try:
from backend.services.email_service import EmailService from backend.services.email_service import EmailService
task_info = { from backend.utils.json_helpers import build_task_info
'thread_id': payload.get('ThreadId', '未知'),
'name': getattr(task, 'name', '打卡任务') # 使用辅助函数构建 task_info(从 task 对象提取信息)
} task_info = build_task_info(task)
# 只发送打卡失败通知(内容已说明Token失效) # 只发送打卡失败通知(内容已说明Token失效)
EmailService.notify_check_in_result(task.user, task_info, False, "Token 已失效,需要重新授权") EmailService.notify_check_in_result(task.user, task_info, False, "Token 已失效,需要重新授权")
except Exception as e: except Exception as e:
+9 -5
View File
@@ -59,9 +59,10 @@ def get_session_status(session_id: str) -> str:
content = f.read() content = f.read()
if not content: if not content:
return None return None
data = json.loads(content) from backend.utils.json_helpers import safe_parse_json
data = safe_parse_json(content, {})
return data.get('status') return data.get('status')
except (IOError, json.JSONDecodeError) as e: except IOError as e:
logger.error(f"读取会话文件 {filepath} 失败: {e}") logger.error(f"读取会话文件 {filepath} 失败: {e}")
return None return None
@@ -80,8 +81,9 @@ def get_session_data(session_id: str) -> dict:
content = f.read() content = f.read()
if not content: if not content:
return None return None
return json.loads(content) from backend.utils.json_helpers import safe_parse_json
except (IOError, json.JSONDecodeError) as e: return safe_parse_json(content, {})
except IOError as e:
logger.error(f"读取会话文件 {filepath} 失败: {e}") logger.error(f"读取会话文件 {filepath} 失败: {e}")
return None return None
@@ -106,11 +108,13 @@ def cancel_session(session_id: str) -> bool:
try: try:
with FileLock(lock_path, timeout=5): with FileLock(lock_path, timeout=5):
# 读取当前会话数据 # 读取当前会话数据
from backend.utils.json_helpers import safe_parse_json
with open(filepath, 'r', encoding='utf-8') as f: with open(filepath, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
if not content: if not content:
return False return False
data = json.loads(content) data = safe_parse_json(content, {})
# 如果已经成功,不允许取消 # 如果已经成功,不允许取消
if data.get('status') == 'success': if data.get('status') == 'success':