按 2026-11-02 需求补齐计划交付后端能力: - 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝; 管理员判断改为按数据库角色重新判定,防止权限码持有者绕过 - 系统监控:单进程采集资源、请求与异常指标,分钟级聚合, 阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义 - 试用期计提:按回款日判定并写入快照,年度结清与调整单联动; 人员模型补试用期、离职日期与工资版本历史字段 - 计提模板:公式骨架受控,页面维护名称与参数绑定 - T+ 采集:自动化配置表与进度查询 内置角色禁停用保护对齐模块注释;核算规则版本提为常量并 补充批量核算失败堆栈日志。 影响范围:后端全部模块、数据库迁移 V22-V28 验证:uv run ruff check 通过;922 个单元测试全部通过; 集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
80 lines
2.6 KiB
Python
80 lines
2.6 KiB
Python
"""认证相关的请求与响应模型。"""
|
||
|
||
from datetime import datetime
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||
|
||
from app.core.constants import CAPTCHA_LENGTH, PASSWORD_MAX_BYTES
|
||
|
||
|
||
class CaptchaOut(BaseModel):
|
||
"""验证码响应。
|
||
|
||
图片以 base64 内联返回而非单独的 URL:让 id 与图片在同一响应中成对产生,
|
||
避免前端分两次请求时拿到不匹配的组合。
|
||
"""
|
||
|
||
captcha_id: str = Field(description="验证码标识,登录时需回传")
|
||
image_base64: str = Field(description="PNG 图片的 base64,不含 data: 前缀")
|
||
expires_in: int = Field(description="有效期(秒)")
|
||
|
||
|
||
class LoginIn(BaseModel):
|
||
"""登录请求。"""
|
||
|
||
model_config = ConfigDict(str_strip_whitespace=True)
|
||
|
||
username: str = Field(min_length=1, max_length=64, description="登录用户名")
|
||
password: str = Field(min_length=1, description="密码")
|
||
captcha_id: str = Field(min_length=1, max_length=64, description="验证码标识")
|
||
captcha_code: str = Field(
|
||
min_length=CAPTCHA_LENGTH,
|
||
max_length=CAPTCHA_LENGTH,
|
||
description="用户填写的验证码",
|
||
)
|
||
|
||
@field_validator("password")
|
||
@classmethod
|
||
def _check_password_bytes(cls, value: str) -> str:
|
||
"""按**字节数**校验密码长度。
|
||
|
||
必须在这里拦下:bcrypt 5.0 起对超过 72 字节的密码直接抛 ValueError,
|
||
而中文一个字符占 3 字节——按字符数校验(如 max_length=20)根本拦不住,
|
||
24 个汉字就会让哈希函数抛异常、登录接口 500。
|
||
"""
|
||
if len(value.encode("utf-8")) > PASSWORD_MAX_BYTES:
|
||
raise ValueError(f"密码超过 {PASSWORD_MAX_BYTES} 字节上限")
|
||
return value
|
||
|
||
|
||
class UserBrief(BaseModel):
|
||
"""用户简要信息。"""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
id: int
|
||
username: str
|
||
real_name: str | None = None
|
||
roles: list[str] = Field(default_factory=list, description="角色编码列表")
|
||
can_write_payroll: bool = False
|
||
payroll_approval_enabled: bool = False
|
||
permissions: list[str] = Field(default_factory=list)
|
||
authorization_revision: int = 0
|
||
is_payroll_maker: bool = False
|
||
maker_required_permissions: list[str] = Field(default_factory=list)
|
||
|
||
|
||
class LoginOut(BaseModel):
|
||
"""登录成功响应。"""
|
||
|
||
access_token: str
|
||
token_type: str = "Bearer"
|
||
expires_in: int = Field(description="有效期(秒)")
|
||
user: UserBrief
|
||
|
||
|
||
class CurrentUserOut(UserBrief):
|
||
"""当前登录用户信息。"""
|
||
|
||
last_login_time: datetime | None = None
|