Files
opsx-licence/backend/internal/service/appconfig/registry.go
T
Logan.Li 83a3630404
CI / backend (push) Canceled after 0s
feat(settings): 运行配置中心——.env 业务参数统一到后台页面维护
- 新增 app_configs 表与 appconfig 注册表:读取优先级 页面 → .env → 默认,删除即回退
- 页面可配 7 项:支付回调基址、成功回跳、Webhook 密钥(加密入库)、未验签放行开关、CORS、可信代理、GeoIP 路径
- 引导密钥(JWT_SECRET / OPSLIC_MASTER_KEY / OFFLINE_LICENSE_SEED)与部署参数只读展示掩码+指纹,永不入库
- 校验:URL 格式、密钥长度≥16、CIDR、release 下禁止开启未验签回调;变更写审计日志
- 消费点改造:支付回调地址、Waffo 回跳、webhook 验签、可信代理、CORS、GeoIP 统一走配置中心
- 前端新增「审计与设置 → 运行配置」页(分组表格 + 来源标记 + 编辑弹窗 + 恢复默认)
- .env.example 补注释:哪些必须留在 .env、哪些已收敛到页面
2026-11-01 06:17:58 +08:00

230 lines
9.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package appconfig
// 运行配置注册表:项目里所有「运维/业务参数」的唯一清单。
//
// 约定(重要):
// - Editable=true 的项可在后台「系统设置 → 运行配置」页维护,落库 app_configs,
// 读取优先级 DB → 环境变量 → Default;
// - Editable=false 的项只做展示(值来自环境变量),用于回答「哪些配置必须留在 .env」,
// 尤其是启动引导密钥:它们决定能否解开库里的密文 / 能否签发令牌,
// 放进数据库等于把钥匙锁进自己锁的箱子,因此永不入库; // Logan.Li
// - 新增配置时先在这里登记,再在消费点用 String/Bool/List 读取,避免散落的 os.Getenv。 // Logan.Li
// Kind 配置值类型 // Logan.Li
const (
KindString = "string" // 普通字符串
KindBool = "bool" // 开关,存 "1" / "0"
KindList = "list" // 逗号分隔列表
KindSecret = "secret" // 机密字符串(落库 AES-GCM 加密,页面只回显掩码)
)
// 配置分组顺序(页面按此顺序渲染) // Logan.Li
var GroupOrder = []string{"支付与回调", "安全与访问", "引导密钥(仅环境变量)", "部署参数(仅环境变量)"}
// Item 单个配置项的元数据 // Logan.Li
type Item struct {
Key string // 键名 = 环境变量名
Label string // 中文名
Group string // 分组
Description string // 说明(页面直接展示)
Kind string // 见 Kind 常量
Default string // 环境变量与数据库都缺省时的值
Editable bool // 是否允许页面编辑
RequiresRestart bool // 改动是否需要重启容器才生效
Danger bool // 高风险开关,页面二次确认
ReadOnlyReason string // Editable=false 时的原因说明
Placeholder string // 输入提示
}
// items 全量注册表(顺序即页面顺序) // Logan.Li
var items = []Item{
// ============ 支付与回调 ============
{
Key: "PAYMENT_PUBLIC_BASE_URL",
Label: "支付回调对外基址",
Group: "支付与回调",
Description: "生成微信/支付宝回调地址(notify_url)用的对外域名,需与浏览器访问域名一致且公网可达。",
Kind: KindString,
Default: "https://licence.rlwj.cn",
Editable: true,
Placeholder: "https://licence.rlwj.cn",
},
{
Key: "SUCCESS_URL",
Label: "支付成功回跳地址",
Group: "支付与回调",
Description: "买家付款完成后跳回的页面。Waffo 页签填写了同名字段时以页签为准,此项作为兜底。",
Kind: KindString,
Default: "",
Editable: true,
Placeholder: "https://licence.rlwj.cn/pay/success",
},
{
Key: "PAYMENT_CERT_DIR",
Label: "支付证书目录",
Group: "支付与回调",
Description: "容器内证书/私钥落地目录(0700),必须在数据卷内,否则重建容器即丢证书。",
Kind: KindString,
Default: "/data/payment-certs",
Editable: false,
ReadOnlyReason: "属于容器部署参数(数据卷路径),随镜像/编排交付,改错会导致证书丢失",
},
// ============ 安全与访问 ============
{
Key: "INTERNAL_WEBHOOK_SECRET",
Label: "内部 Webhook 共享密钥",
Group: "安全与访问",
Description: "通用支付回调 /api/v1/webhooks/payment 的验签密钥;未配置时回落到已启用网关的 WebhookSecret。留空表示不单独配置。",
Kind: KindSecret,
Default: "",
Editable: true,
Placeholder: "至少 16 位随机串",
},
{
Key: "ALLOW_UNVERIFIED_WEBHOOK",
Label: "允许未验签回调",
Group: "安全与访问",
Description: "仅本地调试使用。开启后未配置密钥也放行回调,任何人都能伪造「已付款」订单;生产(GIN_MODE=release)下禁止开启。",
Kind: KindBool,
Default: "0",
Editable: true,
Danger: true,
},
{
Key: "CORS_ORIGINS",
Label: "跨域白名单",
Group: "安全与访问",
Description: "允许跨域访问 API 的 Origin 列表(逗号分隔,含协议与端口)。留空 = 不限制来源。",
Kind: KindList,
Default: "",
Editable: true,
RequiresRestart: true,
Placeholder: "https://app.example.com,tauri://localhost",
},
{
Key: "TRUSTED_PROXIES",
Label: "可信反代网段",
Group: "安全与访问",
Description: "反向代理的内网 CIDR(逗号分隔),用于取真实客户端 IP;配置错误会导致限流与审计 IP 失真。",
Kind: KindList,
Default: "",
Editable: true,
RequiresRestart: true,
Placeholder: "127.0.0.1,172.16.0.0/12",
},
{
Key: "GEOIP_DB_PATH",
Label: "GeoIP 库路径",
Group: "安全与访问",
Description: "GeoLite2-City.mmdb 在容器内的路径,用于订单/激活的地理分布统计;文件不存在时静默降级。",
Kind: KindString,
Default: "/data/geoip/GeoLite2-City.mmdb",
Editable: true,
RequiresRestart: true,
Placeholder: "/data/geoip/GeoLite2-City.mmdb",
},
// ============ 引导密钥(仅环境变量) ============
{
Key: "JWT_SECRET",
Label: "登录令牌签名密钥",
Group: "引导密钥(仅环境变量)",
Description: "签发/校验管理员登录令牌。入库即与历史令牌脱钩,且数据库泄露等于直接交出后台登录能力,故只允许环境变量。",
Kind: KindSecret,
Editable: false,
ReadOnlyReason: "启动引导密钥:登录鉴权在数据库可用之前就要用它,且必须与历史令牌保持一致",
},
{
Key: "OPSLIC_MASTER_KEY",
Label: "机密字段信封加密主密钥",
Group: "引导密钥(仅环境变量)",
Description: "解开库里全部密文(支付私钥、证书口令、本页机密项)的根密钥,64 位 hex。丢失=所有密文不可解,泄露=所有密文失效。",
Kind: KindSecret,
Editable: false,
ReadOnlyReason: "根密钥必须独立于数据库存放,否则加密形同虚设;更换需先离线重加密全部密文",
},
{
Key: "OFFLINE_LICENSE_SEED",
Label: "离线授权签名种子",
Group: "引导密钥(仅环境变量)",
Description: "Ed25519 种子(64 位 hex),签发离线授权证。更换后已发出的离线证将无法验证,需同步桌面端公钥,故不提供页面修改。",
Kind: KindSecret,
Editable: false,
ReadOnlyReason: "更换会使既有离线授权证全部失效(桌面端内置公钥需同步升级),须人工走轮换流程",
},
// ============ 部署参数(仅环境变量) ============
{
Key: "PORT",
Label: "服务监听端口",
Group: "部署参数(仅环境变量)",
Kind: KindString,
Default: "9001",
Editable: false,
ReadOnlyReason: "进程启动参数:端口在读取数据库之前就已绑定",
},
{
Key: "GIN_MODE",
Label: "运行模式",
Group: "部署参数(仅环境变量)",
Kind: KindString,
Default: "debug",
Editable: false,
ReadOnlyReason: "进程启动参数:决定日志级别与安全开关的 fail-closed 行为",
},
{
Key: "DB_DRIVER",
Label: "数据库驱动",
Group: "部署参数(仅环境变量)",
Kind: KindString,
Default: "sqlite",
Editable: false,
ReadOnlyReason: "启动引导参数:数据库连接建立前无法读取本表",
},
{
Key: "DB_NAME",
Label: "数据库文件 / 库名",
Group: "部署参数(仅环境变量)",
Kind: KindString,
Default: "db.sqlite3",
Editable: false,
ReadOnlyReason: "启动引导参数:数据库连接建立前无法读取本表",
},
{
Key: "HOST_PORT",
Label: "宿主机映射端口",
Group: "部署参数(仅环境变量)",
Kind: KindString,
Default: "9001",
Editable: false,
ReadOnlyReason: "docker compose 编排参数,由宿主机侧读取,进程内不可见",
},
{
Key: "VERSION",
Label: "版本标记",
Group: "部署参数(仅环境变量)",
Kind: KindString,
Default: "",
Editable: false,
ReadOnlyReason: "构建期注入的镜像版本标记,运行时只读",
},
}
// Items 返回注册表副本(调用方可自由修改,不污染全局) // Logan.Li
func Items() []Item {
out := make([]Item, len(items))
copy(out, items)
return out
}
// Lookup 按键查注册项 // Logan.Li
func Lookup(key string) (Item, bool) {
for _, it := range items {
if it.Key == key {
return it, true
}
}
return Item{}, false
}