Compare commits

..

3 Commits

Author SHA1 Message Date
9f220dc025 🎨 添加微信支付证书 2025-07-25 23:03:11 +08:00
7951f77f47 🎨 优化文章相关接口 2025-07-25 23:02:57 +08:00
bb2a68fb61 🎨 完善订单和微信支付功能 2025-07-25 23:02:43 +08:00
15 changed files with 637 additions and 51 deletions

View File

@@ -1,3 +1,131 @@
package app package app
import (
"git.echol.cn/loser/lckt/global"
"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/utils/user_jwt"
"git.echol.cn/loser/lckt/utils/wechat"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type OrderApi struct{} type OrderApi struct{}
// CreateOrder APP新建订单
func (o *OrderApi) CreateOrder(c *gin.Context) {
var p app.Order
if err := c.ShouldBind(&p); err != nil {
global.GVA_LOG.Error("支付订单参数错误", zap.Error(err))
r.FailWithMessage("支付订单参数错误", c)
return
}
order, err := orderService.Create(&p)
if err != nil {
global.GVA_LOG.Error("创建订单失败", zap.Error(err))
r.FailWithMessage("创建订单失败", c)
return
}
r.OkWithData(order, c)
}
func (o *OrderApi) PayOrder(context *gin.Context) {
var p request.PayReq
if err := context.ShouldBind(&p); err != nil {
global.GVA_LOG.Error("支付订单参数错误", zap.Error(err))
r.FailWithMessage("支付订单参数错误", context)
return
}
pay, err := orderService.Pay(p, context)
if err != nil {
global.GVA_LOG.Error("支付订单失败", zap.Error(err))
r.FailWithMessage("支付订单失败:"+err.Error(), context)
return
}
r.OkWithDetailed(pay, "支付订单成功", context)
}
// NotifyOrder 微信支付回调通知
func (o *OrderApi) NotifyOrder(context *gin.Context) {
err := wechat.NotifyHandle(context)
if err != nil {
global.GVA_LOG.Error("微信支付回调处理失败", zap.Error(err))
r.FailWithMessage("微信支付回调处理失败:"+err.Error(), context)
return
}
r.OkWithMessage("微信支付回调处理成功", context)
}
func (o *OrderApi) GetOrderDetail(context *gin.Context) {
id := context.Param("id")
if id == "" {
global.GVA_LOG.Error("获取订单详情参数错误: ID不能为空")
r.FailWithMessage("获取订单详情参数错误: ID不能为空", context)
return
}
order, err := orderService.GetOrderDetail(id)
if err != nil {
global.GVA_LOG.Error("获取订单详情失败", zap.Error(err))
r.FailWithMessage("获取订单详情失败", context)
return
}
r.OkWithData(order, context)
}
func (o *OrderApi) GetOrderList(context *gin.Context) {
var p request.GetOrderList
if err := context.ShouldBind(&p); err != nil {
global.GVA_LOG.Error("获取订单列表参数错误", zap.Error(err))
r.FailWithMessage("获取订单列表参数错误", context)
return
}
// 获取当前用户ID
userId := user_jwt.GetUserID(context)
if userId == 0 {
global.GVA_LOG.Error("获取用户ID失败")
r.FailWithMessage("获取用户ID失败", context)
return
}
// 获取订单列表
orders, total, err := orderService.GetOrderList(p, userId)
if err != nil {
global.GVA_LOG.Error("获取订单列表失败", zap.Error(err))
r.FailWithMessage("获取订单列表失败:"+err.Error(), context)
return
}
r.OkWithDetailed(
r.PageResult{
List: orders,
Total: total,
Page: p.Page,
PageSize: p.PageSize,
}, "获取订单列表成功", context)
}
func (o *OrderApi) BalancePay(context *gin.Context) {
var p request.BalancePay
if err := context.ShouldBind(&p); err != nil {
global.GVA_LOG.Error("余额支付参数错误", zap.Error(err))
r.FailWithMessage("余额支付参数错误", context)
return
}
err := orderService.BalancePay(p)
if err != nil {
global.GVA_LOG.Error("余额支付失败", zap.Error(err))
r.FailWithMessage("余额支付失败:"+err.Error(), context)
return
}
r.OkWithMessage("余额支付成功", context)
}

View File

@@ -249,13 +249,13 @@ wechat:
pay-list: pay-list:
- type: wxpay - type: wxpay
alias-name: wxpay-1 alias-name: wxpay-1
app-id: 1 app-id: wx3d21df18d7f8f9fc
mch-id: 1 mch-id: 1646874753
v3-key: 1a3sd8561d5179Df152D4789aD38wG9s v3-key: 1a3sd8561d5179Df152D4789aD38wG9s
cert-path: 1 cert-path: /resource/cert/apiclient_cert.pem
key-path: 1 key-path: /resource/cert/apiclient_key.pem
notify-url: 1 notify-url: http://lckt.hnlc5588.cn/app_order/notify
serial-no: 1 serial-no: 59A891FB403EC7A1CF2090DB9C8EC704BD43B101
- type: wxpay2 - type: wxpay2
alias-name: wxpay-2 alias-name: wxpay-2
app-id: 2 app-id: 2

View File

@@ -119,6 +119,7 @@ func Routers() *gin.Engine {
{ {
appRouter.InitAppUserRouter(AppAuthGroup, PublicGroup) appRouter.InitAppUserRouter(AppAuthGroup, PublicGroup)
appRouter.InitBannerRouter(PrivateGroup, PublicGroup) // Banner相关路由 appRouter.InitBannerRouter(PrivateGroup, PublicGroup) // Banner相关路由
appRouter.InitOrderRouter(AppAuthGroup) // 订单相关路由
} }
//插件路由安装 //插件路由安装

View File

@@ -44,5 +44,6 @@ func main() {
} }
wechat.InitWeOfficial() // 初始化微信公众号SDK wechat.InitWeOfficial() // 初始化微信公众号SDK
wechat.InitWechatPay() // 初始化微信支付SDK
core.RunWindowsServer() core.RunWindowsServer()
} }

