PayrollManagementSystem/backend/app/api/v1/auth.py
liuwenxin 1461a8d113 feat(后端): 添加授权体系、系统监控与试用期计提
按 2026-11-02 需求补齐计划交付后端能力:
- 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝;
  管理员判断改为按数据库角色重新判定,防止权限码持有者绕过
- 系统监控:单进程采集资源、请求与异常指标,分钟级聚合,
  阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义
- 试用期计提:按回款日判定并写入快照,年度结清与调整单联动;
  人员模型补试用期、离职日期与工资版本历史字段
- 计提模板:公式骨架受控,页面维护名称与参数绑定
- T+ 采集:自动化配置表与进度查询

内置角色禁停用保护对齐模块注释;核算规则版本提为常量并
补充批量核算失败堆栈日志。

影响范围:后端全部模块、数据库迁移 V22-V28
验证:uv run ruff check 通过;922 个单元测试全部通过;
集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
2026-11-03 11:20:55 +08:00

81 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""认证接口:验证码、登录、登出、当前用户。"""
from typing import Annotated
from fastapi import APIRouter, Depends, Request
from app.api.deps import DbSession, client_ip, get_current_user, get_raw_token
from app.core.config import get_settings
from app.core.response import ApiResponse, ok
from app.models import User
from app.schemas.auth import CaptchaOut, CurrentUserOut, LoginIn, LoginOut
from app.services import auth_service, captcha_service
router = APIRouter(prefix="/auth", tags=["认证"])
@router.get("/captcha", response_model=ApiResponse[CaptchaOut], summary="获取图形验证码")
def get_captcha() -> ApiResponse[CaptchaOut]:
"""签发图形验证码。无需认证。
图片以 base64 内联返回而非单独 URL:保证 id 与图片成对产生,
避免前端分两次请求时拿到不匹配的组合。
"""
captcha_id, image_base64, expires_in = captcha_service.issue()
return ok(CaptchaOut(captcha_id=captcha_id, image_base64=image_base64, expires_in=expires_in))
@router.post("/login", response_model=ApiResponse[LoginOut], summary="登录")
def login(payload: LoginIn, request: Request, session: DbSession) -> ApiResponse[LoginOut]:
"""账号 + 密码 + 图形验证码登录。
失败一律返回 **400 或 403,绝不使用 401**:前端的 401 拦截器会清空令牌
并跳转登录页,若登录接口自身返回 401,用户输错一次密码就会触发
「清空 + 跳转 + 报错」的组合动作,登录页反复重挂载、验证码反复刷新。
另注意:验证码经 GETDEL 消费后即失效,**前端必须在登录失败后刷新验证码**,
否则第二次提交必然报「验证码错误」。
"""
result = auth_service.login(
session,
username=payload.username,
password=payload.password,
captcha_id=payload.captcha_id,
captcha_code=payload.captcha_code,
ip=client_ip(request),
user_agent=request.headers.get("user-agent"),
)
return ok(result, message="登录成功")
@router.post("/logout", response_model=ApiResponse[None], summary="登出")
def logout(
token: Annotated[str, Depends(get_raw_token)],
_current_user: Annotated[User, Depends(get_current_user)],
) -> ApiResponse[None]:
"""登出:把当前令牌加入黑名单,使其立即失效。
同时依赖 `get_current_user` 做真实鉴权 —— 仅凭原始令牌字符串不能证明
它是有效的(可能已过期或已被拉黑)。
"""
auth_service.logout(token)
return ok(message="已退出登录")
@router.get("/me", response_model=ApiResponse[CurrentUserOut], summary="当前登录用户")
def me(current_user: Annotated[User, Depends(get_current_user)]) -> ApiResponse[CurrentUserOut]:
"""返回当前登录用户的信息与角色。前端刷新页面后用它恢复会话。"""
from app.services.authorization_service import session_info, user_session
authorization = session_info(user_session(current_user), current_user)
return ok(
CurrentUserOut(
id=current_user.id,
username=current_user.username,
real_name=current_user.real_name,
**authorization,
payroll_approval_enabled=get_settings().payroll_approval_enabled,
last_login_time=current_user.last_login_time,
)
)