初始提交:岩美 pay 收款服务(支付宝当面付 + 多商户多渠道架构)

- Go/Gin/GORM + 纯 Go SQLite(无 cgo)
- Channel 多渠道接口:支付宝当面付(precreate)/电脑网站支付(page.pay) 已实现,微信占位
- 多商户 merchants 表,回调验签+金额核对+幂等+查单兜底
- 收款页/结果页/二维码端点;docs/ 设计文档与部署 Runbook
- 密钥走环境变量/Bitwarden,不入库

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-25 07:21:48 +08:00
commit 6da6964451
29 changed files with 2205 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
package config
import (
"log"
"strings"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
AlipaySandbox AlipaySandboxConfig `mapstructure:"alipay_sandbox"`
QuerySync QuerySyncConfig `mapstructure:"query_sync"`
}
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"` // 支付宝公钥(公钥模式)
}
// 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("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("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)
}
}
+32
View File
@@ -0,0 +1,32 @@
# 支付服务配置(模板)
# ⚠️ 密钥严禁写进本文件!一律走环境变量(生产由 Bitwarden 经 rbw 灌入):
# ALIPAY_APP_PRIVATE_KEY / ALIPAY_PUBLIC_KEY
server:
port: "8080"
mode: "debug" # debug | release
# 拼 notify_url / return_url 的根地址。
# 本机只测「下单→跳收银台」用 http://localhost:8080 即可;
# 要测异步回调 notify 需公网可达地址(内网穿透,或部署到服务器用 IP/域名)。
base_url: "http://localhost:8080"
database:
driver: "sqlite" # sqlite(默认,纯 Go 无需 gcc | mysql
dsn: "pay.db" # sqlite 文件路径;mysql 时填 user:pass@tcp(host:3306)/db?...
# 启动时据此 upsert 一个支付宝商户。把 enabled 改 true、填 app_id,密钥走环境变量。
# 沙箱信息:https://open.alipay.com/develop/sandbox/app
alipay_sandbox:
enabled: false
production: false # false=沙箱网关;true=正式网关(真钱)
merchant_code: "yanmei"
merchant_name: "岩美(北京)技术有限公司"
app_id: "" # 沙箱/生产 APPID
app_private_key: "" # 留空!走环境变量 ALIPAY_APP_PRIVATE_KEY
alipay_public_key: "" # 留空!走环境变量 ALIPAY_PUBLIC_KEY
# 兜底主动查单:定时把待支付订单拿去查,防异步回调丢失
query_sync:
enabled: true
interval_sec: 30
max_age_min: 30