feat(backend): 在线购买/续费对接 pay 收款中枢——下单/webhook 验签入账/续期叠加/查单兜底
- POST /license/purchase(仅管理员)建单并调 pay 下单,返回收银台 pay_url - POST /pay/callback 公开接收器:HMAC 验签+时间戳窗口+按 out_trade_no 幂等+金额逐分核对,同事务续期 - 续期与兑换券同口径:未过期从到期日叠加、已过期从现在起算,写入 tier/max_devices/features - 后台每 60s 查单兜底防 webhook 丢失;closed 标 failed - 新表 license_purchases(schema.sql/AutoMigrate/testutil 同步);配置 PAY_SECRET/PAY_BASE_URL/PAY_RETURN_URL - 契约真相源 ~/code/pay-contract openapi.yaml v1.0.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -14,6 +14,16 @@ type Config struct {
|
||||
Storage StorageConfig
|
||||
Session SessionConfig
|
||||
RateLimit RateLimitConfig
|
||||
Pay PayConfig
|
||||
}
|
||||
|
||||
// PayConfig pay 收款中枢对接(契约:~/code/pay-contract openapi.yaml v1.0.0)。
|
||||
// Secret 为 jiu↔pay 双向 HMAC 共享密钥(pay 侧 BIZ_JIU_SECRET 同值),只经环境变量注入;
|
||||
// 为空时购买接口返回 503,webhook 一律拒绝。
|
||||
type PayConfig struct {
|
||||
BaseURL string `mapstructure:"base_url"` // pay 服务地址
|
||||
Secret string `mapstructure:"secret"` // HMAC 共享密钥(PAY_SECRET)
|
||||
ReturnURL string `mapstructure:"return_url"` // 支付完成回跳页(pay 会拼 out_trade_no)
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -86,6 +96,9 @@ func Load() {
|
||||
_ = viper.BindEnv("storage.base_url", "STORAGE_BASE_URL")
|
||||
_ = viper.BindEnv("storage.public_url", "STORAGE_PUBLIC_URL")
|
||||
_ = viper.BindEnv("storage.web_dir", "STORAGE_WEB_DIR")
|
||||
_ = viper.BindEnv("pay.base_url", "PAY_BASE_URL")
|
||||
_ = viper.BindEnv("pay.secret", "PAY_SECRET")
|
||||
_ = viper.BindEnv("pay.return_url", "PAY_RETURN_URL")
|
||||
|
||||
// 默认值
|
||||
viper.SetDefault("server.port", "8080")
|
||||
@@ -116,6 +129,8 @@ func Load() {
|
||||
viper.SetDefault("storage.base_url", "http://localhost:8080/images")
|
||||
viper.SetDefault("storage.public_url", "http://localhost:8081")
|
||||
viper.SetDefault("storage.web_dir", "./web")
|
||||
viper.SetDefault("pay.base_url", "https://pay.51yanmei.com")
|
||||
viper.SetDefault("pay.return_url", "https://jiu.51yanmei.com/license/result/")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Println("[config] no config file found, using defaults and env vars")
|
||||
|
||||
Reference in New Issue
Block a user