PayrollManagementSystem/backend
liuwenxin a02b6c3ec8 feat(business): 回款管理接口(批次号/手动指定批次/已核算禁改/合计回写)
- 批次 HK 生成;shipment_id 同业务强校验(统一失败面防探测)
- 已核算回款禁改禁删(M3 防重锚点);payment_days 派生(发货日首日=第 1 天)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 12:25:34 +08:00
..
alembic feat(business): 新增业务数据七表及 V5 迁移(业务/明细/状态留痕/发货/回款/评级配置) 2026-10-13 11:22:34 +08:00
app feat(business): 回款管理接口(批次号/手动指定批次/已核算禁改/合计回写) 2026-10-13 12:25:34 +08:00
tests docs(test): 基础数据模块测试记录(48 例/审计盲区缺陷/待补用例更新) 2026-10-13 11:24:29 +08:00
.env.example chore: 初始化项目结构与登录鉴权模块 2026-10-09 18:46:36 +08:00
.python-version chore: 初始化项目结构与登录鉴权模块 2026-10-09 18:46:36 +08:00
alembic.ini chore: 初始化项目结构与登录鉴权模块 2026-10-09 18:46:36 +08:00
pyproject.toml feat(system): 计提参数版本管理、变更留痕与计算预览接口 2026-10-12 20:24:17 +08:00
README.md test(system): M1 参数体系集成用例、e2e 脚本与文档更新 2026-10-13 01:05:07 +08:00
uv.lock feat(system): 计提参数版本管理、变更留痕与计算预览接口 2026-10-12 20:24:17 +08:00

后端

AI摘要:衍水集团营销人员工资管理系统的后端服务。FastAPI + SQLAlchemy(同步)+ MySQL + Redis,Python 3.12 由 uv 锁定。六层结构 api / services / repositories / models / schemas / core。已实现登录鉴权、系统管理(用户/角色/操作日志/日志查询)与 M1 参数体系(岗位角色管理、计提参数版本化/留痕/对比/预览/导出,2026-10-13);业务模块(基础/业务数据、核算、报表)按路线图推进。

职责

  • 提供 REST 接口(统一前缀 /api)
  • 登录鉴权:图形验证码、JWT、登录限流、登出黑名单、登录日志
  • 系统管理(需求 2.5.1):用户管理、角色管理、岗位角色(#28 可扩展+公式模板绑定)、操作日志(自动审计)、操作与登录日志查询
  • 计提参数体系(需求 #28):16 项参数目录、按生效日期版本化(回款到账日取值)、变更批次留痕/对比/导出、计算预览
  • 后续承担:基础数据、业务数据、绩效核算、报表统计、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 是 当前用户与角色

系统管理接口(需求 2.5.1,全部要求管理员权限)

方法 路径 说明
GET / POST /api/v1/system/users 用户列表(分页/筛选)/ 新增用户
GET / PUT / DELETE /api/v1/system/users/{id} 用户详情 / 编辑(含角色分配)/ 删除(物理删除)
PUT /api/v1/system/users/{id}/password 重置密码(提升令牌版本,旧令牌立即失效)
PUT /api/v1/system/users/{id}/status 启用 / 禁用(禁用同样提升令牌版本)
GET /api/v1/system/users/meta/roles 角色下拉选项(仅启用角色)
GET / POST /api/v1/system/roles 角色列表(含停用与绑定用户数)/ 新增角色
PUT / DELETE /api/v1/system/roles/{id} 编辑(不含编码)/ 删除(内置与有绑定的角色受保护)
PUT /api/v1/system/roles/{id}/status 启用 / 禁用(内置 admin 禁停)
GET /api/v1/system/operation-logs 操作日志查询(操作人/模块/操作/结果/时间范围;不含 request_data 大字段)
GET /api/v1/system/operation-logs/{id} 操作日志详情(含已脱敏的请求参数,展开行懒加载)
GET /api/v1/system/login-logs 登录日志查询(用户名/IP/结果/时间范围)

全部写操作自动写入操作日志(成功与失败都记,机制见下)。

统一响应 {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 这类字符数校验拦不住。

操作日志:审计上下文 + 独立提交

写操作统一经 services/audit_service.py 的 audit() 上下文管理器留痕(用法见 api/v1/system/users.py):

  • 调用形态为 DTO:audit(session, operator=..., record=AuditRecord(module, action, summary, request_data), meta=...);落库条目为 OperationLogEntry
  • 正常退出记成功;抛异常先 session.rollback() 再记失败(success=0 + 中文错误原因),随后原异常继续传播
  • 日志独立 commit,整体兜底不外抛 —— 方向是「审计可丢,业务不可断」;反向理解:业务失败回滚不会带走失败记录,而那正是审计最需要的
  • 操作人标量在进入上下文时固化:失败路径先 rollback 会使 ORM 对象过期,之后再读 id/username 会触发额外查询(账号并发被删时 ObjectDeletedError 还会被兜底吞掉)
  • 请求元信息(ip / user-agent / method / path)经路由层的 RequestMetaDep 传入 service
  • commit 之后的任何查询/ORM 读取都要前置(提到 commit 前或用已固化的局部变量):业务已提交却因后置查询抛错会被记成「失败」,产生与事实相反的审计
  • 文本列按 models/operation_log.py 的长度常量截断;request_data 先经结构化脱敏(递归按键名整值抹除 password/token 等,不依赖文本模式匹配)再经 core.logging.scrub 文本兜底,最后按 UTF-8 字节边界截断(encode 带 errors="replace" 兜底孤立代理字符)—— 超长/编码异常都会在 MySQL 或 encode 处抛出,被兜底吞掉后表现为「日志莫名消失」
  • 日志列表与详情两段式:operation-logs 列表排除 request_data(TEXT 大字段,随列表返回会让翻页负担随表增长),展开详情走 GET /operation-logs/{id} 懒加载
  • 模块/操作词表在 core/constants.py(MODULE_* / ACTION_*),前端 utils/constants.ts 与其对齐

角色管理保护规则(后端是唯一防线,前端按钮禁用只是体验):admin 角色禁删禁停;有用户绑定的角色禁删(user_roles 外键是 CASCADE;删除前走 SELECT ... FOR UPDATE 锁行,堵住并发绑定窗口);role_code 创建后不可改;停用角色不可再分配(_resolve_roles 校验 status,停用只影响新分配、不影响既有授权)。

常见问题

起服务报 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。