bce8cd9923
一、RBAC 权限管理(角色 → 可见页面) - 新增 admin_role 表 + 权限目录 permissions.py:角色持有一组页面 key,登录后左侧只展示这些页 - 内建角色 管理员(super_admin 全权锁定)/运营/财务/技术,页集对齐原型;key 承重(require_role 用),另加中文 label 展示 - 角色 CRUD 端点(仅 super_admin)+ 目录端点;登录/me 下发当前角色有效可见页 - admins 加删除、角色存在性校验;可复看已确定登录密码:UI 建的账号留存明文 plain_password,脚本建的超管不留存 二、系统配置下发修复 - 首页数据「保存即生效」:显式保存(apply_now)对 real/manual 直接落配置目标值、绕过只增不减护栏(修「改了 app 端不变」),护栏仍管自动 tick - 首页轮播新增数据源三选一:mixed(真实优先+种子)/real(只真实)/seed(只种子),get_feed 分支 + /marquee-seeds/mode 端点 - 福利页 Tab 隐藏 任务/里程碑,及看广告的单次金币/每轮次数/信息流开关:CONFIG_DEFS 加 hidden + list_config 过滤,业务读取默认值不受影响 三、比价记录店/商品搜索 - comparison_record 加 product_names 派生列(从下单 items 拼商品名),迁移建列并回填历史行 - admin 比价记录列表店/商品分列可搜:走 product_names 普通列 LIKE,规避 SQLite JSON 中文 ensure_ascii 转义搜不到的坑 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
3.6 KiB
Python
91 lines
3.6 KiB
Python
"""admin 运营配置:列出所有可配项 + 改某项(带审计 + 改值校验)。
|
|
|
|
配置项定义见 app.core.config_schema.CONFIG_DEFS;业务读配置 fallback 默认(见 app_config repo)。
|
|
权限:operator / finance 都可改(运营调奖励、财务调额度),super 恒可。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated, Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
|
|
from app.admin.audit import write_audit
|
|
from app.admin.deps import AdminDb, get_client_ip, get_current_admin, require_role
|
|
from app.admin.schemas.config import ConfigItemOut, ConfigUpdateRequest
|
|
from app.core.config_schema import CONFIG_DEFS
|
|
from app.core.rewards import SIGNIN_CYCLE_LEN
|
|
from app.models.admin import AdminUser
|
|
from app.repositories import app_config
|
|
|
|
router = APIRouter(
|
|
prefix="/admin/api/config",
|
|
tags=["admin-config"],
|
|
dependencies=[Depends(get_current_admin)],
|
|
)
|
|
|
|
|
|
def _validate(key: str, value: Any) -> None:
|
|
"""按配置项 type 校验新值,不合法抛 ValueError(router 转 400)。"""
|
|
t = CONFIG_DEFS[key]["type"]
|
|
if t == "int":
|
|
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
raise ValueError("需为非负整数")
|
|
elif t == "int_list":
|
|
if not isinstance(value, list) or not value:
|
|
raise ValueError("需为非空整数列表")
|
|
if not all(isinstance(x, int) and not isinstance(x, bool) and x >= 0 for x in value):
|
|
raise ValueError("列表元素需为非负整数")
|
|
if key == "signin_rewards" and len(value) != SIGNIN_CYCLE_LEN:
|
|
raise ValueError(f"签到档位必须正好 {SIGNIN_CYCLE_LEN} 个(对应 {SIGNIN_CYCLE_LEN} 天循环)")
|
|
elif t == "dict_str_int":
|
|
if not isinstance(value, dict) or not all(
|
|
isinstance(k, str) and isinstance(v, int) and not isinstance(v, bool)
|
|
for k, v in value.items()
|
|
):
|
|
raise ValueError("需为 {字符串: 整数} 映射")
|
|
elif t == "bool":
|
|
if not isinstance(value, bool):
|
|
raise ValueError("需为布尔值")
|
|
|
|
|
|
def _item(db, key: str) -> ConfigItemOut:
|
|
for item in app_config.list_all(db):
|
|
if item["key"] == key:
|
|
return ConfigItemOut(**item)
|
|
raise HTTPException(status_code=404, detail="未知配置项")
|
|
|
|
|
|
@router.get("", response_model=list[ConfigItemOut], summary="所有可配项 + 当前值(不含 hidden)")
|
|
def list_config(db: AdminDb) -> list[ConfigItemOut]:
|
|
# hidden 项(已下线/由专用页管理,如福利页任务·里程碑·看广告调参、首页轮播数据源)不在本页渲染。
|
|
return [
|
|
ConfigItemOut(**item)
|
|
for item in app_config.list_all(db)
|
|
if not CONFIG_DEFS[item["key"]].get("hidden")
|
|
]
|
|
|
|
|
|
@router.patch("/{key}", response_model=ConfigItemOut, summary="改某项配置(带审计)")
|
|
def update_config(
|
|
key: str,
|
|
body: ConfigUpdateRequest,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator", "finance"))],
|
|
db: AdminDb,
|
|
) -> ConfigItemOut:
|
|
if key not in CONFIG_DEFS:
|
|
raise HTTPException(status_code=404, detail="未知配置项")
|
|
try:
|
|
_validate(key, body.value)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
|
before = app_config.get_value(db, key)
|
|
app_config.set_value(db, key, body.value, admin_id=admin.id, commit=False)
|
|
write_audit(
|
|
db, admin, action="config.set", target_type="config", target_id=key,
|
|
detail={"before": before, "after": body.value}, ip=get_client_ip(request), commit=False,
|
|
)
|
|
db.commit()
|
|
return _item(db, key)
|