需求 2.5.1 用户账号管理,全部要求管理员权限(require_admin 依赖):
- 接口:列表/详情/新增/编辑/删除/重置密码/启停/角色下拉,
角色下拉放 /users/meta/roles 以避开 /{user_id} 的路由匹配冲突
- 删除、禁用、改角色三处自我保护:不允许对当前登录账号执行,
避免管理员误操作后立刻失去全部管理权限且无法自行恢复
- 重置密码与禁用均提升 token_version,使已签发令牌立即失效,
不依赖 Redis 黑名单
- 密码按字节数校验(bcrypt 5.0 硬上限 72 字节,中文一字 3 字节)
- 用户列表按 id 倒序:create_time 为秒级精度,同秒数据分页结果不确定
- 修复 replace_roles 未同步关系集合的缺陷:改用 user.user_roles
集合追加,此前 session.add(UserRole(...)) 会表现为保存成功但返回空角色
- update_user 的自我保护原先依赖返回 None 的占位函数,恒不生效,
改为显式传入 operator_id
Co-Authored-By: Claude Code <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| alembic | ||
| app | ||
| tests | ||
| .env.example | ||
| .python-version | ||
| alembic.ini | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
后端
AI摘要:衍水集团营销人员工资管理系统的后端服务。FastAPI + SQLAlchemy(同步)+ MySQL + Redis,Python 3.12 由 uv 锁定。六层结构
api / services / repositories / models / schemas / core。当前实现登录鉴权与健康检查,业务模块待需求澄清后开发。
职责
- 提供 REST 接口(统一前缀
/api) - 登录鉴权:图形验证码、JWT、登录限流、登出黑名单、登录日志
- 后续承担:基础数据、业务数据、绩效核算、报表统计、T+ 数据同步
技术栈与版本
| 组件 | 版本 | 说明 |
|---|---|---|
| Python | 3.12 | uv 锁定。系统默认是 3.14,不要用——第三方库轮子覆盖不如 3.12 完备 |
| FastAPI / Starlette | 0.143 / 1.7 | starlette 显式限定 <2:FastAPI 对它的约束无上界,升级时可能漂移 |
| SQLAlchemy | 2.1 | 同步引擎,路由写 def 由 FastAPI 放入线程池 |
| PyMySQL | 1.2 | 必须带 [rsa]:MySQL 8.0 默认 caching_sha2_password,缺 cryptography 时首次连接抛 RuntimeError,且报错在建连阶段、极易误读为「密码错误」 |
| alembic | 1.20 | 迁移;运行期与迁移共用同一份 mysql+pymysql 连接串 |
| redis | 8.1 | 同步客户端 |
| pwdlib | 0.3 | 密码哈希。不用 passlib(已停更且与 bcrypt ≥4.1 不兼容)。算法显式指定 BcryptHasher,不用 recommended() |
| captcha | 0.7 | 仅用其内置字体文件,渲染已自绘(见 core/captcha_image.py) |
分层结构与红线
app/
├── api/ HTTP 层:路由、依赖注入、参数校验
│ ├── deps.py 公共依赖:get_db、get_current_user、get_raw_token
│ └── v1/ 版本化业务路由
├── services/ 业务编排 + **唯一事务边界**(commit/rollback 只在此层)
├── repositories/ 数据访问,**只 flush 不 commit**
├── models/ ORM 映射,不含业务逻辑
├── schemas/ 请求/响应模型,禁止把 ORM 对象直接当 response_model
└── core/ 横切基础设施:配置、安全、连接、异常、响应、日志
| 层 | 可以做 | 不可做 |
|---|---|---|
api/ |
参数绑定、调 service、返回统一响应 | 写业务判断、直接拼 SQL |
services/ |
编排多个 repository、提交事务 | — |
repositories/ |
读写数据、flush | commit()、抛业务异常 |
models/ |
表映射 | 业务逻辑 |
core/ |
基础设施 | 业务规则 |
核心接口
| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| GET | /api/health |
否 | 探活,上报 app/db/redis 三项状态 |
| GET | /api/v1/auth/captcha |
否 | 签发图形验证码,返回 id + 图片 base64 |
| POST | /api/v1/auth/login |
否 | 账号 + 密码 + 验证码 → JWT |
| POST | /api/v1/auth/logout |
是 | 令牌入黑名单,立即失效 |
| GET | /api/v1/auth/me |
是 | 当前用户与角色 |
统一响应 {code, message, data},分页为 list/total/pageNum/pageSize。
状态码:400 参数错误 / 401 未登录 / 403 无权限 / 404 不存在 / 500 服务端异常。
登录失败一律 400 或 403,绝不使用 401 —— 前端把 401 一律当作「清 token + 跳登录页」, 若登录接口自身返回 401,用户输错一次密码就会触发「清空 + 跳转 + 报错」的风暴。
常用命令
uv sync # 安装依赖
uv run uvicorn app.main:app --reload --port 8000 # 起服务(开发务必加 --reload)
uv run alembic upgrade head # 应用迁移
uv run alembic revision --autogenerate -m "说明" # 生成迁移
uv run python -m app.cli create-admin # 建管理员(幂等,已存在不覆盖)
uv run pytest tests/unit -q # 纯单元测试,无需容器
uv run pytest -q # 全部测试
uv run ruff check . # 代码检查
uv run ruff check --fix . # 代码检查并自动修复可修项
uv run ruff format . # 格式化代码
Python 版本由 .python-version 与 pyproject.toml 的 requires-python 双重锁定。
代码检查与格式化
配置在 pyproject.toml 的 [tool.ruff]。三处刻意关闭的规则,不要误以为是漏配:
| 规则 | 关闭原因 |
|---|---|
E501 行宽 |
已由 formatter 控制,lint 重复报无意义;长中文注释极易触发 |
B008 参数默认值调用 |
FastAPI 依赖注入要求在参数默认值处写 Depends(),是框架约定而非缺陷 |
RUF001/002/003 易混淆 Unicode |
该规则默认针对英文文本,会把中文的全角逗号、句号、括号全部误报(本项目实测 800+ 条)。中文里全角标点正是规范写法 |
另有 known-first-party = ["app", "tests"]:本项目 [tool.uv] package = false(不作为包安装),不显式声明的话 ruff 无法识别 app 是第一方包,会把第三方依赖排到它后面。
关键实现约定
JWT 时间一律用 UTC
exp 是 UTC 时间戳,而库列存本地时间(容器 TZ=Asia/Shanghai)。混算会产生 8 小时偏差,
表现为「刚登录就过期」或「过期了还能用」。库列用 datetime.now(),JWT 用 datetime.now(timezone.utc)。
「离职即失效」用 users.token_version,不依赖 Redis
禁用 / 改密 / 强制下线只需 UPDATE users SET token_version = token_version + 1。
鉴权时比对 JWT 的 ver 声明与库中值,Redis 故障时依然生效。
Redis 黑名单只负责「用户主动登出」。
Redis 键 TTL 由业务语义决定,不要套用固定值
| 键 | TTL | 依据 |
|---|---|---|
ys:captcha:{id} |
300s | 规范要求 |
ys:login:fail:{username} |
900s | = 锁定时长,写死会导致锁定提前失效 |
ys:token:blacklist:{jti} |
令牌剩余有效期 | 写死会导致「登出 N 分钟后令牌又可用」 |
验证码用 GETDEL 原子取出即删
写成「先 GET 再 DEL」存在竞态窗口,并发请求可用同一验证码重复提交。
密码按字节数校验,不按字符数
bcrypt 5.0 起对超过 72 字节的密码直接抛 ValueError(此前是静默截断)。
中文一个字符占 3 字节,24 个汉字即到顶——max_length=20 这类字符数校验拦不住。
常见问题
起服务报 APP_SECRET_KEY 相关错误 —— backend/.env 未填或不足 32 字符。这是刻意的 fail-fast:空密钥签发 JWT 是安全问题,必须启动即失败。
连数据库抛 RuntimeError: 'cryptography' package is required —— 依赖被改回不带 [rsa] 的 pymysql。装回 pymysql[rsa]。
alembic upgrade head 报表已存在 —— 开发库是 init-dev.sql 建的、alembic_version 未写入。手工补版本记录,或 down -v 重建。
改了代码不生效 —— 启动时没加 --reload。