添加支付宝和微信充值功能,重构交易处理逻辑,优化资源创建与支付链接生成

This commit is contained in:
2025-04-18 16:22:38 +08:00
parent f6a97545c5
commit a7e59fb1d7
14 changed files with 939 additions and 224 deletions

View File

@@ -1,19 +1,14 @@
package handlers
import (
"platform/pkg/env"
"platform/pkg/u"
"platform/web/auth"
"platform/web/common"
g "platform/web/globals"
q "platform/web/queries"
s "platform/web/services"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"github.com/smartwalle/alipay/v3"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
)
// region ListResourcePss
@@ -143,7 +138,7 @@ func AllResource(c *fiber.Ctx) error {
// endregion
// region CreateResourcePrepared
// region CompleteResource
type CreateResourceReq struct {
s.CreateResourceData
@@ -172,34 +167,21 @@ func PrepareResourceByAlipay(c *fiber.Ctx) error {
return err
}
// 生成订单
tradeNo, err := s.ID.GenSerial(c.Context())
if err != nil {
return err
}
// 调用外部接口
alipayResp, err := g.Alipay.TradePagePay(alipay.TradePagePay{
QRPayMode: "4",
Trade: alipay.Trade{
OutTradeNo: tradeNo,
TotalAmount: strconv.FormatFloat(req.GetPrice(), 'f', 2, 64),
Subject: "购买套餐 - " + req.GetName(),
ProductCode: "FAST_INSTANT_TRADE_PAY",
TimeExpire: time.Now().Add(30 * time.Minute).Format("2006-01-02 15:04:05"),
},
})
if err != nil {
return err
}
// 保存交易信息
err = s.Resource.PrepareResource(c.Context(), &req.CreateResourceData, authContext.Payload.Id, tradeNo, 1)
result, err := s.Resource.PrepareResource(
c.Context(),
&req.CreateResourceData,
authContext.Payload.Id,
s.TransactionMethodAlipay,
)
if err != nil {
return err
}
// 返回结果
return c.JSON(CreateResourceResp{
TradeNo: tradeNo,
PayURL: alipayResp.String(),
TradeNo: result.TradeNo,
PayURL: result.PayURL,
})
}
@@ -217,35 +199,21 @@ func PrepareResourceByWechat(c *fiber.Ctx) error {
return err
}
// 生成订单号
tradeNo, err := s.ID.GenSerial(c.Context())
if err != nil {
return err
}
// 调用外部接口
wechatPayResp, _, err := g.WechatPay.Native.Prepay(c.Context(), native.PrepayRequest{
Mchid: &env.WechatPayMchId,
Appid: &env.WechatPayAppId,
Description: u.P("购买套餐 - " + req.GetName()),
OutTradeNo: &tradeNo,
TimeExpire: u.P(time.Now().Add(30 * time.Minute)),
NotifyUrl: &env.WechatPayCallbackUrl,
Amount: &native.Amount{
Total: u.P(int64(req.GetPrice() * 100)),
},
})
// 保存交易信息
err = s.Resource.PrepareResource(c.Context(), &req.CreateResourceData, authContext.Payload.Id, tradeNo, 2)
result, err := s.Resource.PrepareResource(
c.Context(),
&req.CreateResourceData,
authContext.Payload.Id,
s.TransactionMethodWeChat,
)
if err != nil {
return err
}
// 返回结果
return c.JSON(CreateResourceResp{
TradeNo: tradeNo,
PayURL: *wechatPayResp.CodeUrl,
TradeNo: result.TradeNo,
PayURL: result.PayURL,
})
}
@@ -263,28 +231,16 @@ func CreateResourceByAlipay(c *fiber.Ctx) error {
}
// 验证支付结果
alipayResp, err := g.Alipay.TradeQuery(c.Context(), alipay.TradeQuery{
OutTradeNo: req.TradeNo,
result, err := s.Transaction.VerifyTransaction(c.Context(), &s.TransactionVerifyData{
TradeNo: req.TradeNo,
Method: s.TransactionMethodAlipay,
})
if err != nil {
return err
}
if alipayResp.TradeStatus != "TRADE_SUCCESS" {
return fiber.NewError(fiber.StatusBadRequest, "支付未完成,请确认后重试")
}
// 创建套餐
payment, err := strconv.ParseFloat(alipayResp.ReceiptAmount, 64)
if err != nil {
return err
}
paidAt, err := time.Parse("2006-01-02 15:04:05", alipayResp.SendPayDate)
if err != nil {
return err
}
err = s.Resource.CreateResourcePrepared(c.Context(), req.TradeNo, alipayResp.TradeNo, payment, paidAt)
// 完成套餐创建
err = s.Resource.CompleteResource(c.Context(), req.TradeNo, result)
if err != nil {
return err
}
@@ -306,27 +262,16 @@ func CreateResourceByWechat(c *fiber.Ctx) error {
}
// 验证支付结果
wechatPayResp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(c.Context(), native.QueryOrderByOutTradeNoRequest{
OutTradeNo: &req.TradeNo,
Mchid: &env.WechatPayMchId,
result, err := s.Transaction.VerifyTransaction(c.Context(), &s.TransactionVerifyData{
TradeNo: req.TradeNo,
Method: s.TransactionMethodWeChat,
})
if err != nil {
return err
}
if *wechatPayResp.TradeState != "SUCCESS" {
return fiber.NewError(fiber.StatusBadRequest, "支付未完成,请确认后重试")
}
// 创建套餐
payment := float64(*wechatPayResp.Amount.PayerTotal) / 100
paidAt, err := time.Parse(time.RFC3339, *wechatPayResp.SuccessTime)
if err != nil {
return err
}
err = s.Resource.CreateResourcePrepared(c.Context(), req.TradeNo, *wechatPayResp.OutTradeNo, payment, paidAt)
// 完成套餐创建
err = s.Resource.CompleteResource(c.Context(), req.TradeNo, result)
if err != nil {
return err
}
@@ -349,7 +294,7 @@ func CreateResourceByBalance(c *fiber.Ctx) error {
}
// 创建套餐
err = s.Resource.CreateResourceImmediately(&req.CreateResourceData, authCtx.Payload.Id)
err = s.Resource.CreateResource(&req.CreateResourceData, authCtx.Payload.Id)
if err != nil {
return err
}

View File

@@ -1,14 +1,19 @@
package handlers
import (
"fmt"
"log/slog"
"net/http"
g "platform/web/globals"
q "platform/web/queries"
s "platform/web/services"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"github.com/smartwalle/alipay/v3"
"github.com/valyala/fasthttp/fasthttpadaptor"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
)
// region AlipayCallback
@@ -16,73 +21,87 @@ import (
func AlipayCallback(c *fiber.Ctx) error {
// 解析请求
req := make(map[string][]string)
c.Context().QueryArgs().VisitAll(func(key, value []byte) {
req[string(key)] = append(req[string(key)], string(value))
})
c.Context().PostArgs().VisitAll(func(key, value []byte) {
req[string(key)] = append(req[string(key)], string(value))
})
notification, err := g.Alipay.DecodeNotification(req)
httpRequest := new(http.Request)
if err := fasthttpadaptor.ConvertRequest(c.Context(), httpRequest, false); err != nil {
return err
}
if err := httpRequest.ParseForm(); err != nil {
return err
}
notification, err := g.Alipay.DecodeNotification(httpRequest.Form)
if err != nil {
return err
}
slog.Debug("支付宝支付回调", "notification", fmt.Sprintf("%+v", notification))
// todo 退款通知
if isRefund(notification) {
return act(c)
}
// 查询交易信息
trade, err := q.Q.Trade.Where(q.Trade.InnerNo.Eq(notification.OutTradeNo)).Take()
if err != nil {
// 跳过测试通知
return act(c)
}
switch notification.NotifyType {
// 支付成功
case string(alipay.TradeStatusSuccess):
if isRefund(notification) {
break
}
// 收集交易状态
payment, err := strconv.ParseFloat(notification.TotalAmount, 64)
if err != nil {
return err
}
paidAt, err := time.Parse("2006-01-02 15:04:05", notification.GmtPayment)
if err != nil {
return err
}
verified := &s.TransactionVerifyResult{
TransId: notification.TradeNo,
Payment: payment,
Time: paidAt,
}
switch trade.Type {
err = s.Resource.CreateResourcePrepared(
c.Context(),
notification.OutTradeNo,
notification.TradeNo,
payment,
paidAt,
)
if err != nil {
return err
// 余额充值
case 0:
err := s.User.RechargeConfirm(c.Context(), notification.OutTradeNo, verified)
if err != nil {
return err
}
// 购买产品
case 1:
err = s.Resource.CompleteResource(c.Context(), notification.OutTradeNo, verified)
if err != nil {
return err
}
}
// 支付关闭
case string(alipay.TradeStatusClosed):
switch trade.Type {
if isRefund(notification) {
break
// 购买产品
case 1:
cancelAt, err := time.Parse("2006-01-02 15:04:05", notification.GmtClose)
if err != nil {
return err
}
err = s.Resource.CancelResource(c.Context(), notification.OutTradeNo, cancelAt, s.TransactionMethodAlipay)
if err != nil {
return err
}
}
cancelAt, err := time.Parse("2006-01-02 15:04:05", notification.GmtClose)
if err != nil {
return err
}
err = s.Resource.CancelResource(c.Context(), notification.OutTradeNo, cancelAt)
if err != nil {
return err
}
default:
}
g.Alipay.ACKNotification(AdapterWriter{
c: c,
})
return nil
return act(c)
}
type AdapterWriter struct {
@@ -105,12 +124,72 @@ func isRefund(notification *alipay.Notification) bool {
return notification.OutBizNo != "" || notification.RefundFee != "" || notification.GmtRefund != ""
}
func act(c *fiber.Ctx) error {
g.Alipay.ACKNotification(AdapterWriter{c: c})
return nil
}
// endregion
// region WechatPayCallback
func WechatPayCallback(c *fiber.Ctx) error {
// 解析请求参数
req := new(http.Request)
if err := fasthttpadaptor.ConvertRequest(c.Context(), req, false); err != nil {
return err
}
content := new(payments.Transaction)
_, err := g.WechatPay.Notify.ParseNotifyRequest(c.Context(), req, content)
if err != nil {
return err
}
slog.Debug("微信支付回调", "content", fmt.Sprintf("%+v", content))
// 查询交易信息
trade, err := q.Q.Trade.Where(q.Trade.InnerNo.Eq(*content.OutTradeNo)).Take()
if err != nil {
// 跳过测试通知
return nil
}
switch *content.TradeState {
// 支付成功
case "SUCCESS":
// 收集交易状态
payment := float64(*content.Amount.PayerTotal) / 100
paidAt, err := time.Parse(time.RFC3339, *content.SuccessTime)
if err != nil {
return err
}
verified := &s.TransactionVerifyResult{
TransId: *content.TransactionId,
Payment: payment,
Time: paidAt,
}
switch {
// 余额充值
case trade.Type == 0:
err := s.User.RechargeConfirm(c.Context(), *content.OutTradeNo, verified)
if err != nil {
return err
}
// 购买产品
case trade.Type == 1:
err = s.Resource.CompleteResource(c.Context(), *content.OutTradeNo, verified)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -2,8 +2,11 @@ package handlers
import (
"errors"
"platform/web/auth"
q "platform/web/queries"
"platform/web/services"
s "platform/web/services"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
@@ -25,7 +28,7 @@ func GetUserByToken(c *fiber.Ctx) error {
}
// 查询会话信息
session, err := services.Session.Find(c.Context(), req.Token)
session, err := s.Session.Find(c.Context(), req.Token)
if err != nil {
return err
}
@@ -47,3 +50,160 @@ func GetUserByToken(c *fiber.Ctx) error {
}
// endregion
// region recharge
type RechargePrepareReq struct {
Amount float64 `json:"amount" validate:"required,min=0.01"`
}
type RechargePrepareResp struct {
TradeNo string `json:"trade_no"`
PayURL string `json:"pay_url"`
}
type RechargeConfirmReq struct {
TradeNo string `json:"trade_no" validate:"required"`
}
type RechargeConfirmResp struct {
Status string `json:"status"`
}
// RechargePrepareAlipay 通过支付宝充值
func RechargePrepareAlipay(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{})
if err != nil {
return err
}
// 解析请求参数
req := new(RechargePrepareReq)
if err := c.BodyParser(req); err != nil {
return err
}
// 保存交易信息
var result *s.TransactionPrepareResult
err = q.Q.Transaction(func(tx *q.Query) error {
result, err = s.Transaction.PrepareTransaction(c.Context(), tx, authContext.Payload.Id, &s.TransactionPrepareData{
Subject: "账户充值 - " + strconv.FormatFloat(req.Amount, 'f', 2, 64) + "元",
Amount: req.Amount,
ExpireAt: time.Now().Add(30 * time.Minute),
Type: s.TransactionTypeRecharge,
Method: s.TransactionMethodAlipay,
})
return err
})
if err != nil {
return err
}
// 返回结果
return c.JSON(RechargePrepareResp{
TradeNo: result.TradeNo,
PayURL: result.PayURL,
})
}
func RechargeConfirmAlipay(c *fiber.Ctx) error {
// 检查权限
_, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{})
if err != nil {
return err
}
// 解析请求参数
req := new(RechargeConfirmReq)
if err := c.BodyParser(req); err != nil {
return err
}
// 验证支付结果
result, err := s.Transaction.VerifyTransaction(c.Context(), &s.TransactionVerifyData{
TradeNo: req.TradeNo,
Method: s.TransactionMethodAlipay,
})
if err != nil {
return err
}
// 更新数据库
err = s.User.RechargeConfirm(c.Context(), req.TradeNo, result)
if err != nil {
return err
}
return c.JSON(fiber.Map{"status": "success"})
}
func RechargePrepareWechat(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{})
if err != nil {
return err
}
// 解析请求参数
req := new(RechargePrepareReq)
if err := c.BodyParser(req); err != nil {
return err
}
// 保存交易信息
var result *s.TransactionPrepareResult
err = q.Q.Transaction(func(tx *q.Query) error {
result, err = s.Transaction.PrepareTransaction(c.Context(), tx, authContext.Payload.Id, &s.TransactionPrepareData{
Subject: "账户充值 - " + strconv.FormatFloat(req.Amount, 'f', 2, 64) + "元",
Amount: req.Amount,
ExpireAt: time.Now().Add(30 * time.Minute),
Type: s.TransactionTypeRecharge,
Method: s.TransactionMethodWeChat,
})
return err
})
if err != nil {
return err
}
// 返回结果
return c.JSON(RechargePrepareResp{
TradeNo: result.TradeNo,
PayURL: result.PayURL,
})
}
func RechargeConfirmWechat(c *fiber.Ctx) error {
// 检查权限
_, err := auth.Protect(c, []s.PayloadType{s.PayloadUser}, []string{})
if err != nil {
return err
}
// 解析请求参数
req := new(struct {
TradeNo string `json:"trade_no" validate:"required"`
})
if err := c.BodyParser(req); err != nil {
return err
}
// 验证支付结果
result, err := s.Transaction.VerifyTransaction(c.Context(), &s.TransactionVerifyData{
TradeNo: req.TradeNo,
})
if err != nil {
return err
}
// 更新数据库
err = s.User.RechargeConfirm(c.Context(), req.TradeNo, result)
if err != nil {
return err
}
return c.JSON(fiber.Map{"status": "success"})
}
// endregion