🎨 新增支付动态配置&优化支付回调&新增三方支付

This commit is contained in:
2025-12-09 21:47:22 +08:00
parent eaa5cdc100
commit 854c16e11c
26 changed files with 2963 additions and 72 deletions

3
.gitignore vendored
View File

@@ -22,4 +22,5 @@ log
# Go workspace file
go.work
.kiro
.vscode
/test

View File

@@ -5,6 +5,8 @@ import (
"git.echol.cn/loser/lckt/model/app"
"git.echol.cn/loser/lckt/model/app/request"
r "git.echol.cn/loser/lckt/model/common/response"
"git.echol.cn/loser/lckt/model/system"
"git.echol.cn/loser/lckt/utils/pay"
"git.echol.cn/loser/lckt/utils/user_jwt"
"git.echol.cn/loser/lckt/utils/wechat"
"github.com/gin-gonic/gin"
@@ -13,6 +15,57 @@ import (
type OrderApi struct{}
// GetPayMethods 获取可用的支付方式列表(用户端)
// @Summary 获取可用的支付方式列表
// @Description 获取所有启用的支付配置返回前端所需的mode字段
// @Tags 订单
// @Accept application/json
// @Produce application/json
// @Success 200 {object} r.Response{data=[]PayMethodVO} "获取成功"
// @Router /app_order/pay_methods [get]
func (o *OrderApi) GetPayMethods(c *gin.Context) {
var payConfigs []system.PayConfig
if err := global.GVA_DB.Where("enable = ?", true).
Order("sort ASC").
Find(&payConfigs).Error; err != nil {
global.GVA_LOG.Error("获取支付方式列表失败", zap.Error(err))
r.FailWithMessage("获取失败", c)
return
}
// 返回前端所需的支付方式信息
type PayMethodVO struct {
Name string `json:"name"` // 支付名称,如"神奇支付-主账号"
Code string `json:"code"` // 支付配置编码,如"shenqi_main"
Type string `json:"type"` // 支付类型: wechat/shenqi
Modes []string `json:"modes"` // 可用的支付模式
Remark string `json:"remark"` // 备注说明
}
var methods []PayMethodVO
for _, config := range payConfigs {
// 使用 GetAvailableModes 方法获取实际启用的模式
availableModes := config.GetAvailableModes()
// 如果没有可用模式,跳过该配置
if len(availableModes) == 0 {
continue
}
method := PayMethodVO{
Name: config.Name,
Code: config.Code,
Type: config.Type,
Modes: availableModes,
Remark: config.Remark,
}
methods = append(methods, method)
}
r.OkWithData(methods, c)
}
// CreateOrder APP新建订单
func (o *OrderApi) CreateOrder(c *gin.Context) {
var p app.Order
@@ -62,6 +115,83 @@ func (o *OrderApi) NotifyOrder(context *gin.Context) {
r.OkWithMessage("微信支付回调处理成功", context)
}
// ShenqiPayNotify 神奇支付回调通知
// @Summary 神奇支付回调通知
// @Description 处理神奇支付平台的支付结果通知
// @Tags 订单
// @Accept x-www-form-urlencoded
// @Produce plain
// @Param code path string false "支付配置编码"
// @Success 200 {string} string "success"
// @Failure 400 {string} string "fail"
// @Router /app_order/shenqi/notify/{code} [get]
func (o *OrderApi) ShenqiPayNotify(c *gin.Context) {
// 获取支付配置编码
payCode := c.Param("code")
var client *pay.Client
var err error
if payCode != "" {
// 从数据库获取配置
var config system.PayConfig
if err := global.GVA_DB.Where("code = ? AND enable = ?", payCode, true).First(&config).Error; err != nil {
global.GVA_LOG.Error("获取神奇支付配置失败", zap.String("code", payCode), zap.Error(err))
c.String(400, "fail")
return
}
// 验证支付类型
if config.Type != "shenqi" {
global.GVA_LOG.Error("支付配置类型错误", zap.String("type", config.Type))
c.String(400, "fail")
return
}
// 获取神奇支付配置
shenqiConfig := config.GetShenqiConfig()
client, err = pay.NewClient(pay.Config{
PID: shenqiConfig.PID,
PrivateKey: shenqiConfig.PrivateKey,
PlatformPubKey: shenqiConfig.PlatformPubKey,
BaseURL: shenqiConfig.BaseURL,
})
} else {
// 兼容旧配置:从配置文件读取
if !global.GVA_CONFIG.ShenqiPay.Enable {
global.GVA_LOG.Error("神奇支付未启用")
c.String(400, "fail")
return
}
client, err = pay.NewClient(pay.Config{
PID: global.GVA_CONFIG.ShenqiPay.PID,
PrivateKey: global.GVA_CONFIG.ShenqiPay.PrivateKey,
PlatformPubKey: global.GVA_CONFIG.ShenqiPay.PlatformPubKey,
BaseURL: global.GVA_CONFIG.ShenqiPay.BaseURL,
})
}
if err != nil {
global.GVA_LOG.Error("创建神奇支付客户端失败", zap.Error(err))
c.String(400, "fail")
return
}
// 创建回调处理器并处理通知
handler := pay.NewNotifyHandler(client)
_, err = handler.HandleNotify(c.Request.URL.Query())
if err != nil {
global.GVA_LOG.Error("神奇支付回调处理失败", zap.Error(err))
c.String(400, "fail")
return
}
// 返回 success 告知支付平台处理成功
c.String(200, "success")
}
// GetOrderDetail 获取订单详情
func (o *OrderApi) GetOrderDetail(context *gin.Context) {
id := context.Param("id")

View File

@@ -3,13 +3,14 @@ package app
import (
"errors"
"fmt"
common "git.echol.cn/loser/lckt/model/common/request"
user2 "git.echol.cn/loser/lckt/model/user"
"gorm.io/gorm"
"net/http"
"strconv"
"time"
common "git.echol.cn/loser/lckt/model/common/request"
user2 "git.echol.cn/loser/lckt/model/user"
"gorm.io/gorm"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/app"
r "git.echol.cn/loser/lckt/model/common/response"
@@ -269,19 +270,6 @@ func (*AppUserApi) PwdLogin(ctx *gin.Context) {
}
}()
adcodes := utils.CheckIPInAdcodes(loginLog.Address)
if !adcodes {
global.GVA_LOG.Warn("异常登录地址", zap.String("address", loginLog.Address), zap.Uint("userId", user.ID))
user.Status = 0
if err := global.GVA_DB.Save(&user).Error; err != nil {
global.GVA_LOG.Error("禁用用户失败!", zap.Error(err))
}
r.Banned("用户已被禁用", ctx)
return
}
// 生成token
token, claims, err := user_jwt.LoginToken(user)
if err != nil {

View File

@@ -23,6 +23,7 @@ type ApiGroup struct {
AutoCodeTemplateApi
SysParamsApi
SysVersionApi
PayConfigApi
}
var (
@@ -46,4 +47,5 @@ var (
autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory
autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate
sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService
payConfigService = service.ServiceGroupApp.SystemServiceGroup.PayConfigService
)

View File

@@ -0,0 +1,204 @@
package system
import (
"strconv"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/common/response"
"git.echol.cn/loser/lckt/model/system/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type PayConfigApi struct{}
// GetPayConfigList 获取支付配置列表
// @Tags PayConfig
// @Summary 获取支付配置列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data query request.PayConfigSearch true "分页获取支付配置列表"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
// @Router /payConfig/list [get]
func (p *PayConfigApi) GetPayConfigList(c *gin.Context) {
var req request.PayConfigSearch
if err := c.ShouldBindQuery(&req); err != nil {
response.FailWithMessage("参数错误: "+err.Error(), c)
return
}
// 处理 enable 参数:如果参数不存在或为空字符串,设置为 nil表示不筛选
enableParam := c.Query("enable")
if enableParam == "" {
req.Enable = nil
}
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 10
}
list, total, err := payConfigService.GetPayConfigList(req)
if err != nil {
global.GVA_LOG.Error("获取支付配置列表失败", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithDetailed(response.PageResult{
List: list,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, "获取成功", c)
}
// GetPayConfigByID 根据ID获取支付配置
// @Tags PayConfig
// @Summary 根据ID获取支付配置
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param id path int true "支付配置ID"
// @Success 200 {object} response.Response{data=system.PayConfig,msg=string} "获取成功"
// @Router /payConfig/{id} [get]
func (p *PayConfigApi) GetPayConfigByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
response.FailWithMessage("参数错误", c)
return
}
config, err := payConfigService.GetPayConfigByID(uint(id))
if err != nil {
global.GVA_LOG.Error("获取支付配置失败", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(config, c)
}
// GetEnabledPayConfigs 获取启用的支付配置列表
// @Tags PayConfig
// @Summary 获取启用的支付配置列表
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=[]system.PayConfig,msg=string} "获取成功"
// @Router /payConfig/enabled [get]
func (p *PayConfigApi) GetEnabledPayConfigs(c *gin.Context) {
list, err := payConfigService.GetEnabledPayConfigs()
if err != nil {
global.GVA_LOG.Error("获取启用的支付配置失败", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c)
return
}
response.OkWithData(list, c)
}
// CreatePayConfig 创建支付配置
// @Tags PayConfig
// @Summary 创建支付配置
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.PayConfigCreate true "创建支付配置"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /payConfig [post]
func (p *PayConfigApi) CreatePayConfig(c *gin.Context) {
var req request.PayConfigCreate
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage("参数错误: "+err.Error(), c)
return
}
if err := payConfigService.CreatePayConfig(req); err != nil {
global.GVA_LOG.Error("创建支付配置失败", zap.Error(err))
response.FailWithMessage("创建失败: "+err.Error(), c)
return
}
response.OkWithMessage("创建成功", c)
}
// UpdatePayConfig 更新支付配置
// @Tags PayConfig
// @Summary 更新支付配置
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.PayConfigUpdate true "更新支付配置"
// @Success 200 {object} response.Response{msg=string} "更新成功"
// @Router /payConfig [put]
func (p *PayConfigApi) UpdatePayConfig(c *gin.Context) {
var req request.PayConfigUpdate
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage("参数错误: "+err.Error(), c)
return
}
if err := payConfigService.UpdatePayConfig(req); err != nil {
global.GVA_LOG.Error("更新支付配置失败", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// DeletePayConfig 删除支付配置
// @Tags PayConfig
// @Summary 删除支付配置
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param id path int true "支付配置ID"
// @Success 200 {object} response.Response{msg=string} "删除成功"
// @Router /payConfig/{id} [delete]
func (p *PayConfigApi) DeletePayConfig(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
response.FailWithMessage("参数错误", c)
return
}
if err := payConfigService.DeletePayConfig(uint(id)); err != nil {
global.GVA_LOG.Error("删除支付配置失败", zap.Error(err))
response.FailWithMessage("删除失败: "+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
// TogglePayConfig 切换支付配置状态
// @Tags PayConfig
// @Summary 切换支付配置状态
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.PayConfigToggle true "切换状态"
// @Success 200 {object} response.Response{msg=string} "操作成功"
// @Router /payConfig/toggle [post]
func (p *PayConfigApi) TogglePayConfig(c *gin.Context) {
var req request.PayConfigToggle
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage("参数错误: "+err.Error(), c)
return
}
if err := payConfigService.TogglePayConfig(req); err != nil {
global.GVA_LOG.Error("切换支付配置状态失败", zap.Error(err))
response.FailWithMessage("操作失败: "+err.Error(), c)
return
}
response.OkWithMessage("操作成功", c)
}

View File

@@ -21,8 +21,8 @@ zap:
# redis configuration
redis:
name: "hw"
addr: 120.46.165.63:6379
password: "loser765911"
addr: 47.243.221.157:6379
password: "loser765911."
db: 0
useCluster: false
clusterAddrs:
@@ -31,8 +31,8 @@ redis:
- 172.21.0.2:7002
redis-list:
- name: cache
addr: 120.46.165.63:6379
password: "loser765911"
addr: 47.243.221.157:6379
password: "loser765911."
db: 1
useCluster: false
clusterAddrs:
@@ -96,19 +96,19 @@ captcha:
# mysql connect configuration
# 未初始化之前请勿手动修改数据库信息如果一定要手动初始化请看https://gin-vue-admin.com/docs/first_master
mysql:
prefix: ""
port: "3366"
config: charset=utf8mb4&parseTime=True&loc=Local
db-name: lckt
username: lckt
password: loser765911.
path: 219.152.55.29
engine: ""
log-mode: info
max-idle-conns: 10
max-open-conns: 100
singular: false
log-zap: true
prefix: ""
port: "3306"
config: charset=utf8mb4&parseTime=True&loc=Local
db-name: lckt
username: lckt
password: loser765911.
path: 47.243.221.157
engine: ""
log-mode: info
max-idle-conns: 10
max-open-conns: 100
singular: false
log-zap: true
# pgsql connect configuration
# 未初始化之前请勿手动修改数据库信息如果一定要手动初始化请看https://gin-vue-admin.com/docs/first_master
@@ -300,10 +300,23 @@ pay-list:
serial-no: 59A891FB403EC7A1CF2090DB9C8EC704BD43B101
- type: wxpay2
alias-name: wxpay-2
app-id: 2
app-id: xxx
mch-id: 2
v3-key: 2
cert-path: 2
key-path: 2
notify-url: 2
serial-no: 2
v3-key: xxx
cert-path: xxx
key-path: xxx
notify-url: xxx
serial-no: xxx
# 神奇支付配置
shenqi-pay:
enable: true
pid: 1416
# 商户私钥
private-key: MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDkDcXtqEzZ13cbxSJJZQHuRRMDmKLFIclJZcIk2pzFBYhLGdIHFhr+h0pCoqPvf+wKextSHb7mi2UaVtjudiu1i3oMjJXg/sEiCNAZm/E0ylCbrZQogymSNeoKXVLYJyTlaLGm/EQ/BsfyNknoW9o+6+OaQX8a9VJ3jeP0DbmdC2qYG+HfRfnUjRgQXvZuoErRVO6CBAxcgPFOmSM+JNfgw8unEN+UQA+/Ucr3e14rcophOlYQnUowjXl732UdeSDG2Y5MjzonQyQipN5HVEHQSqyMbD4UJsMFh8bBs2gvwePdb/w3qqPFqXawYoAa/RhKEemXn94uGbxcZDwIP8v/AgMBAAECggEALwvoWS7vK8GXgNMaT6nWzCDT21oRujlOHSYw9wlibgLGKzgYa/3Tc87VMwn6Z94sA72B1V7tkwIOyXBBMHCQc9NiSlR2VwQ3M5490AqrqAhUuMkGV5U3bkJRFfJKtOzeX7VJcPl52sa7WOx6MsVAMNrZCWmZnyYc7S0IacCrGgptL/ZW3aJHSnM9SzR0R4eILFIWluyg/0qB5GBY0V77hbHvO/tig9NFQMVRDc+gcb+hF4FxZLFFMDQ2dafLX4GZrra8q89oCV5SV+ZmWt4p4tgMNbGvAKWFR8Zpv1PWB09WmnDrJkTxa6o+lCrCaoaIWo1h4i6TctcLS/ADrwMdgQKBgQD0ZiqGJVJShOI951m87cOMmcOc1wf3uQ/bRdU4xKSPB/jpW7yme1OM1PETeY4pZ58RUsZXoOYQR4hudXKdn4vYKp2eo0ngJEBRk7OlgmRnER91l2MrmtZCHI3JsFV4otVXvkNMz5rX8VxHCfAN/m82PEE3Jqt4SRqVykZA9l2jzQKBgQDu4PxwQJheqb/po/0N9BDbh/XYneWC/hyJyRoCYDU1rAC3ndXq8vHK8XLcFzXy/zRdp7O/6SbUsbQG5lgomnZZkezzjSuG/IdL7Uy/j9VoNZbIa4g50Hsbxeo0tBn4uyzQZ73/23y3N3N3yM65NOynj/qSo4pBgz7OqATXpGL6+wKBgQCssNQzXYPB0yuZ2jNKkCaw3qWd2UNEA3v00G9NYYV1m4iCO9QdPt9Wj9CYljehghqfvwFvQb3Omv0IL+0A+49w+wvM4Ex/GJ5qBhfWR0Byo2K7UHE+inYC2PJHJVX/m+9rxIEBcWBUXbN6bsc03CfxBrp+IdDfeahTV0408OBRsQKBgQC9kA7jaW9A18Yyu27qLr9d5uPCVXK3Y73z3YFlV0GCaQ9cpsUGIGqeWMKEQ2sNfVXp6FYtOfEVojsxqoNVHVZDuBhoIaPkB+u55gMclSCOBNC2FRdSgc3f+UvlLPLbPnXOoDTXoC61GizoZpMdyBDw28HFsj/ZwzQJPK4zDgF6FwKBgQCDAVIjO4qviqzWIBMISkKiFKQzf8is5LKWwRsAkcASaHwOSi+dWjcH+tQ4sozgc7oIwafBZVrhCJdynWh1B6iu2UqjgVIN0lInViacSxNph5pxJp1BbhICJXDVg6kqLaRsBaIJ5T4902nnG7/Sw3VxFaU54Zs77NUOdA7dnDgtUA==
# 平台公钥
platform-pub-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8B3lz9xMJSRHPFgggpaK+6ESw2Ws8oycyG++mtskXGJlmgrgAlrUnWzyJ9Lm6O7zyCMwnn/bGcFO2XBw989330mx2ZcHQPuQ1hSRQi05kBpNif1g7JtFp3B6vBovlQ+oSjsz/cdlJ+xNNYrOM2DNnK2mhINTDU/E5yB4Iwju2AYIjTsSDbzAnyY68XHM7eUv5V96QjPbdH/nsOed1TQinLH5X5CZgz0FukNAVxxoxfGjzZysln05dXWNtcE40k/f1ialqU2GPu2cqHoAmXU2hhYo84MeOhddJP6QPgpoPfAAqB4FLt2F6yMJy17mzCuC/rL3D59LTVLho+lQJbCEcQIDAQAB
notify-url: http://lckt.hnlc5588.c/app_order/shenqi/notify
return-url: http://lckt.hnlc5588.cn/pay/success
base-url: https://www.shenqiyzf.cn

View File

@@ -38,7 +38,8 @@ type Server struct {
// MCP配置
MCP MCP `mapstructure:"mcp" json:"mcp" yaml:"mcp"`
Wechat Wechat `mapstructure:"wechat" json:"wechat" yaml:"wechat"`
Pays []Pays `mapstructure:"pay-list" json:"pay-list" yaml:"pay-list"`
SMS SMS `mapstructure:"sms" json:"sms" yaml:"sms"`
Wechat Wechat `mapstructure:"wechat" json:"wechat" yaml:"wechat"`
Pays []Pays `mapstructure:"pay-list" json:"pay-list" yaml:"pay-list"`
ShenqiPay ShenqiPay `mapstructure:"shenqi-pay" json:"shenqi-pay" yaml:"shenqi-pay"`
SMS SMS `mapstructure:"sms" json:"sms" yaml:"sms"`
}

23
config/shenqi_pay.go Normal file
View File

@@ -0,0 +1,23 @@
package config
// ShenqiPay 神奇支付配置
type ShenqiPay struct {
// Enable 是否启用
Enable bool `mapstructure:"enable" json:"enable" yaml:"enable"`
// PID 商户ID
PID int `mapstructure:"pid" json:"pid" yaml:"pid"`
// PrivateKey 商户私钥
// 支持格式: 纯Base64字符串 或 完整PEM格式
// 示例: MIIEvgIBxxxx... 或 -----BEGIN PRIVATE KEY-----\nMIIEvgIBxxxx...\n-----END PRIVATE KEY-----
PrivateKey string `mapstructure:"private-key" json:"private-key" yaml:"private-key"`
// PlatformPubKey 平台公钥
// 支持格式: 纯Base64字符串 或 完整PEM格式
// 示例: MIIBIjANxxxx... 或 -----BEGIN PUBLIC KEY-----\nMIIBIjANxxxx...\n-----END PUBLIC KEY-----
PlatformPubKey string `mapstructure:"platform-pub-key" json:"platform-pub-key" yaml:"platform-pub-key"`
// NotifyURL 异步通知地址
NotifyURL string `mapstructure:"notify-url" json:"notify-url" yaml:"notify-url"`
// ReturnURL 同步跳转地址
ReturnURL string `mapstructure:"return-url" json:"return-url" yaml:"return-url"`
// BaseURL API基础地址 (可选,默认 https://www.shenqiyzf.cn)
BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"`
}

View File

@@ -1,6 +1,8 @@
package initialize
import (
"os"
"git.echol.cn/loser/lckt/model/app"
"git.echol.cn/loser/lckt/model/article"
"git.echol.cn/loser/lckt/model/bot"
@@ -8,7 +10,6 @@ import (
"git.echol.cn/loser/lckt/model/notice"
"git.echol.cn/loser/lckt/model/user"
"git.echol.cn/loser/lckt/model/vip"
"os"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/example"
@@ -64,6 +65,7 @@ func RegisterTables() {
system.JoinTemplate{},
system.SysParams{},
system.SysVersion{},
system.PayConfig{},
example.ExaFile{},
example.ExaCustomer{},
@@ -90,6 +92,7 @@ func RegisterTables() {
app.With{},
app.Domain{},
user.IpCheck{},
system.PayConfig{},
)
if err != nil {
global.GVA_LOG.Error("register table failed", zap.Error(err))

View File

@@ -107,6 +107,7 @@ func Routers() *gin.Engine {
systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理
systemRouter.InitSysExportTemplateRouter(PrivateGroup, PublicGroup) // 导出模板
systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理
systemRouter.InitPayConfigRouter(PrivateGroup) // 支付配置管理
exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由
exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup, AppAuthGroup) // 文件上传下载功能路由
exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类

View File

@@ -20,10 +20,11 @@ type GetOrderList struct {
}
type PayReq struct {
UserId uint64 `json:"user_id"` // 用户ID
OrderId uint `json:"order_id"`
Mode string `json:"mode"` // h5 jsapi
WxCode string `json:"wx_code"`
PayMethod int `json:"pay_method" vd:"$>0; msg:'参数不能为空'"`
OrderNo string `json:"order_no" vd:"@:len($)>0; msg:'订单编号参数不能为空'"`
UserId uint64 `json:"user_id"` // 用户ID
OrderId uint `json:"order_id"` // 订单ID
Mode string `json:"mode"` // 支付模式: h5, jsapi, alipay, wxpay
PayCode string `json:"pay_code"` // 支付配置编码(从数据库获取)
WxCode string `json:"wx_code"` // 微信授权码
PayMethod int `json:"pay_method" vd:"$>0; msg:'参数不能为空'"` // 支付方式
OrderNo string `json:"order_no" vd:"@:len($)>0; msg:'订单编号参数不能为空'"` // 订单编号
}

View File

@@ -0,0 +1,96 @@
package request
import (
"fmt"
"git.echol.cn/loser/lckt/model/common/request"
)
// PayConfigSearch 支付配置搜索请求
type PayConfigSearch struct {
request.PageInfo
Name string `json:"name" form:"name"` // 支付名称
Code string `json:"code" form:"code"` // 支付编码
Type string `json:"type" form:"type"` // 支付类型
Enable *bool `json:"enable" form:"enable"` // 是否启用
}
// PayConfigCreate 创建支付配置请求
type PayConfigCreate struct {
Name string `json:"name" binding:"required"` // 支付名称
Code string `json:"code" binding:"required"` // 支付编码
Type string `json:"type" binding:"required"` // 支付类型: wechat/shenqi
Enable bool `json:"enable"` // 是否启用
EnabledModes []string `json:"enabledModes"` // 启用的支付模式,如 ["h5","jsapi"] 或 ["alipay","wxpay"]
Sort int `json:"sort"` // 排序
Remark string `json:"remark"` // 备注
// ========== 微信支付配置 ==========
WechatAppID string `json:"wechatAppId"` // 微信AppID
WechatMchID string `json:"wechatMchId"` // 微信商户号
WechatMchApiV3Key string `json:"wechatMchApiV3Key"` // 微信API V3密钥
WechatPrivateKey string `json:"wechatPrivateKey"` // 微信商户私钥
WechatSerialNo string `json:"wechatSerialNo"` // 微信证书序列号
WechatNotifyURL string `json:"wechatNotifyUrl"` // 微信回调地址
// ========== 神奇支付配置 ==========
ShenqiPID int `json:"shenqiPid"` // 神奇支付商户ID
ShenqiPrivateKey string `json:"shenqiPrivateKey"` // 神奇支付商户私钥
ShenqiPlatformPubKey string `json:"shenqiPlatformPubKey"` // 神奇支付平台公钥
ShenqiNotifyURL string `json:"shenqiNotifyUrl"` // 神奇支付回调地址
ShenqiReturnURL string `json:"shenqiReturnUrl"` // 神奇支付同步跳转地址
ShenqiBaseURL string `json:"shenqiBaseUrl"` // 神奇支付API地址
}
// PayConfigUpdate 更新支付配置请求
type PayConfigUpdate struct {
ID uint `json:"id" binding:"required"` // ID
Name string `json:"name"` // 支付名称
Code string `json:"code"` // 支付编码
Type string `json:"type"` // 支付类型
Enable *bool `json:"enable"` // 是否启用
EnabledModes *[]string `json:"enabledModes"` // 启用的支付模式(使用指针来区分不传和传空数组)
Sort *int `json:"sort"` // 排序
Remark string `json:"remark"` // 备注
// ========== 微信支付配置 ==========
WechatAppID *string `json:"wechatAppId"` // 微信AppID
WechatMchID *string `json:"wechatMchId"` // 微信商户号
WechatMchApiV3Key *string `json:"wechatMchApiV3Key"` // 微信API V3密钥
WechatPrivateKey *string `json:"wechatPrivateKey"` // 微信商户私钥
WechatSerialNo *string `json:"wechatSerialNo"` // 微信证书序列号
WechatNotifyURL *string `json:"wechatNotifyUrl"` // 微信回调地址
// ========== 神奇支付配置 ==========
ShenqiPID *int `json:"shenqiPid"` // 神奇支付商户ID
ShenqiPrivateKey *string `json:"shenqiPrivateKey"` // 神奇支付商户私钥
ShenqiPlatformPubKey *string `json:"shenqiPlatformPubKey"` // 神奇支付平台公钥
ShenqiNotifyURL *string `json:"shenqiNotifyUrl"` // 神奇支付回调地址
ShenqiReturnURL *string `json:"shenqiReturnUrl"` // 神奇支付同步跳转地址
ShenqiBaseURL *string `json:"shenqiBaseUrl"` // 神奇支付API地址
}
// PayConfigToggle 切换支付状态请求
type PayConfigToggle struct {
ID uint `json:"id" binding:"required"` // ID
Enable bool `json:"enable"` // 是否启用
}
// Validate 验证创建请求
func (req *PayConfigCreate) Validate() error {
switch req.Type {
case "wechat":
if req.WechatAppID == "" || req.WechatMchID == "" || req.WechatMchApiV3Key == "" ||
req.WechatPrivateKey == "" || req.WechatSerialNo == "" || req.WechatNotifyURL == "" {
return fmt.Errorf("微信支付配置不完整")
}
case "shenqi":
if req.ShenqiPID == 0 || req.ShenqiPrivateKey == "" || req.ShenqiPlatformPubKey == "" ||
req.ShenqiNotifyURL == "" || req.ShenqiReturnURL == "" {
return fmt.Errorf("神奇支付配置不完整")
}
default:
return fmt.Errorf("不支持的支付类型: %s", req.Type)
}
return nil
}

View File

@@ -0,0 +1,161 @@
package system
import (
"database/sql/driver"
"encoding/json"
"git.echol.cn/loser/lckt/global"
)
// StringArray 字符串数组类型用于存储JSON数组
type StringArray []string
// Scan 实现 sql.Scanner 接口
func (s *StringArray) Scan(value interface{}) error {
if value == nil {
*s = nil
return nil
}
bytes, ok := value.([]byte)
if !ok {
*s = nil
return nil
}
// 空字符串或 "null" 视为 nil
if len(bytes) == 0 || string(bytes) == "null" {
*s = nil
return nil
}
return json.Unmarshal(bytes, s)
}
// Value 实现 driver.Valuer 接口
func (s StringArray) Value() (driver.Value, error) {
// 如果是 nil slice返回 NULL
if s == nil {
return nil, nil
}
// 否则序列化为 JSON包括空数组 []
return json.Marshal(s)
}
// MarshalJSON 实现 json.Marshaler 接口
func (s StringArray) MarshalJSON() ([]byte, error) {
if s == nil {
return []byte("[]"), nil
}
return json.Marshal([]string(s))
}
// PayConfig 支付配置模型
type PayConfig struct {
global.GVA_MODEL
// Name 支付名称(用于显示)
Name string `gorm:"column:name;type:varchar(50);not null;comment:支付名称" json:"name"`
// Code 支付编码(唯一标识)
Code string `gorm:"column:code;type:varchar(50);not null;uniqueIndex;comment:支付编码" json:"code"`
// Type 支付类型: wechat微信支付, shenqi神奇支付
Type string `gorm:"column:type;type:varchar(20);not null;comment:支付类型" json:"type"`
// Enable 是否启用
Enable bool `gorm:"column:enable;default:false;comment:是否启用" json:"enable"`
// EnabledModes 启用的支付模式JSON数组如 ["h5","jsapi"] 或 ["alipay","wxpay"]
EnabledModes StringArray `gorm:"column:enabled_modes;type:json;comment:启用的支付模式" json:"enabledModes"`
// Sort 排序
Sort int `gorm:"column:sort;default:0;comment:排序" json:"sort"`
// Remark 备注
Remark string `gorm:"column:remark;type:varchar(255);comment:备注" json:"remark"`
// ========== 微信支付配置字段 ==========
WechatAppID string `gorm:"column:wechat_app_id;type:varchar(50);comment:微信AppID" json:"wechatAppId"`
WechatMchID string `gorm:"column:wechat_mch_id;type:varchar(50);comment:微信商户号" json:"wechatMchId"`
WechatMchApiV3Key string `gorm:"column:wechat_mch_api_v3_key;type:varchar(255);comment:微信APIV3密钥" json:"wechatMchApiV3Key"`
WechatPrivateKey string `gorm:"column:wechat_private_key;type:text;comment:微信商户私钥" json:"wechatPrivateKey"`
WechatSerialNo string `gorm:"column:wechat_serial_no;type:varchar(100);comment:微信证书序列号" json:"wechatSerialNo"`
WechatNotifyURL string `gorm:"column:wechat_notify_url;type:varchar(255);comment:微信回调地址" json:"wechatNotifyUrl"`
// ========== 神奇支付配置字段 ==========
ShenqiPID int `gorm:"column:shenqi_pid;comment:神奇支付商户ID" json:"shenqiPid"`
ShenqiPrivateKey string `gorm:"column:shenqi_private_key;type:text;comment:神奇支付商户私钥" json:"shenqiPrivateKey"`
ShenqiPlatformPubKey string `gorm:"column:shenqi_platform_pub_key;type:text;comment:神奇支付平台公钥" json:"shenqiPlatformPubKey"`
ShenqiNotifyURL string `gorm:"column:shenqi_notify_url;type:varchar(255);comment:神奇支付回调地址" json:"shenqiNotifyUrl"`
ShenqiReturnURL string `gorm:"column:shenqi_return_url;type:varchar(255);comment:神奇支付同步跳转地址" json:"shenqiReturnUrl"`
ShenqiBaseURL string `gorm:"column:shenqi_base_url;type:varchar(255);comment:神奇支付API地址" json:"shenqiBaseUrl"`
}
func (PayConfig) TableName() string {
return "sys_pay_config"
}
// GetAvailableModes 获取可用的支付模式
// 如果 EnabledModes 为空,返回该支付类型的所有默认模式
func (p *PayConfig) GetAvailableModes() []string {
// 如果已配置启用的模式,直接返回
if len(p.EnabledModes) > 0 {
return p.EnabledModes
}
// 否则返回默认的所有模式
switch p.Type {
case "wechat":
return []string{"h5", "jsapi"}
case "shenqi":
return []string{"alipay", "wxpay"}
default:
return []string{}
}
}
// IsModeEnabled 检查指定模式是否启用
func (p *PayConfig) IsModeEnabled(mode string) bool {
availableModes := p.GetAvailableModes()
for _, m := range availableModes {
if m == mode {
return true
}
}
return false
}
// GetWechatConfig 获取微信支付配置
func (p *PayConfig) GetWechatConfig() *WechatPayConfig {
return &WechatPayConfig{
AppID: p.WechatAppID,
MchID: p.WechatMchID,
MchApiV3Key: p.WechatMchApiV3Key,
PrivateKey: p.WechatPrivateKey,
SerialNo: p.WechatSerialNo,
NotifyURL: p.WechatNotifyURL,
}
}
// GetShenqiConfig 获取神奇支付配置
func (p *PayConfig) GetShenqiConfig() *ShenqiPayConfig {
return &ShenqiPayConfig{
PID: p.ShenqiPID,
PrivateKey: p.ShenqiPrivateKey,
PlatformPubKey: p.ShenqiPlatformPubKey,
NotifyURL: p.ShenqiNotifyURL,
ReturnURL: p.ShenqiReturnURL,
BaseURL: p.ShenqiBaseURL,
}
}
// WechatPayConfig 微信支付配置
type WechatPayConfig struct {
AppID string `json:"appId"` // 小程序/公众号AppID
MchID string `json:"mchId"` // 商户号
MchApiV3Key string `json:"mchApiV3Key"` // API V3密钥
PrivateKey string `json:"privateKey"` // 商户私钥
SerialNo string `json:"serialNo"` // 证书序列号
NotifyURL string `json:"notifyUrl"` // 回调地址
}
// ShenqiPayConfig 神奇支付配置
type ShenqiPayConfig struct {
PID int `json:"pid"` // 商户ID
PrivateKey string `json:"privateKey"` // 商户私钥
PlatformPubKey string `json:"platformPubKey"` // 平台公钥
NotifyURL string `json:"notifyUrl"` // 回调地址
ReturnURL string `json:"returnUrl"` // 同步跳转地址
BaseURL string `json:"baseUrl"` // API地址
}

View File

@@ -10,14 +10,17 @@ func (r *OrderRouter) InitOrderRouter(AppRouter, SysteamRouter, PublicRouter *gi
publicRouter := PublicRouter.Group("app_order")
systeamRouter := SysteamRouter.Group("order")
{
appRouter.POST("", orderApi.CreateOrder) // 创建订单
appRouter.POST("/wechat/pay", orderApi.PayOrder) // 微信支付订单
appRouter.GET("/list", orderApi.AppGetOrderList) // 获取订单列表
appRouter.GET(":id", orderApi.GetOrderDetail) // 获取订单详情
appRouter.POST("/balance/pay", orderApi.BalancePay) // 余额支付
appRouter.POST("", orderApi.CreateOrder) // 创建订单
appRouter.POST("/wechat/pay", orderApi.PayOrder) // 微信支付订单
appRouter.GET("/list", orderApi.AppGetOrderList) // 获取订单列表
appRouter.GET(":id", orderApi.GetOrderDetail) // 获取订单详情
appRouter.POST("/balance/pay", orderApi.BalancePay) // 余额支付
appRouter.GET("/pay_methods", orderApi.GetPayMethods) // 获取支付方式列表
}
{
publicRouter.POST("/notify", orderApi.NotifyOrder) // 微信支付回调通知
publicRouter.POST("/notify", orderApi.NotifyOrder) // 微信支付回调通知
publicRouter.GET("/shenqi/notify", orderApi.ShenqiPayNotify) // 神奇支付回调通知(兼容旧配置)
publicRouter.GET("/shenqi/notify/:code", orderApi.ShenqiPayNotify) // 神奇支付回调通知(数据库配置)
}
{
systeamRouter.GET("/list", orderApi.GetOrderList) // 获取订单列表

View File

@@ -20,6 +20,7 @@ type RouterGroup struct {
SysExportTemplateRouter
SysParamsRouter
SysVersionRouter
PayConfigRouter
}
var (

View File

@@ -0,0 +1,23 @@
package system
import (
v1 "git.echol.cn/loser/lckt/api/v1"
"github.com/gin-gonic/gin"
)
type PayConfigRouter struct{}
// InitPayConfigRouter 初始化支付配置路由
func (r *PayConfigRouter) InitPayConfigRouter(Router *gin.RouterGroup) {
payConfigRouter := Router.Group("payConfig")
payConfigApi := v1.ApiGroupApp.SystemApiGroup.PayConfigApi
{
payConfigRouter.GET("list", payConfigApi.GetPayConfigList) // 获取支付配置列表
payConfigRouter.GET("enabled", payConfigApi.GetEnabledPayConfigs) // 获取启用的支付配置
payConfigRouter.GET(":id", payConfigApi.GetPayConfigByID) // 根据ID获取支付配置
payConfigRouter.POST("", payConfigApi.CreatePayConfig) // 创建支付配置
payConfigRouter.PUT("", payConfigApi.UpdatePayConfig) // 更新支付配置
payConfigRouter.DELETE(":id", payConfigApi.DeletePayConfig) // 删除支付配置
payConfigRouter.POST("toggle", payConfigApi.TogglePayConfig) // 切换支付配置状态
}
}

View File

@@ -2,7 +2,6 @@ package app
import (
"fmt"
"git.echol.cn/loser/lckt/model/vip"
"strconv"
"strings"
"time"
@@ -10,7 +9,11 @@ import (
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/app"
"git.echol.cn/loser/lckt/model/app/request"
"git.echol.cn/loser/lckt/model/system"
userM "git.echol.cn/loser/lckt/model/user"
"git.echol.cn/loser/lckt/model/vip"
"git.echol.cn/loser/lckt/utils/pay"
payReq "git.echol.cn/loser/lckt/utils/pay/request"
"git.echol.cn/loser/lckt/utils/wechat"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
@@ -32,23 +35,204 @@ func (s *OrderService) Pay(p request.PayReq, ctx *gin.Context) (interface{}, err
return "", fmt.Errorf("订单已过期")
}
if p.Mode == "h5" {
payConf, err := wechat.H5Pay(&order, ctx)
if err != nil {
global.GVA_LOG.Error("微信支付订单失败", zap.Error(err))
return "", err
}
return payConf, nil
} else if p.Mode == "jsapi" {
payConf, err := wechat.JSAPIPay(&order, ctx)
if err != nil {
global.GVA_LOG.Error("微信支付订单失败", zap.Error(err))
return "", err
}
return payConf, nil
if order.Status == 2 {
global.GVA_LOG.Error("订单已支付", zap.Int64("order_id", int64(order.ID)))
return "", fmt.Errorf("订单已支付")
}
return "", nil
// 如果指定了支付编码,优先使用数据库配置
if p.PayCode != "" {
return s.payByCode(&order, p.PayCode, p.Mode, ctx)
}
// 兼容旧的支付方式
switch p.Mode {
case "h5":
payConf, err := wechat.H5Pay(&order, ctx)
if err != nil {
global.GVA_LOG.Error("微信H5支付失败", zap.Error(err))
return "", err
}
return payConf, nil
case "jsapi":
payConf, err := wechat.JSAPIPay(&order, ctx)
if err != nil {
global.GVA_LOG.Error("微信JSAPI支付失败", zap.Error(err))
return "", err
}
return payConf, nil
case "shenqi", "shenqi_alipay", "shenqi_wxpay":
// 兼容旧配置:从配置文件读取
return s.shenqiPayFromConfig(&order, p.Mode, ctx)
default:
return "", fmt.Errorf("不支持的支付方式: %s", p.Mode)
}
}
// payByCode 根据支付编码发起支付
func (s *OrderService) payByCode(order *app.Order, payCode, mode string, ctx *gin.Context) (interface{}, error) {
// 获取支付配置
var config system.PayConfig
if err := global.GVA_DB.Where("code = ? AND enable = ?", payCode, true).First(&config).Error; err != nil {
global.GVA_LOG.Error("获取支付配置失败", zap.String("pay_code", payCode), zap.Error(err))
return "", fmt.Errorf("支付配置不存在或未启用")
}
// 根据支付类型调用不同的支付方法
switch config.Type {
case "shenqi":
return s.shenqiPay(order, payCode, mode, ctx)
case "wechat":
// TODO: 实现微信支付从数据库配置
return "", fmt.Errorf("微信支付数据库配置暂未实现")
case "alipay":
// TODO: 实现支付宝支付
return "", fmt.Errorf("支付宝支付暂未实现")
default:
return "", fmt.Errorf("不支持的支付类型: %s", config.Type)
}
}
// shenqiPayFromConfig 从配置文件读取神奇支付配置(兼容旧逻辑)
func (s *OrderService) shenqiPayFromConfig(order *app.Order, mode string, ctx *gin.Context) (interface{}, error) {
// 检查神奇支付是否启用
if !global.GVA_CONFIG.ShenqiPay.Enable {
return "", fmt.Errorf("神奇支付未启用")
}
// 创建客户端
client, err := pay.NewClient(pay.Config{
PID: global.GVA_CONFIG.ShenqiPay.PID,
PrivateKey: global.GVA_CONFIG.ShenqiPay.PrivateKey,
PlatformPubKey: global.GVA_CONFIG.ShenqiPay.PlatformPubKey,
BaseURL: global.GVA_CONFIG.ShenqiPay.BaseURL,
})
if err != nil {
global.GVA_LOG.Error("创建神奇支付客户端失败", zap.Error(err))
return "", fmt.Errorf("支付初始化失败: %v", err)
}
// 确定支付方式
payType := pay.PayTypeAlipay
if mode == "shenqi_wxpay" {
payType = pay.PayTypeWxpay
}
// 金额转换 (数据库存储的是分,神奇支付需要元)
money := fmt.Sprintf("%.2f", float64(order.Price)/100.0)
// 创建订单
resp, err := client.CreateOrder(&payReq.CreateOrder{
Method: pay.MethodWeb,
Device: pay.DeviceMobile,
Type: payType,
OutTradeNo: order.OrderNo,
NotifyURL: global.GVA_CONFIG.ShenqiPay.NotifyURL,
ReturnURL: global.GVA_CONFIG.ShenqiPay.ReturnURL,
Name: order.Name,
Money: money,
ClientIP: ctx.ClientIP(),
Param: fmt.Sprintf("order_id=%d", order.ID),
})
if err != nil {
global.GVA_LOG.Error("神奇支付下单失败", zap.Error(err))
return "", fmt.Errorf("支付下单失败: %v", err)
}
if !resp.IsSuccess() {
global.GVA_LOG.Error("神奇支付下单失败", zap.String("msg", resp.Msg))
return "", fmt.Errorf("支付下单失败: %s", resp.Msg)
}
global.GVA_LOG.Info("神奇支付下单成功",
zap.String("order_no", order.OrderNo),
zap.String("trade_no", resp.TradeNo),
zap.String("pay_type", resp.PayType))
// 返回支付信息
return map[string]interface{}{
"trade_no": resp.TradeNo,
"pay_type": resp.PayType,
"pay_info": resp.PayInfo,
}, nil
}
// shenqiPay 神奇支付
// payCode: 支付配置编码,用于从数据库获取配置
// mode: 支付模式,如 alipay, wxpay
func (s *OrderService) shenqiPay(order *app.Order, payCode, mode string, ctx *gin.Context) (interface{}, error) {
// 从数据库获取支付配置
var config system.PayConfig
if err := global.GVA_DB.Where("code = ? AND enable = ?", payCode, true).First(&config).Error; err != nil {
global.GVA_LOG.Error("获取神奇支付配置失败", zap.String("pay_code", payCode), zap.Error(err))
return "", fmt.Errorf("支付配置不存在或未启用")
}
// 验证是否是神奇支付类型
if config.Type != "shenqi" {
return "", fmt.Errorf("该配置不是神奇支付类型")
}
// 获取神奇支付配置
shenqiConfig := config.GetShenqiConfig()
// 创建客户端
client, err := pay.NewClient(pay.Config{
PID: shenqiConfig.PID,
PrivateKey: shenqiConfig.PrivateKey,
PlatformPubKey: shenqiConfig.PlatformPubKey,
BaseURL: shenqiConfig.BaseURL,
})
if err != nil {
global.GVA_LOG.Error("创建神奇支付客户端失败", zap.Error(err))
return "", fmt.Errorf("支付初始化失败: %v", err)
}
// 确定支付方式
payType := pay.PayTypeAlipay
if mode == "wxpay" || strings.HasSuffix(mode, "_wxpay") {
payType = pay.PayTypeWxpay
}
// 金额转换 (数据库存储的是分,神奇支付需要元)
money := fmt.Sprintf("%.2f", float64(order.Price)/100.0)
// 创建订单
resp, err := client.CreateOrder(&payReq.CreateOrder{
Method: pay.MethodWeb,
Device: pay.DeviceMobile,
Type: payType,
OutTradeNo: order.OrderNo,
NotifyURL: shenqiConfig.NotifyURL,
ReturnURL: shenqiConfig.ReturnURL,
Name: order.Name,
Money: money,
ClientIP: ctx.ClientIP(),
Param: fmt.Sprintf("order_id=%d&pay_code=%s", order.ID, payCode),
})
if err != nil {
global.GVA_LOG.Error("神奇支付下单失败", zap.Error(err))
return "", fmt.Errorf("支付下单失败: %v", err)
}
if !resp.IsSuccess() {
global.GVA_LOG.Error("神奇支付下单失败", zap.String("msg", resp.Msg))
return "", fmt.Errorf("支付下单失败: %s", resp.Msg)
}
global.GVA_LOG.Info("神奇支付下单成功",
zap.String("order_no", order.OrderNo),
zap.String("trade_no", resp.TradeNo),
zap.String("pay_type", resp.PayType))
// 返回支付信息
return map[string]interface{}{
"trade_no": resp.TradeNo,
"pay_type": resp.PayType,
"pay_info": resp.PayInfo,
}, nil
}
func (s *OrderService) Create(o *app.Order) (*app.Order, error) {

View File

@@ -18,6 +18,7 @@ type ServiceGroup struct {
SysExportTemplateService
SysParamsService
SysVersionService
PayConfigService
AutoCodePlugin autoCodePlugin
AutoCodePackage autoCodePackage
AutoCodeHistory autoCodeHistory

View File

@@ -0,0 +1,291 @@
package system
import (
"errors"
"sync"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/system"
"git.echol.cn/loser/lckt/model/system/request"
"gorm.io/gorm"
)
type PayConfigService struct{}
// 缓存支付配置
var (
payConfigCache = make(map[string]*system.PayConfig)
payConfigCacheLock sync.RWMutex
)
// GetPayConfigList 获取支付配置列表
func (s *PayConfigService) GetPayConfigList(req request.PayConfigSearch) (list []system.PayConfig, total int64, err error) {
db := global.GVA_DB.Model(&system.PayConfig{})
if req.Name != "" {
db = db.Where("name LIKE ?", "%"+req.Name+"%")
}
if req.Code != "" {
db = db.Where("code LIKE ?", "%"+req.Code+"%")
}
if req.Type != "" {
db = db.Where("type = ?", req.Type)
}
if req.Enable != nil {
db = db.Where("enable = ?", *req.Enable)
}
err = db.Count(&total).Error
if err != nil {
return
}
limit := req.PageSize
offset := req.PageSize * (req.Page - 1)
err = db.Order("sort ASC, id ASC").Limit(limit).Offset(offset).Find(&list).Error
return
}
// GetPayConfigByID 根据ID获取支付配置
func (s *PayConfigService) GetPayConfigByID(id uint) (config system.PayConfig, err error) {
err = global.GVA_DB.Where("id = ?", id).First(&config).Error
return
}
// GetPayConfigByCode 根据编码获取支付配置
func (s *PayConfigService) GetPayConfigByCode(code string) (config system.PayConfig, err error) {
// 先从缓存获取
payConfigCacheLock.RLock()
if cached, ok := payConfigCache[code]; ok {
payConfigCacheLock.RUnlock()
return *cached, nil
}
payConfigCacheLock.RUnlock()
// 从数据库获取
err = global.GVA_DB.Where("code = ? AND enable = ?", code, true).First(&config).Error
if err != nil {
return
}
// 写入缓存
payConfigCacheLock.Lock()
payConfigCache[code] = &config
payConfigCacheLock.Unlock()
return
}
// GetEnabledPayConfigs 获取所有启用的支付配置
func (s *PayConfigService) GetEnabledPayConfigs() (list []system.PayConfig, err error) {
err = global.GVA_DB.Where("enable = ?", true).Order("sort ASC").Find(&list).Error
return
}
// GetPayConfigsByType 根据类型获取启用的支付配置
func (s *PayConfigService) GetPayConfigsByType(payType string) (list []system.PayConfig, err error) {
err = global.GVA_DB.Where("type = ? AND enable = ?", payType, true).Order("sort ASC").Find(&list).Error
return
}
// CreatePayConfig 创建支付配置
func (s *PayConfigService) CreatePayConfig(req request.PayConfigCreate) error {
// 验证配置完整性
if err := req.Validate(); err != nil {
return err
}
// 检查编码是否已存在
var count int64
if err := global.GVA_DB.Model(&system.PayConfig{}).Where("code = ?", req.Code).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("支付编码已存在")
}
config := system.PayConfig{
Name: req.Name,
Code: req.Code,
Type: req.Type,
Enable: req.Enable,
EnabledModes: system.StringArray(req.EnabledModes),
Sort: req.Sort,
Remark: req.Remark,
}
// 根据类型设置配置字段
switch req.Type {
case "wechat":
config.WechatAppID = req.WechatAppID
config.WechatMchID = req.WechatMchID
config.WechatMchApiV3Key = req.WechatMchApiV3Key
config.WechatPrivateKey = req.WechatPrivateKey
config.WechatSerialNo = req.WechatSerialNo
config.WechatNotifyURL = req.WechatNotifyURL
case "shenqi":
config.ShenqiPID = req.ShenqiPID
config.ShenqiPrivateKey = req.ShenqiPrivateKey
config.ShenqiPlatformPubKey = req.ShenqiPlatformPubKey
config.ShenqiNotifyURL = req.ShenqiNotifyURL
config.ShenqiReturnURL = req.ShenqiReturnURL
config.ShenqiBaseURL = req.ShenqiBaseURL
}
if err := global.GVA_DB.Create(&config).Error; err != nil {
return err
}
// 清除缓存
s.clearCache()
return nil
}
// UpdatePayConfig 更新支付配置
func (s *PayConfigService) UpdatePayConfig(req request.PayConfigUpdate) error {
// 检查是否存在
var config system.PayConfig
if err := global.GVA_DB.Where("id = ?", req.ID).First(&config).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("支付配置不存在")
}
return err
}
// 如果修改了编码,检查新编码是否已存在
if req.Code != "" && req.Code != config.Code {
var count int64
if err := global.GVA_DB.Model(&system.PayConfig{}).Where("code = ? AND id != ?", req.Code, req.ID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("支付编码已存在")
}
}
// 构建更新数据
updates := make(map[string]interface{})
if req.Name != "" {
updates["name"] = req.Name
}
if req.Code != "" {
updates["code"] = req.Code
}
if req.Type != "" {
updates["type"] = req.Type
}
if req.Enable != nil {
updates["enable"] = *req.Enable
}
// 处理 EnabledModes使用指针类型来区分"不更新"和"更新为空数组"
if req.EnabledModes != nil {
// 将 *[]string 转换为 StringArray 类型
sa := system.StringArray(*req.EnabledModes)
updates["enabled_modes"] = sa
}
if req.Sort != nil {
updates["sort"] = *req.Sort
}
if req.Remark != "" {
updates["remark"] = req.Remark
}
// 微信支付配置
if req.WechatAppID != nil {
updates["wechat_app_id"] = *req.WechatAppID
}
if req.WechatMchID != nil {
updates["wechat_mch_id"] = *req.WechatMchID
}
if req.WechatMchApiV3Key != nil {
updates["wechat_mch_api_v3_key"] = *req.WechatMchApiV3Key
}
if req.WechatPrivateKey != nil {
updates["wechat_private_key"] = *req.WechatPrivateKey
}
if req.WechatSerialNo != nil {
updates["wechat_serial_no"] = *req.WechatSerialNo
}
if req.WechatNotifyURL != nil {
updates["wechat_notify_url"] = *req.WechatNotifyURL
}
// 神奇支付配置
if req.ShenqiPID != nil {
updates["shenqi_pid"] = *req.ShenqiPID
}
if req.ShenqiPrivateKey != nil {
updates["shenqi_private_key"] = *req.ShenqiPrivateKey
}
if req.ShenqiPlatformPubKey != nil {
updates["shenqi_platform_pub_key"] = *req.ShenqiPlatformPubKey
}
if req.ShenqiNotifyURL != nil {
updates["shenqi_notify_url"] = *req.ShenqiNotifyURL
}
if req.ShenqiReturnURL != nil {
updates["shenqi_return_url"] = *req.ShenqiReturnURL
}
if req.ShenqiBaseURL != nil {
updates["shenqi_base_url"] = *req.ShenqiBaseURL
}
if err := global.GVA_DB.Model(&config).Updates(updates).Error; err != nil {
return err
}
// 清除缓存
s.clearCache()
return nil
}
// DeletePayConfig 删除支付配置
func (s *PayConfigService) DeletePayConfig(id uint) error {
if err := global.GVA_DB.Delete(&system.PayConfig{}, id).Error; err != nil {
return err
}
// 清除缓存
s.clearCache()
return nil
}
// TogglePayConfig 切换支付配置状态
func (s *PayConfigService) TogglePayConfig(req request.PayConfigToggle) error {
if err := global.GVA_DB.Model(&system.PayConfig{}).Where("id = ?", req.ID).Update("enable", req.Enable).Error; err != nil {
return err
}
// 清除缓存
s.clearCache()
return nil
}
// clearCache 清除支付配置缓存
func (s *PayConfigService) clearCache() {
payConfigCacheLock.Lock()
payConfigCache = make(map[string]*system.PayConfig)
payConfigCacheLock.Unlock()
}
// GetShenqiPayConfig 获取神奇支付配置
func (s *PayConfigService) GetShenqiPayConfig(code string) (*system.ShenqiPayConfig, error) {
config, err := s.GetPayConfigByCode(code)
if err != nil {
return nil, err
}
if config.Type != "shenqi" {
return nil, errors.New("该配置不是神奇支付类型")
}
return config.GetShenqiConfig(), nil
}
// GetWechatPayConfig 获取微信支付配置
func (s *PayConfigService) GetWechatPayConfig(code string) (*system.WechatPayConfig, error) {
config, err := s.GetPayConfigByCode(code)
if err != nil {
return nil, err
}
if config.Type != "wechat" {
return nil, errors.New("该配置不是微信支付类型")
}
return config.GetWechatConfig(), nil
}

View File

@@ -0,0 +1,97 @@
package system
import (
"context"
sysModel "git.echol.cn/loser/lckt/model/system"
"git.echol.cn/loser/lckt/service/system"
"gorm.io/gorm"
)
type initPayConfig struct{}
const initOrderPayConfig = system.InitOrderSystem + 1
// auto run
func init() {
system.RegisterInit(initOrderPayConfig, &initPayConfig{})
}
func (i *initPayConfig) InitializerName() string {
return sysModel.PayConfig{}.TableName()
}
func (i *initPayConfig) MigrateTable(ctx context.Context) (context.Context, error) {
db, ok := ctx.Value("db").(*gorm.DB)
if !ok {
return ctx, system.ErrMissingDBContext
}
return ctx, db.AutoMigrate(&sysModel.PayConfig{})
}
func (i *initPayConfig) TableCreated(ctx context.Context) bool {
db, ok := ctx.Value("db").(*gorm.DB)
if !ok {
return false
}
return db.Migrator().HasTable(&sysModel.PayConfig{})
}
func (i *initPayConfig) InitializeData(ctx context.Context) (context.Context, error) {
db, ok := ctx.Value("db").(*gorm.DB)
if !ok {
return ctx, system.ErrMissingDBContext
}
entities := []sysModel.PayConfig{
{
Name: "微信支付-默认",
Code: "wechat_default",
Type: "wechat",
Enable: false,
Sort: 1,
Remark: "默认微信支付配置,请在后台修改密钥等信息",
// 微信支付配置(需要在后台管理界面修改)
WechatAppID: "wx3d21df18d7f8f9fc",
WechatMchID: "1646874753",
WechatMchApiV3Key: "1a3sd8561d5179Df152D4789aD38wG9s",
WechatPrivateKey: "", // 需要填写
WechatSerialNo: "59A891FB403EC7A1CF2090DB9C8EC704BD43B101",
WechatNotifyURL: "http://lckt.hnlc5588.cn/app_order/notify",
},
{
Name: "神奇支付-默认",
Code: "shenqi_default",
Type: "shenqi",
Enable: false,
Sort: 2,
Remark: "默认神奇支付配置请在后台修改商户ID和密钥",
// 神奇支付配置(需要在后台管理界面修改)
ShenqiPID: 1001,
ShenqiPrivateKey: "", // 需要填写商户私钥
ShenqiPlatformPubKey: "", // 需要填写平台公钥
ShenqiNotifyURL: "http://lckt.hnlc5588.cn/app_order/shenqi/notify/shenqi_default",
ShenqiReturnURL: "http://lckt.hnlc5588.cn/pay/success",
ShenqiBaseURL: "https://www.shenqiyzf.cn",
},
}
if err := db.Create(&entities).Error; err != nil {
return ctx, err
}
return ctx, nil
}
func (i *initPayConfig) DataInserted(ctx context.Context) bool {
db, ok := ctx.Value("db").(*gorm.DB)
if !ok {
return false
}
var count int64
if err := db.Model(&sysModel.PayConfig{}).
Where("code IN (?)", []string{"wechat_default", "shenqi_default"}).
Count(&count).Error; err != nil {
return false
}
return count >= 2
}

480
utils/pay/client.go Normal file
View File

@@ -0,0 +1,480 @@
// Package pay 神奇支付SDK客户端
package pay
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"git.echol.cn/loser/lckt/utils/pay/request"
"git.echol.cn/loser/lckt/utils/pay/resp"
)
// Config 客户端配置
type Config struct {
// PID 商户ID (必填)
PID int
// PrivateKey 商户私钥 (必填PEM格式)
PrivateKey string
// PlatformPubKey 平台公钥 (必填PEM格式)
PlatformPubKey string
// BaseURL API基础地址 (可选,默认 https://www.shenqiyzf.cn)
BaseURL string
// Timeout 请求超时时间 (可选默认30秒)
Timeout time.Duration
}
// Client 神奇支付客户端
type Client struct {
config Config
signer *Signer
httpClient *http.Client
}
// NewClient 创建客户端
func NewClient(config Config) (*Client, error) {
// 参数校验
if config.PID == 0 {
return nil, errors.New("商户ID(PID)不能为空")
}
if config.PrivateKey == "" {
return nil, errors.New("商户私钥不能为空")
}
if config.PlatformPubKey == "" {
return nil, errors.New("平台公钥不能为空")
}
// 默认值
if config.BaseURL == "" {
config.BaseURL = DefaultBaseURL
}
if config.Timeout == 0 {
config.Timeout = 30 * time.Second
}
// 创建签名器
signer, err := NewSigner(config.PrivateKey, config.PlatformPubKey)
if err != nil {
return nil, err
}
return &Client{
config: config,
signer: signer,
httpClient: &http.Client{
Timeout: config.Timeout,
},
}, nil
}
// doPost 发起POST请求
func (c *Client) doPost(path string, params map[string]string) (map[string]interface{}, error) {
// 添加公共参数
params["pid"] = strconv.Itoa(c.config.PID)
params["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10)
// 签名
if err := c.signer.SignParams(params); err != nil {
return nil, fmt.Errorf("签名失败: %w", err)
}
// 构建表单
form := url.Values{}
for k, v := range params {
form.Set(k, v)
}
// 发起请求
apiURL := c.config.BaseURL + path
httpResp, err := c.httpClient.PostForm(apiURL, form)
if err != nil {
return nil, fmt.Errorf("请求失败: %w", err)
}
defer httpResp.Body.Close()
// 读取响应
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
// 解析JSON
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %w, body: %s", err, string(body))
}
return result, nil
}
// parseAndVerify 解析响应并验签
func (c *Client) parseAndVerify(result map[string]interface{}, target interface{}) error {
// 转换为JSON再解析到目标结构
jsonBytes, err := json.Marshal(result)
if err != nil {
return fmt.Errorf("序列化响应失败: %w", err)
}
if err := json.Unmarshal(jsonBytes, target); err != nil {
return fmt.Errorf("解析响应失败: %w", err)
}
// 验签 (仅成功响应需要验签)
if code, ok := result["code"].(float64); ok && code == 0 {
if sign, ok := result["sign"].(string); ok && sign != "" {
if err := c.signer.VerifyParams(MapToStringMap(result)); err != nil {
return fmt.Errorf("验签失败: %w", err)
}
}
}
return nil
}
// ==================== 支付接口 ====================
// CreateOrder 统一下单
func (c *Client) CreateOrder(req *request.CreateOrder) (*resp.CreateOrder, error) {
params := map[string]string{
"method": req.Method,
"type": req.Type,
"out_trade_no": req.OutTradeNo,
"notify_url": req.NotifyURL,
"return_url": req.ReturnURL,
"name": req.Name,
"money": req.Money,
"clientip": req.ClientIP,
}
// 可选参数
if req.Device != "" {
params["device"] = req.Device
}
if req.Param != "" {
params["param"] = req.Param
}
if req.AuthCode != "" {
params["auth_code"] = req.AuthCode
}
if req.SubOpenid != "" {
params["sub_openid"] = req.SubOpenid
}
if req.SubAppid != "" {
params["sub_appid"] = req.SubAppid
}
if req.ChannelID > 0 {
params["channel_id"] = strconv.Itoa(req.ChannelID)
}
result, err := c.doPost(PathPayCreate, params)
if err != nil {
return nil, err
}
response := &resp.CreateOrder{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// BuildSubmitURL 构建页面跳转支付URL
func (c *Client) BuildSubmitURL(req *request.SubmitOrder) (string, error) {
params := map[string]string{
"pid": strconv.Itoa(c.config.PID),
"out_trade_no": req.OutTradeNo,
"notify_url": req.NotifyURL,
"return_url": req.ReturnURL,
"name": req.Name,
"money": req.Money,
"timestamp": strconv.FormatInt(time.Now().Unix(), 10),
}
// 可选参数
if req.Type != "" {
params["type"] = req.Type
}
if req.Param != "" {
params["param"] = req.Param
}
if req.ChannelID > 0 {
params["channel_id"] = strconv.Itoa(req.ChannelID)
}
// 签名
if err := c.signer.SignParams(params); err != nil {
return "", fmt.Errorf("签名失败: %w", err)
}
// 构建URL
u, _ := url.Parse(c.config.BaseURL + PathPaySubmit)
query := u.Query()
for k, v := range params {
query.Set(k, v)
}
u.RawQuery = query.Encode()
return u.String(), nil
}
// QueryOrder 订单查询
func (c *Client) QueryOrder(req *request.QueryOrder) (*resp.QueryOrder, error) {
params := make(map[string]string)
if req.TradeNo != "" {
params["trade_no"] = req.TradeNo
}
if req.OutTradeNo != "" {
params["out_trade_no"] = req.OutTradeNo
}
result, err := c.doPost(PathPayQuery, params)
if err != nil {
return nil, err
}
response := &resp.QueryOrder{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// Refund 订单退款
func (c *Client) Refund(req *request.Refund) (*resp.Refund, error) {
params := map[string]string{
"money": req.Money,
}
if req.TradeNo != "" {
params["trade_no"] = req.TradeNo
}
if req.OutTradeNo != "" {
params["out_trade_no"] = req.OutTradeNo
}
if req.OutRefundNo != "" {
params["out_refund_no"] = req.OutRefundNo
}
result, err := c.doPost(PathPayRefund, params)
if err != nil {
return nil, err
}
response := &resp.Refund{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// RefundQuery 退款查询
func (c *Client) RefundQuery(req *request.RefundQuery) (*resp.RefundQuery, error) {
params := make(map[string]string)
if req.RefundNo != "" {
params["refund_no"] = req.RefundNo
}
if req.OutRefundNo != "" {
params["out_refund_no"] = req.OutRefundNo
}
result, err := c.doPost(PathRefundQuery, params)
if err != nil {
return nil, err
}
response := &resp.RefundQuery{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// ==================== 通知处理 ====================
// VerifyNotify 验证支付通知 (从 url.Values 解析)
func (c *Client) VerifyNotify(values url.Values) (*resp.Notify, error) {
params := make(map[string]string)
for k := range values {
params[k] = values.Get(k)
}
return c.verifyNotifyFromMap(params)
}
// VerifyNotifyFromMap 验证支付通知 (从 map 解析)
func (c *Client) VerifyNotifyFromMap(params map[string]string) (*resp.Notify, error) {
return c.verifyNotifyFromMap(params)
}
// verifyNotifyFromMap 验证通知的内部实现
func (c *Client) verifyNotifyFromMap(params map[string]string) (*resp.Notify, error) {
// 验签
if err := c.signer.VerifyParams(params); err != nil {
return nil, fmt.Errorf("通知验签失败: %w", err)
}
// 验证交易状态
if params["trade_status"] != TradeStatusSuccess {
return nil, fmt.Errorf("交易状态异常: %s", params["trade_status"])
}
// 构建通知对象
notify := &resp.Notify{
TradeNo: params["trade_no"],
OutTradeNo: params["out_trade_no"],
ApiTradeNo: params["api_trade_no"],
Type: params["type"],
TradeStatus: params["trade_status"],
Addtime: params["addtime"],
Endtime: params["endtime"],
Name: params["name"],
Money: params["money"],
Param: params["param"],
Buyer: params["buyer"],
Timestamp: params["timestamp"],
Sign: params["sign"],
SignType: params["sign_type"],
}
if pidStr := params["pid"]; pidStr != "" {
if pid, err := strconv.Atoi(pidStr); err == nil {
notify.PID = pid
}
}
return notify, nil
}
// ==================== 商户接口 ====================
// GetMerchantInfo 查询商户信息
func (c *Client) GetMerchantInfo() (*resp.MerchantInfo, error) {
params := make(map[string]string)
result, err := c.doPost(PathMerchantInfo, params)
if err != nil {
return nil, err
}
response := &resp.MerchantInfo{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// GetOrderList 查询订单列表
func (c *Client) GetOrderList(req *request.OrderList) (*resp.OrderList, error) {
params := map[string]string{
"offset": strconv.Itoa(req.Offset),
"limit": strconv.Itoa(req.Limit),
}
if req.Status >= 0 {
params["status"] = strconv.Itoa(req.Status)
}
result, err := c.doPost(PathMerchantOrder, params)
if err != nil {
return nil, err
}
response := &resp.OrderList{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// ==================== 转账接口 ====================
// Transfer 发起转账
func (c *Client) Transfer(req *request.Transfer) (*resp.Transfer, error) {
params := map[string]string{
"type": req.Type,
"account": req.Account,
"money": req.Money,
}
if req.Name != "" {
params["name"] = req.Name
}
if req.Remark != "" {
params["remark"] = req.Remark
}
if req.OutBizNo != "" {
params["out_biz_no"] = req.OutBizNo
}
if req.BookID != "" {
params["bookid"] = req.BookID
}
result, err := c.doPost(PathTransfer, params)
if err != nil {
return nil, err
}
response := &resp.Transfer{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// TransferQuery 查询转账
func (c *Client) TransferQuery(req *request.TransferQuery) (*resp.TransferQuery, error) {
params := make(map[string]string)
if req.BizNo != "" {
params["biz_no"] = req.BizNo
}
if req.OutBizNo != "" {
params["out_biz_no"] = req.OutBizNo
}
result, err := c.doPost(PathTransferQuery, params)
if err != nil {
return nil, err
}
response := &resp.TransferQuery{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}
// GetBalance 查询可用余额
func (c *Client) GetBalance() (*resp.Balance, error) {
params := make(map[string]string)
result, err := c.doPost(PathBalance, params)
if err != nil {
return nil, err
}
response := &resp.Balance{}
if err := c.parseAndVerify(result, response); err != nil {
return nil, err
}
return response, nil
}

109
utils/pay/constants.go Normal file
View File

@@ -0,0 +1,109 @@
// Package pay 神奇支付SDK常量定义
package pay
// API 地址
const (
// DefaultBaseURL 默认API基础地址
DefaultBaseURL = "https://www.shenqiyzf.cn"
// API 路径
PathPayCreate = "/api/pay/create" // 统一下单
PathPaySubmit = "/api/pay/submit" // 页面跳转支付
PathPayQuery = "/api/pay/query" // 订单查询
PathPayRefund = "/api/pay/refund" // 订单退款
PathRefundQuery = "/api/pay/refundquery" // 退款查询
PathMerchantInfo = "/api/merchant/info" // 商户信息
PathMerchantOrder = "/api/merchant/orders" // 订单列表
PathTransfer = "/api/transfer/submit" // 转账发起
PathTransferQuery = "/api/transfer/query" // 转账查询
PathBalance = "/api/transfer/balance" // 余额查询
)
// 支付方式
const (
PayTypeAlipay = "alipay" // 支付宝
PayTypeWxpay = "wxpay" // 微信支付
PayTypeQQpay = "qqpay" // QQ钱包
)
// 接口类型 (Method)
const (
MethodWeb = "web" // 通用网页支付
MethodJump = "jump" // 跳转支付
MethodJsapi = "jsapi" // JSAPI支付(小程序/公众号)
MethodApp = "app" // APP支付
MethodScan = "scan" // 付款码支付
MethodApplet = "applet" // 小程序支付
)
// 设备类型
const (
DevicePC = "pc" // 电脑浏览器(默认)
DeviceMobile = "mobile" // 手机浏览器
DeviceQQ = "qq" // 手机QQ内浏览器
DeviceWechat = "wechat" // 微信内浏览器
DeviceAlipay = "alipay" // 支付宝客户端
)
// 发起支付类型 (PayType)
const (
PayTypeResultJump = "jump" // 返回支付跳转url
PayTypeResultHTML = "html" // 返回html代码
PayTypeResultQrcode = "qrcode" // 返回支付二维码
PayTypeResultURLScheme = "urlscheme" // 返回小程序跳转url scheme
PayTypeResultJsapi = "jsapi" // 返回JSAPI支付参数
PayTypeResultApp = "app" // 返回APP支付参数
PayTypeResultScan = "scan" // 付款码支付成功
PayTypeResultWxPlugin = "wxplugin" // 返回微信小程序插件参数
PayTypeResultWxApp = "wxapp" // 返回拉起微信小程序参数
)
// 订单支付状态
const (
OrderStatusUnpaid = 0 // 未支付
OrderStatusPaid = 1 // 已支付
OrderStatusRefunded = 2 // 已退款
OrderStatusFrozen = 3 // 已冻结
OrderStatusPreAuth = 4 // 预授权
)
// 退款状态
const (
RefundStatusFail = 0 // 退款失败
RefundStatusSuccess = 1 // 退款成功
)
// 转账状态
const (
TransferStatusProcessing = 0 // 正在处理
TransferStatusSuccess = 1 // 转账成功
TransferStatusFail = 2 // 转账失败
)
// 转账方式
const (
TransferTypeAlipay = "alipay" // 支付宝
TransferTypeWxpay = "wxpay" // 微信支付
TransferTypeQQpay = "qqpay" // QQ钱包
TransferTypeBank = "bank" // 银行卡
)
// 商户状态
const (
MerchantStatusBanned = 0 // 已封禁
MerchantStatusNormal = 1 // 正常
MerchantStatusPending = 2 // 待审核
)
// 结算方式
const (
SettleTypeAlipay = 1 // 支付宝
SettleTypeWechat = 2 // 微信
SettleTypeQQ = 3 // QQ钱包
SettleTypeBank = 4 // 银行卡
)
// 交易状态
const (
TradeStatusSuccess = "TRADE_SUCCESS" // 交易成功
)

313
utils/pay/notify.go Normal file
View File

@@ -0,0 +1,313 @@
// Package pay 神奇支付回调处理
package pay
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/app"
"git.echol.cn/loser/lckt/model/user"
"git.echol.cn/loser/lckt/model/vip"
"git.echol.cn/loser/lckt/utils/pay/resp"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// NotifyHandler 支付回调处理器
type NotifyHandler struct {
client *Client
}
// NewNotifyHandler 创建回调处理器
func NewNotifyHandler(client *Client) *NotifyHandler {
return &NotifyHandler{client: client}
}
// HandleNotify 处理支付回调通知
// 返回值: notify(通知内容), err(错误信息)
// 调用方应在 err == nil 时返回 "success" 给支付平台
func (h *NotifyHandler) HandleNotify(values url.Values) (*resp.Notify, error) {
// 1. 验签并解析通知
notify, err := h.client.VerifyNotify(values)
if err != nil {
global.GVA_LOG.Error("神奇支付回调验签失败", zap.Error(err))
return nil, fmt.Errorf("验签失败: %w", err)
}
// 2. 处理订单
if err := h.processOrder(notify); err != nil {
global.GVA_LOG.Error("神奇支付回调处理订单失败",
zap.String("out_trade_no", notify.OutTradeNo),
zap.Error(err))
return notify, err
}
global.GVA_LOG.Info("神奇支付回调处理成功",
zap.String("out_trade_no", notify.OutTradeNo),
zap.String("trade_no", notify.TradeNo),
zap.String("money", notify.Money))
return notify, nil
}
// HandleNotifyFromMap 从map处理支付回调通知
func (h *NotifyHandler) HandleNotifyFromMap(params map[string]string) (*resp.Notify, error) {
notify, err := h.client.VerifyNotifyFromMap(params)
if err != nil {
global.GVA_LOG.Error("神奇支付回调验签失败", zap.Error(err))
return nil, fmt.Errorf("验签失败: %w", err)
}
if err := h.processOrder(notify); err != nil {
global.GVA_LOG.Error("神奇支付回调处理订单失败",
zap.String("out_trade_no", notify.OutTradeNo),
zap.Error(err))
return notify, err
}
global.GVA_LOG.Info("神奇支付回调处理成功",
zap.String("out_trade_no", notify.OutTradeNo),
zap.String("trade_no", notify.TradeNo),
zap.String("money", notify.Money))
return notify, nil
}
// processOrder 处理订单业务逻辑(使用事务)
func (h *NotifyHandler) processOrder(notify *resp.Notify) error {
if notify.OutTradeNo == "" {
return errors.New("商户订单号为空")
}
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 1. 查询并锁定订单(防止并发)
var order app.Order
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_no = ?", notify.OutTradeNo).
First(&order).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("订单不存在: %s", notify.OutTradeNo)
}
return fmt.Errorf("查询订单失败: %w", err)
}
// 2. 幂等性检查:如果订单已支付,直接返回成功
if order.Status == 2 {
global.GVA_LOG.Info("订单已处理,跳过重复回调",
zap.String("order_no", notify.OutTradeNo))
return nil
}
// 3. 验证订单状态
if order.Status != 1 {
return fmt.Errorf("订单状态异常: %d", order.Status)
}
// 4. 更新订单状态为已支付
if err := tx.Model(&order).Updates(map[string]interface{}{
"status": 2,
"pay_type": h.getPayType(notify.Type),
}).Error; err != nil {
return fmt.Errorf("更新订单状态失败: %w", err)
}
// 5. 根据订单类型处理业务
switch order.OrderType {
case 1: // 课程订单
return h.handleCourseOrder(tx, &order)
case 2: // 全站VIP订单
return h.handleVipOrder(tx, &order)
case 3: // 讲师包月订单
return h.handleTeacherVipOrder(tx, &order)
default:
global.GVA_LOG.Warn("未知订单类型", zap.Int("order_type", order.OrderType))
return nil
}
})
}
// handleCourseOrder 处理课程订单
func (h *NotifyHandler) handleCourseOrder(tx *gorm.DB, order *app.Order) error {
// 计算讲师分成
return h.addTeacherCommission(tx, order.TeacherId, order.Price)
}
// handleVipOrder 处理VIP订单
func (h *NotifyHandler) handleVipOrder(tx *gorm.DB, order *app.Order) error {
// 1. 查询VIP信息
var vipInfo vip.Vip
if err := tx.Where("id = ?", order.VipId).First(&vipInfo).Error; err != nil {
return fmt.Errorf("查询VIP信息失败: %w", err)
}
// 2. 查询用户信息(锁定)
var userInfo user.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", order.UserId).
First(&userInfo).Error; err != nil {
return fmt.Errorf("查询用户信息失败: %w", err)
}
// 3. 计算会员过期时间
now := time.Now()
var expireTime time.Time
if userInfo.VipExpireTime != "" {
if parsed, err := time.Parse("2006-01-02", userInfo.VipExpireTime); err == nil && parsed.After(now) {
// 未过期,在原有基础上增加
expireTime = parsed.AddDate(0, 0, int(vipInfo.Expiration))
} else {
// 已过期或解析失败,从当前时间开始
expireTime = now.AddDate(0, 0, int(vipInfo.Expiration))
}
} else {
expireTime = now.AddDate(0, 0, int(vipInfo.Expiration))
}
// 4. 确定用户标签
userLabel := userInfo.UserLabel
if vipInfo.Level == 1 {
userLabel = 2 // VIP
} else if vipInfo.Level == 2 {
userLabel = 3 // SVIP
}
// 5. 更新用户信息
if err := tx.Model(&userInfo).Updates(map[string]interface{}{
"is_vip": 1,
"vip_expire_time": expireTime.Format("2006-01-02"),
"user_label": userLabel,
}).Error; err != nil {
return fmt.Errorf("更新用户VIP状态失败: %w", err)
}
return nil
}
// handleTeacherVipOrder 处理讲师包月订单
func (h *NotifyHandler) handleTeacherVipOrder(tx *gorm.DB, order *app.Order) error {
// 1. 解析讲师VIP ID列表
ids := strings.Split(order.TeacherVipId, ",")
now := time.Now()
for _, idStr := range ids {
idStr = strings.TrimSpace(idStr)
if idStr == "" {
continue
}
teacherVipId, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
global.GVA_LOG.Warn("解析讲师VIP ID失败", zap.String("id", idStr), zap.Error(err))
continue
}
// 2. 查询或创建用户讲师VIP记录
var teacherVip app.UserTeacherVip
err = tx.Where("teacher_id = ? AND user_id = ? AND teacher_vip_id = ?",
order.TeacherId, order.UserId, teacherVipId).
Order("id DESC").
First(&teacherVip).Error
if err == nil {
// 记录存在,计算新的过期时间
var newExpireAt time.Time
if expireTime, parseErr := time.Parse("2006-01-02 15:04:05", teacherVip.ExpireAt); parseErr == nil {
if teacherVip.IsExpire == 1 && expireTime.After(now) {
// 未过期,在原有基础上加一个月
newExpireAt = expireTime.AddDate(0, 1, 0)
} else {
// 已过期,从当前时间加一个月
newExpireAt = now.AddDate(0, 1, 0)
}
} else {
newExpireAt = now.AddDate(0, 1, 0)
}
// 更新记录
if err := tx.Model(&teacherVip).Updates(map[string]interface{}{
"expire_at": newExpireAt.Format("2006-01-02 15:04:05"),
"is_expire": 1,
}).Error; err != nil {
return fmt.Errorf("更新讲师VIP记录失败: %w", err)
}
} else if errors.Is(err, gorm.ErrRecordNotFound) {
// 记录不存在,创建新记录
newTeacherVip := app.UserTeacherVip{
TeacherId: uint(order.TeacherId),
UserId: uint(order.UserId),
TeacherVipId: uint(teacherVipId),
ExpireAt: now.AddDate(0, 1, 0).Format("2006-01-02 15:04:05"),
IsExpire: 1,
}
if err := tx.Create(&newTeacherVip).Error; err != nil {
return fmt.Errorf("创建讲师VIP记录失败: %w", err)
}
} else {
return fmt.Errorf("查询讲师VIP记录失败: %w", err)
}
}
// 3. 计算讲师分成
return h.addTeacherCommission(tx, order.TeacherId, order.Price)
}
// addTeacherCommission 增加讲师分成
func (h *NotifyHandler) addTeacherCommission(tx *gorm.DB, teacherId uint64, priceInCent int64) error {
if teacherId == 0 {
return nil
}
// 查询讲师信息(锁定)
var teacher user.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", teacherId).
First(&teacher).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
global.GVA_LOG.Warn("讲师不存在", zap.Uint64("teacher_id", teacherId))
return nil
}
return fmt.Errorf("查询讲师信息失败: %w", err)
}
// 计算分成金额 (价格单位是分,分成比例是百分比)
priceInYuan := float64(priceInCent) / 100.0
commission := priceInYuan * float64(teacher.ExpectRate) / 100.0
if commission <= 0 {
return nil
}
// 更新讲师余额
if err := tx.Model(&teacher).
Update("balance", gorm.Expr("balance + ?", commission)).Error; err != nil {
return fmt.Errorf("更新讲师余额失败: %w", err)
}
global.GVA_LOG.Info("讲师分成成功",
zap.Uint64("teacher_id", teacherId),
zap.Float64("commission", commission),
zap.Int("rate", teacher.ExpectRate))
return nil
}
// getPayType 根据支付方式获取支付类型
func (h *NotifyHandler) getPayType(payType string) int {
switch payType {
case PayTypeWxpay:
return 1 // 微信
case PayTypeAlipay:
return 2 // 支付宝
case PayTypeQQpay:
return 4 // QQ钱包
default:
return 1
}
}

View File

@@ -0,0 +1,159 @@
// Package request 神奇支付SDK请求结构体
package request
// CreateOrder 统一下单请求
type CreateOrder struct {
// Method 接口类型(必填)
// 可选值: web(通用网页支付) / jump(跳转支付) / jsapi(JSAPI支付) / app(APP支付) / scan(付款码支付) / applet(小程序支付)
Method string
// Device 设备类型(仅web需要)
// 可选值: pc(电脑浏览器) / mobile(手机浏览器) / qq(QQ浏览器) / wechat(微信浏览器) / alipay(支付宝客户端)
Device string
// Type 支付方式(必填)
// 可选值: alipay(支付宝) / wxpay(微信支付) / qqpay(QQ钱包)
Type string
// OutTradeNo 商户订单号(必填)
OutTradeNo string
// NotifyURL 异步通知地址(必填)
NotifyURL string
// ReturnURL 跳转通知地址(必填)
ReturnURL string
// Name 商品名称(必填)超过127字节会自动截取
Name string
// Money 商品金额(必填)单位最大2位小数
Money string
// ClientIP 用户IP地址(必填)
ClientIP string
// Param 业务扩展参数(可选),支付后原样返回
Param string
// AuthCode 被扫支付授权码(scan模式需要)
AuthCode string
// SubOpenid 用户Openid(jsapi需要)
SubOpenid string
// SubAppid 公众号/小程序AppId(jsapi需要)
SubAppid string
// ChannelID 自定义通道ID(可选)对应进件商户列表的ID
ChannelID int
}
// SubmitOrder 页面跳转支付请求(用于构建URL)
type SubmitOrder struct {
// Type 支付方式,不传会跳转到收银台
// 可选值: alipay(支付宝) / wxpay(微信支付) / qqpay(QQ钱包)
Type string
// OutTradeNo 商户订单号(必填)
OutTradeNo string
// NotifyURL 异步通知地址(必填)
NotifyURL string
// ReturnURL 跳转通知地址(必填)
ReturnURL string
// Name 商品名称(必填)
Name string
// Money 商品金额(必填),单位:元
Money string
// Param 业务扩展参数(可选)
Param string
// ChannelID 自定义通道ID(可选)
ChannelID int
}
// QueryOrder 订单查询请求
type QueryOrder struct {
// TradeNo 平台订单号(与商户订单号二选一)
TradeNo string
// OutTradeNo 商户订单号(与平台订单号二选一)
OutTradeNo string
}
// Refund 订单退款请求
type Refund struct {
// TradeNo 平台订单号(与商户订单号二选一)
TradeNo string
// OutTradeNo 商户订单号(与平台订单号二选一)
OutTradeNo string
// Money 退款金额(必填),单位:元
Money string
// OutRefundNo 商户退款单号(可选),可避免重复请求退款
OutRefundNo string
}
// RefundQuery 退款查询请求
type RefundQuery struct {
// RefundNo 平台退款单号(与商户退款单号二选一)
RefundNo string
// OutRefundNo 商户退款单号(与平台退款单号二选一)
OutRefundNo string
}
// OrderList 订单列表查询请求
type OrderList struct {
// Offset 查询偏移从0开始
Offset int
// Limit 每页条数最大50
Limit int
// Status 过滤订单状态(可选)
// -1: 不过滤, 0: 未支付, 1: 已支付
Status int
}
// Transfer 转账请求
type Transfer struct {
// Type 转账方式(必填)
// 可选值: alipay(支付宝) / wxpay(微信) / qqpay(QQ钱包) / bank(银行卡)
Type string
// Account 收款方账号(必填)
// 支付宝账号 / 微信OpenId / 银行卡号
Account string
// Name 收款方姓名(可选),传入则校验账号与姓名是否匹配
Name string
// Money 转账金额(必填),单位:元
Money string
// Remark 转账备注(可选)
Remark string
// OutBizNo 转账交易号(可选),可避免重复请求转账
OutBizNo string
// BookID 安全发账本ID(可选),仅支付宝安全发转账可传
BookID string
}
// TransferQuery 转账查询请求
type TransferQuery struct {
// BizNo 系统交易号(与商户交易号二选一)
BizNo string
// OutBizNo 商户交易号(与系统交易号二选一)
OutBizNo string
}

357
utils/pay/resp/response.go Normal file
View File

@@ -0,0 +1,357 @@
// Package resp 神奇支付SDK响应结构体
package resp
// Base 基础响应字段
type Base struct {
Code int `json:"code"` // 返回状态码0为成功
Msg string `json:"msg"` // 返回信息/错误信息
Timestamp string `json:"timestamp"` // 时间戳
Sign string `json:"sign"` // 签名
SignType string `json:"sign_type"` // 签名类型
}
// IsSuccess 判断是否成功
func (b *Base) IsSuccess() bool {
return b.Code == 0
}
// CreateOrder 统一下单响应
type CreateOrder struct {
Base
// TradeNo 平台订单号
TradeNo string `json:"trade_no"`
// PayType 发起支付类型
// jump: 跳转url / html: html代码 / qrcode: 二维码 / urlscheme: 小程序url scheme
// jsapi: JSAPI参数 / app: APP支付参数 / scan: 付款码成功 / wxplugin: 小程序插件 / wxapp: 拉起小程序
PayType string `json:"pay_type"`
// PayInfo 发起支付参数根据PayType不同返回不同内容
PayInfo string `json:"pay_info"`
}
// QueryOrder 订单查询响应
type QueryOrder struct {
Base
// TradeNo 平台订单号
TradeNo string `json:"trade_no"`
// OutTradeNo 商户订单号
OutTradeNo string `json:"out_trade_no"`
// ApiTradeNo 接口订单号(微信/支付宝返回的单号)
ApiTradeNo string `json:"api_trade_no"`
// Type 支付方式
Type string `json:"type"`
// Status 支付状态
// 0: 未支付 / 1: 已支付 / 2: 已退款 / 3: 已冻结 / 4: 预授权
Status int `json:"status"`
// PID 商户ID
PID int `json:"pid"`
// Addtime 订单创建时间
Addtime string `json:"addtime"`
// Endtime 订单完成时间(仅支付完成才返回)
Endtime string `json:"endtime"`
// Name 商品名称
Name string `json:"name"`
// Money 商品金额
Money string `json:"money"`
// RefundMoney 已退款金额(仅部分退款情况返回)
RefundMoney string `json:"refundmoney"`
// Param 业务扩展参数
Param string `json:"param"`
// Buyer 支付用户标识(一般为openid)
Buyer string `json:"buyer"`
// ClientIP 支付用户IP
ClientIP string `json:"clientip"`
}
// IsPaid 判断是否已支付
func (q *QueryOrder) IsPaid() bool {
return q.Status == 1
}
// IsRefunded 判断是否已退款
func (q *QueryOrder) IsRefunded() bool {
return q.Status == 2
}
// Refund 订单退款响应
type Refund struct {
Base
// RefundNo 平台退款单号
RefundNo string `json:"refund_no"`
// OutRefundNo 商户退款单号
OutRefundNo string `json:"out_refund_no"`
// TradeNo 平台订单号
TradeNo string `json:"trade_no"`
// Money 退款金额
Money string `json:"money"`
// ReduceMoney 扣减商户余额
ReduceMoney string `json:"reducemoney"`
}
// RefundQuery 退款查询响应
type RefundQuery struct {
Base
// RefundNo 平台退款单号
RefundNo string `json:"refund_no"`
// OutRefundNo 商户退款单号
OutRefundNo string `json:"out_refund_no"`
// TradeNo 平台订单号
TradeNo string `json:"trade_no"`
// OutTradeNo 商户订单号
OutTradeNo string `json:"out_trade_no"`
// Money 退款金额
Money string `json:"money"`
// ReduceMoney 扣减商户余额
ReduceMoney string `json:"reducemoney"`
// Status 退款状态: 0失败 / 1成功
Status int `json:"status"`
// Addtime 退款时间
Addtime string `json:"addtime"`
}
// IsRefundSuccess 判断退款是否成功
func (r *RefundQuery) IsRefundSuccess() bool {
return r.Status == 1
}
// Notify 支付结果通知
type Notify struct {
// PID 商户ID
PID int `json:"pid"`
// TradeNo 平台订单号
TradeNo string `json:"trade_no"`
// OutTradeNo 商户订单号
OutTradeNo string `json:"out_trade_no"`
// ApiTradeNo 接口订单号
ApiTradeNo string `json:"api_trade_no"`
// Type 支付方式
Type string `json:"type"`
// TradeStatus 交易状态,固定为 TRADE_SUCCESS
TradeStatus string `json:"trade_status"`
// Addtime 订单创建时间
Addtime string `json:"addtime"`
// Endtime 订单完成时间
Endtime string `json:"endtime"`
// Name 商品名称
Name string `json:"name"`
// Money 商品金额
Money string `json:"money"`
// Param 业务扩展参数
Param string `json:"param"`
// Buyer 支付用户标识
Buyer string `json:"buyer"`
// Timestamp 时间戳
Timestamp string `json:"timestamp"`
// Sign 签名
Sign string `json:"sign"`
// SignType 签名类型
SignType string `json:"sign_type"`
}
// MerchantInfo 商户信息响应
type MerchantInfo struct {
Base
// PID 商户ID
PID int `json:"pid"`
// Status 商户状态: 0封禁 / 1正常 / 2待审核
Status int `json:"status"`
// PayStatus 支付状态: 0关闭 / 1开启
PayStatus int `json:"pay_status"`
// SettleStatus 结算状态: 0关闭 / 1开启
SettleStatus int `json:"settle_status"`
// Money 商户余额
Money string `json:"money"`
// SettleType 结算方式: 1支付宝 / 2微信 / 3QQ钱包 / 4银行卡
SettleType int `json:"settle_type"`
// SettleAccount 结算账户
SettleAccount string `json:"settle_account"`
// SettleName 结算账户姓名
SettleName string `json:"settle_name"`
// OrderNum 订单总数量
OrderNum int `json:"order_num"`
// OrderNumToday 今日订单数量
OrderNumToday int `json:"order_num_today"`
// OrderNumLastday 昨日订单数量
OrderNumLastday int `json:"order_num_lastday"`
// OrderMoneyToday 今日订单收入
OrderMoneyToday string `json:"order_money_today"`
// OrderMoneyLastday 昨日订单收入
OrderMoneyLastday string `json:"order_money_lastday"`
}
// IsNormal 判断商户状态是否正常
func (m *MerchantInfo) IsNormal() bool {
return m.Status == 1
}
// IsPayEnabled 判断支付是否开启
func (m *MerchantInfo) IsPayEnabled() bool {
return m.PayStatus == 1
}
// OrderList 订单列表响应
type OrderList struct {
Base
// Data 订单列表
Data []OrderItem `json:"data"`
}
// OrderItem 订单列表项
type OrderItem struct {
TradeNo string `json:"trade_no"`
OutTradeNo string `json:"out_trade_no"`
ApiTradeNo string `json:"api_trade_no"`
Type string `json:"type"`
Status int `json:"status"`
PID int `json:"pid"`
Addtime string `json:"addtime"`
Endtime string `json:"endtime"`
Name string `json:"name"`
Money string `json:"money"`
Param string `json:"param"`
Buyer string `json:"buyer"`
ClientIP string `json:"clientip"`
}
// Transfer 转账响应
type Transfer struct {
Base
// Status 转账状态: 0正在处理 / 1转账成功
Status int `json:"status"`
// BizNo 系统交易号
BizNo string `json:"biz_no"`
// OutBizNo 商户交易号
OutBizNo string `json:"out_biz_no"`
// OrderID 接口转账单号(支付宝/微信返回)
OrderID string `json:"orderid"`
// PayDate 转账完成时间
PayDate string `json:"paydate"`
// CostMoney 转账花费金额
CostMoney string `json:"cost_money"`
}
// IsTransferSuccess 判断转账是否成功
func (t *Transfer) IsTransferSuccess() bool {
return t.Status == 1
}
// IsProcessing 判断是否正在处理
func (t *Transfer) IsProcessing() bool {
return t.Status == 0
}
// TransferQuery 转账查询响应
type TransferQuery struct {
Base
// Status 转账状态: 0正在处理 / 1转账成功 / 2转账失败
Status int `json:"status"`
// ErrMsg 转账失败原因(status=2时返回)
ErrMsg string `json:"errmsg"`
// BizNo 系统交易号
BizNo string `json:"biz_no"`
// OutBizNo 商户交易号
OutBizNo string `json:"out_biz_no"`
// OrderID 接口转账单号
OrderID string `json:"orderid"`
// PayDate 转账完成时间
PayDate string `json:"paydate"`
// Amount 转账金额
Amount string `json:"amount"`
// CostMoney 转账花费金额
CostMoney string `json:"cost_money"`
// Remark 转账备注
Remark string `json:"remark"`
}
// IsTransferSuccess 判断转账是否成功
func (t *TransferQuery) IsTransferSuccess() bool {
return t.Status == 1
}
// IsTransferFailed 判断转账是否失败
func (t *TransferQuery) IsTransferFailed() bool {
return t.Status == 2
}
// Balance 余额查询响应
type Balance struct {
Base
// AvailableMoney 商户可用余额
AvailableMoney string `json:"available_money"`
// TransferRate 转账手续费率(%)
TransferRate string `json:"transfer_rate"`
}

249
utils/pay/sign.go Normal file
View File

@@ -0,0 +1,249 @@
// Package pay 神奇支付SDK签名工具
package pay
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"sort"
"strconv"
"strings"
)
// Signer RSA签名器
type Signer struct {
privateKey *rsa.PrivateKey // 商户私钥
platformPubKey *rsa.PublicKey // 平台公钥
}
// NewSigner 创建签名器
func NewSigner(privateKeyPEM, platformPubKeyPEM string) (*Signer, error) {
privateKey, err := parsePrivateKey(privateKeyPEM)
if err != nil {
return nil, fmt.Errorf("解析商户私钥失败: %w", err)
}
platformPubKey, err := parsePublicKey(platformPubKeyPEM)
if err != nil {
return nil, fmt.Errorf("解析平台公钥失败: %w", err)
}
return &Signer{
privateKey: privateKey,
platformPubKey: platformPubKey,
}, nil
}
// parsePrivateKey 解析RSA私钥
// 支持: 纯Base64字符串、PKCS1格式、PKCS8格式
func parsePrivateKey(keyStr string) (*rsa.PrivateKey, error) {
keyStr = strings.TrimSpace(keyStr)
if keyStr == "" {
return nil, errors.New("私钥不能为空")
}
// 提取纯Base64内容
base64Content := extractBase64Content(keyStr)
if base64Content == "" {
return nil, errors.New("私钥内容为空")
}
// 验证是否是有效的Base64并解码
derBytes, err := base64.StdEncoding.DecodeString(base64Content)
if err != nil {
return nil, fmt.Errorf("私钥Base64解码失败: %w", err)
}
// 直接尝试解析DER格式不需要PEM包装
// 尝试 PKCS8 格式
if key, err := x509.ParsePKCS8PrivateKey(derBytes); err == nil {
if rsaKey, ok := key.(*rsa.PrivateKey); ok {
return rsaKey, nil
}
return nil, errors.New("解析成功但不是RSA私钥")
}
// 尝试 PKCS1 格式
if rsaKey, err := x509.ParsePKCS1PrivateKey(derBytes); err == nil {
return rsaKey, nil
}
// 检查是否误传了公钥
if _, err := x509.ParsePKIXPublicKey(derBytes); err == nil {
return nil, errors.New("您配置的是公钥而不是私钥!请到神奇支付商户后台获取正确的【商户私钥】")
}
return nil, fmt.Errorf("无法解析私钥格式,密钥长度=%d字节请确认私钥内容正确", len(derBytes))
}
// extractBase64Content 从各种格式中提取纯Base64内容
func extractBase64Content(keyStr string) string {
// 去除首尾空白
keyStr = strings.TrimSpace(keyStr)
// 如果包含PEM头尾提取中间内容
if strings.Contains(keyStr, "-----BEGIN") {
keyStr = strings.ReplaceAll(keyStr, "\r\n", "\n")
lines := strings.Split(keyStr, "\n")
var base64Lines []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "-----") {
continue
}
base64Lines = append(base64Lines, line)
}
keyStr = strings.Join(base64Lines, "")
}
// 移除所有空白字符
keyStr = strings.ReplaceAll(keyStr, "\n", "")
keyStr = strings.ReplaceAll(keyStr, "\r", "")
keyStr = strings.ReplaceAll(keyStr, " ", "")
keyStr = strings.ReplaceAll(keyStr, "\t", "")
return keyStr
}
// parsePublicKey 解析RSA公钥
// 支持: 纯Base64字符串、PKIX格式、PKCS1格式
func parsePublicKey(keyStr string) (*rsa.PublicKey, error) {
keyStr = strings.TrimSpace(keyStr)
if keyStr == "" {
return nil, errors.New("公钥不能为空")
}
// 提取纯Base64内容
base64Content := extractBase64Content(keyStr)
if base64Content == "" {
return nil, errors.New("公钥内容为空")
}
// 验证是否是有效的Base64并解码
derBytes, err := base64.StdEncoding.DecodeString(base64Content)
if err != nil {
return nil, fmt.Errorf("公钥Base64解码失败: %w", err)
}
// 尝试 PKIX 格式 (最常用)
if pub, err := x509.ParsePKIXPublicKey(derBytes); err == nil {
if rsaKey, ok := pub.(*rsa.PublicKey); ok {
return rsaKey, nil
}
return nil, errors.New("不是RSA公钥")
}
// 尝试 PKCS1 格式
if rsaKey, err := x509.ParsePKCS1PublicKey(derBytes); err == nil {
return rsaKey, nil
}
return nil, fmt.Errorf("无法解析公钥格式,密钥长度=%d字节", len(derBytes))
}
// Sign 使用商户私钥进行SHA256WithRSA签名
func (s *Signer) Sign(data string) (string, error) {
hash := sha256.Sum256([]byte(data))
signature, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, hash[:])
if err != nil {
return "", fmt.Errorf("签名失败: %w", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
// Verify 使用平台公钥验证签名
func (s *Signer) Verify(data, signature string) error {
sig, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return fmt.Errorf("签名base64解码失败: %w", err)
}
hash := sha256.Sum256([]byte(data))
if err := rsa.VerifyPKCS1v15(s.platformPubKey, crypto.SHA256, hash[:], sig); err != nil {
return fmt.Errorf("验签失败: %w", err)
}
return nil
}
// BuildSignString 构建待签名字符串
// 规则:
// 1. 获取所有非空参数,排除 sign 和 sign_type
// 2. 按照 key 的 ASCII 码升序排序
// 3. 用 & 连接成 key=value 格式
func BuildSignString(params map[string]string) string {
// 过滤并收集有效的 key
var keys []string
for k, v := range params {
if k == "sign" || k == "sign_type" || v == "" {
continue
}
keys = append(keys, k)
}
// 按 ASCII 码升序排序
sort.Strings(keys)
// 拼接成 key=value 格式
var pairs []string
for _, k := range keys {
pairs = append(pairs, k+"="+params[k])
}
return strings.Join(pairs, "&")
}
// SignParams 对参数进行签名,会自动添加 sign 和 sign_type 字段
func (s *Signer) SignParams(params map[string]string) error {
signStr := BuildSignString(params)
sign, err := s.Sign(signStr)
if err != nil {
return err
}
params["sign"] = sign
params["sign_type"] = "RSA"
return nil
}
// VerifyParams 验证参数签名
func (s *Signer) VerifyParams(params map[string]string) error {
sign, ok := params["sign"]
if !ok || sign == "" {
return errors.New("缺少签名参数")
}
signStr := BuildSignString(params)
return s.Verify(signStr, sign)
}
// MapToStringMap 将 interface{} map 转换为 string map (用于验签)
func MapToStringMap(data map[string]interface{}) map[string]string {
result := make(map[string]string)
for k, v := range data {
if v == nil {
continue
}
switch val := v.(type) {
case string:
result[k] = val
case float64:
// JSON 数字默认解析为 float64
if val == float64(int64(val)) {
result[k] = strconv.FormatInt(int64(val), 10)
} else {
result[k] = strconv.FormatFloat(val, 'f', -1, 64)
}
case int:
result[k] = strconv.Itoa(val)
case int64:
result[k] = strconv.FormatInt(val, 10)
case bool:
result[k] = strconv.FormatBool(val)
default:
result[k] = fmt.Sprintf("%v", v)
}
}
return result
}