key_version=288 的记录(id=345 与 id=349)。is_active=0 的记录(如 id=345,346,347,348),而期望是仅用 expire_at 判断是否失效,is_active 保持为 1。api_keys 表由 backend-admin 与 backend-client 共用。backend-admin 只负责插入新密钥(is_active=1),从未把已有密钥改为 is_active=0。
把密钥标成“非活跃”的逻辑在 backend-client 的 app/services/key_service.py 中:
| 位置 | 行为 |
|---|---|
get_public_key (约 45–47 行) |
若当前取到的密钥已过期(expire_at < now),则 api_key.is_active = 0 并 commit,再取“下一个”密钥返回。 |
get_active_key_pair (约 84–85 行) |
同上:发现过期就把该密钥 is_active = 0。 |
rotate_keys (约 199–204 行) |
轮换时先把所有 is_active=1 的密钥统一 update({"is_active": 0}),再生成新密钥。 |
check_and_rotate_expired_keys (约 391–399 行) |
把所有“已过期且 is_active=1”的密钥遍历并置为 key.is_active = 0。 |
因此:is_active=0 是 client 端在“发现过期”或“轮换/检查过期”时写库导致的。若设计上希望“只凭 expire_at 判断是否有效、不修改 is_active”,则不应在 client 里改 is_active。
admin 定时任务(backend-admin/app/background_tasks.py 中 _key_expiry_check_task)的逻辑是:
current_key = 满足 is_active=1 且 key_version 最大的那条。key_advance_generate_minutes):
key_version == current_key.key_version + 1 且 is_active == 1 的密钥。current_key.key_version + 1。时间线可以还原为:
is_active=1。get_public_key / get_active_key_pair 或 check_and_rotate_expired_keys 中把 288 的 is_active 置为 0。key_version=288 且 is_active=1?is_active=0,所以不存在。is_active=1。因此:重复的 key_version=288 是因为 client 把“旧 288”标成 is_active=0 后,admin 的“当前密钥”回退到 287,又按“没有 288 且 is_active=1”的条件重新生成了 288。
is_active;“当前密钥” = is_active=1 且版本最大;是否过期依赖 expire_at 的查询条件(如 encrypt 里 expire_at > now)。is_active=0,并依赖 is_active=1 取“当前密钥”。两边的约定不一致:admin 不写 is_active=0,client 会写;admin 的定时任务又依赖“is_active=1 + 版本号”判断是否已有下一版密钥,一旦 client 把某版本置为 0,admin 就会认为“下一版不存在”从而再次生成同版本号的新密钥,导致版本号重复。
api_keys 的“是否有效”仅由 expire_at 和 is_deleted 决定;所有地方都不再写入 is_active=0,插入时一律 is_active=1。get_public_key、get_active_key_pair:若当前密钥已过期,不要写 api_key.is_active = 0;只按“未删除且未过期”重新查下一条密钥并返回(或返回错误)。rotate_keys:若仍由 client 触发轮换,不应“把所有 is_active=1 置 0”;只生成新密钥即可(或不再在 client 做全局置 0)。check_and_rotate_expired_keys:不要对过期密钥写 is_active=0;只负责在需要时生成新密钥(或移除该逻辑,由 admin 定时任务统一生成)。key_version == current_key.key_version + 1 且未删除且未过期(或直接存在该版本即可),而不再要求 is_active==1,这样即使 client 误写了 is_active=0 也不会重复生成。这样可以从根源上避免 is_active=0 被误用,并避免因 is_active 被改成 0 而触发的版本号重复。
api_keys(包括 is_active);backend-client 只读 api_keys,不更新任何字段。APIKey.is_active 的赋值和 update;取密钥时只按 is_deleted=0 与 expire_at > now(及需要的 key_version)查询。建议优先采用方案 A,逻辑更简单,且与“用过期时间判断失效”的预期一致。
is_deleted=1 或仅作历史保留,避免业务再使用。is_active 改回 1(仅做数据修复);若已过期,可保持为 0 或按清理策略标记删除。修复 SQL 需根据你们对“保留哪条 288、是否物理删除”的决策再写,此处不给出具体 SQL,以免误删。
文档生成时间:2026-02-21