🎨 新增支付动态配置&优化支付回调&新增三方支付
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,4 +22,5 @@ log
|
||||
# Go workspace file
|
||||
go.work
|
||||
.kiro
|
||||
|
||||
.vscode
|
||||
/test
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
204
api/v1/system/sys_pay_config.go
Normal file
204
api/v1/system/sys_pay_config.go
Normal 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)
|
||||
}
|
||||
59
config.yaml
59
config.yaml
@@ -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
|
||||
|
||||
@@ -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
23
config/shenqi_pay.go
Normal 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"`
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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) // 文件上传下载分类
|
||||
|
||||
@@ -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:'订单编号参数不能为空'"` // 订单编号
|
||||
}
|
||||
|
||||
96
model/system/request/sys_pay_config.go
Normal file
96
model/system/request/sys_pay_config.go
Normal 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
|
||||
}
|
||||
161
model/system/sys_pay_config.go
Normal file
161
model/system/sys_pay_config.go
Normal 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地址
|
||||
}
|
||||
@@ -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) // 获取订单列表
|
||||
|
||||
@@ -20,6 +20,7 @@ type RouterGroup struct {
|
||||
SysExportTemplateRouter
|
||||
SysParamsRouter
|
||||
SysVersionRouter
|
||||
PayConfigRouter
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
23
router/system/sys_pay_config.go
Normal file
23
router/system/sys_pay_config.go
Normal 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) // 切换支付配置状态
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -18,6 +18,7 @@ type ServiceGroup struct {
|
||||
SysExportTemplateService
|
||||
SysParamsService
|
||||
SysVersionService
|
||||
PayConfigService
|
||||
AutoCodePlugin autoCodePlugin
|
||||
AutoCodePackage autoCodePackage
|
||||
AutoCodeHistory autoCodeHistory
|
||||
|
||||
291
service/system/sys_pay_config.go
Normal file
291
service/system/sys_pay_config.go
Normal 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
|
||||
}
|
||||
97
source/system/pay_config.go
Normal file
97
source/system/pay_config.go
Normal 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
480
utils/pay/client.go
Normal 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
109
utils/pay/constants.go
Normal 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
313
utils/pay/notify.go
Normal 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
|
||||
}
|
||||
}
|
||||
159
utils/pay/request/request.go
Normal file
159
utils/pay/request/request.go
Normal 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
357
utils/pay/resp/response.go
Normal 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
249
utils/pay/sign.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user