View File

@@ -8,12 +8,15 @@ type Order struct {
UserId uint64 `gorm:"column:user_id;type:bigint(20) unsigned;comment:用户ID;NOT NULL" json:"user_id"` UserId uint64 `gorm:"column:user_id;type:bigint(20) unsigned;comment:用户ID;NOT NULL" json:"user_id"`
OrderType int `gorm:"column:order_type;type:int(11);comment:订单类型 |1 课程|2 vip|;NOT NULL" json:"order_type"` OrderType int `gorm:"column:order_type;type:int(11);comment:订单类型 |1 课程|2 vip|;NOT NULL" json:"order_type"`
ArticleId uint `gorm:"column:article_id;type:bigint(20) unsigned;default:0;comment:文章ID;NOT NULL" json:"article_id"` ArticleId uint `gorm:"column:article_id;type:bigint(20) unsigned;default:0;comment:文章ID;NOT NULL" json:"article_id"`
Title string `gorm:"column:title;type:varchar(255);comment:订单标题;NOT NULL" json:"title"`
Name string `gorm:"column:name;type:varchar(255);comment:名称;NOT NULL" json:"name"` Name string `gorm:"column:name;type:varchar(255);comment:名称;NOT NULL" json:"name"`
Price int64 `gorm:"column:price;type:int(11) unsigned;default:0;comment:订单价格;NOT NULL" json:"price"` Price int64 `gorm:"column:price;type:int(11) unsigned;default:0;comment:订单价格;NOT NULL" json:"price"`
Phone string `gorm:"column:phone;type:varchar(11);comment:手机号;NOT NULL" json:"phone"` Phone string `gorm:"column:phone;type:varchar(11);comment:手机号;NOT NULL" json:"phone"`
TeacherId uint64 `gorm:"column:teacher_Id;type:bigint(20);comment:教师Id;NOT NULL" json:"teacher_Id"` TeacherId uint64 `gorm:"column:teacher_Id;type:bigint(20);comment:教师Id;NOT NULL" json:"teacher_Id"`
Status int `gorm:"column:status;type:int(11);default:1;comment:订单状态 |1 未付款|2 已付款|3 已过期|;NOT NULL" json:"status"` Status int `gorm:"column:status;type:int(11);default:1;comment:订单状态 |1 未付款|2 已付款|3 已过期|;NOT NULL" json:"status"`
Desc string `gorm:"column:desc;type:varchar(24);comment:订单描述;NOT NULL" json:"desc"` Desc string `gorm:"column:desc;type:varchar(24);comment:订单描述;NOT NULL" json:"desc"`
OpenId string `gorm:"column:open_id;type:varchar(64);comment:用户OpenId;NOT NULL" json:"open_id"`
PayType int `gorm:"column:pay_type;type:int(11);default:1;comment:支付方式 |1 微信|2 支付宝|3 余额|;NOT NULL" json:"pay_type"`
} }
// TableName Order表 // TableName Order表

View File

@@ -0,0 +1,23 @@
package request
import common "git.echol.cn/loser/lckt/model/common/request"
type BalancePay struct {
UserId uint `json:"user_id" binding:"required"` // 用户ID
OrderNo string `json:"order_no" binding:"required"` // 订单号
OrderId uint `json:"order_id" binding:"required"` // 订单ID
}
type GetOrderList struct {
common.PageInfo
Status int `json:"status" form:"status"` // 订单状态
}
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:'订单编号参数不能为空'"`
}

Binary file not shown.

View File

