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
+64 -73
View File
@@ -18,6 +18,7 @@ router = APIRouter()
class BatchToggleTasksRequest(BaseModel):
"""批量启用/禁用任务请求"""
task_ids: List[int]
is_active: bool
@@ -26,7 +27,7 @@ class BatchToggleTasksRequest(BaseModel):
async def batch_toggle_tasks(
request: BatchToggleTasksRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
批量启用或禁用任务的自动打卡功能(需要管理员权限)
@@ -47,12 +48,11 @@ async def batch_toggle_tasks(
return {
"success": True,
"message": f"{'启用' if request.is_active else '禁用'} {count} 个任务",
"count": count
"count": count,
}
except Exception as e:
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)}"
)
@@ -60,7 +60,7 @@ async def batch_toggle_tasks(
async def batch_check_in(
request: BatchCheckInRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
批量触发任务打卡(需要管理员权限)
@@ -74,15 +74,14 @@ async def batch_check_in(
return result
except Exception as e:
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)}"
)
@router.get("/logs", summary="获取系统日志")
async def get_system_logs(
lines: int = Query(200, ge=1, le=2000, description="读取的日志行数"),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
获取系统日志(需要管理员权限)
@@ -95,40 +94,34 @@ async def get_system_logs(
log_file = settings.LOG_FILE
if not log_file.exists():
return {
"success": True,
"message": "日志文件不存在",
"logs": "日志文件不存在"
}
return {"success": True, "message": "日志文件不存在", "logs": "日志文件不存在"}
# 使用 deque 高效读取最后 N 行,避免将整个文件加载到内存
from collections import deque
with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
with open(log_file, "r", encoding="utf-8", errors="ignore") as f:
# 使用 deque 保持最后 N 行,内存占用固定
last_lines = deque(f, maxlen=lines)
# 返回字符串格式(不是数组)
log_content = ''.join(last_lines)
log_content = "".join(last_lines)
return {
"success": True,
"message": f"读取了最后 {len(last_lines)} 行日志",
"logs": log_content
"logs": log_content,
}
except Exception as e:
logger.error(f"读取日志失败: {str(e)}")
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)}"
)
@router.get("/stats", summary="获取系统统计")
async def get_system_stats(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)
):
"""
获取系统统计信息(需要管理员权限)
@@ -159,33 +152,39 @@ async def get_system_stats(
# 今日打卡记录数
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
today_records = db.query(CheckInRecord).filter(
CheckInRecord.check_in_time >= today_start
).count()
today_records = (
db.query(CheckInRecord).filter(CheckInRecord.check_in_time >= today_start).count()
)
# 今日成功打卡数
today_success = db.query(CheckInRecord).filter(
CheckInRecord.check_in_time >= today_start,
CheckInRecord.status == "success"
).count()
today_success = (
db.query(CheckInRecord)
.filter(CheckInRecord.check_in_time >= today_start, CheckInRecord.status == "success")
.count()
)
# 今日失败打卡数
today_failure = db.query(CheckInRecord).filter(
CheckInRecord.check_in_time >= today_start,
CheckInRecord.status == "failure"
).count()
today_failure = (
db.query(CheckInRecord)
.filter(CheckInRecord.check_in_time >= today_start, CheckInRecord.status == "failure")
.count()
)
# 今日时间范围外打卡数
today_out_of_time = db.query(CheckInRecord).filter(
CheckInRecord.check_in_time >= today_start,
CheckInRecord.status == "out_of_time"
).count()
today_out_of_time = (
db.query(CheckInRecord)
.filter(
CheckInRecord.check_in_time >= today_start, CheckInRecord.status == "out_of_time"
)
.count()
)
# 今日异常打卡数
today_unknown = db.query(CheckInRecord).filter(
CheckInRecord.check_in_time >= today_start,
CheckInRecord.status == "unknown"
).count()
today_unknown = (
db.query(CheckInRecord)
.filter(CheckInRecord.check_in_time >= today_start, CheckInRecord.status == "unknown")
.count()
)
# Token 即将过期的用户数(7天内)
# 使用 SQL 直接查询,避免 N+1 问题
@@ -198,28 +197,32 @@ async def get_system_stats(
# 条件:authorization 不为空、jwt_exp 不为 "0"、且在未来 7 天内过期
from sqlalchemy import cast, Integer, and_
expiring_users = db.query(User).filter(
and_(
User.authorization.isnot(None),
User.authorization != "",
User.jwt_exp.isnot(None),
User.jwt_exp != "0",
cast(User.jwt_exp, Integer) > current_timestamp, # 未过期
cast(User.jwt_exp, Integer) < expiring_soon_timestamp # 7天内过期
expiring_users = (
db.query(User)
.filter(
and_(
User.authorization.isnot(None),
User.authorization != "",
User.jwt_exp.isnot(None),
User.jwt_exp != "0",
cast(User.jwt_exp, Integer) > current_timestamp, # 未过期
cast(User.jwt_exp, Integer) < expiring_soon_timestamp, # 7天内过期
)
)
).count()
.count()
)
return {
"users": {
"total": total_users,
"admin": admin_users,
"regular": total_users - admin_users,
"active": approved_users # 使用已审批用户数
"active": approved_users, # 使用已审批用户数
},
"tasks": {
"total": total_tasks,
"active": active_tasks,
"inactive": total_tasks - active_tasks
"inactive": total_tasks - active_tasks,
},
"check_in_records": {
"total": total_records,
@@ -227,24 +230,20 @@ async def get_system_stats(
"today_success": today_success,
"today_failure": today_failure,
"today_out_of_time": today_out_of_time,
"today_unknown": today_unknown
"today_unknown": today_unknown,
},
"tokens": {
"expiring_soon": expiring_users
}
"tokens": {"expiring_soon": expiring_users},
}
except Exception as e:
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)}"
)
@router.get("/users/pending", response_model=List[UserResponse], summary="获取待审批用户")
async def get_pending_users(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user)
):
"""
获取所有待审批的用户(需要管理员权限)
@@ -255,7 +254,7 @@ async def get_pending_users(
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"获取待审批用户失败: {str(e)}"
detail=f"获取待审批用户失败: {str(e)}",
)
@@ -263,7 +262,7 @@ async def get_pending_users(
async def approve_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
审批通过指定用户(需要管理员权限)
@@ -272,18 +271,14 @@ async def approve_user(
result = AdminService.approve_user(user_id, db)
if not result["success"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=result["message"]
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=result["message"])
return result
except HTTPException:
raise
except Exception as e:
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)}"
)
@@ -291,7 +286,7 @@ async def approve_user(
async def reject_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
拒绝并删除指定用户(需要管理员权限)
@@ -300,16 +295,12 @@ async def reject_user(
result = AdminService.reject_user(user_id, db)
if not result["success"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=result["message"]
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=result["message"])
return result
except HTTPException:
raise
except Exception as e:
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)}"
)
+12 -28
View File
@@ -21,10 +21,7 @@ router = APIRouter()
@router.post("/request_qrcode", response_model=dict, summary="请求 QQ 扫码二维码")
@limiter.limit("10/minute") # 每分钟最多10次请求
async def request_qrcode(
request_obj: QRCodeRequest,
request: Request,
response: Response,
db: Session = Depends(get_db)
request_obj: QRCodeRequest, request: Request, response: Response, db: Session = Depends(get_db)
):
"""
请求 QQ 扫码二维码
@@ -44,7 +41,7 @@ async def request_qrcode(
raise BusinessLogicError(
message="注册过于频繁,请 10 分钟后再试",
error_code="RATE_LIMIT_EXCEEDED",
status_code=429
status_code=429,
)
else:
# 生成新的 Cookie
@@ -67,22 +64,18 @@ async def request_qrcode(
value=reg_cookie,
max_age=600, # 10 分钟
httponly=True,
samesite="lax"
samesite="lax",
)
return result
except Exception as e:
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)}"
)
@router.get("/qrcode_status/{session_id}", response_model=dict, summary="检查二维码扫描状态")
async def get_qrcode_status(
session_id: str,
db: Session = Depends(get_db)
):
async def get_qrcode_status(session_id: str, db: Session = Depends(get_db)):
"""
检查二维码扫描状态
@@ -104,15 +97,12 @@ async def get_qrcode_status(
return result
except Exception as e:
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)}"
)
@router.delete("/qrcode_session/{session_id}", response_model=dict, summary="取消二维码登录会话")
async def cancel_qrcode_session(
session_id: str
):
async def cancel_qrcode_session(session_id: str):
"""
取消二维码登录会话
@@ -125,16 +115,12 @@ async def cancel_qrcode_session(
return result
except Exception as e:
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)}"
)
@router.post("/verify_token", response_model=dict, summary="验证 JWT Token 有效性")
async def verify_token(
request: TokenVerifyRequest,
db: Session = Depends(get_db)
):
async def verify_token(request: TokenVerifyRequest, db: Session = Depends(get_db)):
"""
验证 JWT Token 有效性(网站登录认证)
@@ -152,8 +138,7 @@ async def verify_token(
return result
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"验证 Token 失败: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"验证 Token 失败: {str(e)}"
)
@@ -162,7 +147,7 @@ async def verify_token(
async def alias_login(
login_data: AliasLoginRequest,
request: Request, # slowapi需要的request参数
db: Session = Depends(get_db)
db: Session = Depends(get_db),
):
"""
别名+密码登录(仅限已设置密码的用户)
@@ -187,6 +172,5 @@ async def alias_login(
return result
except Exception as e:
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)}"
)
+49 -64
View File
@@ -18,9 +18,7 @@ router = APIRouter()
@router.post("/manual/{task_id}", summary="手动触发打卡(异步)")
async def manual_check_in(
task_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
task_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
):
"""
手动触发指定任务的打卡(异步方式,立即返回)
@@ -31,33 +29,24 @@ async def manual_check_in(
"""
# 验证任务归属
if not TaskService.verify_task_ownership(task_id, current_user.id, db):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权访问此任务"
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权访问此任务")
task = TaskService.get_task(task_id, db)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
try:
result = CheckInService.start_async_check_in(task, "manual", db)
return result
except Exception as e:
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)}"
)
@router.get("/record/{record_id}/status", summary="查询打卡记录状态")
async def get_check_in_record_status(
record_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
record_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
):
"""
查询指定打卡记录的状态
@@ -73,10 +62,7 @@ async def get_check_in_record_status(
# 验证记录归属(通过任务归属)
if not TaskService.verify_task_ownership(record.task_id, current_user.id, db):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权访问此记录"
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权访问此记录")
return {
"record_id": record.id,
@@ -85,19 +71,25 @@ async def get_check_in_record_status(
"response_text": record.response_text,
"error_message": record.error_message,
"trigger_type": record.trigger_type,
"check_in_time": record.check_in_time
"check_in_time": record.check_in_time,
}
@router.get("/task/{task_id}/records", response_model=PaginatedResponse[CheckInRecordResponse], summary="查看任务的打卡记录")
@router.get(
"/task/{task_id}/records",
response_model=PaginatedResponse[CheckInRecordResponse],
summary="查看任务的打卡记录",
)
async def get_task_check_in_records(
task_id: int,
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=500, description="限制记录数"),
status_filter: Optional[str] = Query(None, alias="status", description="过滤状态 (success/failure)"),
status_filter: Optional[str] = Query(
None, alias="status", description="过滤状态 (success/failure)"
),
trigger_type: Optional[str] = Query(None, description="过滤触发类型 (scheduler/manual)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
current_user: User = Depends(get_current_user),
):
"""
查看指定任务的打卡记录
@@ -112,36 +104,33 @@ async def get_task_check_in_records(
"""
# 验证任务归属
if not TaskService.verify_task_ownership(task_id, current_user.id, db):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权访问此任务"
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权访问此任务")
try:
records, total = CheckInService.get_task_records(
task_id, db, skip, limit, status_filter, trigger_type
)
return PaginatedResponse(
records=records,
total=total,
skip=skip,
limit=limit
)
return PaginatedResponse(records=records, total=total, skip=skip, limit=limit)
except Exception as e:
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)}"
)
@router.get("/my-records", response_model=PaginatedResponse[CheckInRecordResponse], summary="查看当前用户的所有打卡记录")
@router.get(
"/my-records",
response_model=PaginatedResponse[CheckInRecordResponse],
summary="查看当前用户的所有打卡记录",
)
async def get_my_check_in_records(
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=500, description="限制记录数"),
status_filter: Optional[str] = Query(None, alias="status", description="过滤状态 (success/failure)"),
status_filter: Optional[str] = Query(
None, alias="status", description="过滤状态 (success/failure)"
),
trigger_type: Optional[str] = Query(None, description="过滤触发类型 (scheduler/manual)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
current_user: User = Depends(get_current_user),
):
"""
查看当前用户所有任务的打卡记录
@@ -155,28 +144,27 @@ async def get_my_check_in_records(
records, total = CheckInService.get_user_records(
current_user.id, db, skip, limit, status_filter, trigger_type
)
return PaginatedResponse(
records=records,
total=total,
skip=skip,
limit=limit
)
return PaginatedResponse(records=records, total=total, skip=skip, limit=limit)
except Exception as e:
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)}"
)
@router.get("/records", response_model=PaginatedResponse[CheckInRecordResponse], summary="查看所有打卡记录(管理员)")
@router.get(
"/records",
response_model=PaginatedResponse[CheckInRecordResponse],
summary="查看所有打卡记录(管理员)",
)
async def get_all_check_in_records(
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=500, description="限制记录数"),
task_id: Optional[int] = Query(None, description="过滤任务 ID"),
status_filter: Optional[str] = Query(None, alias="status", description="过滤状态 (success/failure)"),
status_filter: Optional[str] = Query(
None, alias="status", description="过滤状态 (success/failure)"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
查看所有打卡记录(需要管理员权限)
@@ -189,26 +177,24 @@ async def get_all_check_in_records(
try:
records, total = CheckInService.get_all_records(db, skip, limit, task_id, status_filter)
# 为每条记录添加用户和任务信息
enriched_records = [CheckInService.enrich_record_with_user_task_info(record, db) for record in records]
return PaginatedResponse(
records=enriched_records,
total=total,
skip=skip,
limit=limit
)
enriched_records = [
CheckInService.enrich_record_with_user_task_info(record, db) for record in records
]
return PaginatedResponse(records=enriched_records, total=total, skip=skip, limit=limit)
except Exception as e:
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)}"
)
@router.get("/records/count", summary="获取打卡记录统计(管理员)")
async def get_check_in_records_count(
task_id: Optional[int] = Query(None, description="过滤任务 ID"),
status_filter: Optional[str] = Query(None, alias="status", description="过滤状态 (success/failure)"),
status_filter: Optional[str] = Query(
None, alias="status", description="过滤状态 (success/failure)"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
获取打卡记录统计(需要管理员权限)
@@ -229,6 +215,5 @@ async def get_check_in_records_count(
return {"total": total}
except Exception as e:
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)}"
)
+19 -34
View File
@@ -14,15 +14,18 @@ router = APIRouter()
class CronValidateRequest(BaseModel):
"""Cron 表达式验证请求"""
cron_expression: str = Field(..., min_length=9, description="Crontab 表达式")
# create_task_from_template: 已在 templates.py 中定义
@router.get("/", response_model=List[TaskResponse], summary="获取当前用户的任务列表")
async def get_tasks(
include_inactive: bool = True,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
db: Session = Depends(get_db),
):
"""
获取当前用户的所有打卡任务
@@ -36,16 +39,13 @@ async def get_tasks(
return enriched_tasks
except Exception as e:
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)}"
)
@router.get("/{task_id}", response_model=TaskResponse, summary="获取任务详情")
async def get_task(
task_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
task_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)
):
"""
获取指定任务的详情
@@ -66,7 +66,7 @@ async def update_task(
task_id: int,
task_data: TaskUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
db: Session = Depends(get_db),
):
"""
更新指定任务的信息
@@ -82,19 +82,14 @@ async def update_task(
task = TaskService.update_task(task_id, task_data, db)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
return task
@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT, summary="删除任务")
async def delete_task(
task_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
task_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)
):
"""
删除指定任务
@@ -110,17 +105,12 @@ async def delete_task(
success = TaskService.delete_task(task_id, db)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
@router.post("/{task_id}/toggle", response_model=TaskResponse, summary="切换任务启用状态")
async def toggle_task(
task_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
task_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)
):
"""
切换任务的启用/禁用状态
@@ -136,10 +126,7 @@ async def toggle_task(
task = TaskService.toggle_task(task_id, db)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="任务不存在"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
return task
@@ -167,8 +154,7 @@ async def validate_cron_expression(request: CronValidateRequest):
if not cron_expr:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="cron_expression 是必需的"
status_code=status.HTTP_400_BAD_REQUEST, detail="cron_expression 是必需的"
)
try:
@@ -179,18 +165,17 @@ async def validate_cron_expression(request: CronValidateRequest):
# 生成接下来的 5 个执行时间
cron = croniter(cron_expr, datetime.now())
next_times = [cron.get_next(datetime).strftime('%Y-%m-%d %H:%M:%S') for _ in range(5)]
next_times = [cron.get_next(datetime).strftime("%Y-%m-%d %H:%M:%S") for _ in range(5)]
return {
"valid": True,
"message": "有效的 Crontab 表达式",
"next_times": next_times,
"description": generate_cron_description(cron_expr)
"description": generate_cron_description(cron_expr),
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"无效的 Crontab 表达式: {str(e)}"
status_code=status.HTTP_400_BAD_REQUEST, detail=f"无效的 Crontab 表达式: {str(e)}"
)
@@ -203,11 +188,11 @@ def generate_cron_description(cron_expr: str) -> str:
minute, hour, day, month, dow = parts
descriptions = []
if hour == '*' and minute == '*':
if hour == "*" and minute == "*":
descriptions.append("每分钟")
elif hour == '*':
elif hour == "*":
descriptions.append(f"每小时的第 {minute} 分钟")
elif day == '*' and month == '*' and dow == '*':
elif day == "*" and month == "*" and dow == "*":
descriptions.append(f"每天 {hour}:{minute:0>2}")
else:
descriptions.append(f"复杂的时间表: {cron_expr}")
+23 -38
View File
@@ -9,7 +9,7 @@ from backend.schemas.template import (
TemplateUpdate,
TemplateResponse,
TaskFromTemplateRequest,
TemplatePreviewResponse
TemplatePreviewResponse,
)
from backend.schemas.task import TaskResponse
from backend.services.template_service import TemplateService
@@ -23,7 +23,7 @@ async def get_all_templates(
limit: int = Query(100, ge=1, le=500, description="限制记录数"),
is_active: Optional[bool] = Query(None, description="过滤启用状态"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
current_user: User = Depends(get_current_user),
):
"""
获取所有模板列表(普通用户可访问)
@@ -37,8 +37,7 @@ async def get_all_templates(
return templates
except Exception as e:
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)}"
)
@@ -47,7 +46,7 @@ async def get_active_templates(
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=500, description="限制记录数"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
current_user: User = Depends(get_current_user),
):
"""
获取所有启用的模板(用户创建任务时使用)
@@ -60,16 +59,13 @@ async def get_active_templates(
return templates
except Exception as e:
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)}"
)
@router.get("/{template_id}", response_model=TemplateResponse, summary="获取单个模板详情")
async def get_template(
template_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
template_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
):
"""
获取单个模板的详细信息(普通用户只能访问启用的模板)
@@ -78,26 +74,22 @@ async def get_template(
"""
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 not current_user.is_admin and template.is_active is not True:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权访问此模板"
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权访问此模板")
return template
@router.get("/{template_id}/preview", response_model=TemplatePreviewResponse, summary="预览模板生成的 payload")
@router.get(
"/{template_id}/preview",
response_model=TemplatePreviewResponse,
summary="预览模板生成的 payload",
)
async def preview_template(
template_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
template_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
):
"""
预览模板生成的 payload(使用默认值,普通用户只能访问启用的模板)
@@ -106,17 +98,11 @@ async def preview_template(
"""
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 not current_user.is_admin and template.is_active is not True:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权访问此模板"
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权访问此模板")
try:
preview_payload = TemplateService.generate_preview_payload(template, db)
@@ -127,12 +113,11 @@ async def preview_template(
"template_id": template.id,
"template_name": template.name,
"preview_payload": preview_payload,
"field_config": merged_config
"field_config": merged_config,
}
except Exception as e:
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)}"
)
@@ -140,7 +125,7 @@ async def preview_template(
async def create_template(
template_data: TemplateCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
创建新的打卡任务模板(仅管理员)
@@ -158,7 +143,7 @@ async def update_template(
template_id: int,
template_data: TemplateUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
更新模板信息(仅管理员)
@@ -176,7 +161,7 @@ async def update_template(
async def delete_template(
template_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
删除模板(仅管理员)
@@ -191,7 +176,7 @@ async def delete_template(
async def create_task_from_template(
request: TaskFromTemplateRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
current_user: User = Depends(get_current_user),
):
"""
从模板创建打卡任务
@@ -209,6 +194,6 @@ async def create_task_from_template(
user_id=current_user.id,
task_name=request.task_name,
db=db,
cron_expression=request.cron_expression
cron_expression=request.cron_expression,
)
return task
+36 -40
View File
@@ -3,7 +3,13 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from backend.models import get_db, User
from backend.schemas.user import UserCreate, UserUpdate, UserResponse, TokenStatus, UserUpdateProfile
from backend.schemas.user import (
UserCreate,
UserUpdate,
UserResponse,
TokenStatus,
UserUpdateProfile,
)
from backend.schemas.task import TaskResponse
from backend.services.user_service import UserService
from backend.services.task_service import TaskService
@@ -13,11 +19,16 @@ from backend.exceptions import ValidationError, AuthorizationError, ResourceNotF
router = APIRouter()
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED, summary="创建用户(管理员)")
@router.post(
"",
response_model=UserResponse,
status_code=status.HTTP_201_CREATED,
summary="创建用户(管理员)",
)
async def create_user(
user_data: UserCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
创建用户(需要管理员权限)
@@ -33,15 +44,12 @@ async def create_user(
raise ValidationError(str(e))
except Exception as e:
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)}"
)
@router.get("/me", response_model=UserResponse, summary="获取当前用户信息")
async def get_current_user_info(
current_user: User = Depends(get_current_user)
):
async def get_current_user_info(current_user: User = Depends(get_current_user)):
"""
获取当前登录用户的信息
"""
@@ -61,9 +69,7 @@ async def get_current_user_info(
@router.get("/me/status", response_model=dict, summary="获取当前用户审批状态")
async def get_user_status(
current_user: User = Depends(get_current_user)
):
async def get_user_status(current_user: User = Depends(get_current_user)):
"""
获取用户审批状态(不要求审批通过)
"""
@@ -71,7 +77,7 @@ async def get_user_status(
"user_id": current_user.id,
"alias": current_user.alias,
"is_approved": current_user.is_approved,
"created_at": current_user.created_at.isoformat() if current_user.created_at else None
"created_at": current_user.created_at.isoformat() if current_user.created_at else None,
}
@@ -79,7 +85,7 @@ async def get_user_status(
async def update_current_user_profile(
profile_data: UserUpdateProfile,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
current_user: User = Depends(get_current_user),
):
"""
更新当前用户的个人信息
@@ -99,15 +105,12 @@ async def update_current_user_profile(
raise ValidationError(str(e))
except Exception as e:
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)}"
)
@router.get("/me/token_status", response_model=TokenStatus, summary="获取当前用户打卡 Token 状态")
async def get_current_user_token_status(
current_user: User = Depends(get_current_user)
):
async def get_current_user_token_status(current_user: User = Depends(get_current_user)):
"""
获取当前用户的打卡 Token 状态(authorization token,非 JWT
@@ -123,7 +126,7 @@ async def get_current_user_token_status(
"jwt_exp": current_user.jwt_exp,
"expires_at": result.get("expires_at"),
"days_until_expiry": result.get("days_remaining"),
"expiring_soon": result.get("expiring_soon", False)
"expiring_soon": result.get("expiring_soon", False),
}
@@ -131,7 +134,7 @@ async def get_current_user_token_status(
async def get_current_user_tasks(
include_inactive: bool = Query(True, description="是否包含未启用的任务"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
current_user: User = Depends(get_current_user),
):
"""
获取当前登录用户的所有打卡任务
@@ -143,8 +146,7 @@ async def get_current_user_tasks(
return tasks
except Exception as e:
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)}"
)
@@ -155,7 +157,7 @@ async def get_all_users(
search: Optional[str] = Query(None, description="搜索关键词(alias"),
role: Optional[str] = Query(None, description="过滤角色 (user/admin)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
获取所有用户列表(需要管理员权限)
@@ -170,16 +172,13 @@ async def get_all_users(
return users
except Exception as e:
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)}"
)
@router.get("/{user_id}", response_model=UserResponse, summary="获取指定用户")
async def get_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
user_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
):
"""
获取指定用户信息
@@ -203,7 +202,7 @@ async def update_user(
user_id: int,
user_data: UserUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
current_user: User = Depends(get_current_user),
):
"""
更新用户信息
@@ -236,28 +235,26 @@ async def update_user(
new_approved_value = user.is_approved
is_approved_now = True if new_approved_value else False
is_admin = (current_user.role == "admin")
needs_notification = (is_admin and (not was_approved_before) and is_approved_now)
is_admin = current_user.role == "admin"
needs_notification = is_admin and (not was_approved_before) and is_approved_now
if needs_notification:
try:
from backend.services.email_service import EmailService
EmailService.notify_user_approved(user)
except Exception as e:
# 邮件发送失败不影响审批操作
import logging
logging.getLogger(__name__).error(f"发送审批通过邮件失败: {e}")
return user
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
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)}"
)
@@ -265,7 +262,7 @@ async def update_user(
async def delete_user(
user_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_user)
current_user: User = Depends(get_current_admin_user),
):
"""
删除用户(需要管理员权限)
@@ -277,6 +274,5 @@ async def delete_user(
raise ResourceNotFoundError(str(e))
except Exception as e:
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)}"
)