feat: 配置API(分组校验管线/整份版本/回滚/重置出厂/试金石/平台设置)
- config_validation.validate_group: 九组校验管线(类型/键 allowlist/数值 范围/prompts 大小上限 50KB·100KB/wall 全量交 compile_wall/push.prefix 跨租户查重 spec E1),错误清单中文带字段路径 - config_routes: GET/PUT config(整组替换原样入版本,MAX+1,热 reload)/ reset(按 Tenant.profile 取出厂档案组)/versions 列表·详情/rollback (旧版复制为新版本,历史不可变)/wall/test 试金石(逐句判定,不落库 不 bump)/settings 平台设置 CRUD(仅超管,resolver 接线归 Phase 3) - db.write_config_version: 版本写入公共路径(创建向导与配置 API 单一 事实源);Tenant 落 profile 列(重置出厂按它取对应档案) - _profile_snapshot/_blankify 迁 tenant_source(两路由模块共用); db_source_app/super_client 夹具收拢 conftest(两测试文件共用) - 测试: 519 → 537 全绿 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -23,10 +23,12 @@ DEFAULT_CONFIG 满足不了。
|
||||
|
||||
M-2a 陷阱:Site.brand_overlay 是 ORM JSON 列——写时**整字典赋值*,不做
|
||||
按键局部更新(局部改不标记脏,静默丢写)。
|
||||
|
||||
Task 6 增量:出厂档案快照助手(_profile_snapshot/_blankify)迁至
|
||||
tenant_source.py(配置 API 的重置出厂同样消费);版本 1 写入改走
|
||||
db.write_config_version(与配置 API 的 PUT/rollback/reset 同一版本写入
|
||||
路径,单一事实源);Tenant 落 profile 列(重置出厂按它取对应档案组)。
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
@@ -42,50 +44,23 @@ from app.platform.auth import (
|
||||
require_super,
|
||||
require_tenant,
|
||||
)
|
||||
from app.platform.db import AdminUser, Site, SiteDomain, Tenant, TenantConfigVersion
|
||||
from app.platform.defaults import DEFAULT_CONFIG
|
||||
from app.platform.db import (
|
||||
AdminUser,
|
||||
Site,
|
||||
SiteDomain,
|
||||
Tenant,
|
||||
TenantConfigVersion,
|
||||
write_config_version,
|
||||
)
|
||||
from app.platform.registry import _normalize_host
|
||||
from app.platform.tenant_source import FileTenantSource
|
||||
from app.platform.tenant_source import _profile_snapshot
|
||||
|
||||
router = APIRouter(prefix="/api/platform/tenants", tags=["platform-tenants"])
|
||||
|
||||
# 出厂档案目录:与 app.main.SEED_DIR 同一锚定规则(仓库根/seed_profiles),
|
||||
# 但不 import app.main——main 模块级挂路由,反向导入制造循环依赖。
|
||||
SEED_DIR = pathlib.Path(__file__).resolve().parents[3] / "seed_profiles"
|
||||
|
||||
PROFILES = ("auto", "jiancai", "blank")
|
||||
STATUSES = ("active", "disabled")
|
||||
SLUG_RE = re.compile(r"^[a-z0-9-]+$")
|
||||
|
||||
# blank 身份组清理清单(brand 的这几个键置出厂默认;其余 brand 字段保留)
|
||||
_IDENTITY_BRAND_KEYS = ("title", "whatsapp", "email", "privacy_url")
|
||||
|
||||
|
||||
def _blankify(snapshot: dict) -> dict:
|
||||
"""出厂档案快照 → blank 形态:身份组置出厂默认,其余(含 wall/prompts)
|
||||
原样保留。深拷贝后改——纯函数纪律,绝不动入参。"""
|
||||
out = copy.deepcopy(snapshot)
|
||||
brand = dict(out.get("brand") or {})
|
||||
for key in _IDENTITY_BRAND_KEYS:
|
||||
brand[key] = DEFAULT_CONFIG["brand"][key]
|
||||
out["brand"] = brand
|
||||
push = dict(out.get("push") or {})
|
||||
push["prefix"] = DEFAULT_CONFIG["push"]["prefix"]
|
||||
out["push"] = push
|
||||
engine = dict(out.get("engine") or {})
|
||||
engine["context_seed_keys"] = dict(
|
||||
DEFAULT_CONFIG["engine"]["context_seed_keys"])
|
||||
out["engine"] = engine
|
||||
return out
|
||||
|
||||
|
||||
def _profile_snapshot(profile: str) -> dict:
|
||||
"""profile → 首版快照:auto/jiancai 读对应出厂档案原样;blank 读 jiancai
|
||||
档案再去身份组(wall/prompts 保留——见模块 docstring)。"""
|
||||
base_slug = "jiancai" if profile == "blank" else profile
|
||||
snapshot = FileTenantSource(SEED_DIR).read_tenant(base_slug).snapshot
|
||||
return _blankify(snapshot) if profile == "blank" else snapshot
|
||||
|
||||
|
||||
# ---- 校验助手 -------------------------------------------------------------------
|
||||
|
||||
@@ -253,7 +228,8 @@ async def create_tenant(
|
||||
for domain in domains:
|
||||
await _assert_domain_free(session, domain)
|
||||
|
||||
tenant = Tenant(slug=body.slug, name=body.name, status="active")
|
||||
tenant = Tenant(slug=body.slug, name=body.name, status="active",
|
||||
profile=body.profile)
|
||||
session.add(tenant)
|
||||
await session.flush()
|
||||
site = Site(tenant_id=tenant.id, slug="main", name="main")
|
||||
@@ -268,12 +244,13 @@ async def create_tenant(
|
||||
password_hash=password_hash,
|
||||
salt=salt,
|
||||
must_change_password=True))
|
||||
session.add(TenantConfigVersion(
|
||||
tenant_id=tenant.id,
|
||||
version=1,
|
||||
config_json=json.dumps(snapshot, ensure_ascii=False),
|
||||
# 版本写入走公共 helper(Task 6:与配置 API 的 PUT/rollback/reset
|
||||
# 同一条 MAX(version)+1 路径;新租户无历史版本 → 落版本 1)
|
||||
version = await write_config_version(
|
||||
session, tenant.id, snapshot,
|
||||
updated_by=f"super#{user.user_id}",
|
||||
comment=f"创建向导: profile={body.profile}"))
|
||||
comment=f"创建向导: profile={body.profile}")
|
||||
assert version == 1
|
||||
try:
|
||||
await session.commit()
|
||||
except IntegrityError as exc:
|
||||
|
||||
Reference in New Issue
Block a user