Files
track/backend/app/models/__init__.py

27 lines
784 B
Python
Raw Normal View History

"""模型包 — 导入 Base 及所有模型,供 Alembic 自动发现"""
from app.models.base import Base
from app.models.production_order import ProductionOrder
from app.models.product import Product
from app.models.task import Task, TaskRecord
from app.models.task_log import TaskLog
from app.models.notification import Notification
from app.models.app_version import AppVersion
from app.models.message import ProductMessage
from app.models.holiday import Holiday
feat(audit): 新增操作审计日志(表/中间件/查询接口)+ 角色常量收敛 背景:系统此前没有操作审计。task_logs 的 task_id 是 NOT NULL 外键,只能挂在 任务上,且全项目仅 4 处写入点 —— 登录、导出、产品增删改、收编完全不留痕。 需求方整理的问题清单里「无审计日志查看页」正源于此:不是没有页面,是没数据。 设计参考 MOM(KCGL) 的 audit_logs / audit_listener,但按 Track 栈做了取舍: 1) 写入时机:MOM 用 SQLAlchemy event listener + 同事务写入,优点是零侵入, 缺点是**业务回滚时审计一起消失**,而失败/被拒的操作(越权尝试、参数错误) 恰恰最需要留痕。Track 改为响应生成后用**独立 session** 写入: - 业务回滚不影响审计(已验证 422/401 失败操作同样落库) - 审计写入失败也不影响业务(全包裹 try/except) - 代价:非原子提交,响应后进程立即被 kill 可能丢一条(已注释说明取舍) 2) 采集方式:中间件自动采集写操作 + 导出/下载/打印这类「读但敏感」的 GET。 路径段推导 module/action/target_id。不做手写埋点,因为手写必然漏 —— task_logs 只有 4 处写入点就是前车之鉴。 3) 增量价值:新增 request_id 字段,与 core/logging.py 的结构化日志打通, 凭一个 ID 就能从审计记录直接跳到那一次接口日志。MOM 无此字段。 4) 敏感信息:details 经 sanitize_details 递归剔除 password/token/secret 等键; 中间件不读请求体,登录明文密码不会落库(已断言表内无密码痕迹)。 配套改动: - core/roles.py:角色常量与 is_admin 收敛为单一事实来源。此前同一份 「管理员角色」规则散在 task_service、products.py 内联判断和前端 constants/task.ts 三处,已因此发生过「移动端漏判 SUPERVISOR 误挡主管」。 task_service 改为从 core.roles 导入同名常量,保持既有引用可用。 - core/deps.py:抽出 require_roles/require_admin 可复用依赖,替代内联判断。 - main.py:500 响应显式补 X-Request-ID 头 —— 该响应由 ServerErrorMiddleware 生成,位于 RequestContextMiddleware 外层,中间件没机会写头。 - auth.py:登录校验前把「尝试的账号」写入 request.state,使登录事件 (含失败登录)可归属到人,可用于追踪暴力破解。 验证:本地起 PostgreSQL 17 + 迁移后跑端到端测试,32/32 通过 (TestClient 每个请求新建事件循环,与模块级 asyncpg 连接池冲突会报 "got Future attached to a different loop",故改用 httpx.AsyncClient + ASGITransport 单循环;生产 uvicorn 单循环无此问题)。
2026-09-21 02:23:19 +00:00
from app.models.audit_log import AuditLog
feat(audit): 每日活动打点表 —— 修正日活「上线/下线时间」口径 【问题】 上线/下线时间此前取自审计记录(写操作)时间,当天只翻看、没做写操作的人 会被整条漏掉;而取登录时间更错(Refresh Token 有效期 7 天,用户不必每天登录, 会出现「登录次数 0 却操作 35 次」的自相矛盾报表)。 【方案 C+:一天一人一行的小状态表】 - 新增 user_daily_seen(user_id, day, first_seen_at, last_seen_at), 迁移 k1l2m3n4o5p6(紧接 j1k2l3m4n5o6) - 打点挂在 RequestContextMiddleware —— 它是最外层,能覆盖【所有】请求, 含不被审计的普通 GET。挂审计中间件没用:那里只记写操作,正是漏人的原因 - 节流:进程内缓存,同一用户 2 分钟内只落盘一次,把"每请求一次写库" 压到"每人每 2 分钟一次";代价是末次活动时间最多落后 2 分钟 - UPSERT on_conflict_do_update 只刷 last_seen_at,first_seen_at 保持当天首次值 - 打点失败全部吞掉并记日志,绝不影响业务请求 【为什么不复用 audit_logs】 「末次活动」是需要不断 UPDATE 的状态,而审计流水必须只增不改 —— 能改的审计记录等于没有审计价值。写进审计表还会让表随访问量线性膨胀。 【查询合并】 get_daily_usage 改为「活动表 ∪ 审计表」并集:只有活动记录的(只看不操作) 和只有审计记录的(本表上线前的历史数据)都会出现。 上线/下线时间取两者的【最早 / 最晚】而非"活动表优先"——打点有 2 分钟节流、 跨零点或写库失败时可能晚于当天第一次写操作,取 min/max 后结果永不劣于任一来源。 零前端改动、零移动端发版:打点在服务端,PC 与移动端同一套口径、同一张表。
2026-09-21 13:05:35 +08:00
from app.models.user_daily_seen import UserDailySeen
__all__ = [
"Base",
"ProductionOrder",
"Product",
"Task",
"TaskRecord",
"TaskLog",
"Notification",
"AppVersion",
"ProductMessage",
"Holiday",
feat(audit): 新增操作审计日志(表/中间件/查询接口)+ 角色常量收敛 背景:系统此前没有操作审计。task_logs 的 task_id 是 NOT NULL 外键,只能挂在 任务上,且全项目仅 4 处写入点 —— 登录、导出、产品增删改、收编完全不留痕。 需求方整理的问题清单里「无审计日志查看页」正源于此:不是没有页面,是没数据。 设计参考 MOM(KCGL) 的 audit_logs / audit_listener,但按 Track 栈做了取舍: 1) 写入时机:MOM 用 SQLAlchemy event listener + 同事务写入,优点是零侵入, 缺点是**业务回滚时审计一起消失**,而失败/被拒的操作(越权尝试、参数错误) 恰恰最需要留痕。Track 改为响应生成后用**独立 session** 写入: - 业务回滚不影响审计(已验证 422/401 失败操作同样落库) - 审计写入失败也不影响业务(全包裹 try/except) - 代价:非原子提交,响应后进程立即被 kill 可能丢一条(已注释说明取舍) 2) 采集方式:中间件自动采集写操作 + 导出/下载/打印这类「读但敏感」的 GET。 路径段推导 module/action/target_id。不做手写埋点,因为手写必然漏 —— task_logs 只有 4 处写入点就是前车之鉴。 3) 增量价值:新增 request_id 字段,与 core/logging.py 的结构化日志打通, 凭一个 ID 就能从审计记录直接跳到那一次接口日志。MOM 无此字段。 4) 敏感信息:details 经 sanitize_details 递归剔除 password/token/secret 等键; 中间件不读请求体,登录明文密码不会落库(已断言表内无密码痕迹)。 配套改动: - core/roles.py:角色常量与 is_admin 收敛为单一事实来源。此前同一份 「管理员角色」规则散在 task_service、products.py 内联判断和前端 constants/task.ts 三处,已因此发生过「移动端漏判 SUPERVISOR 误挡主管」。 task_service 改为从 core.roles 导入同名常量,保持既有引用可用。 - core/deps.py:抽出 require_roles/require_admin 可复用依赖,替代内联判断。 - main.py:500 响应显式补 X-Request-ID 头 —— 该响应由 ServerErrorMiddleware 生成,位于 RequestContextMiddleware 外层,中间件没机会写头。 - auth.py:登录校验前把「尝试的账号」写入 request.state,使登录事件 (含失败登录)可归属到人,可用于追踪暴力破解。 验证:本地起 PostgreSQL 17 + 迁移后跑端到端测试,32/32 通过 (TestClient 每个请求新建事件循环,与模块级 asyncpg 连接池冲突会报 "got Future attached to a different loop",故改用 httpx.AsyncClient + ASGITransport 单循环;生产 uvicorn 单循环无此问题)。
2026-09-21 02:23:19 +00:00
"AuditLog",
feat(audit): 每日活动打点表 —— 修正日活「上线/下线时间」口径 【问题】 上线/下线时间此前取自审计记录(写操作)时间,当天只翻看、没做写操作的人 会被整条漏掉;而取登录时间更错(Refresh Token 有效期 7 天,用户不必每天登录, 会出现「登录次数 0 却操作 35 次」的自相矛盾报表)。 【方案 C+:一天一人一行的小状态表】 - 新增 user_daily_seen(user_id, day, first_seen_at, last_seen_at), 迁移 k1l2m3n4o5p6(紧接 j1k2l3m4n5o6) - 打点挂在 RequestContextMiddleware —— 它是最外层,能覆盖【所有】请求, 含不被审计的普通 GET。挂审计中间件没用:那里只记写操作,正是漏人的原因 - 节流:进程内缓存,同一用户 2 分钟内只落盘一次,把"每请求一次写库" 压到"每人每 2 分钟一次";代价是末次活动时间最多落后 2 分钟 - UPSERT on_conflict_do_update 只刷 last_seen_at,first_seen_at 保持当天首次值 - 打点失败全部吞掉并记日志,绝不影响业务请求 【为什么不复用 audit_logs】 「末次活动」是需要不断 UPDATE 的状态,而审计流水必须只增不改 —— 能改的审计记录等于没有审计价值。写进审计表还会让表随访问量线性膨胀。 【查询合并】 get_daily_usage 改为「活动表 ∪ 审计表」并集:只有活动记录的(只看不操作) 和只有审计记录的(本表上线前的历史数据)都会出现。 上线/下线时间取两者的【最早 / 最晚】而非"活动表优先"——打点有 2 分钟节流、 跨零点或写库失败时可能晚于当天第一次写操作,取 min/max 后结果永不劣于任一来源。 零前端改动、零移动端发版:打点在服务端,PC 与移动端同一套口径、同一张表。
2026-09-21 13:05:35 +08:00
"UserDailySeen",
]