【问题】 上线/下线时间此前取自审计记录(写操作)时间,当天只翻看、没做写操作的人 会被整条漏掉;而取登录时间更错(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 与移动端同一套口径、同一张表。
54 lines
2.2 KiB
Python
54 lines
2.2 KiB
Python
"""add_user_daily_seen
|
||
|
||
Revision ID: k1l2m3n4o5p6
|
||
Revises: j1k2l3m4n5o6
|
||
Create Date: 2026-09-21
|
||
|
||
每日用户活动表(user_daily_seen)
|
||
--------------------------------
|
||
一天一人一行,记录当天首次 / 末次活动时刻,供日活报表计算
|
||
「上线时间 / 下线时间」。
|
||
|
||
为什么不复用 audit_logs:
|
||
· 上线/下线时间不能取登录时间 —— Refresh Token 有效期 7 天,用户不必每天
|
||
重新登录,「登录次数 0 却操作 35 次」的报表没有意义。
|
||
· 也不能只取写操作时间 —— 审计中间件只记写操作,普通 GET 不入账,
|
||
当天只翻看的人会被漏掉。
|
||
· 更不能把活动写进审计表 —— 「末次活动」是需要不断 UPDATE 的状态,
|
||
而审计流水必须只增不改;能改的审计记录等于没有审计价值。
|
||
|
||
存量数据无需回填:本表从上线时刻开始记录;日活接口对更早的日期会自动
|
||
回退到审计表的写操作时间去推算。
|
||
"""
|
||
from typing import Sequence, Union
|
||
|
||
import sqlalchemy as sa
|
||
from alembic import op
|
||
|
||
revision: str = "k1l2m3n4o5p6"
|
||
down_revision: Union[str, None] = "j1k2l3m4n5o6"
|
||
branch_labels: Union[str, Sequence[str], None] = None
|
||
depends_on: Union[str, Sequence[str], None] = None
|
||
|
||
|
||
def upgrade() -> None:
|
||
op.create_table(
|
||
"user_daily_seen",
|
||
sa.Column("user_id", sa.String(64), primary_key=True,
|
||
comment="操作人账号(逻辑外键→MOM)"),
|
||
sa.Column("day", sa.Date(), primary_key=True,
|
||
comment="北京时间自然日"),
|
||
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False,
|
||
comment="当天首次活动时刻"),
|
||
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False,
|
||
comment="当天末次活动时刻"),
|
||
)
|
||
# 日活查询按日期区间扫,给 day 单独建索引。
|
||
# (主键是 (user_id, day),前缀是 user_id,按 day 过滤用不上,故需补一条)
|
||
op.create_index("ix_user_daily_seen_day", "user_daily_seen", ["day"])
|
||
|
||
|
||
def downgrade() -> None:
|
||
op.drop_index("ix_user_daily_seen_day", table_name="user_daily_seen")
|
||
op.drop_table("user_daily_seen")
|