Files
pay/config/config.go
wangjia fd5f56f7de feat: 微信V3 + 支付宝手机站/UA自适应 + 业务对接(签名下单/webhook) + 通用多业务
- 微信支付 V3 Native 渠道 (wechat.go):Native下单/回调AES-GCM解密验签/查单
- 支付宝:手机网站支付 wap.pay + 按 UA 自适应(PC page.pay扫码 / 手机拉App);qr_pay_mode=2 完整扫码收银台
- 业务对接:下单接口扩展(biz_system/biz_ref/return_url)+ HMAC 签名鉴权;支付成功 webhook 主动推送业务方 + 60s 重试 + BizNotifyLog
- 通用多业务:config.biz 改 map,加业务只改配置(BIZ_<SYS>_SECRET/_CALLBACK_URL)
- seedPlans:四档真实套餐 + promo_first_month(¥1) + test_liandiao(0.01),均挂 biz_code;/products 暴露 biz_code
- 删除沙箱 pay.html;对接设计文档入 docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UQmqWmV67sXGLrb3U1XXn
2026-07-03 22:44:34 +08:00

133 lines
5.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 (
"log"
"os"
"strings"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
AlipaySandbox AlipaySandboxConfig `mapstructure:"alipay_sandbox"`
Wechat WechatConfig `mapstructure:"wechat"`
QuerySync QuerySyncConfig `mapstructure:"query_sync"`
// Biz 通用:任意业务系统(jiu / dudu / …)在 config 的 biz.<name> 下声明即可接入,无需改代码。
Biz map[string]BizSystemConfig `mapstructure:"biz"`
}
type ServerConfig struct {
Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"` // debug | release
BaseURL string `mapstructure:"base_url"` // 拼 notify_url / return_url 的公网根地址;沙箱回调需公网可达(内网穿透)
}
type DatabaseConfig struct {
Driver string `mapstructure:"driver"` // sqlite | mysql
DSN string `mapstructure:"dsn"` // sqlite 为文件路径;mysql 为完整 DSN
}
// AlipaySandboxConfig 启动时据此 upsert 一个支付宝(沙箱)商户,方便开箱联调。
// 生产/多商户请改为通过管理接口或 seed 写入 merchants 表。
type AlipaySandboxConfig struct {
Enabled bool `mapstructure:"enabled"`
Production bool `mapstructure:"production"` // false=沙箱网关;true=正式网关(真钱)
MerchantCode string `mapstructure:"merchant_code"` // 业务标识,如 yanmei
MerchantName string `mapstructure:"merchant_name"`
AppID string `mapstructure:"app_id"`
AppPrivateKey string `mapstructure:"app_private_key"` // 应用私钥(PKCS1/PKCS8 皆可,无 PEM 头亦可)
AlipayPublicKey string `mapstructure:"alipay_public_key"` // 支付宝公钥(公钥模式)
}
// WechatConfig 启动时据此 upsert 一个微信商户(V3 Native)。
// 密钥(商户私钥 / APIv3 密钥)严禁写进 config.yaml,走环境变量(生产由 Bitwarden 经 rbw 灌入)。
type WechatConfig struct {
Enabled bool `mapstructure:"enabled"`
MerchantCode string `mapstructure:"merchant_code"` // 业务标识,如 yanmei-wx(与支付宝商户区分)
MerchantName string `mapstructure:"merchant_name"`
MchID string `mapstructure:"mch_id"` // 微信支付商户号
AppID string `mapstructure:"app_id"` // 绑定的 APPID(公众号/小程序/移动应用;Native 可用商户号绑定的任一)
CertSerial string `mapstructure:"cert_serial"` // 商户 API 证书序列号
PrivateKey string `mapstructure:"private_key"` // 商户 API 私钥 apiclient_key.pem(走环境变量)
APIv3Key string `mapstructure:"apiv3_key"` // APIv3 密钥(回调解密,走环境变量)
}
// BizSystemConfig 单个业务系统的对接配置。secret 双向共用(校验其下单请求签名 + 给回调 payload 签名)。
// 密钥/回调按约定从环境变量注入:BIZ_<SYSTEM>_SECRET / BIZ_<SYSTEM>_CALLBACK_URL,严禁写进 config.yaml。
type BizSystemConfig struct {
CallbackURL string `mapstructure:"callback_url"` // 支付成功回调业务方接收器地址
Secret string `mapstructure:"secret"` // HMAC 共享密钥
}
// BizByName 按业务系统名取配置(供下单鉴权 / webhook 回调用)。未声明或无密钥则视为未接入。
func (c *Config) BizByName(system string) (BizSystemConfig, bool) {
cfg, ok := c.Biz[system]
if !ok || cfg.Secret == "" {
return BizSystemConfig{}, false
}
return cfg, true
}
// QuerySyncConfig 兜底主动查单:定时把待支付订单拿去 alipay.trade.query 核对,防回调丢失。
type QuerySyncConfig struct {
Enabled bool `mapstructure:"enabled"`
IntervalSec int `mapstructure:"interval_sec"` // 轮询间隔(秒)
MaxAgeMin int `mapstructure:"max_age_min"` // 只查创建时间在该分钟数内的待支付单
}
var C Config
func Load() {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("./config")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
// 敏感项支持环境变量覆盖(部署时不写进 config.yaml
_ = viper.BindEnv("alipay_sandbox.app_private_key", "ALIPAY_APP_PRIVATE_KEY")
_ = viper.BindEnv("alipay_sandbox.alipay_public_key", "ALIPAY_PUBLIC_KEY")
_ = viper.BindEnv("wechat.private_key", "WECHAT_MCH_PRIVATE_KEY")
_ = viper.BindEnv("wechat.apiv3_key", "WECHAT_APIV3_KEY")
_ = viper.BindEnv("database.dsn", "DATABASE_DSN")
viper.SetDefault("server.port", "8080")
viper.SetDefault("server.mode", "debug")
viper.SetDefault("server.base_url", "http://localhost:8080")
viper.SetDefault("database.driver", "sqlite")
viper.SetDefault("database.dsn", "pay.db")
viper.SetDefault("alipay_sandbox.enabled", false)
viper.SetDefault("alipay_sandbox.merchant_code", "yanmei")
viper.SetDefault("alipay_sandbox.merchant_name", "演么测试商户")
viper.SetDefault("wechat.enabled", false)
viper.SetDefault("wechat.merchant_code", "yanmei-wx")
viper.SetDefault("wechat.merchant_name", "岩美(北京)技术有限公司")
viper.SetDefault("query_sync.enabled", true)
viper.SetDefault("query_sync.interval_sec", 30)
viper.SetDefault("query_sync.max_age_min", 30)
if err := viper.ReadInConfig(); err != nil {
log.Println("[config] 未找到 config.yaml,使用默认值 + 环境变量")
}
if err := viper.Unmarshal(&C); err != nil {
log.Fatalf("[config] 解析配置失败: %v", err)
}
// 各业务系统密钥/回调按约定从环境变量注入(不写进 config.yaml):
// BIZ_<SYSTEM>_SECRET / BIZ_<SYSTEM>_CALLBACK_URLSYSTEM 为大写业务名,如 BIZ_JIU_SECRET)。
for name, bc := range C.Biz {
up := strings.ToUpper(name)
if bc.Secret == "" {
bc.Secret = os.Getenv("BIZ_" + up + "_SECRET")
}
if bc.CallbackURL == "" {
bc.CallbackURL = os.Getenv("BIZ_" + up + "_CALLBACK_URL")
}
C.Biz[name] = bc
}
}