@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEKzCCAxOgAwIBAgIUWaiR+0A+x6HPIJDbnI7HBL1DsQEwDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUwNzIzMDU1MDU5WhcNMzAwNzIyMDU1MDU5WjCBhDETMBEGA1UEAwwK
MTY0Njg3NDc1MzEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTAwLgYDVQQL
DCfmtbflj6PpvpnljY7pk4HlnZrmiJDnlLXlrZDllYbliqHllYbooYwxCzAJBgNV
BAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAMUD4rf7ct0zUxJKGww+8c38L/IBx9yeNpOc7lav+CEwqZvieesy
p1I2hbY+hwOZm23//0Vxcnl6fkr5wdidE/RsN808sGkYsetJ6q7LZsgsxnbaQd7w
rBc4GlOh1Q4teErbjNTUMuozYzikEjMjgZMxs7i5WJnFMMVvgJS4681UKH8MQG93
BnaaD5zEyofAnOov7mvC0KZ0RpqwiLdUsMaPzYILzccOv7Tj82Z/qLw8AFcI+7L1
mq8trZxmSPXP5Fypz+1VKLKR9MaELUuQLPouewDjXkKTBnxkhoWow6huuz3qyUUo
i7uJtnmDn6Eu+60ch61BB5ws3rhWDpltR/0CAwEAAaOBuTCBtjAJBgNVHRMEAjAA
MAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDovL2V2
Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUwREJD
MDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJFMTJC
MjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IBAQBk
WmviQM60S/l4I4/RtQWtiZS+85fk2ePVSk5gBUS27W4yTw+aXy5RYaCLcOVXTXje
PpAfE7QvkjZxMg/k0SJR0/NbgDNF6xuCbMj78+czWVCkqHwPjY4hWRdsM9s44Orp
lwFrG/UNRhPgXcdwNQj8MNzy3nGeO0HWbJFBWcuRhSSwXpxv3Sh6pZviL3QA+lrV
7QB7d0fEu8sSGbwnIB0oXl7y78l2/D20p57TKHsq5IPfQfDdExNXKV60ikMt/MdP
C4ygbeRpSAobr6qMTXcpWlbH3KfIPOUFny3nkN8C6lm//QUl7ynOXkECcmIHY6XF
yMx70ORdoBl1DaZ369P9
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFA+K3+3LdM1MS
ShsMPvHN/C/yAcfcnjaTnO5Wr/ghMKmb4nnrMqdSNoW2PocDmZtt//9FcXJ5en5K
+cHYnRP0bDfNPLBpGLHrSequy2bILMZ22kHe8KwXOBpTodUOLXhK24zU1DLqM2M4
pBIzI4GTMbO4uViZxTDFb4CUuOvNVCh/DEBvdwZ2mg+cxMqHwJzqL+5rwtCmdEaa
sIi3VLDGj82CC83HDr+04/Nmf6i8PABXCPuy9ZqvLa2cZkj1z+Rcqc/tVSiykfTG
hC1LkCz6LnsA415CkwZ8ZIaFqMOobrs96slFKIu7ibZ5g5+hLvutHIetQQecLN64
Vg6ZbUf9AgMBAAECggEBAI4q66PQM2cj7kI4b6Q6l8sIvKBqYIr3MHL8v5CWkvuA
XiQ7Hbd3af6NkZedL1iNs/eAz/iQkQbQOepoqFVjpE6w+OOFc9ejFmCvikZwSM8S
YHTLstTp34Ux2u2WzmPYtAFwxQOfzM3sHyF0ZB269Xn+V65pMWJlRXhzqdmoR6B2
YYLzHVBZAbgj7IlS+f1WPkkbrb3BIs71tYyPS3Ng6nhGgXrnqmE5fhm93wR9KR5R
RcgzFGH/8qD2LndmJz8tYtjvlq016Pp40VrYabIrvX6apglayShLQ+2XUe8VKH2Q
dQPIKL3R0ZeDeqwn4PTMclBYd/nXFSbIQndufTDY84ECgYEA8RTcCPOFYkoDeQIr
yKqWo1P1cKsB/bGzvLmo+FqFQij5lmj9iDdvpSQJ58Uj+TFy5qNRT6VueF8U4pNd
Fmcbw05cH+9NzAXji6aYWCargbmhW4L+rzub+TQmdAfqXbBknVgrLenY4LFnFFm1
SIdJRid3vCwiRuhcjecg4FasR+kCgYEA0TTw4ZGxPCX5qt00PUFvuGpqFxFqZ7Ah
wapwQTUY48WeC0Pg7Iq6ipnhjggkgYLkGc1klmTwJXKV0acIS5/DNclepMWZPwIc
+GcKclQsy8MHm+L2tPGBQosRGiSrXi5LzBIQsYSyhBRDUSqu+yj7TTd2XqpUkLAw
qdrWiIjVBvUCgYAOkh0uaVGBfEmzcZ8l1LGgE339HkjThX8AhBQjVo1BT2quXZAd
QIR97ayvlmmzMPrp16sdbjk8Czse6pswtHCoID9PKs5/60cydJI2mbe58nc/Ka6s
9qRZrn44exX+LaAXJnINp1mVUwOQ5k8foBWcqNwCwoQb1wVpCjQhevuUqQKBgQC1
SA+3Fr0iprF6iqWastoxThzSEmhGowwNOjh9eJoxvOsfTdlYfzn3ojIeFhY0F4y6
gw03eQ3TFUCXZAq/JRhNwkl9tC//tkAOS5N00FXk1wH/5aLr1h2w4LqYEdBhEvLh
SYInoRnjc3+FlNv9jVx9Y6Lxkt0mZ1YzyQp/UzptBQKBgFTOonik05rMHGlEg9Ng
unjDAEK+7tXhM/RKxWDs7pZeGbdl1c/NDOX4wIbuK2cCkV0v/qfg6Ex2Obf5rqM4
R5lrs9y30rZbwmfms8G3cimTAsOcLqObKkBED+g07t+3bw44JXLwYLPm/z//bIcI
fkxyxKL5ZJhP3/bPJJj8UAq3
-----END PRIVATE KEY-----

