fix: 终审修复——站点停用接线/prefix查重补创建reset/默认凭据警示+取舍补录

- I-1 站点停用全链接线: Site 数据类加 status 字段; DB 源 _load_one 与
  文件源 read_tenant 均只输出 active 站点(停用站点域名不进 registry
  索引 → 该域名 404;全停用=合法运营状态输出空 sites;文件源无 status
  键默认 active 兼容)。E2E: PATCH 站点 disabled → 其域名 404、复启用
  恢复;源级直测两源过滤。
- I-3 prefix 跨租户查重补两口: db.other_push_prefixes 共用助手
  (MAX(version) 逐租户当前前缀,排除自身);创建向导(出厂档案前缀被占
  → 400 提示改 blank/改占用方)与 reset push(重置回出厂撞车 → 400 提示
  走 PUT)接入;PUT 原有查重改用同一助手。rollback 维持豁免(历史不可变
  语义优先,取舍清单记档)。既有测试第二租户改 blank profile 避撞。
- I-4 默认凭据警示: lifespan 启动检测 ADMIN_PASSWORD 仍为 Settings 字段
  默认值(非硬编码第二份,防双源漂移)→ logger.warning 醒目提示;
  CLAUDE.md Phase 4 部署记档补 ADMIN_PASSWORD 生产要求。
- I-2/M-2 取舍补录: 密钥掩码未实现(GET config/settings 明文返回
  api_key/smtp 密码,Phase 3 前须定 __SET__ 哨兵方案);站点管理权限现为
  仅超管(Phase 3 对接时决策放开或改 spec);取舍清单第 2 条同步刷新
  (rollback 现为唯一豁免口)。

全量回归: 571 passed(基线 566 + 新增 5)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 12:40:21 +08:00
parent 6cd343e24a
commit 6839cb94d2
14 changed files with 281 additions and 52 deletions
+12 -28
View File
@@ -47,6 +47,7 @@ from app.platform.db import (
PlatformSetting,
Tenant,
TenantConfigVersion,
other_push_prefixes,
write_config_version,
)
from app.platform.defaults import DEFAULT_CONFIG
@@ -105,33 +106,6 @@ async def _load_current(
return row, snapshot
async def _other_prefixes(
session: AsyncSession, exclude_tenant_id: int) -> frozenset:
"""各租户**当前版本**的 push.prefix 集合(spec E1 查重数据源,排除自身;
空/坏数据不参战)。MAX(version) 子查询定位每租户当前版。"""
max_versions = (
select(TenantConfigVersion.tenant_id.label("tid"),
func.max(TenantConfigVersion.version).label("mv"))
.group_by(TenantConfigVersion.tenant_id).subquery())
rows = (await session.execute(
select(TenantConfigVersion.tenant_id, TenantConfigVersion.config_json)
.join(max_versions, (TenantConfigVersion.tenant_id == max_versions.c.tid)
& (TenantConfigVersion.version == max_versions.c.mv)))).all()
prefixes = set()
for tenant_id, config_json in rows:
if tenant_id == exclude_tenant_id:
continue
try:
snapshot = json.loads(config_json)
except ValueError:
continue
push = snapshot.get("push") if isinstance(snapshot, dict) else None
prefix = push.get("prefix") if isinstance(push, dict) else None
if isinstance(prefix, str) and prefix:
prefixes.add(prefix)
return frozenset(prefixes)
def _updated_by(user: UserContext) -> str:
return f"{user.role}#{user.user_id}"
@@ -163,7 +137,7 @@ async def put_config_group(
async with AsyncSession(engine, expire_on_commit=False) as session:
tenant = await _get_tenant_or_404(session, slug)
context = ValidationContext(
other_prefixes=await _other_prefixes(session, tenant.id))
other_prefixes=await other_push_prefixes(session, tenant.id))
errors = validate_group(group, body, context)
if errors:
raise HTTPException(status_code=400, detail="; ".join(errors))
@@ -203,6 +177,16 @@ async def reset_config_group(
value = copy.deepcopy(factory[group])
else:
value = copy.deepcopy(DEFAULT_CONFIG[group])
# push 组重置补跨租户查重(终审 I-3):重置回出厂 prefix 可能撞上
# 现在的占用方(本租户改过前缀后,出厂前缀被别人取走)。
if group == "push" and isinstance(value, dict):
prefix = value.get("prefix")
if (isinstance(prefix, str) and prefix and prefix
in await other_push_prefixes(session, tenant.id)):
raise HTTPException(
status_code=400,
detail=f"重置后的 push.prefix {prefix!r} 已被其他租户"
f"占用——请改用 PUT /config/push 指定其他前缀")
_, current = await _load_current(session, tenant.id, slug)
new_snapshot = dict(current)
new_snapshot[group] = value