6da6964451
- Go/Gin/GORM + 纯 Go SQLite(无 cgo) - Channel 多渠道接口:支付宝当面付(precreate)/电脑网站支付(page.pay) 已实现,微信占位 - 多商户 merchants 表,回调验签+金额核对+幂等+查单兜底 - 收款页/结果页/二维码端点;docs/ 设计文档与部署 Runbook - 密钥走环境变量/Bitwarden,不入库 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
125 lines
3.3 KiB
Go
125 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"github.com/wangjia/pay/config"
|
|
"github.com/wangjia/pay/internal/channel"
|
|
"github.com/wangjia/pay/internal/model"
|
|
"github.com/wangjia/pay/internal/router"
|
|
)
|
|
|
|
func main() {
|
|
config.Load()
|
|
|
|
db := initDB()
|
|
autoMigrate(db)
|
|
seed(db)
|
|
|
|
reg := channel.NewRegistry(db)
|
|
|
|
gin.SetMode(config.C.Server.Mode)
|
|
r := gin.New()
|
|
r.Use(gin.Logger(), gin.Recovery())
|
|
|
|
orderSvc := router.Setup(r, db, reg)
|
|
|
|
if config.C.QuerySync.Enabled {
|
|
orderSvc.StartQuerySync(
|
|
time.Duration(config.C.QuerySync.IntervalSec)*time.Second,
|
|
time.Duration(config.C.QuerySync.MaxAgeMin)*time.Minute,
|
|
)
|
|
log.Printf("查单兜底已启动:每 %ds 一次", config.C.QuerySync.IntervalSec)
|
|
}
|
|
|
|
addr := ":" + config.C.Server.Port
|
|
log.Printf("支付服务启动 %s (mode=%s, base_url=%s)", addr, config.C.Server.Mode, config.C.Server.BaseURL)
|
|
if err := r.Run(addr); err != nil {
|
|
log.Fatalf("启动失败: %v", err)
|
|
}
|
|
}
|
|
|
|
func initDB() *gorm.DB {
|
|
logLevel := logger.Silent
|
|
if config.C.Server.Mode == "debug" {
|
|
logLevel = logger.Info
|
|
}
|
|
gormCfg := &gorm.Config{Logger: logger.Default.LogMode(logLevel), TranslateError: true}
|
|
|
|
switch config.C.Database.Driver {
|
|
case "sqlite", "":
|
|
db, err := gorm.Open(sqlite.Open(config.C.Database.DSN), gormCfg)
|
|
if err != nil {
|
|
log.Fatalf("连接 sqlite 失败: %v", err)
|
|
}
|
|
return db
|
|
case "mysql":
|
|
log.Fatal("mysql 驱动尚未启用:go get gorm.io/driver/mysql 后在 initDB 接入")
|
|
return nil
|
|
default:
|
|
log.Fatalf("未知 database.driver: %s", config.C.Database.Driver)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func autoMigrate(db *gorm.DB) {
|
|
if err := db.AutoMigrate(
|
|
&model.Merchant{},
|
|
&model.Product{},
|
|
&model.Order{},
|
|
&model.NotifyLog{},
|
|
); err != nil {
|
|
log.Fatalf("自动迁移失败: %v", err)
|
|
}
|
|
log.Println("AutoMigrate 完成")
|
|
}
|
|
|
|
// seed 据 config 的 alipay_sandbox upsert 一个支付宝商户,并为其补两个测试套餐。
|
|
func seed(db *gorm.DB) {
|
|
sb := config.C.AlipaySandbox
|
|
if !sb.Enabled {
|
|
log.Println("[seed] alipay_sandbox.enabled=false,跳过沙箱商户初始化")
|
|
return
|
|
}
|
|
var m model.Merchant
|
|
err := db.Where("code = ?", sb.MerchantCode).First(&m).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
m = model.Merchant{
|
|
Code: sb.MerchantCode,
|
|
Name: sb.MerchantName,
|
|
Channel: "alipay",
|
|
}
|
|
} else if err != nil {
|
|
log.Fatalf("[seed] 查询商户失败: %v", err)
|
|
}
|
|
m.Production = sb.Production
|
|
m.Enabled = true
|
|
m.AppID = sb.AppID
|
|
m.AppPrivateKey = sb.AppPrivateKey
|
|
m.AlipayPublicKey = sb.AlipayPublicKey
|
|
if err := db.Save(&m).Error; err != nil {
|
|
log.Fatalf("[seed] 保存商户失败: %v", err)
|
|
}
|
|
log.Printf("[seed] 支付宝沙箱商户就绪: code=%s app_id=%s", m.Code, m.AppID)
|
|
|
|
var cnt int64
|
|
db.Model(&model.Product{}).Where("merchant_id = ?", m.ID).Count(&cnt)
|
|
if cnt == 0 {
|
|
samples := []model.Product{
|
|
{MerchantID: m.ID, Name: "测试套餐 A", Description: "沙箱联调用", Price: "0.01", Active: true, Sort: 1},
|
|
{MerchantID: m.ID, Name: "测试套餐 B", Description: "沙箱联调用", Price: "0.02", Active: true, Sort: 2},
|
|
}
|
|
if err := db.Create(&samples).Error; err != nil {
|
|
log.Fatalf("[seed] 创建测试套餐失败: %v", err)
|
|
}
|
|
log.Printf("[seed] 已为商户 %s 创建 %d 个测试套餐", m.Code, len(samples))
|
|
}
|
|
}
|