View File

@@ -5,7 +5,9 @@ import api "git.echol.cn/loser/lckt/api/v1"
type RouterGroup struct { type RouterGroup struct {
UserRouter UserRouter
BannerRouter BannerRouter
OrderRouter
} }
var userApi = api.ApiGroupApp.AppApiGroup.AppUserApi var userApi = api.ApiGroupApp.AppApiGroup.AppUserApi
var bannerApi = api.ApiGroupApp.AppApiGroup.BannerApi var bannerApi = api.ApiGroupApp.AppApiGroup.BannerApi
var orderApi = api.ApiGroupApp.AppApiGroup.OrderApi

18
router/app/order.go Normal file
View File

@@ -0,0 +1,18 @@
package app
import "github.com/gin-gonic/gin"
type OrderRouter struct{}
// InitOrderRouter 初始化订单路由
func (r *OrderRouter) InitOrderRouter(AppRouter *gin.RouterGroup) {
appRouter := AppRouter.Group("app_order")
{
appRouter.POST("", orderApi.CreateOrder) // 创建订单
appRouter.POST("/wechat/pay", orderApi.PayOrder) // 微信支付订单
appRouter.GET("/list", orderApi.GetOrderList) // 获取订单列表
appRouter.GET(":id", orderApi.GetOrderDetail) // 获取订单详情
appRouter.POST("/balance/pay", orderApi.BalancePay) // 余额支付
appRouter.POST("/notify", orderApi.NotifyOrder) // 微信支付回调通知
}
}

View File

@@ -1,8 +1,160 @@
package app package app
import (
"fmt"
"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/user"
"git.echol.cn/loser/lckt/utils/wechat"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type OrderService struct{} type OrderService struct{}
// Pay 发起支付 // Pay 发起支付
func (s *OrderService) Pay(orderId string, userId int64) (string, error) { func (s *OrderService) Pay(p request.PayReq, ctx *gin.Context) (interface{}, error) {
order := app.Order{}
err := global.GVA_DB.Where("id = ? AND order_no = ?", p.OrderId, p.OrderNo).First(&order).Error
if err != nil {
global.GVA_LOG.Error("查询订单失败", zap.Error(err))
return "", err
}
if order.Status == 3 {
global.GVA_LOG.Error("订单已过期", zap.Int64("order_id", int64(order.ID)))
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
}
return "", nil return "", nil
} }
func (s *OrderService) Create(o *app.Order) (*app.Order, error) {
// 生成订单号
o.OrderNo = wechat.GenerateOrderNum()
// 查询订单商品价格
price := 0
if o.OrderType == 1 {
err := global.GVA_DB.Table("article").Select("price").Where("id = ?", o.ArticleId).Scan(&price).Error
if err != nil {
global.GVA_LOG.Error("查询商品价格失败", zap.Error(err))
return nil, err
}
} else {
err := global.GVA_DB.Table("lckt_vip").Select("price").Where("id = ?", o.ArticleId).Scan(&price).Error
if err != nil {
global.GVA_LOG.Error("查询VIP价格失败", zap.Error(err))
return nil, err
}
}
o.Price = int64(price)
o.Status = 1 // 设置订单状态为未付款
// 设置openid
openId := ""
err := global.GVA_DB.Table("app_user").Where("id = ?", o.UserId).Select("open_id").Scan(&openId).Error
if err != nil {
global.GVA_LOG.Error("查询用户OpenID失败,请检查微信是否绑定", zap.Error(err))
return nil, fmt.Errorf("查询用户OpenID失败,请检查微信是否绑定")
}
o.OpenId = openId
err = global.GVA_DB.Create(&o).Error
if err != nil {
global.GVA_LOG.Error("创建订单失败", zap.Error(err))
return nil, err
}
return o, nil
}
func (s *OrderService) GetOrderDetail(id string) (order app.Order, err error) {
err = global.GVA_DB.Where("id = ?", id).First(&order).Error
if err != nil {
global.GVA_LOG.Error("获取订单详情失败", zap.Error(err))
return app.Order{}, err
}
return order, nil
}
func (s *OrderService) BalancePay(p request.BalancePay) error {
order := app.Order{}
err := global.GVA_DB.Where("id = ? AND order_no = ?", p.OrderId, p.OrderNo).First(&order).Error
if err != nil {
global.GVA_LOG.Error("查询订单失败", zap.Error(err))
return err
}
if order.Status != 1 {
global.GVA_LOG.Error("订单状态错误,无法支付", zap.Int("status", order.Status))
return err
}
// 检查用户余额是否足够
var user user.User
err = global.GVA_DB.Where("id = ?", p.UserId).Select("id,balance").First(&user).Error
if err != nil {
global.GVA_LOG.Error("查询用户信息失败", zap.Error(err))
return err
}
// 将user.Balance转为int64类型进行比较
balance := int64(user.Balance)
if balance < order.Price/100 { // 订单价格是以分为单位存储的
global.GVA_LOG.Error("用户余额不足", zap.Int64("balance", balance), zap.Int64("order_price", order.Price))
return fmt.Errorf("用户余额不足")
}
// 扣除用户余额
balance -= order.Price / 100
err = global.GVA_DB.Model(&user).Where("id = ?", p.UserId).Update("balance", balance).Error
if err != nil {
global.GVA_LOG.Error("扣除用户余额失败", zap.Error(err))
return err
}
// 更新订单状态为已付款
order.Status = 2
err = global.GVA_DB.Model(&order).Where("id = ?", order.ID).Update("status", 2).Error
if err != nil {
global.GVA_LOG.Error("更新订单状态失败", zap.Error(err))
return err
}
global.GVA_LOG.Info("余额支付成功", zap.Int64("user_id", int64(p.UserId)), zap.String("order_no", order.OrderNo))
return nil
}
func (s *OrderService) GetOrderList(p request.GetOrderList, id uint) (orders []app.Order, total int64, err error) {
limit := p.PageSize
offset := p.PageSize * (p.Page - 1)
db := global.GVA_DB.Model(&app.Order{}).Where("user_id = ?", id)
if p.Keyword != "" {
db = db.Where("title LIKE ? ", "%"+p.Keyword+"%")
}
if p.Status != 0 {
db = db.Where("status = ?", p.Status)
}
err = db.Count(&total).Error
err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&orders).Error
if err != nil {
global.GVA_LOG.Error("获取订单列表失败", zap.Error(err))
return nil, 0, err
}
return
}

