# 基础设施：系统配置（tb_system_config）

> 状态：独立方案来源稿；最终口径以7月迭代标准评审稿为准。
> 被依赖：需求 09（C端支付限制）

---

## 一、设计目标

将散落在代码里的"写死配置"提取到数据库，平台管理员可通过后台页面修改，无需重新部署。

```mermaid
sequenceDiagram
    actor Admin as 平台超管
    participant Web as 后台配置页
    participant API as SystemConfig Application
    participant DB as PostgreSQL
    participant Redis as Redis
    participant Biz as 业务读取方

    Admin->>Web: 修改受控配置表单
    Web->>API: PUT /api/admin/system/config/{config_key}
    API->>API: 按配置 key 注册规则校验类型和值域
    API->>DB: 更新值并写审计日志
    API->>Redis: 删除对应缓存
    API-->>Web: 返回最新配置和更新时间
    Biz->>Redis: 下次读取缓存未命中
    Biz->>DB: 读取最新配置并回填缓存
```

---

## 二、数据库

```sql
-- 迁移文件：YYYYMMDD_create_tb_system_config.sql
CREATE TABLE tb_system_config (
    id          BIGSERIAL PRIMARY KEY,
    config_key  VARCHAR(100) NOT NULL,           -- 唯一键，格式：module.group.name
    config_value TEXT NOT NULL DEFAULT '',        -- 值（string/number/json字符串）
    value_type  VARCHAR(20) NOT NULL DEFAULT 'string', -- string | int | bool | json
    module      VARCHAR(50) NOT NULL DEFAULT 'general', -- 所属模块（便于按模块查询）
    description TEXT,                             -- 中文说明（前端展示用）
    is_readonly BOOLEAN NOT NULL DEFAULT FALSE,  -- 是否只读（代码内部，不允许后台改）
    creator     BIGINT NOT NULL DEFAULT 0,
    updater     BIGINT NOT NULL DEFAULT 0,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT uq_system_config_key UNIQUE (config_key)
);

COMMENT ON TABLE tb_system_config IS '系统全局配置表';

-- 初始化数据
INSERT INTO tb_system_config (config_key, config_value, value_type, module, description) VALUES
-- C端支付限制（需求9）
('c2b.payment.card_allowed_methods',   '["alipay","wallet"]',   'json',   'c2b.payment', '卡资产C端允许的支付方式（alipay/wechat/wallet）'),
('c2b.payment.device_allowed_methods', '["wechat","wallet"]',   'json',   'c2b.payment', '设备C端允许的支付方式');
```

需求02不写入全局默认实名策略。H5 始终读取卡或设备自身的 `realname_policy`；新建资产使用模型默认 `after_order`，避免全局 Key 与资产字段产生两套优先级。

---

## 三、Model

```go
// internal/model/system_config.go

// SystemConfig 系统全局配置模型
type SystemConfig struct {
    ID          uint      `gorm:"column:id;primaryKey" json:"id"`
    ConfigKey   string    `gorm:"column:config_key;uniqueIndex;not null" json:"config_key"`
    ConfigValue string    `gorm:"column:config_value;type:text;not null;default:''" json:"config_value"`
    ValueType   string    `gorm:"column:value_type;type:varchar(20);not null;default:'string'" json:"value_type"`
    Module      string    `gorm:"column:module;type:varchar(50);not null;default:'general';index" json:"module"`
    Description string    `gorm:"column:description;type:text" json:"description"`
    IsReadonly  bool      `gorm:"column:is_readonly;not null;default:false" json:"is_readonly"`
    Creator     uint      `gorm:"column:creator;not null;default:0" json:"creator"`
    Updater     uint      `gorm:"column:updater;not null;default:0" json:"updater"`
    CreatedAt   time.Time `gorm:"column:created_at" json:"created_at"`
    UpdatedAt   time.Time `gorm:"column:updated_at" json:"updated_at"`
}

func (SystemConfig) TableName() string { return "tb_system_config" }
```

---

## 四、Store

