c53ce896f7
华为应用市场审核要求新手引导的「快速设置」权限步必须可被用户关闭,平时又要 保住权限开启率,故做成后台可切的两态开关,送审期间切开、过审后收回。 - app_config 新增 huawei_review 行(default / review),空库与脏值一律回退 default = 上线至今的现状,宁可不给退出按钮也不误放开 - admin: GET/PATCH /admin/api/huawei-review,权限 operator/tech,切换写审计 - 客户端: GET /api/v1/platform/huawei-review 不鉴权(引导页在登录前就展示), 下发 onboarding_closable;机型 gate 由客户端做,故此处不判 ROM Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: 左辰勇 <exinglang@gmail.com> Reviewed-on: #147 Co-authored-by: zuochenyong <zuochenyong@wonderable.ai> Co-committed-by: zuochenyong <zuochenyong@wonderable.ai>
189 lines
8.1 KiB
Python
189 lines
8.1 KiB
Python
"""运营配置读写。
|
|
|
|
get_value:业务读配置,DB 没有则 fallback 到 CONFIG_DEFS 默认(= 原 rewards 常量,空表行为不变)。
|
|
不缓存:配置读频率低(每次福利操作读一次,key 主键查极快),admin 改了立即生效、跨进程一致。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.config_schema import CONFIG_DEFS
|
|
from app.models.app_config import AppConfig
|
|
|
|
|
|
def get_value(db: Session, key: str) -> Any:
|
|
"""读配置值。DB 有用 DB,没有 fallback 到 CONFIG_DEFS 默认。未知 key 抛 KeyError。"""
|
|
row = db.get(AppConfig, key)
|
|
if row is not None:
|
|
return row.value
|
|
return CONFIG_DEFS[key]["default"]
|
|
|
|
|
|
def set_value(
|
|
db: Session, key: str, value: Any, *, admin_id: int, commit: bool = True
|
|
) -> AppConfig:
|
|
"""写配置(admin 用)。未知 key 抛 KeyError。commit=False 让调用方与审计同事务。"""
|
|
if key not in CONFIG_DEFS:
|
|
raise KeyError(f"unknown config key: {key}")
|
|
row = db.get(AppConfig, key)
|
|
if row is None:
|
|
row = AppConfig(key=key, value=value, updated_by_admin_id=admin_id)
|
|
db.add(row)
|
|
else:
|
|
row.value = value
|
|
row.updated_by_admin_id = admin_id
|
|
if commit:
|
|
db.commit()
|
|
db.refresh(row)
|
|
else:
|
|
db.flush()
|
|
return row
|
|
|
|
|
|
def list_all(db: Session) -> list[dict]:
|
|
"""所有可配项:default + 当前值(DB 有则用 DB) + 元信息。给 admin 配置页渲染。"""
|
|
overrides = {r.key: r for r in db.execute(select(AppConfig)).scalars().all()}
|
|
out: list[dict] = []
|
|
for key, defn in CONFIG_DEFS.items():
|
|
row = overrides.get(key)
|
|
out.append(
|
|
{
|
|
"key": key,
|
|
"label": defn["label"],
|
|
"group": defn["group"],
|
|
"type": defn["type"],
|
|
"help": defn.get("help"),
|
|
"default": defn["default"],
|
|
"value": row.value if row is not None else defn["default"],
|
|
"overridden": row is not None,
|
|
"updated_at": row.updated_at.isoformat() if row is not None else None,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
# ── 最新 App 版本(OTA 检查更新)──────────────────────────────────────────────
|
|
# 版本信息不是"运营可配置项",不进 CONFIG_DEFS:它由发布流程(发车)/应急整组写入、客户端读取做
|
|
# OTA,语义是"发布系统状态"而非"运营调参"。故走下面专用读写,直接操作 AppConfig 表(复用表不复用 repo)。
|
|
APP_VERSION_KEY = "latest_app_version"
|
|
|
|
|
|
def get_app_version(db: Session) -> dict | None:
|
|
"""读最新 App 版本信息(OTA)。无记录返回 None(客户端视为无更新)。"""
|
|
row = db.get(AppConfig, APP_VERSION_KEY)
|
|
return row.value if row is not None else None
|
|
|
|
|
|
def set_app_version(db: Session, data: dict, *, commit: bool = True) -> AppConfig:
|
|
"""整组原子覆盖最新版本信息(发布流程/应急写,非 admin,故 updated_by_admin_id 留空)。"""
|
|
row = db.get(AppConfig, APP_VERSION_KEY)
|
|
if row is None:
|
|
row = AppConfig(key=APP_VERSION_KEY, value=data, updated_by_admin_id=None)
|
|
db.add(row)
|
|
else:
|
|
row.value = data
|
|
if commit:
|
|
db.commit()
|
|
db.refresh(row)
|
|
else:
|
|
db.flush()
|
|
return row
|
|
|
|
|
|
# ── 穿山甲广告配置(admin 可配,客户端动态拉)──────────────────────────────────
|
|
# 同 app_version:结构化 dict,复用 AppConfig 表不进 CONFIG_DEFS(它是一组关联配置,不是散项)。
|
|
# 默认值 = 客户端当前内置的硬编码 ID(AdConfig.kt),空库时下发默认 = 维持现状。
|
|
# reward_mkey 是 GroMore S2S 验签密钥(机密),只后端验签用、绝不下发客户端(见 platform/ad-config)。
|
|
AD_CONFIG_KEY = "ad_config"
|
|
_AD_CONFIG_DEFAULTS: dict[str, Any] = {
|
|
"app_id": "5830519", # 穿山甲应用ID(正式)
|
|
"reward_code_id": "104099389", # 福利页激励视频位
|
|
# 比价与领券共用同一穿山甲 Draw 代码位(默认 104098712 = 后台「Draw信息流」位),靠 feed_scene
|
|
# (comparison/coupon)区分收益;运营可在后台把两者拆成不同代码位。字段从旧 *_feed_code_id 改名为 *_draw_code_id。
|
|
"compare_draw_code_id": "104098712", # 比价 Draw 代码位(104098712 = Draw 信息流位)
|
|
"coupon_draw_code_id": "104098712", # 领券 Draw 代码位(初始同比价,运营可拆)
|
|
"reward_mkey": "", # 激励位 GroMore 验签密钥(空则回退 .env PANGLE_REWARD_SECRET*)
|
|
"reward_enabled": True, # 福利激励视频开关
|
|
"compare_ad_enabled": True, # 比价广告开关
|
|
"coupon_ad_enabled": True, # 领券广告开关
|
|
"withdrawal_ad_enabled": True, # 提现激励视频开关(关=直接放行提现,不挡用户的钱)
|
|
}
|
|
|
|
|
|
def get_ad_config(db: Session) -> dict:
|
|
"""读广告配置。DB 无则返回默认(=客户端内置值,维持现状);DB 有则与默认 merge
|
|
(新增字段向后兼容,老记录缺的字段用默认补)。"""
|
|
row = db.get(AppConfig, AD_CONFIG_KEY)
|
|
merged = dict(_AD_CONFIG_DEFAULTS)
|
|
if row is not None and isinstance(row.value, dict):
|
|
merged.update(row.value)
|
|
return merged
|
|
|
|
|
|
def set_ad_config(db: Session, data: dict, *, admin_id: int, commit: bool = True) -> AppConfig:
|
|
"""admin 写广告配置。与现有值 merge(部分更新),只接受已知字段(防脏键)。"""
|
|
row = db.get(AppConfig, AD_CONFIG_KEY)
|
|
merged = dict(_AD_CONFIG_DEFAULTS)
|
|
if row is not None and isinstance(row.value, dict):
|
|
merged.update(row.value)
|
|
merged.update({k: v for k, v in data.items() if k in _AD_CONFIG_DEFAULTS})
|
|
if row is None:
|
|
row = AppConfig(key=AD_CONFIG_KEY, value=merged, updated_by_admin_id=admin_id)
|
|
db.add(row)
|
|
else:
|
|
row.value = merged
|
|
row.updated_by_admin_id = admin_id
|
|
if commit:
|
|
db.commit()
|
|
db.refresh(row)
|
|
else:
|
|
db.flush()
|
|
return row
|
|
|
|
|
|
# ── 华为审核开关(admin 可切,仅华为 ROM 客户端拉)────────────────────────────────
|
|
# 同 ad_config:复用 AppConfig 表但不进 CONFIG_DEFS——它由「华为审核开关」专用页管理,
|
|
# 有自己的两态语义,混进通用系统配置页只会显示成一个没头没尾的 on/off。
|
|
# default → 新手引导页(快速设置)强制展示,用户无法关闭(= 上线至今的现状)
|
|
# review → 快速设置的权限步左上角出现退出按钮,可直接进首页(过华为应用市场审核用)
|
|
# 空库 = default = 行为完全不变。客户端经 /api/v1/platform/huawei-review 拉取。
|
|
HUAWEI_REVIEW_KEY = "huawei_review"
|
|
HUAWEI_REVIEW_MODES = ("default", "review")
|
|
_HUAWEI_REVIEW_DEFAULTS: dict[str, Any] = {
|
|
"mode": "default",
|
|
}
|
|
|
|
|
|
def get_huawei_review(db: Session) -> dict:
|
|
"""读华为审核开关。DB 无 / 脏值一律回退 default(宁可不给退出按钮,也不误放开)。"""
|
|
row = db.get(AppConfig, HUAWEI_REVIEW_KEY)
|
|
merged = dict(_HUAWEI_REVIEW_DEFAULTS)
|
|
if row is not None and isinstance(row.value, dict):
|
|
merged.update(row.value)
|
|
if merged.get("mode") not in HUAWEI_REVIEW_MODES:
|
|
merged["mode"] = _HUAWEI_REVIEW_DEFAULTS["mode"]
|
|
return merged
|
|
|
|
|
|
def set_huawei_review(db: Session, mode: str, *, admin_id: int, commit: bool = True) -> AppConfig:
|
|
"""admin 切换华为审核开关。非法 mode 抛 ValueError(路由转 400)。"""
|
|
if mode not in HUAWEI_REVIEW_MODES:
|
|
raise ValueError(f"invalid mode: {mode} (expected one of {list(HUAWEI_REVIEW_MODES)})")
|
|
row = db.get(AppConfig, HUAWEI_REVIEW_KEY)
|
|
value = {"mode": mode}
|
|
if row is None:
|
|
row = AppConfig(key=HUAWEI_REVIEW_KEY, value=value, updated_by_admin_id=admin_id)
|
|
db.add(row)
|
|
else:
|
|
row.value = value
|
|
row.updated_by_admin_id = admin_id
|
|
if commit:
|
|
db.commit()
|
|
db.refresh(row)
|
|
else:
|
|
db.flush()
|
|
return row
|