mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 14:06:28 +00:00
refactor: v2
backend & frontend
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
from typing import List
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.models import get_db, User, CheckInTask
|
||||
from backend.schemas.check_in import BatchCheckInRequest
|
||||
from backend.schemas.user import UserResponse
|
||||
from backend.services.check_in_service import CheckInService
|
||||
from backend.services.admin_service import AdminService
|
||||
from backend.dependencies import get_current_admin_user
|
||||
from backend.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class BatchToggleTasksRequest(BaseModel):
|
||||
"""批量启用/禁用任务请求"""
|
||||
task_ids: List[int]
|
||||
is_active: bool
|
||||
|
||||
|
||||
@router.post("/batch_toggle_tasks", summary="批量启用/禁用任务")
|
||||
async def batch_toggle_tasks(
|
||||
request: BatchToggleTasksRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
批量启用或禁用任务的自动打卡功能(需要管理员权限)
|
||||
|
||||
- **task_ids**: 任务 ID 列表
|
||||
- **is_active**: true 为启用,false 为禁用
|
||||
"""
|
||||
try:
|
||||
count = 0
|
||||
for task_id in request.task_ids:
|
||||
task = db.query(CheckInTask).filter(CheckInTask.id == task_id).first()
|
||||
if task:
|
||||
task.is_active = request.is_active
|
||||
count += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"已{'启用' if request.is_active else '禁用'} {count} 个任务",
|
||||
"count": count
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"批量操作失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/batch_check_in", summary="批量触发打卡")
|
||||
async def batch_check_in(
|
||||
request: BatchCheckInRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
批量触发任务打卡(需要管理员权限)
|
||||
|
||||
- **task_ids**: 任务 ID 列表
|
||||
|
||||
返回每个任务的打卡结果
|
||||
"""
|
||||
try:
|
||||
result = CheckInService.batch_check_in_tasks(request.task_ids, db)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
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)
|
||||
):
|
||||
"""
|
||||
获取系统日志(需要管理员权限)
|
||||
|
||||
- **lines**: 读取最后 N 行日志
|
||||
|
||||
返回日志内容(字符串格式)
|
||||
"""
|
||||
try:
|
||||
log_file = settings.LOG_FILE
|
||||
|
||||
if not log_file.exists():
|
||||
return {
|
||||
"success": True,
|
||||
"message": "日志文件不存在",
|
||||
"logs": "日志文件不存在"
|
||||
}
|
||||
|
||||
# 使用 deque 高效读取最后 N 行,避免将整个文件加载到内存
|
||||
from collections import deque
|
||||
|
||||
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)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"读取了最后 {len(last_lines)} 行日志",
|
||||
"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)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats", summary="获取系统统计")
|
||||
async def get_system_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
获取系统统计信息(需要管理员权限)
|
||||
|
||||
返回用户数、任务数、打卡记录数等统计信息
|
||||
"""
|
||||
try:
|
||||
from backend.models import CheckInRecord
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 总用户数
|
||||
total_users = db.query(User).count()
|
||||
|
||||
# 管理员用户数
|
||||
admin_users = db.query(User).filter(User.role == "admin").count()
|
||||
|
||||
# 已审批的用户数(is_approved=True的用户)
|
||||
approved_users = db.query(User).filter(User.is_approved == True).count()
|
||||
|
||||
# 总任务数
|
||||
total_tasks = db.query(CheckInTask).count()
|
||||
|
||||
# 启用的任务数
|
||||
active_tasks = db.query(CheckInTask).filter(CheckInTask.is_active == True).count()
|
||||
|
||||
# 总打卡记录数
|
||||
total_records = db.query(CheckInRecord).count()
|
||||
|
||||
# 今日打卡记录数
|
||||
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_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_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()
|
||||
|
||||
# Token 即将过期的用户数(7天内)
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
expiring_soon_timestamp = current_timestamp + (7 * 24 * 60 * 60) # 7天后
|
||||
|
||||
expiring_users = 0
|
||||
for user in db.query(User).all():
|
||||
if user.jwt_exp and user.jwt_exp != "0":
|
||||
try:
|
||||
exp_timestamp = int(user.jwt_exp)
|
||||
if current_timestamp < exp_timestamp < expiring_soon_timestamp:
|
||||
expiring_users += 1
|
||||
except ValueError:
|
||||
# jwt_exp 格式不正确,跳过此用户
|
||||
logger.debug(f"用户 {user.id} 的 jwt_exp 格式不正确: {user.jwt_exp}")
|
||||
continue
|
||||
|
||||
return {
|
||||
"users": {
|
||||
"total": total_users,
|
||||
"admin": admin_users,
|
||||
"regular": total_users - admin_users,
|
||||
"active": approved_users # 使用已审批用户数
|
||||
},
|
||||
"tasks": {
|
||||
"total": total_tasks,
|
||||
"active": active_tasks,
|
||||
"inactive": total_tasks - active_tasks
|
||||
},
|
||||
"check_in_records": {
|
||||
"total": total_records,
|
||||
"today": today_records,
|
||||
"today_success": today_success,
|
||||
"today_failure": today_failure,
|
||||
"today_out_of_time": today_out_of_time,
|
||||
"today_unknown": today_unknown
|
||||
},
|
||||
"tokens": {
|
||||
"expiring_soon": expiring_users
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
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)
|
||||
):
|
||||
"""
|
||||
获取所有待审批的用户(需要管理员权限)
|
||||
"""
|
||||
try:
|
||||
users = AdminService.get_pending_users(db)
|
||||
return users
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"获取待审批用户失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/approve", response_model=dict, summary="审批通过用户")
|
||||
async def approve_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
审批通过指定用户(需要管理员权限)
|
||||
"""
|
||||
try:
|
||||
result = AdminService.approve_user(user_id, db)
|
||||
|
||||
if not result["success"]:
|
||||
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)}"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/users/{user_id}/reject", response_model=dict, summary="拒绝用户")
|
||||
async def reject_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
拒绝并删除指定用户(需要管理员权限)
|
||||
"""
|
||||
try:
|
||||
result = AdminService.reject_user(user_id, db)
|
||||
|
||||
if not result["success"]:
|
||||
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)}"
|
||||
)
|
||||
@@ -0,0 +1,150 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request, Response
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.models import get_db
|
||||
from backend.schemas.auth import (
|
||||
QRCodeRequest,
|
||||
QRCodeResponse,
|
||||
QRCodeStatusResponse,
|
||||
TokenVerifyRequest,
|
||||
TokenVerifyResponse,
|
||||
AliasLoginRequest,
|
||||
AliasLoginResponse,
|
||||
)
|
||||
from backend.services.auth_service import AuthService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/request_qrcode", response_model=dict, summary="请求 QQ 扫码二维码")
|
||||
async def request_qrcode(
|
||||
request_obj: QRCodeRequest,
|
||||
req: Request,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
请求 QQ 扫码二维码
|
||||
|
||||
- **alias**: 用户别名
|
||||
|
||||
返回会话 ID,用于后续查询扫码状态
|
||||
"""
|
||||
from backend.services.registration_manager import registration_manager
|
||||
import secrets
|
||||
|
||||
# 检查注册限流 Cookie
|
||||
reg_cookie = req.cookies.get("reg_limit")
|
||||
|
||||
if reg_cookie:
|
||||
if not registration_manager.check_registration_cookie(reg_cookie):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
detail="注册过于频繁,请 10 分钟后再试"
|
||||
)
|
||||
else:
|
||||
# 生成新的 Cookie
|
||||
reg_cookie = secrets.token_urlsafe(16)
|
||||
|
||||
# 获取客户端 IP
|
||||
client_ip = req.client.host if req.client else "unknown"
|
||||
|
||||
# 如果有代理,尝试从 X-Forwarded-For 获取真实 IP
|
||||
forwarded_for = req.headers.get("X-Forwarded-For")
|
||||
if forwarded_for:
|
||||
client_ip = forwarded_for.split(",")[0].strip()
|
||||
|
||||
try:
|
||||
result = AuthService.request_qrcode(request_obj.alias, client_ip, db)
|
||||
|
||||
# 设置限流 Cookie(10 分钟)
|
||||
response.set_cookie(
|
||||
key="reg_limit",
|
||||
value=reg_cookie,
|
||||
max_age=600, # 10 分钟
|
||||
httponly=True,
|
||||
samesite="lax"
|
||||
)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
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)
|
||||
):
|
||||
"""
|
||||
检查二维码扫描状态
|
||||
|
||||
- **session_id**: 会话 ID
|
||||
|
||||
状态说明:
|
||||
- pending: 正在初始化
|
||||
- waiting_scan: 等待扫描(包含二维码图片 Base64)
|
||||
- success: 扫描成功(包含 user_id 和 authorization)
|
||||
- error: 发生错误
|
||||
"""
|
||||
try:
|
||||
result = AuthService.get_qrcode_status(session_id, db)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"查询扫码状态失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/verify_token", response_model=dict, summary="验证 Token 有效性")
|
||||
async def verify_token(
|
||||
request: TokenVerifyRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
验证 Token 有效性
|
||||
|
||||
- **authorization**: Token(可带或不带 "Bearer " 前缀)
|
||||
|
||||
返回 Token 是否有效以及相关信息
|
||||
"""
|
||||
try:
|
||||
result = AuthService.verify_token(request.authorization, db)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"验证 Token 失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/alias_login", response_model=dict, summary="别名+密码登录")
|
||||
async def alias_login(
|
||||
request: AliasLoginRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
别名+密码登录(仅限已设置密码的用户)
|
||||
|
||||
- **alias**: 用户别名
|
||||
- **password**: 密码
|
||||
|
||||
返回登录结果,成功时包含 user_id 和 authorization
|
||||
|
||||
注意:
|
||||
- 用户必须已设置密码才能使用此方式登录
|
||||
- Token 必须仍然有效(未过期)
|
||||
- 如果 Token 已过期,请使用扫码登录重新获取
|
||||
"""
|
||||
try:
|
||||
result = AuthService.alias_login(request.alias, request.password, db)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"别名登录失败: {str(e)}"
|
||||
)
|
||||
@@ -0,0 +1,221 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.models import get_db, User, CheckInTask, CheckInRecord
|
||||
from backend.schemas.check_in import (
|
||||
ManualCheckInRequest,
|
||||
CheckInRecordResponse,
|
||||
CheckInResultResponse,
|
||||
)
|
||||
from backend.services.check_in_service import CheckInService
|
||||
from backend.services.task_service import TaskService
|
||||
from backend.dependencies import get_current_user, get_current_admin_user
|
||||
|
||||
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**: 任务 ID
|
||||
|
||||
返回打卡记录 ID,可以通过 /record/{record_id}/status 查询打卡状态
|
||||
"""
|
||||
# 验证任务归属
|
||||
if not TaskService.verify_task_ownership(task_id, current_user.id, db):
|
||||
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="任务不存在"
|
||||
)
|
||||
|
||||
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)}"
|
||||
)
|
||||
|
||||
|
||||
@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**: 打卡记录 ID
|
||||
|
||||
返回状态:pending(进行中)、success(成功)、failure(失败)
|
||||
"""
|
||||
# 获取打卡记录
|
||||
record = db.query(CheckInRecord).filter(CheckInRecord.id == record_id).first()
|
||||
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):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权访问此记录"
|
||||
)
|
||||
|
||||
return {
|
||||
"record_id": record.id,
|
||||
"task_id": record.task_id,
|
||||
"status": record.status,
|
||||
"response_text": record.response_text,
|
||||
"error_message": record.error_message,
|
||||
"trigger_type": record.trigger_type,
|
||||
"check_in_time": record.check_in_time
|
||||
}
|
||||
|
||||
|
||||
@router.get("/task/{task_id}/records", response_model=List[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)"),
|
||||
trigger_type: Optional[str] = Query(None, description="过滤触发类型 (scheduler/manual)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
查看指定任务的打卡记录
|
||||
|
||||
- **task_id**: 任务 ID
|
||||
- **skip**: 跳过记录数
|
||||
- **limit**: 限制记录数
|
||||
- **status**: 过滤状态 (success/failure)
|
||||
- **trigger_type**: 过滤触发类型 (scheduler/manual)
|
||||
|
||||
用户只能查看自己的任务记录
|
||||
"""
|
||||
# 验证任务归属
|
||||
if not TaskService.verify_task_ownership(task_id, current_user.id, db):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权访问此任务"
|
||||
)
|
||||
|
||||
try:
|
||||
records = CheckInService.get_task_records(
|
||||
task_id, db, skip, limit, status_filter, trigger_type
|
||||
)
|
||||
return records
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"获取打卡记录失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/my-records", response_model=List[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)"),
|
||||
trigger_type: Optional[str] = Query(None, description="过滤触发类型 (scheduler/manual)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
查看当前用户所有任务的打卡记录
|
||||
|
||||
- **skip**: 跳过记录数
|
||||
- **limit**: 限制记录数
|
||||
- **status**: 过滤状态 (success/failure)
|
||||
- **trigger_type**: 过滤触发类型 (scheduler/manual)
|
||||
"""
|
||||
try:
|
||||
records = CheckInService.get_user_records(
|
||||
current_user.id, db, skip, limit, status_filter, trigger_type
|
||||
)
|
||||
return records
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"获取打卡记录失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@router.get("/records", response_model=List[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)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
查看所有打卡记录(需要管理员权限)
|
||||
|
||||
- **skip**: 跳过记录数
|
||||
- **limit**: 限制记录数
|
||||
- **task_id**: 过滤指定任务的记录
|
||||
- **status**: 过滤指定状态的记录
|
||||
"""
|
||||
try:
|
||||
records = 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 enriched_records
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
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)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
获取打卡记录统计(需要管理员权限)
|
||||
|
||||
返回符合条件的记录总数
|
||||
"""
|
||||
try:
|
||||
query = db.query(CheckInRecord)
|
||||
|
||||
if task_id:
|
||||
query = query.filter(CheckInRecord.task_id == task_id)
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(CheckInRecord.status == status_filter)
|
||||
|
||||
total = query.count()
|
||||
|
||||
return {"total": total}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"获取统计失败: {str(e)}"
|
||||
)
|
||||
@@ -0,0 +1,251 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from backend.models import get_db, User
|
||||
from backend.schemas.task import TaskCreate, TaskUpdate, TaskResponse
|
||||
from backend.services.task_service import TaskService
|
||||
from backend.dependencies import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=TaskResponse, status_code=status.HTTP_201_CREATED, summary="创建打卡任务")
|
||||
async def create_task(
|
||||
task_data: TaskCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
创建新的打卡任务(基于模板)
|
||||
|
||||
现在的任务创建流程:
|
||||
1. 管理员在后台创建模板(包含完整的 payload_config)
|
||||
2. 用户基于模板创建任务,填写字段值
|
||||
3. 系统自动生成完整的 payload_config
|
||||
|
||||
注意:直接创建任务的方式已废弃,请使用模板接口。
|
||||
"""
|
||||
try:
|
||||
task = TaskService.create_task(current_user.id, task_data, db)
|
||||
return task
|
||||
except ValueError as 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)}"
|
||||
)
|
||||
|
||||
|
||||
@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)
|
||||
):
|
||||
"""
|
||||
获取当前用户的所有打卡任务
|
||||
|
||||
- **include_inactive**: 是否包含未启用的任务(默认 true)
|
||||
"""
|
||||
try:
|
||||
tasks = TaskService.get_user_tasks(current_user.id, db, include_inactive)
|
||||
# 为每个任务添加额外信息
|
||||
enriched_tasks = [TaskService.enrich_task_with_check_in_info(task, db) for task in tasks]
|
||||
return enriched_tasks
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
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)
|
||||
):
|
||||
"""
|
||||
获取指定任务的详情
|
||||
|
||||
需要验证任务属于当前用户
|
||||
"""
|
||||
# 验证任务归属
|
||||
if not TaskService.verify_task_ownership(task_id, current_user.id, db):
|
||||
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="任务不存在"
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
@router.put("/{task_id}", response_model=TaskResponse, summary="更新任务")
|
||||
async def update_task(
|
||||
task_id: int,
|
||||
task_data: TaskUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
更新指定任务的信息
|
||||
|
||||
需要验证任务属于当前用户
|
||||
"""
|
||||
# 验证任务归属
|
||||
if not TaskService.verify_task_ownership(task_id, current_user.id, db):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权访问此任务"
|
||||
)
|
||||
|
||||
task = TaskService.update_task(task_id, task_data, db)
|
||||
|
||||
if not task:
|
||||
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)
|
||||
):
|
||||
"""
|
||||
删除指定任务
|
||||
|
||||
需要验证任务属于当前用户,删除后会同时删除所有关联的打卡记录
|
||||
"""
|
||||
# 验证任务归属
|
||||
if not TaskService.verify_task_ownership(task_id, current_user.id, db):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权访问此任务"
|
||||
)
|
||||
|
||||
success = TaskService.delete_task(task_id, db)
|
||||
|
||||
if not success:
|
||||
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)
|
||||
):
|
||||
"""
|
||||
切换任务的启用/禁用状态
|
||||
|
||||
需要验证任务属于当前用户
|
||||
"""
|
||||
# 验证任务归属
|
||||
if not TaskService.verify_task_ownership(task_id, current_user.id, db):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权访问此任务"
|
||||
)
|
||||
|
||||
task = TaskService.toggle_task(task_id, db)
|
||||
|
||||
if not task:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="任务不存在"
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
@router.post("/validate-cron", summary="验证 Crontab 表达式")
|
||||
async def validate_cron_expression(request: dict):
|
||||
"""
|
||||
验证 Crontab 表达式并预览下一个执行时间
|
||||
|
||||
请求体: {"cron_expression": "0 20 * * *"}
|
||||
|
||||
返回:
|
||||
{
|
||||
"valid": true,
|
||||
"message": "有效的 Crontab 表达式",
|
||||
"next_times": [
|
||||
"2024-01-02 20:00:00",
|
||||
"2024-01-03 20:00:00",
|
||||
...
|
||||
],
|
||||
"description": "每天 20:00"
|
||||
}
|
||||
"""
|
||||
cron_expr = request.get('cron_expression', '').strip()
|
||||
|
||||
if not cron_expr:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="cron_expression 是必需的"
|
||||
)
|
||||
|
||||
try:
|
||||
from croniter import croniter
|
||||
|
||||
if not croniter.is_valid(cron_expr):
|
||||
raise ValueError("无效的格式")
|
||||
|
||||
# 生成接下来的 5 个执行时间
|
||||
cron = croniter(cron_expr, datetime.now())
|
||||
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)
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"无效的 Crontab 表达式: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
def generate_cron_description(cron_expr: str) -> str:
|
||||
"""生成 Crontab 表达式的人类可读描述"""
|
||||
parts = cron_expr.split()
|
||||
if len(parts) != 5:
|
||||
return cron_expr
|
||||
|
||||
minute, hour, day, month, dow = parts
|
||||
|
||||
descriptions = []
|
||||
if hour == '*' and minute == '*':
|
||||
descriptions.append("每分钟")
|
||||
elif hour == '*':
|
||||
descriptions.append(f"每小时的第 {minute} 分钟")
|
||||
elif day == '*' and month == '*' and dow == '*':
|
||||
descriptions.append(f"每天 {hour}:{minute:0>2}")
|
||||
else:
|
||||
descriptions.append(f"复杂的时间表: {cron_expr}")
|
||||
|
||||
return ", ".join(descriptions)
|
||||
@@ -0,0 +1,212 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.models import User
|
||||
from backend.dependencies import get_db, get_current_user, get_current_admin_user
|
||||
from backend.schemas.template import (
|
||||
TemplateCreate,
|
||||
TemplateUpdate,
|
||||
TemplateResponse,
|
||||
TaskFromTemplateRequest,
|
||||
TemplatePreviewResponse
|
||||
)
|
||||
from backend.schemas.task import TaskResponse
|
||||
from backend.services.template_service import TemplateService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[TemplateResponse], summary="获取所有模板列表")
|
||||
async def get_all_templates(
|
||||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||||
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)
|
||||
):
|
||||
"""
|
||||
获取所有模板列表(普通用户可访问)
|
||||
|
||||
- **skip**: 跳过记录数
|
||||
- **limit**: 限制记录数
|
||||
- **is_active**: 过滤启用状态
|
||||
"""
|
||||
try:
|
||||
templates = TemplateService.get_all_templates(db, skip, limit, is_active)
|
||||
return templates
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"获取模板列表失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/active", response_model=List[TemplateResponse], summary="获取启用的模板列表")
|
||||
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)
|
||||
):
|
||||
"""
|
||||
获取所有启用的模板(用户创建任务时使用)
|
||||
|
||||
- **skip**: 跳过记录数
|
||||
- **limit**: 限制记录数
|
||||
"""
|
||||
try:
|
||||
templates = TemplateService.get_all_templates(db, skip, limit, is_active=True)
|
||||
return templates
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
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**: 模板 ID
|
||||
"""
|
||||
template = TemplateService.get_template(template_id, db)
|
||||
if not template:
|
||||
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="无权访问此模板"
|
||||
)
|
||||
|
||||
return template
|
||||
|
||||
|
||||
@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)
|
||||
):
|
||||
"""
|
||||
预览模板生成的 payload(使用默认值,普通用户只能访问启用的模板)
|
||||
|
||||
- **template_id**: 模板 ID
|
||||
"""
|
||||
template = TemplateService.get_template(template_id, db)
|
||||
if not template:
|
||||
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="无权访问此模板"
|
||||
)
|
||||
|
||||
try:
|
||||
preview_payload = TemplateService.generate_preview_payload(template, db)
|
||||
# 使用合并后的配置
|
||||
merged_config = TemplateService.merge_parent_config(template, db)
|
||||
|
||||
return {
|
||||
"template_id": template.id,
|
||||
"template_name": template.name,
|
||||
"preview_payload": preview_payload,
|
||||
"field_config": merged_config
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"生成预览失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", response_model=TemplateResponse, summary="创建新模板(管理员)")
|
||||
async def create_template(
|
||||
template_data: TemplateCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
创建新的打卡任务模板(仅管理员)
|
||||
|
||||
- **name**: 模板名称
|
||||
- **description**: 模板描述
|
||||
- **field_config**: 字段配置(JSON)
|
||||
- **is_active**: 是否启用
|
||||
"""
|
||||
return TemplateService.create_template(template_data, db)
|
||||
|
||||
|
||||
@router.put("/{template_id}", response_model=TemplateResponse, summary="更新模板(管理员)")
|
||||
async def update_template(
|
||||
template_id: int,
|
||||
template_data: TemplateUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
更新模板信息(仅管理员)
|
||||
|
||||
- **template_id**: 模板 ID
|
||||
- **name**: 模板名称
|
||||
- **description**: 模板描述
|
||||
- **field_config**: 字段配置(JSON)
|
||||
- **is_active**: 是否启用
|
||||
"""
|
||||
return TemplateService.update_template(template_id, template_data, db)
|
||||
|
||||
|
||||
@router.delete("/{template_id}", summary="删除模板(管理员)")
|
||||
async def delete_template(
|
||||
template_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
删除模板(仅管理员)
|
||||
|
||||
- **template_id**: 模板 ID
|
||||
"""
|
||||
TemplateService.delete_template(template_id, db)
|
||||
return {"message": "模板删除成功"}
|
||||
|
||||
|
||||
@router.post("/create-task", response_model=TaskResponse, summary="从模板创建任务")
|
||||
async def create_task_from_template(
|
||||
request: TaskFromTemplateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
从模板创建打卡任务
|
||||
|
||||
- **template_id**: 模板 ID
|
||||
- **thread_id**: 接龙项目 ID
|
||||
- **field_values**: 用户填写的字段值
|
||||
- **task_name**: 任务名称(可选)
|
||||
"""
|
||||
task = TemplateService.create_task_from_template(
|
||||
template_id=request.template_id,
|
||||
thread_id=request.thread_id,
|
||||
field_values=request.field_values,
|
||||
user_id=current_user.id,
|
||||
task_name=request.task_name,
|
||||
db=db
|
||||
)
|
||||
return task
|
||||
@@ -0,0 +1,294 @@
|
||||
from typing import List, Optional
|
||||
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.task import TaskResponse
|
||||
from backend.services.user_service import UserService
|
||||
from backend.services.task_service import TaskService
|
||||
from backend.dependencies import get_current_user, get_current_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@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)
|
||||
):
|
||||
"""
|
||||
创建用户(需要管理员权限)
|
||||
|
||||
- **jwt_sub**: QQ 扫码登录的唯一用户标识
|
||||
- **alias**: 用户别名(用于登录)
|
||||
- **role**: 角色(可选,默认 "user")
|
||||
"""
|
||||
try:
|
||||
user = UserService.create_user(user_data, db)
|
||||
return user
|
||||
except ValueError as 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)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse, summary="获取当前用户信息")
|
||||
async def get_current_user_info(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取当前登录用户的信息
|
||||
"""
|
||||
# 创建响应对象,手动添加 has_password 字段
|
||||
user_dict = {
|
||||
"id": current_user.id,
|
||||
"alias": current_user.alias,
|
||||
"jwt_sub": current_user.jwt_sub,
|
||||
"role": current_user.role,
|
||||
"is_approved": current_user.is_approved,
|
||||
"jwt_exp": current_user.jwt_exp,
|
||||
"email": current_user.email,
|
||||
"has_password": bool(current_user.password_hash),
|
||||
"created_at": current_user.created_at,
|
||||
"updated_at": current_user.updated_at,
|
||||
}
|
||||
return user_dict
|
||||
|
||||
|
||||
@router.get("/me/status", response_model=dict, summary="获取当前用户审批状态")
|
||||
async def get_user_status(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取用户审批状态(不要求审批通过)
|
||||
"""
|
||||
return {
|
||||
"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
|
||||
}
|
||||
|
||||
|
||||
@router.put("/me/profile", response_model=UserResponse, summary="更新个人信息")
|
||||
async def update_current_user_profile(
|
||||
profile_data: UserUpdateProfile,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
更新当前用户的个人信息
|
||||
|
||||
- **alias**: 新别名(可选)
|
||||
- **current_password**: 当前密码(修改密码时必填)
|
||||
- **new_password**: 新密码(可选)
|
||||
|
||||
注意:
|
||||
- 修改密码时必须提供 current_password 和 new_password
|
||||
- 首次设置密码时不需要 current_password
|
||||
"""
|
||||
try:
|
||||
user = UserService.update_user_profile(current_user.id, profile_data, db)
|
||||
return user
|
||||
except ValueError as 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)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/me/token_status", response_model=TokenStatus, summary="获取当前用户 Token 状态")
|
||||
async def get_current_user_token_status(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取当前用户的 Token 状态
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
is_valid = True
|
||||
days_until_expiry = None
|
||||
expires_at = None
|
||||
expiring_soon = False
|
||||
|
||||
if current_user.jwt_exp and current_user.jwt_exp != "0":
|
||||
try:
|
||||
exp_timestamp = int(current_user.jwt_exp)
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
expires_at = exp_timestamp
|
||||
|
||||
if current_timestamp > exp_timestamp:
|
||||
is_valid = False
|
||||
else:
|
||||
days_until_expiry = (exp_timestamp - current_timestamp) // 86400
|
||||
# 检查是否在30分钟内过期
|
||||
minutes_until_expiry = (exp_timestamp - current_timestamp) // 60
|
||||
expiring_soon = minutes_until_expiry <= 30
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"is_valid": is_valid,
|
||||
"jwt_exp": current_user.jwt_exp,
|
||||
"jwt_sub": current_user.jwt_sub,
|
||||
"expires_at": expires_at,
|
||||
"days_until_expiry": days_until_expiry,
|
||||
"expiring_soon": expiring_soon
|
||||
}
|
||||
|
||||
|
||||
@router.get("/me/tasks", response_model=List[TaskResponse], summary="获取当前用户的任务列表")
|
||||
async def get_current_user_tasks(
|
||||
include_inactive: bool = Query(True, description="是否包含未启用的任务"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取当前登录用户的所有打卡任务
|
||||
|
||||
- **include_inactive**: 是否包含未启用的任务(默认 True)
|
||||
"""
|
||||
try:
|
||||
tasks = TaskService.get_user_tasks(current_user.id, db, include_inactive)
|
||||
return tasks
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"获取任务列表失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=List[UserResponse], summary="获取所有用户(管理员)")
|
||||
async def get_all_users(
|
||||
skip: int = Query(0, ge=0, description="跳过记录数"),
|
||||
limit: int = Query(100, ge=1, le=500, description="限制记录数"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词(alias 或 jwt_sub)"),
|
||||
role: Optional[str] = Query(None, description="过滤角色 (user/admin)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
获取所有用户列表(需要管理员权限)
|
||||
|
||||
- **skip**: 跳过记录数
|
||||
- **limit**: 限制记录数
|
||||
- **search**: 搜索关键词(模糊匹配 alias 或 jwt_sub)
|
||||
- **role**: 过滤角色(user/admin)
|
||||
"""
|
||||
try:
|
||||
users = UserService.get_all_users(db, skip, limit, search, role)
|
||||
return users
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
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)
|
||||
):
|
||||
"""
|
||||
获取指定用户信息
|
||||
|
||||
- 普通用户只能查看自己的信息
|
||||
- 管理员可以查看所有用户信息
|
||||
"""
|
||||
# 检查权限
|
||||
if current_user.role != "admin" and current_user.id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足,只能查看自己的信息"
|
||||
)
|
||||
|
||||
user = UserService.get_user_by_id(user_id, db)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"用户 ID {user_id} 不存在"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/{user_id}", response_model=UserResponse, summary="更新用户信息")
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
user_data: UserUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
更新用户信息
|
||||
|
||||
- 普通用户只能更新自己的部分信息(不包括 role)
|
||||
- 管理员可以更新所有用户的所有信息
|
||||
"""
|
||||
# 检查权限
|
||||
if current_user.role != "admin":
|
||||
if current_user.id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足,只能更新自己的信息"
|
||||
)
|
||||
# 普通用户不能修改 role
|
||||
if user_data.role is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="普通用户不能修改角色"
|
||||
)
|
||||
|
||||
try:
|
||||
user = UserService.update_user(user_id, user_data, db)
|
||||
return user
|
||||
except ValueError as 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)}"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, summary="删除用户(管理员)")
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
删除用户(需要管理员权限)
|
||||
"""
|
||||
try:
|
||||
UserService.delete_user(user_id, db)
|
||||
return None
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"删除用户失败: {str(e)}"
|
||||
)
|
||||
Reference in New Issue
Block a user