mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
refactor(structure): reorganize app layout
BREAKING CHANGE: root backend/frontend directories and old run/manage entrypoints were removed. Use apps/backend, apps/frontend, and python main.py commands instead.
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
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天内)
|
||||
# 使用 SQL 直接查询,避免 N+1 问题
|
||||
from backend.utils.time_helpers import now_timestamp
|
||||
|
||||
current_timestamp = now_timestamp()
|
||||
expiring_soon_timestamp = current_timestamp + (7 * 24 * 60 * 60) # 7天后
|
||||
|
||||
# 直接在数据库层面筛选即将过期的用户
|
||||
# 条件: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天内过期
|
||||
)
|
||||
).count()
|
||||
|
||||
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,192 @@
|
||||
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
|
||||
from backend.exceptions import BusinessLogicError
|
||||
from backend.limiter import limiter
|
||||
|
||||
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)
|
||||
):
|
||||
"""
|
||||
请求 QQ 扫码二维码
|
||||
|
||||
- **alias**: 用户别名
|
||||
|
||||
返回会话 ID,用于后续查询扫码状态
|
||||
"""
|
||||
from backend.services.registration_manager import registration_manager
|
||||
import secrets
|
||||
|
||||
# 检查注册限流 Cookie
|
||||
reg_cookie = request.cookies.get("reg_limit")
|
||||
|
||||
if reg_cookie:
|
||||
if not registration_manager.check_registration_cookie(reg_cookie):
|
||||
raise BusinessLogicError(
|
||||
message="注册过于频繁,请 10 分钟后再试",
|
||||
error_code="RATE_LIMIT_EXCEEDED",
|
||||
status_code=429
|
||||
)
|
||||
else:
|
||||
# 生成新的 Cookie
|
||||
reg_cookie = secrets.token_urlsafe(16)
|
||||
|
||||
# 获取客户端 IP
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
|
||||
# 如果有代理,尝试从 X-Forwarded-For 获取真实 IP
|
||||
forwarded_for = request.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: 扫描成功(包含 JWT token 和 user 信息)
|
||||
- error: 发生错误
|
||||
|
||||
认证架构说明:
|
||||
- 扫码成功后返回 JWT token(用于网站登录,21天有效期)
|
||||
- 同时更新数据库中的 authorization token(用于打卡业务)
|
||||
- 两种 token 分别管理,互不影响
|
||||
"""
|
||||
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.delete("/qrcode_session/{session_id}", response_model=dict, summary="取消二维码登录会话")
|
||||
async def cancel_qrcode_session(
|
||||
session_id: str
|
||||
):
|
||||
"""
|
||||
取消二维码登录会话
|
||||
|
||||
- **session_id**: 会话 ID
|
||||
|
||||
用于用户关闭二维码对话框时,终止后台的 Selenium 进程
|
||||
"""
|
||||
try:
|
||||
result = AuthService.cancel_qrcode_session(session_id)
|
||||
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="验证 JWT Token 有效性")
|
||||
async def verify_token(
|
||||
request: TokenVerifyRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
验证 JWT Token 有效性(网站登录认证)
|
||||
|
||||
- **authorization**: JWT Token(可带或不带 "Bearer " 前缀)
|
||||
|
||||
返回 Token 是否有效以及用户信息
|
||||
|
||||
注意:
|
||||
- 此接口验证的是 JWT token(用于网站登录,21天有效期)
|
||||
- 不验证打卡业务的 authorization token(存储在数据库中)
|
||||
- JWT token 过期需要重新登录,但打卡 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="别名+密码登录")
|
||||
@limiter.limit("5/minute") # 每分钟最多5次登录尝试
|
||||
async def alias_login(
|
||||
login_data: AliasLoginRequest,
|
||||
request: Request, # slowapi需要的request参数
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
别名+密码登录(仅限已设置密码的用户)
|
||||
|
||||
- **alias**: 用户别名
|
||||
- **password**: 密码
|
||||
|
||||
返回登录结果,成功时包含 JWT token 和 user 信息
|
||||
|
||||
认证架构说明:
|
||||
- 登录成功后返回 JWT token(用于网站登录,21天有效期)
|
||||
- 如果数据库中的打卡 authorization token 过期,会返回警告信息
|
||||
- 打卡 token 过期不影响网站登录,但无法自动打卡,建议扫码更新
|
||||
|
||||
注意:
|
||||
- 用户必须已设置密码才能使用此方式登录
|
||||
- 即使打卡 token 已过期,仍然可以使用密码登录网站
|
||||
- 如需更新打卡 token,请使用扫码登录
|
||||
"""
|
||||
try:
|
||||
result = AuthService.alias_login(login_data.alias, login_data.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,234 @@
|
||||
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,
|
||||
PaginatedResponse,
|
||||
)
|
||||
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(失败)
|
||||
"""
|
||||
from backend.utils.db_helpers import get_or_404
|
||||
|
||||
# 获取打卡记录
|
||||
record = get_or_404(CheckInRecord, record_id, db, "打卡记录不存在")
|
||||
|
||||
# 验证记录归属(通过任务归属)
|
||||
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=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)"),
|
||||
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, total = CheckInService.get_task_records(
|
||||
task_id, db, skip, limit, status_filter, trigger_type
|
||||
)
|
||||
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)}"
|
||||
)
|
||||
|
||||
|
||||
@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)"),
|
||||
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, 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
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"获取打卡记录失败: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@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)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin_user)
|
||||
):
|
||||
"""
|
||||
查看所有打卡记录(需要管理员权限)
|
||||
|
||||
- **skip**: 跳过记录数
|
||||
- **limit**: 限制记录数
|
||||
- **task_id**: 过滤指定任务的记录
|
||||
- **status**: 过滤指定状态的记录
|
||||
"""
|
||||
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
|
||||
)
|
||||
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,215 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime, timedelta
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from backend.models import get_db, User
|
||||
from backend.schemas.task import TaskUpdate, TaskResponse
|
||||
from backend.services.task_service import TaskService
|
||||
from backend.dependencies import get_current_user
|
||||
|
||||
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)
|
||||
):
|
||||
"""
|
||||
获取当前用户的所有打卡任务
|
||||
|
||||
- **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)
|
||||
):
|
||||
"""
|
||||
获取指定任务的详情
|
||||
|
||||
需要验证任务属于当前用户
|
||||
"""
|
||||
from backend.models import CheckInTask
|
||||
from backend.utils.db_helpers import get_owned_or_403
|
||||
|
||||
# 获取任务并验证归属
|
||||
task = get_owned_or_403(CheckInTask, task_id, current_user.id, db) # type: ignore
|
||||
|
||||
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)
|
||||
):
|
||||
"""
|
||||
更新指定任务的信息
|
||||
|
||||
需要验证任务属于当前用户
|
||||
"""
|
||||
from backend.models import CheckInTask
|
||||
from backend.utils.db_helpers import get_owned_or_403
|
||||
|
||||
# 验证任务归属并获取任务
|
||||
get_owned_or_403(CheckInTask, task_id, current_user.id, db) # type: ignore
|
||||
|
||||
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)
|
||||
):
|
||||
"""
|
||||
删除指定任务
|
||||
|
||||
需要验证任务属于当前用户,删除后会同时删除所有关联的打卡记录
|
||||
"""
|
||||
from backend.models import CheckInTask
|
||||
from backend.utils.db_helpers import get_owned_or_403
|
||||
|
||||
# 验证任务归属
|
||||
get_owned_or_403(CheckInTask, task_id, current_user.id, db) # type: ignore
|
||||
|
||||
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)
|
||||
):
|
||||
"""
|
||||
切换任务的启用/禁用状态
|
||||
|
||||
需要验证任务属于当前用户
|
||||
"""
|
||||
from backend.models import CheckInTask
|
||||
from backend.utils.db_helpers import get_owned_or_403
|
||||
|
||||
# 验证任务归属
|
||||
get_owned_or_403(CheckInTask, task_id, current_user.id, db) # type: ignore
|
||||
|
||||
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: CronValidateRequest):
|
||||
"""
|
||||
验证 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.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,214 @@
|
||||
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**: 任务名称(可选)
|
||||
- **cron_expression**: Cron 表达式(可选,默认每天 20:00)
|
||||
"""
|
||||
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,
|
||||
cron_expression=request.cron_expression
|
||||
)
|
||||
return task
|
||||
@@ -0,0 +1,282 @@
|
||||
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
|
||||
from backend.exceptions import ValidationError, AuthorizationError, ResourceNotFoundError
|
||||
|
||||
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)
|
||||
):
|
||||
"""
|
||||
创建用户(需要管理员权限)
|
||||
|
||||
- **alias**: 用户别名(用于登录)
|
||||
- **role**: 角色(可选,默认 "user")
|
||||
- **email**: 邮箱地址(可选)
|
||||
"""
|
||||
try:
|
||||
user = UserService.create_user(user_data, db)
|
||||
return user
|
||||
except ValueError as e:
|
||||
raise ValidationError(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,
|
||||
"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 ValidationError(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 状态(authorization token,非 JWT)
|
||||
|
||||
注意:此接口检查的是打卡业务 token,不是网站登录 JWT token
|
||||
"""
|
||||
from backend.services.auth_service import AuthService
|
||||
|
||||
# 使用统一的验证方法
|
||||
result = AuthService.verify_checkin_authorization(current_user)
|
||||
|
||||
return {
|
||||
"is_valid": result["is_valid"],
|
||||
"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)
|
||||
}
|
||||
|
||||
|
||||
@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)"),
|
||||
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)
|
||||
- **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 AuthorizationError("权限不足,只能查看自己的信息")
|
||||
|
||||
user = UserService.get_user_by_id(user_id, db)
|
||||
if not user:
|
||||
raise ResourceNotFoundError(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 AuthorizationError("权限不足,只能更新自己的信息")
|
||||
# 普通用户不能修改 role
|
||||
if user_data.role is not None:
|
||||
raise AuthorizationError("普通用户不能修改角色")
|
||||
|
||||
try:
|
||||
# 获取更新前的用户状态
|
||||
old_user = UserService.get_user_by_id(user_id, db)
|
||||
if not old_user:
|
||||
raise ResourceNotFoundError(f"用户 ID {user_id} 不存在")
|
||||
|
||||
# 保存更新前的审批状态 (先读取后转换为 Python bool)
|
||||
old_approved_value = old_user.is_approved
|
||||
was_approved_before = True if old_approved_value else False
|
||||
|
||||
# 更新用户信息
|
||||
user = UserService.update_user(user_id, user_data, db)
|
||||
|
||||
# 检查是否需要发送审批通过邮件
|
||||
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)
|
||||
|
||||
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)
|
||||
)
|
||||
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 ResourceNotFoundError(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