```go
// internal/store/postgres/system_config_store.go

type SystemConfigStore struct {
    db *gorm.DB
}

func (s *SystemConfigStore) GetByKey(ctx context.Context, key string) (*model.SystemConfig, error)
func (s *SystemConfigStore) GetByModule(ctx context.Context, module string) ([]model.SystemConfig, error)
func (s *SystemConfigStore) UpdateValue(ctx context.Context, key string, value string, updaterID uint) error
func (s *SystemConfigStore) BatchGet(ctx context.Context, keys []string) (map[string]*model.SystemConfig, error)
```

---

## 五、配置读取辅助包

业务代码不直接操作 Store，通过辅助函数读取，带 Redis 缓存（5分钟TTL）：

```go
// pkg/sysconfig/config.go

// GetString 读取字符串配置，返回默认值
func GetString(ctx context.Context, key string, defaultVal string) string

// GetStringSlice 读取 JSON 数组配置
func GetStringSlice(ctx context.Context, key string) ([]string, error)

// GetBool 读取布尔配置
func GetBool(ctx context.Context, key string, defaultVal bool) bool

// InvalidateCache 更新配置后清缓存（在 UpdateValue 后调用）
func InvalidateCache(ctx context.Context, key string)
```

Redis Key：`sys:config:{config_key}`，TTL 5分钟。

业务代码用法：
```go
// 读取卡的允许支付方式
allowedMethods, _ := sysconfig.GetStringSlice(ctx, "c2b.payment.card_allowed_methods")
// 返回 ["alipay","wallet"]
```

---

## 六、API 设计

### 6.1 获取配置列表

```
GET /api/admin/system/config?module=c2b.payment
```

响应：
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "config_key": "c2b.payment.card_allowed_methods",
        "config_value": "[\"alipay\",\"wallet\"]",
        "value_type": "json",
        "description": "卡资产C端允许的支付方式",
        "is_readonly": false,
        "updated_at": "2026-07-11T10:00:00Z"
      }
    ]
  }
}
```

### 6.2 更新配置

```
PUT /api/admin/system/config/{config_key}
```

请求体：
```json
{
  "config_value": "[\"alipay\",\"wallet\",\"wechat\"]"
}
```

权限：仅平台超管可操作。

### DTO

```go
// internal/model/dto/system_config_dto.go

// SystemConfigListRequest 配置列表查询请求
type SystemConfigListRequest struct {
    Module string `query:"module" description:"按模块过滤（可选）"`
}

// SystemConfigItem 配置项响应
type SystemConfigItem struct {
    ConfigKey   string    `json:"config_key"`
    ConfigValue string    `json:"config_value"`
    ValueType   string    `json:"value_type" description:"值类型 (string/int/bool/json)"`
    Module      string    `json:"module"`
    Description string    `json:"description"`
    IsReadonly  bool      `json:"is_readonly"`
    UpdatedAt   time.Time `json:"updated_at"`
}

// UpdateSystemConfigRequest 更新配置请求
type UpdateSystemConfigRequest struct {
    ConfigValue string `json:"config_value" validate:"required" description:"新配置值"`
}
```

更新接口不能只校验 `value_type`。Application 需要按 `config_key` 注册允许值，例如支付方式只能来自 `alipay/wechat/wallet`，实名策略只能来自 `none/before_order/after_order`。未知 key 默认只读，禁止通过通用页面写入任意系统配置。

---

## 七、前端对接

### 页面：系统设置 > 系统配置

**初期可以做一个通用的 Key-Value 管理页面，按 module 分组展示。**

调用流程：
1. 进入页面 → `GET /api/admin/system/config`（不传 module = 返回全部）
2. 按 module 分组展示，`is_readonly=true` 的配置只读
3. 修改某项 → `PUT /api/admin/system/config/{config_key}`，body: `{config_value}`
4. 更新成功后后端立即删除该 key 的缓存，前端提示“配置已更新”并刷新当前值

**C端支付限制配置展示建议**（针对 `c2b.payment` 模块）：

不要让后台用户手动填 JSON，前端渲染成 CheckboxGroup：

```
卡资产允许支付方式：
  ☑ 支付宝   ☑ 钱包   ☐ 微信

设备允许支付方式：
  ☐ 支付宝   ☑ 钱包   ☑ 微信
```

前端把选中项序列化成 `["alipay","wallet"]` 提交。

通用 Key-Value 页面只作为管理壳层，已知业务配置必须使用受控组件（单选、复选或开关），不向运营人员暴露 JSON 文本框。
