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
This commit is contained in:
@@ -22,6 +22,7 @@ func main() {
|
||||
db := initDB()
|
||||
autoMigrate(db)
|
||||
seed(db)
|
||||
seedWechat(db)
|
||||
|
||||
reg := channel.NewRegistry(db)
|
||||
|
||||
@@ -39,6 +40,9 @@ func main() {
|
||||
log.Printf("查单兜底已启动:每 %ds 一次", config.C.QuerySync.IntervalSec)
|
||||
}
|
||||
|
||||
// 业务回调重试兜底(webhook 丢失/业务方短暂不可用时补推)。
|
||||
orderSvc.StartBizNotifyRetry(60 * time.Second)
|
||||
|
||||
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 {
|
||||
@@ -75,6 +79,7 @@ func autoMigrate(db *gorm.DB) {
|
||||
&model.Product{},
|
||||
&model.Order{},
|
||||
&model.NotifyLog{},
|
||||
&model.BizNotifyLog{},
|
||||
); err != nil {
|
||||
log.Fatalf("自动迁移失败: %v", err)
|
||||
}
|
||||
@@ -121,4 +126,84 @@ func seed(db *gorm.DB) {
|
||||
}
|
||||
log.Printf("[seed] 已为商户 %s 创建 %d 个测试套餐", m.Code, len(samples))
|
||||
}
|
||||
|
||||
seedPlans(db, m.ID)
|
||||
}
|
||||
|
||||
// seedPlans 幂等 upsert 业务套餐(带 biz_code,供 jiu 等业务系统按 biz_code 查 product)。
|
||||
// 价格权威在此表;权益(时长/档位/设备/功能)由业务方按 biz_code 映射。
|
||||
func seedPlans(db *gorm.DB, merchantID uint64) {
|
||||
plans := []model.Product{
|
||||
{Name: "联调测试套餐", Description: "1 分钱端到端联调用(标准权益)", Price: "0.01", BizCode: "test_liandiao", Sort: 0},
|
||||
{Name: "限时优惠 · 1元首月体验", Description: "标准权益 · 30 天 · 每门店限一次(限次由 jiu 侧校验)", Price: "1.00", BizCode: "promo_first_month", Sort: 1},
|
||||
{Name: "月付 · 标准", Description: "单店/单仓库 · 2 客户端 · 千张图片分享", Price: "299.00", BizCode: "monthly_standard", Sort: 2},
|
||||
{Name: "月付 · 高级", Description: "单店/多仓库 · 5 客户端 · 万张照片 · 免费 AI 周/月度商业分析", Price: "599.00", BizCode: "monthly_pro", Sort: 3},
|
||||
{Name: "年付 · 标准", Description: "单店/单仓库 · 2 客户端 · 千张图片分享", Price: "2999.00", BizCode: "annual_standard", Sort: 4},
|
||||
{Name: "年付 · 高级", Description: "单店/多仓库 · 5 客户端 · 万张照片 · 免费 AI 周/月度商业分析", Price: "5999.00", BizCode: "annual_pro", Sort: 5},
|
||||
}
|
||||
for _, p := range plans {
|
||||
var existing model.Product
|
||||
err := db.Where("biz_code = ?", p.BizCode).First(&existing).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
p.MerchantID = merchantID
|
||||
p.Active = true
|
||||
if err := db.Create(&p).Error; err != nil {
|
||||
log.Fatalf("[seed] 创建套餐 %s 失败: %v", p.BizCode, err)
|
||||
}
|
||||
continue
|
||||
} else if err != nil {
|
||||
log.Fatalf("[seed] 查询套餐 %s 失败: %v", p.BizCode, err)
|
||||
}
|
||||
// 已存在:更新价格/文案/归属(价格权威可从此改)。
|
||||
db.Model(&existing).Updates(map[string]any{
|
||||
"merchant_id": merchantID, "name": p.Name, "description": p.Description,
|
||||
"price": p.Price, "active": true, "sort": p.Sort,
|
||||
})
|
||||
}
|
||||
log.Printf("[seed] 业务套餐就绪:%d 个(含联调 0.01)", len(plans))
|
||||
}
|
||||
|
||||
// seedWechat 据 config 的 wechat upsert 一个微信商户(V3 Native),并为其补两个测试套餐。
|
||||
// 微信无沙箱:wechat.enabled=true 即视为正式(Production=true),只能用真实商户号做 1 分钱实测。
|
||||
func seedWechat(db *gorm.DB) {
|
||||
wc := config.C.Wechat
|
||||
if !wc.Enabled {
|
||||
log.Println("[seed] wechat.enabled=false,跳过微信商户初始化")
|
||||
return
|
||||
}
|
||||
var m model.Merchant
|
||||
err := db.Where("code = ?", wc.MerchantCode).First(&m).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
m = model.Merchant{
|
||||
Code: wc.MerchantCode,
|
||||
Name: wc.MerchantName,
|
||||
Channel: "wechat",
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Fatalf("[seed] 查询微信商户失败: %v", err)
|
||||
}
|
||||
m.Production = true // 微信无沙箱,一律正式网关
|
||||
m.Enabled = true
|
||||
m.MchID = wc.MchID
|
||||
m.WxAppID = wc.AppID
|
||||
m.CertSerial = wc.CertSerial
|
||||
m.WxPrivateKey = wc.PrivateKey
|
||||
m.APIv3Key = wc.APIv3Key
|
||||
if err := db.Save(&m).Error; err != nil {
|
||||
log.Fatalf("[seed] 保存微信商户失败: %v", err)
|
||||
}
|
||||
log.Printf("[seed] 微信商户就绪: code=%s mch_id=%s", m.Code, m.MchID)
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user