"""反馈页二维码卡配置读写(运营后台可配 → App 反馈页同步)。 整张卡(开关 + 二维码图 + 三行文案)作为一个 JSON 对象,复用通用 app_config 表 (key=feedback_qr_card,value 存这个 dict)。表里没这行 → 返回内置默认(= App 反馈页原 硬编码文案,空配置 = 改造前现状,图片走 App 本地兜底)。admin 改 → 写一行 → App 下次拉 GET /api/v1/feedback/config 即生效(跨进程一致)。 不经 app.repositories.app_config(那层按 CONFIG_DEFS 白名单限定 key,且会出现在系统配置页); 本 key 由本模块独占维护,不进 CONFIG_DEFS,所以不会污染系统配置页。 """ from __future__ import annotations from typing import Any from sqlalchemy.orm import Session from app.models.app_config import AppConfig _KEY = "feedback_qr_card" # 默认值镜像 App 反馈页原硬编码:空配置时整体行为与改造前一致。 _DEFAULTS: dict[str, Any] = { "enabled": True, "image_url": None, # None = App 用本地 asset 兜底(group_qr.png),再不行画占位伪码 "title": "长按图片保存二维码", "group_name": "傻瓜比价官方群", "subtitle": "一起唠嗑共创、解锁新玩法", } _FIELDS = tuple(_DEFAULTS.keys()) def _merge(raw: Any) -> dict[str, Any]: """把 DB 存的(可能不全的)dict 叠加到默认上,得到完整配置(5 个字段,无 updated_at)。""" out = dict(_DEFAULTS) if isinstance(raw, dict): for k in _FIELDS: v = raw.get(k) if v is not None: # image_url 的 None 与默认 None 等价,无需特判 out[k] = v return out def get_config(db: Session) -> dict[str, Any]: """完整配置 + updated_at(admin 读 / App 读共用;App schema 会忽略多余的 updated_at)。""" row = db.get(AppConfig, _KEY) cfg = _merge(row.value if row is not None else None) cfg["updated_at"] = row.updated_at.isoformat() if row is not None and row.updated_at else None return cfg def _write(db: Session, value: dict[str, Any], *, admin_id: int, commit: bool) -> dict[str, Any]: """整体覆写该行(value 须为完整 5 字段 dict),返回合并后的完整配置(含 updated_at)。""" 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 # 重新赋整个 dict,SQLAlchemy 能侦测到变更(in-place 改才侦测不到) row.updated_by_admin_id = admin_id if commit: db.commit() db.refresh(row) else: db.flush() out = _merge(row.value) out["updated_at"] = row.updated_at.isoformat() if row.updated_at else None return out def update_config( db: Session, *, enabled: bool | None = None, title: str | None = None, group_name: str | None = None, subtitle: str | None = None, admin_id: int, commit: bool = True, ) -> tuple[dict[str, Any], dict[str, Any]]: """改文案/开关(只改传了的字段;图片走 set_image)。返回 (before, after) 供审计。""" row = db.get(AppConfig, _KEY) before = _merge(row.value if row is not None else None) new_value = {k: before[k] for k in _FIELDS} if enabled is not None: new_value["enabled"] = enabled if title is not None: new_value["title"] = title.strip() if group_name is not None: new_value["group_name"] = group_name.strip() if subtitle is not None: new_value["subtitle"] = subtitle.strip() after = _write(db, new_value, admin_id=admin_id, commit=commit) return before, after def set_image( db: Session, image_url: str | None, *, admin_id: int, commit: bool = True ) -> tuple[dict[str, Any], dict[str, Any]]: """设置/清空二维码图 URL。返回 (before, after);before['image_url'] 供调用方删旧文件。""" row = db.get(AppConfig, _KEY) before = _merge(row.value if row is not None else None) new_value = {k: before[k] for k in _FIELDS} new_value["image_url"] = image_url after = _write(db, new_value, admin_id=admin_id, commit=commit) return before, after