262 lines
8.1 KiB
Go
262 lines
8.1 KiB
Go
package config
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/joho/godotenv"
|
||
)
|
||
|
||
// Config 应用配置(从环境变量读取,启动时加载 .env)
|
||
type Config struct {
|
||
Port string
|
||
Mode string
|
||
DBDSN string
|
||
TrustedProxies []string
|
||
CORSOrigins []string
|
||
Version string // APP_VERSION,打包/部署前写在 .env,/health 返回
|
||
|
||
// 统一 API 域名字段:支付回调、转账回调、apiDomain 等均由 BaseURL 拼接
|
||
BaseURL string // API_BASE_URL,如 https://soulapi.quwanzhi.com(无尾部斜杠)
|
||
|
||
// 微信小程序配置
|
||
WechatAppID string
|
||
WechatAppSecret string
|
||
WechatMchID string
|
||
WechatMchKey string
|
||
WechatNotifyURL string // 由 BaseURL + /api/miniprogram/pay/notify 派生
|
||
WechatMiniProgramState string // 订阅消息跳转版本:developer/formal,从 .env WECHAT_MINI_PROGRAM_STATE 读取
|
||
|
||
// 微信转账配置(API v3)
|
||
WechatAPIv3Key string
|
||
WechatCertPath string
|
||
WechatKeyPath string
|
||
WechatSerialNo string
|
||
WechatTransferURL string // 由 BaseURL + /api/payment/wechat/transfer/notify 派生
|
||
|
||
// 管理端登录(与 next-project 一致:ADMIN_USERNAME / ADMIN_PASSWORD / ADMIN_SESSION_SECRET)
|
||
AdminUsername string
|
||
AdminPassword string
|
||
AdminSessionSecret string
|
||
|
||
// 订单对账定时任务间隔(分钟),0 表示不启动内置定时任务
|
||
SyncOrdersIntervalMinutes int
|
||
|
||
// 存客宝-链接卡若:可选,用于「添加好友」的 apiKey(与 GET scenarios 示例一致时可填 CKB_LEAD_API_KEY)
|
||
CkbLeadAPIKey string
|
||
}
|
||
|
||
// BaseURLJoin 将路径拼接到 BaseURL,path 应以 / 开头
|
||
func (c *Config) BaseURLJoin(path string) string {
|
||
base := strings.TrimSuffix(c.BaseURL, "/")
|
||
if base == "" {
|
||
return ""
|
||
}
|
||
p := strings.TrimSpace(path)
|
||
if p != "" && p[0] != '/' {
|
||
p = "/" + p
|
||
}
|
||
return base + p
|
||
}
|
||
|
||
// 默认 CORS 允许的源(零配置:不设环境变量也能用)
|
||
var defaultCORSOrigins = []string{
|
||
"http://localhost:5174",
|
||
"http://127.0.0.1:5174",
|
||
"https://soul.quwanzhi.com",
|
||
"http://soul.quwanzhi.com",
|
||
"https://souladmin.quwanzhi.com",
|
||
"http://souladmin.quwanzhi.com",
|
||
}
|
||
|
||
// current 由 main 在 Load 后设置,供 handler/middleware 读取
|
||
var current *Config
|
||
|
||
// SetCurrent 设置全局配置(main 启动时调用一次)
|
||
func SetCurrent(cfg *Config) { current = cfg }
|
||
|
||
// Get 返回当前配置,未设置时返回 nil
|
||
func Get() *Config { return current }
|
||
|
||
// parseCORSOrigins 从环境变量 CORS_ORIGINS 读取(逗号分隔),未设置则用默认值
|
||
func parseCORSOrigins() []string {
|
||
s := os.Getenv("CORS_ORIGINS")
|
||
if s == "" {
|
||
return defaultCORSOrigins
|
||
}
|
||
parts := strings.Split(s, ",")
|
||
origins := make([]string, 0, len(parts))
|
||
for _, p := range parts {
|
||
if o := strings.TrimSpace(p); o != "" {
|
||
origins = append(origins, o)
|
||
}
|
||
}
|
||
if len(origins) == 0 {
|
||
return defaultCORSOrigins
|
||
}
|
||
return origins
|
||
}
|
||
|
||
// Load 加载配置,端口等从 .env 读取。
|
||
// 环境区分:APP_ENV=development 加载 .env.development,APP_ENV=production 加载 .env.production;
|
||
// air 运行时通过 env_files 或 full_bin 设置 APP_ENV,开发用 .env.development,部署用 .env.production。
|
||
func Load() (*Config, error) {
|
||
workDir, _ := os.Getwd()
|
||
execDir := ""
|
||
if execPath, err := os.Executable(); err == nil {
|
||
execDir = filepath.Dir(execPath)
|
||
}
|
||
loadEnv := func(name string) {
|
||
for _, dir := range []string{execDir, workDir, "."} {
|
||
if dir == "" {
|
||
continue
|
||
}
|
||
p := filepath.Join(dir, name)
|
||
if _, err := os.Stat(p); err == nil {
|
||
_ = godotenv.Load(p)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
overloadEnv := func(name string) {
|
||
for _, dir := range []string{execDir, workDir, "."} {
|
||
if dir == "" {
|
||
continue
|
||
}
|
||
p := filepath.Join(dir, name)
|
||
if _, err := os.Stat(p); err == nil {
|
||
_ = godotenv.Overload(p)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 1. 加载 .env 作为基础
|
||
loadEnv(".env")
|
||
// 2. 按 APP_ENV 覆盖(优先读已设置的 APP_ENV,如 air 的 env_files 已注入)
|
||
appEnv := strings.ToLower(strings.TrimSpace(os.Getenv("APP_ENV")))
|
||
if appEnv == "development" {
|
||
overloadEnv(".env.development")
|
||
} else if appEnv == "production" {
|
||
overloadEnv(".env.production")
|
||
}
|
||
|
||
port := os.Getenv("PORT")
|
||
if port == "" {
|
||
port = "8080"
|
||
}
|
||
mode := os.Getenv("GIN_MODE")
|
||
if mode == "" {
|
||
mode = "debug"
|
||
}
|
||
dsn := os.Getenv("DB_DSN")
|
||
if dsn == "" {
|
||
dsn = "user:pass@tcp(127.0.0.1:3306)/soul?charset=utf8mb4&parseTime=True"
|
||
}
|
||
version := os.Getenv("APP_VERSION")
|
||
if version == "" {
|
||
version = "0.0.0"
|
||
}
|
||
|
||
// 微信配置
|
||
wechatAppID := os.Getenv("WECHAT_APPID")
|
||
if wechatAppID == "" {
|
||
wechatAppID = "wxb8bbb2b10dec74aa" // 默认小程序AppID
|
||
}
|
||
wechatAppSecret := os.Getenv("WECHAT_APPSECRET")
|
||
if wechatAppSecret == "" {
|
||
wechatAppSecret = "3c1fb1f63e6e052222bbcead9d07fe0c" // 默认小程序AppSecret
|
||
}
|
||
wechatMchID := os.Getenv("WECHAT_MCH_ID")
|
||
if wechatMchID == "" {
|
||
wechatMchID = "1318592501" // 默认商户号
|
||
}
|
||
wechatMchKey := os.Getenv("WECHAT_MCH_KEY")
|
||
if wechatMchKey == "" {
|
||
wechatMchKey = "wx3e31b068be59ddc131b068be59ddc2" // 默认API密钥(v2)
|
||
}
|
||
// 统一域名:API_BASE_URL 派生支付/转账回调,可选 WECHAT_NOTIFY_URL 覆盖
|
||
baseURL := strings.TrimSpace(strings.TrimSuffix(os.Getenv("API_BASE_URL"), "/"))
|
||
if baseURL == "" {
|
||
baseURL = "https://soulapi.quwanzhi.com"
|
||
}
|
||
wechatNotifyURL := os.Getenv("WECHAT_NOTIFY_URL")
|
||
if wechatNotifyURL == "" {
|
||
wechatNotifyURL = baseURL + "/api/miniprogram/pay/notify"
|
||
}
|
||
wechatMiniProgramState := strings.TrimSpace(os.Getenv("WECHAT_MINI_PROGRAM_STATE"))
|
||
if wechatMiniProgramState != "developer" && wechatMiniProgramState != "trial" {
|
||
wechatMiniProgramState = "formal" // 默认正式版,避免生成开发版码导致「开发版已过期」
|
||
}
|
||
|
||
// 转账配置
|
||
wechatAPIv3Key := os.Getenv("WECHAT_APIV3_KEY")
|
||
if wechatAPIv3Key == "" {
|
||
wechatAPIv3Key = "wx3e31b068be59ddc131b068be59ddc2" // 默认 API v3 密钥
|
||
}
|
||
wechatCertPath := os.Getenv("WECHAT_CERT_PATH")
|
||
if wechatCertPath == "" {
|
||
wechatCertPath = "certs/apiclient_cert.pem" // 默认证书路径
|
||
}
|
||
wechatKeyPath := os.Getenv("WECHAT_KEY_PATH")
|
||
if wechatKeyPath == "" {
|
||
wechatKeyPath = "certs/apiclient_key.pem" // 默认私钥路径
|
||
}
|
||
wechatSerialNo := os.Getenv("WECHAT_SERIAL_NO")
|
||
if wechatSerialNo == "" {
|
||
wechatSerialNo = "4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5" // 默认证书序列号
|
||
}
|
||
wechatTransferURL := os.Getenv("WECHAT_TRANSFER_URL")
|
||
if wechatTransferURL == "" {
|
||
wechatTransferURL = baseURL + "/api/payment/wechat/transfer/notify"
|
||
}
|
||
|
||
adminUsername := os.Getenv("ADMIN_USERNAME")
|
||
if adminUsername == "" {
|
||
adminUsername = "admin"
|
||
}
|
||
adminPassword := os.Getenv("ADMIN_PASSWORD")
|
||
if adminPassword == "" {
|
||
adminPassword = "admin123"
|
||
}
|
||
adminSessionSecret := os.Getenv("ADMIN_SESSION_SECRET")
|
||
if adminSessionSecret == "" {
|
||
adminSessionSecret = "soul-admin-secret-change-in-prod"
|
||
}
|
||
// 默认 0:不启动全量定时对账,以「支付发起后单笔订单轮询」为主;需兜底时可设 SYNC_ORDERS_INTERVAL_MINUTES=60 等
|
||
syncOrdersInterval := 0
|
||
if s := os.Getenv("SYNC_ORDERS_INTERVAL_MINUTES"); s != "" {
|
||
if n, e := strconv.Atoi(s); e == nil && n >= 0 {
|
||
syncOrdersInterval = n
|
||
}
|
||
}
|
||
|
||
return &Config{
|
||
Port: port,
|
||
Mode: mode,
|
||
DBDSN: dsn,
|
||
TrustedProxies: []string{"127.0.0.1", "::1"},
|
||
CORSOrigins: parseCORSOrigins(),
|
||
Version: version,
|
||
BaseURL: baseURL,
|
||
WechatAppID: wechatAppID,
|
||
WechatAppSecret: wechatAppSecret,
|
||
WechatMchID: wechatMchID,
|
||
WechatMchKey: wechatMchKey,
|
||
WechatNotifyURL: wechatNotifyURL,
|
||
WechatMiniProgramState: wechatMiniProgramState,
|
||
WechatAPIv3Key: wechatAPIv3Key,
|
||
WechatCertPath: wechatCertPath,
|
||
WechatKeyPath: wechatKeyPath,
|
||
WechatSerialNo: wechatSerialNo,
|
||
WechatTransferURL: wechatTransferURL,
|
||
AdminUsername: adminUsername,
|
||
AdminPassword: adminPassword,
|
||
AdminSessionSecret: adminSessionSecret,
|
||
SyncOrdersIntervalMinutes: syncOrdersInterval,
|
||
CkbLeadAPIKey: strings.TrimSpace(os.Getenv("CKB_LEAD_API_KEY")),
|
||
}, nil
|
||
}
|