Files
yml2213 b76d8dc257 功能:发货链接支持永久有效,默认 TTL=0
- DELIVERY_LINK_TTL_MINUTES 设为 0 或负数时链接永久有效(expires_at 存 NULL),
  默认值由 120 改为 0,本地 .env 同步改为永久
- 新增 delivery_link_generated_at 列区分从未生成与永久链接,兼容历史数据
- 永久链接授权仅校验 HMAC 签名、不校验过期(exp=0),作废/恢复语义不变
- 前端链接有效期列对永久链接显示"永久有效"
2026-08-14 14:26:06 +08:00

186 lines
6.8 KiB
Go
Raw Permalink 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 config
import (
"os"
"path/filepath"
"strconv"
"strings"
"github.com/joho/godotenv"
)
type Config struct {
Port string
JWTSecret string
DatabaseURL string
Mode string // debug / release
// DataEncryptionKey 用于加密保存 API/回调密钥;为空时由 JWT_SECRET 派生。
DataEncryptionKey string
// OpenAPIKey 皮肤源头开放接口标识(Header: X-Api-Key,可公开给对接方)
OpenAPIKey string
// OpenAPISecret 签名密钥(仅用于 HMAC,不走 Header
OpenAPISecret string
// OpenSignSkew 签名时间戳允许偏差(秒)
OpenSignSkew int64
// OpenAPIDebug 开放接口详细调试日志
OpenAPIDebug bool
// LogFile 日志文件路径;非空时同时写控制台与文件(按天分割)
LogFile string
// LogRetainDays 日志文件保留天数(按天分割后自动清理旧文件)
LogRetainDays int
// DeliveryBaseURL 用户侧发货页面基础地址;为空时前端可自行按当前域名拼接。
DeliveryBaseURL string
// DeliveryBFFBaseURL 上游发货 BFF 地址。
DeliveryBFFBaseURL string
// DeliveryChannel 上游测试发货渠道前缀,如 dlc。
DeliveryChannel string
// DeliveryLinkSecret 发货链接签名密钥。
DeliveryLinkSecret string
// DeliveryLinkTTLMinutes 发货链接有效分钟数;<=0 表示永久有效。
DeliveryLinkTTLMinutes int
// FulfillmentProcessingTimeoutMinutes 发货中订单超过该分钟数自动标记失败;<=0 表示关闭。
FulfillmentProcessingTimeoutMinutes int
// FulfillmentTimeoutScanIntervalSeconds 发货超时巡检间隔秒数。
FulfillmentTimeoutScanIntervalSeconds int
// CallbackMaxAttempts 回调推送总尝试次数(首次 + 重试),默认 16(微信式)。
CallbackMaxAttempts int
// CallbackPushTimeoutSeconds 单次回调推送 HTTP 超时秒数。
CallbackPushTimeoutSeconds int
// CallbackRetryScheduleSeconds 回调失败重试间隔序列(秒,逗号分隔,微信/支付宝式固定退避)。
// 为空时使用默认微信式序列。
CallbackRetryScheduleSeconds []int
// UploadDir 凭证等上传文件的存储目录(默认 data/uploads,通过 /uploads 公开访问)。
UploadDir string
// CORSAllowedOrigins 跨域允许的来源(逗号分隔);为空时仅放行常见本地开发端口。
// 生产前后端同域(Caddy 统一入口)时无需配置;仅在前后端分离部署时设置。
CORSAllowedOrigins []string
}
// Load 加载配置:先尝试读取 .env,再读系统环境变量(已存在的系统环境变量优先级更高)
func Load() *Config {
loadDotEnv()
mode := getEnv("GIN_MODE", "debug")
// OPEN_API_DEBUG 优先;未设置时 debug 模式默认开启
debugOpen := getEnvBool("OPEN_API_DEBUG", mode == "debug" || mode == "")
return &Config{
Port: getEnv("PORT", "18080"),
JWTSecret: getEnv("JWT_SECRET", "affiliate-dash-dev-secret-change-me"),
DatabaseURL: getEnv("DATABASE_URL", "postgres://affiliate:affiliate_dev_password@127.0.0.1:15432/affiliate_dash?sslmode=disable&TimeZone=Asia/Shanghai"),
Mode: mode,
DataEncryptionKey: getEnv("DATA_ENCRYPTION_KEY", getEnv("JWT_SECRET", "affiliate-dash-dev-secret-change-me")),
OpenAPIKey: getEnv("OPEN_API_KEY", "sk_source_dev_key_change_me"),
OpenAPISecret: getEnv("OPEN_API_SECRET", "sk_source_dev_secret_change_me"),
OpenSignSkew: int64(getEnvInt("OPEN_SIGN_SKEW", 300)),
OpenAPIDebug: debugOpen,
LogFile: getEnv("LOG_FILE", "logs/app.log"),
LogRetainDays: getEnvInt("LOG_RETAIN_DAYS", 30),
DeliveryBaseURL: getEnv("DELIVERY_BASE_URL", ""),
DeliveryBFFBaseURL: getEnv("DELIVERY_BFF_BASE_URL", "https://www.jxya.top/bff"),
DeliveryChannel: getEnv("DELIVERY_CHANNEL", "dlc"),
DeliveryLinkSecret: getEnv("DELIVERY_LINK_SECRET", getEnv("OPEN_API_SECRET", getEnv("JWT_SECRET", "affiliate-dash-dev-secret-change-me"))),
DeliveryLinkTTLMinutes: getEnvInt("DELIVERY_LINK_TTL_MINUTES", 0),
FulfillmentProcessingTimeoutMinutes: getEnvInt("FULFILLMENT_PROCESSING_TIMEOUT_MINUTES", 30),
FulfillmentTimeoutScanIntervalSeconds: getEnvInt("FULFILLMENT_TIMEOUT_SCAN_INTERVAL_SECONDS", 60),
CallbackMaxAttempts: getEnvInt("CALLBACK_MAX_ATTEMPTS", 16),
CallbackPushTimeoutSeconds: getEnvInt("CALLBACK_PUSH_TIMEOUT_SECONDS", 15),
CallbackRetryScheduleSeconds: getEnvIntList("CALLBACK_RETRY_SCHEDULE", nil),
UploadDir: getEnv("UPLOAD_DIR", "data/uploads"),
CORSAllowedOrigins: getEnvList("CORS_ALLOWED_ORIGINS", nil),
}
}
// getEnvList 解析逗号分隔的字符串列表(如 CORS 来源);空串返回默认值。
func getEnvList(key string, def []string) []string {
v := os.Getenv(key)
if v == "" {
return def
}
var out []string
for _, part := range strings.Split(v, ",") {
if s := strings.TrimSpace(part); s != "" {
out = append(out, s)
}
}
if len(out) == 0 {
return def
}
return out
}
// getEnvIntList 解析逗号分隔的整数列表(如 "15,15,30,180");空串返回 nil。
func getEnvIntList(key string, def []int) []int {
v := os.Getenv(key)
if v == "" {
return def
}
var out []int
for _, part := range strings.Split(v, ",") {
n, err := strconv.Atoi(strings.TrimSpace(part))
if err != nil || n < 1 {
continue
}
out = append(out, n)
}
if len(out) == 0 {
return def
}
return out
}
func getEnvBool(key string, def bool) bool {
v := os.Getenv(key)
if v == "" {
return def
}
switch strings.ToLower(strings.TrimSpace(v)) {
case "1", "true", "yes", "on":
return true
case "0", "false", "no", "off":
return false
default:
return def
}
}
// loadDotEnv 依次尝试常见路径,不覆盖已有环境变量
func loadDotEnv() {
var paths []string
if wd, err := os.Getwd(); err == nil {
paths = append(paths,
filepath.Join(wd, ".env"),
filepath.Join(wd, "..", ".env"), // 在 backend/ 下启动时读仓库根 .env
filepath.Join(wd, "backend", ".env"), // 在仓库根启动时
)
}
paths = append(paths, ".env")
seen := map[string]struct{}{}
for _, p := range paths {
abs, err := filepath.Abs(p)
if err != nil {
continue
}
if _, ok := seen[abs]; ok {
continue
}
seen[abs] = struct{}{}
_ = godotenv.Load(abs) // 忽略不存在;不覆盖已在环境中的变量
}
}
func getEnv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func getEnvInt(key string, def int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}