View File

@@ -75,13 +75,13 @@ func (s ArticleService) APPGetArticleList(pageInfo request.GetList) (list []vo.A
return return
} }
func (s ArticleService) APPGetArticle(id string, userId uint) (article vo.ArticleVo, err error) { func (s ArticleService) APPGetArticle(id string, userId uint) (article *vo.ArticleVo, err error) {
err = global.GVA_DB.Table("article").Where("id = ?", id).First(&article).Error err = global.GVA_DB.Table("article").Where("id = ?", id).First(&article).Error
if err != nil { if err != nil {
global.GVA_LOG.Error("获取文章失败", zap.Error(err)) global.GVA_LOG.Error("获取文章失败", zap.Error(err))
return return
} }
article.IsBuy = 1 // 设置为已购买 article.IsBuy = 1
global.GVA_DB.Table("app_user").Select("avatar").Where("id = ?", article.TeacherId).Scan(&article.TeacherAvatar) global.GVA_DB.Table("app_user").Select("avatar").Where("id = ?", article.TeacherId).Scan(&article.TeacherAvatar)
@@ -89,10 +89,10 @@ func (s ArticleService) APPGetArticle(id string, userId uint) (article vo.Articl
if article.IsFree == 0 { if article.IsFree == 0 {
// 如果不是免费文章,判断用户是否购买过 // 如果不是免费文章,判断用户是否购买过
var count int64 var count int64
err = global.GVA_DB.Model(&app.Order{}).Where("article_id = ? AND user_id = ?", id, userId).Count(&count).Error err = global.GVA_DB.Model(&app.Order{}).Where("article_id = ? AND user_id = ? AND status = 2", id, userId).Count(&count).Error
if err != nil { if err != nil {
global.GVA_LOG.Error("查询用户购买记录失败", zap.Error(err)) global.GVA_LOG.Error("查询用户购买记录失败", zap.Error(err))
return return nil, err
} }
if count == 0 { if count == 0 {
// 用户没有购买过隐藏Content // 用户没有购买过隐藏Content

245
utils/wechat/pay.go Normal file
View File

@@ -0,0 +1,245 @@
package wechat
import (
gfmt "fmt"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/app"
"github.com/ArtisanCloud/PowerLibs/v3/fmt"
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel/models"
"github.com/ArtisanCloud/PowerWeChat/v3/src/payment"
wxRequst "github.com/ArtisanCloud/PowerWeChat/v3/src/payment/notify/request"
"github.com/ArtisanCloud/PowerWeChat/v3/src/payment/order/request"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"log"
"math/rand"
"time"
)
var WechatPay *payment.Payment
// InitWechatPay 初始化微信支付
func InitWechatPay() {
PaymentService, err := payment.NewPayment(&payment.UserConfig{
AppID: "wx3d21df18d7f8f9fc", // 小程序、公众号或者企业微信的appid
MchID: global.GVA_CONFIG.Pays[0].MchId, // 商户号 appID
MchApiV3Key: "1a3sd8561d5179Df152D4789aD38wG9s", // 微信V3接口调用必填
Key: "57s14dFG915486Sd5617f23d45f671Ad",
CertPath: `-----BEGIN CERTIFICATE-----
MIIEKzCCAxOgAwIBAgIUWaiR+0A+x6HPIJDbnI7HBL1DsQEwDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUwNzIzMDU1MDU5WhcNMzAwNzIyMDU1MDU5WjCBhDETMBEGA1UEAwwK
MTY0Njg3NDc1MzEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTAwLgYDVQQL
DCfmtbflj6PpvpnljY7pk4HlnZrmiJDnlLXlrZDllYbliqHllYbooYwxCzAJBgNV
BAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAMUD4rf7ct0zUxJKGww+8c38L/IBx9yeNpOc7lav+CEwqZvieesy
p1I2hbY+hwOZm23//0Vxcnl6fkr5wdidE/RsN808sGkYsetJ6q7LZsgsxnbaQd7w
rBc4GlOh1Q4teErbjNTUMuozYzikEjMjgZMxs7i5WJnFMMVvgJS4681UKH8MQG93
BnaaD5zEyofAnOov7mvC0KZ0RpqwiLdUsMaPzYILzccOv7Tj82Z/qLw8AFcI+7L1
mq8trZxmSPXP5Fypz+1VKLKR9MaELUuQLPouewDjXkKTBnxkhoWow6huuz3qyUUo
i7uJtnmDn6Eu+60ch61BB5ws3rhWDpltR/0CAwEAAaOBuTCBtjAJBgNVHRMEAjAA
MAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDovL2V2
Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUwREJD
MDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJFMTJC
MjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IBAQBk
WmviQM60S/l4I4/RtQWtiZS+85fk2ePVSk5gBUS27W4yTw+aXy5RYaCLcOVXTXje
PpAfE7QvkjZxMg/k0SJR0/NbgDNF6xuCbMj78+czWVCkqHwPjY4hWRdsM9s44Orp
lwFrG/UNRhPgXcdwNQj8MNzy3nGeO0HWbJFBWcuRhSSwXpxv3Sh6pZviL3QA+lrV
7QB7d0fEu8sSGbwnIB0oXl7y78l2/D20p57TKHsq5IPfQfDdExNXKV60ikMt/MdP
C4ygbeRpSAobr6qMTXcpWlbH3KfIPOUFny3nkN8C6lm//QUl7ynOXkECcmIHY6XF
yMx70ORdoBl1DaZ369P9
-----END CERTIFICATE-----
`, // 微信V2接口调用必填
KeyPath: `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFA+K3+3LdM1MS
ShsMPvHN/C/yAcfcnjaTnO5Wr/ghMKmb4nnrMqdSNoW2PocDmZtt//9FcXJ5en5K
+cHYnRP0bDfNPLBpGLHrSequy2bILMZ22kHe8KwXOBpTodUOLXhK24zU1DLqM2M4
pBIzI4GTMbO4uViZxTDFb4CUuOvNVCh/DEBvdwZ2mg+cxMqHwJzqL+5rwtCmdEaa
sIi3VLDGj82CC83HDr+04/Nmf6i8PABXCPuy9ZqvLa2cZkj1z+Rcqc/tVSiykfTG
hC1LkCz6LnsA415CkwZ8ZIaFqMOobrs96slFKIu7ibZ5g5+hLvutHIetQQecLN64
Vg6ZbUf9AgMBAAECggEBAI4q66PQM2cj7kI4b6Q6l8sIvKBqYIr3MHL8v5CWkvuA
XiQ7Hbd3af6NkZedL1iNs/eAz/iQkQbQOepoqFVjpE6w+OOFc9ejFmCvikZwSM8S
YHTLstTp34Ux2u2WzmPYtAFwxQOfzM3sHyF0ZB269Xn+V65pMWJlRXhzqdmoR6B2
YYLzHVBZAbgj7IlS+f1WPkkbrb3BIs71tYyPS3Ng6nhGgXrnqmE5fhm93wR9KR5R
RcgzFGH/8qD2LndmJz8tYtjvlq016Pp40VrYabIrvX6apglayShLQ+2XUe8VKH2Q
dQPIKL3R0ZeDeqwn4PTMclBYd/nXFSbIQndufTDY84ECgYEA8RTcCPOFYkoDeQIr
yKqWo1P1cKsB/bGzvLmo+FqFQij5lmj9iDdvpSQJ58Uj+TFy5qNRT6VueF8U4pNd
Fmcbw05cH+9NzAXji6aYWCargbmhW4L+rzub+TQmdAfqXbBknVgrLenY4LFnFFm1
SIdJRid3vCwiRuhcjecg4FasR+kCgYEA0TTw4ZGxPCX5qt00PUFvuGpqFxFqZ7Ah
wapwQTUY48WeC0Pg7Iq6ipnhjggkgYLkGc1klmTwJXKV0acIS5/DNclepMWZPwIc
+GcKclQsy8MHm+L2tPGBQosRGiSrXi5LzBIQsYSyhBRDUSqu+yj7TTd2XqpUkLAw
qdrWiIjVBvUCgYAOkh0uaVGBfEmzcZ8l1LGgE339HkjThX8AhBQjVo1BT2quXZAd
QIR97ayvlmmzMPrp16sdbjk8Czse6pswtHCoID9PKs5/60cydJI2mbe58nc/Ka6s
9qRZrn44exX+LaAXJnINp1mVUwOQ5k8foBWcqNwCwoQb1wVpCjQhevuUqQKBgQC1
SA+3Fr0iprF6iqWastoxThzSEmhGowwNOjh9eJoxvOsfTdlYfzn3ojIeFhY0F4y6
gw03eQ3TFUCXZAq/JRhNwkl9tC//tkAOS5N00FXk1wH/5aLr1h2w4LqYEdBhEvLh
SYInoRnjc3+FlNv9jVx9Y6Lxkt0mZ1YzyQp/UzptBQKBgFTOonik05rMHGlEg9Ng
unjDAEK+7tXhM/RKxWDs7pZeGbdl1c/NDOX4wIbuK2cCkV0v/qfg6Ex2Obf5rqM4
R5lrs9y30rZbwmfms8G3cimTAsOcLqObKkBED+g07t+3bw44JXLwYLPm/z//bIcI
fkxyxKL5ZJhP3/bPJJj8UAq3
-----END PRIVATE KEY-----
`,
SerialNo: global.GVA_CONFIG.Pays[0].SerialNo, // 商户支付证书序列号
NotifyURL: global.GVA_CONFIG.Pays[0].NotifyUrl, // 微信支付回调地址
HttpDebug: false,
Log: payment.Log{
Level: "debug",
// 可以重定向到你的目录下如果设置File和Error默认会在当前目录下的wechat文件夹下生成日志
File: "/Users/user/wechat/payment/info.log",
Error: "/Users/user/wechat/payment/error.log",
Stdout: false, // 是否打印在终端
},
Http: payment.Http{
Timeout: 30.0,
BaseURI: "https://api.mch.weixin.qq.com",
},
})
if err != nil {
log.Println("初始化微信支付 SDK失败", err)
}
WechatPay = PaymentService
}
// H5Pay 发送订单
func H5Pay(order *app.Order, ctx *gin.Context) (interface{}, error) {
options := &request.RequestH5Prepay{
Amount: &request.H5Amount{
Total: int(order.Price),
Currency: "CNY",
},
Attach: order.Desc,
Description: order.Desc,
OutTradeNo: order.OrderNo, // 这里是商户订单号,不能重复提交给微信
SceneInfo: &request.H5SceneInfo{
PayerClientIP: ctx.ClientIP(), // 用户终端IP
H5Info: &request.H5H5Info{
Type: "Wap",
AppName: "老陈课堂",
},
},
}
// 下单
response, err := WechatPay.Order.TransactionH5(ctx, options)
if err != nil {
global.GVA_LOG.Error("微信支付下单失败", zap.Error(err))
return "", err
}
/*
payConf, err := WechatPay.JSSDK.BridgeConfig(response.PrepayID, true)
if err != nil {
global.GVA_LOG.Error("获取微信支付配置失败", zap.Error(err))
return "", err
}*/
global.GVA_LOG.Info("微信支付配置", zap.Any("payConf", response))
return response, nil
}
func JSAPIPay(order *app.Order, ctx *gin.Context) (interface{}, error) {
options := &request.RequestJSAPIPrepay{
Amount: &request.JSAPIAmount{
Total: int(order.Price),
Currency: "CNY",
},
Attach: order.Name,
Description: order.Desc,
OutTradeNo: order.OrderNo, // 这里是商户订单号,不能重复提交给微信
Payer: &request.JSAPIPayer{
OpenID: order.OpenId, // 用户的openid 记得也是动态的。
},
}
// 下单
response, err := WechatPay.Order.JSAPITransaction(ctx, options)
if err != nil {
panic(err)
}
// payConf大致如下
// {
// "appId": "ww16143ea0101327c7",
// "nonceStr": "jcMfo7lsfM3LPWkLRJb7rQU6WeatStEU",
// "package": "prepay_id=xxxxx",
// "paySign": "hSPF2wU0aYeTq+DJ14ELM...省略部分数据...RrOmlEkZXVxqCdZmniLdA==",
// "signType": "RSA",
// "timeStamp": "1634305101"
// }
payConf, err := WechatPay.JSSDK.BridgeConfig(response.PrepayID, true)
if err != nil {
global.GVA_LOG.Error("获取微信支付配置失败", zap.Error(err))
return "", err
}
return payConf, nil
}
// NotifyHandle 微信支付回调处理
func NotifyHandle(ctx *gin.Context) error {
res, err := WechatPay.HandlePaidNotify(
ctx.Request,
func(message *wxRequst.RequestNotify, transaction *models.Transaction, fail func(message string)) interface{} {
// 看下支付通知事件状态
// 这里可能是微信支付失败的通知,所以可能需要在数据库做一些记录,然后告诉微信我处理完成了。
if message.EventType != "TRANSACTION.SUCCESS" {
fmt.Dump(transaction)
return true
}
if transaction.OutTradeNo != "" {
// 这里对照自有数据库里面的订单做查询以及支付状态改变
// 更新订单状态为已支付
order := app.Order{}
err := global.GVA_DB.Model(&app.Order{}).Where("order_no = ?", transaction.OutTradeNo).First(&order).Error
if err != nil {
global.GVA_LOG.Error("查询订单失败", zap.Error(err))
return nil
}
order.Status = 2 // 设置订单状态为已支付
err = global.GVA_DB.Save(&order).Error
if err != nil {
global.GVA_LOG.Error("更新订单状态失败", zap.Error(err))
return nil
}
log.Printf("订单号:%s 支付成功", transaction.OutTradeNo)
} else {
// 因为微信这个回调不存在订单号,所以可以告诉微信我还没处理成功,等会它会重新发起通知
// 如果不需要直接返回true即可
fail("payment fail")
return nil
}
return true
},
)
// 这里可能是因为不是微信官方调用的无法正常解析出transaction和message所以直接抛错。
if err != nil {
res.StatusCode = 502
err = res.Write(ctx.Writer)
return err
//panic(err)
}
// 这里根据之前返回的是true或者fail框架这边自动会帮你回复微信
err = res.Write(ctx.Writer)
if err != nil {
panic(err)
}
global.GVA_LOG.Info("微信支付回调处理成功", zap.Any("response", res))
return nil
}
func GenerateOrderNum() string {
now := time.Now()
dateStr := now.Format("060102") // 年的后2位+月+日
seconds := int(now.Sub(time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())).Seconds())
secondsStr := gfmt.Sprintf("%05d", seconds)
nanoStr := gfmt.Sprintf("%06d", now.Nanosecond()/1000) // 微秒级6位
rand.Seed(time.Now().UnixNano())
randStr := gfmt.Sprintf("%03d", rand.Intn(1000)) // 3位随机数
orderNo := gfmt.Sprintf("%s%s%s%s%s", "5258", dateStr, secondsStr, nanoStr, randStr)
return orderNo
}

View File

@@ -3,15 +3,12 @@ package wechat
import ( import (
"git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/global"
"github.com/ArtisanCloud/PowerSocialite/v3/src/providers" "github.com/ArtisanCloud/PowerSocialite/v3/src/providers"
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel"
"github.com/ArtisanCloud/PowerWeChat/v3/src/officialAccount" "github.com/ArtisanCloud/PowerWeChat/v3/src/officialAccount"
"github.com/ArtisanCloud/PowerWeChat/v3/src/payment"
"go.uber.org/zap" "go.uber.org/zap"
"log" "log"
) )
var WeOfficial *officialAccount.OfficialAccount var WeOfficial *officialAccount.OfficialAccount
var WechatPay *payment.Payment
// InitWeOfficial 初始化微信公众号 // InitWeOfficial 初始化微信公众号
func InitWeOfficial() { func InitWeOfficial() {
@@ -37,43 +34,6 @@ func InitWeOfficial() {
WeOfficial = OfficialAccountApp WeOfficial = OfficialAccountApp
} }
// InitWechatPay 初始化微信支付
func InitWechatPay() {
PaymentService, err := payment.NewPayment(&payment.UserConfig{
AppID: "wx3d21df18d7f8f9fc\n", // 小程序、公众号或者企业微信的appid
MchID: "[mch_id]", // 商户号 appID
MchApiV3Key: "1a3sd8561d5179Df152D4789aD38wG9s", // 微信V3接口调用必填
Key: "57s14dFG915486Sd5617f23d45f671Ad", // 微信V2接口调用必填
CertPath: "[wx_cert_path]", // 商户后台支付的Cert证书路径
KeyPath: "[wx_key_path]", // 商户后台支付的Key证书路径
SerialNo: "[serial_no]", // 商户支付证书序列号
NotifyURL: "[notify_url]", // 微信支付回调地址
HttpDebug: true,
Log: payment.Log{
Level: "debug",
// 可以重定向到你的目录下如果设置File和Error默认会在当前目录下的wechat文件夹下生成日志
File: "/Users/user/wechat/payment/info.log",
Error: "/Users/user/wechat/payment/error.log",
Stdout: false, // 是否打印在终端
},
Http: payment.Http{
Timeout: 30.0,
BaseURI: "https://api.mch.weixin.qq.com",
},
// 可选,不传默认走程序内存
Cache: kernel.NewRedisClient(&kernel.UniversalOptions{
Addrs: []string{"127.0.0.1:6379"},
Password: "",
DB: 0,
}),
})
if err != nil {
log.Println("初始化微信支付 SDK失败", err)
}
WechatPay = PaymentService
}
func GetUserInfo(code string) *providers.User { func GetUserInfo(code string) *providers.User {
userFromCode, err := WeOfficial.OAuth.UserFromCode(code) userFromCode, err := WeOfficial.OAuth.UserFromCode(code)
if err != nil { if err != nil {