添加支付宝和微信充值功能,重构交易处理逻辑,优化资源创建与支付链接生成
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"platform/pkg/env"
|
||||
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
@@ -14,10 +16,12 @@ var WechatPay *WechatPayClient
|
||||
|
||||
type WechatPayClient struct {
|
||||
Native *native.NativeApiService
|
||||
Notify *notify.Handler
|
||||
}
|
||||
|
||||
func InitWechatPay() {
|
||||
|
||||
// 加载 rsa 密钥文件
|
||||
appPrivateKey, err := utils.LoadPrivateKey(env.WechatPayMchPrivateKeyPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -28,6 +32,7 @@ func InitWechatPay() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 创建 WechatPay 客户端
|
||||
client, err := core.NewClient(context.Background(),
|
||||
option.WithWechatPayPublicKeyAuthCipher(
|
||||
env.WechatPayMchId,
|
||||
@@ -41,7 +46,18 @@ func InitWechatPay() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 创建 WechatPay 通知处理器
|
||||
handler, err := notify.NewRSANotifyHandler(env.WechatPayApiCert, verifiers.NewSHA256WithRSAPubkeyVerifier(
|
||||
env.WechatPayPublicKeyId,
|
||||
*wechatPublicKey,
|
||||
))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 创建 WechatPay 服务
|
||||
WechatPay = &WechatPayClient{
|
||||
Native: &native.NativeApiService{Client: client},
|
||||
Notify: handler,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,6 +30,7 @@ type Trade struct {
|
||||
Type int32 `gorm:"column:type;not null" json:"type"`
|
||||
CancelAt common.LocalDateTime `gorm:"column:cancel_at" json:"cancel_at"`
|
||||
PaidAt common.LocalDateTime `gorm:"column:paid_at" json:"paid_at"`
|
||||
PayURL string `gorm:"column:pay_url" json:"pay_url"`
|
||||
}
|
||||
|
||||
// TableName Trade's table name
|
||||
|
||||
@@ -43,6 +43,7 @@ func newTrade(db *gorm.DB, opts ...gen.DOOption) trade {
|
||||
_trade.Type = field.NewInt32(tableName, "type")
|
||||
_trade.CancelAt = field.NewField(tableName, "cancel_at")
|
||||
_trade.PaidAt = field.NewField(tableName, "paid_at")
|
||||
_trade.PayURL = field.NewString(tableName, "pay_url")
|
||||
|
||||
_trade.fillFieldMap()
|
||||
|
||||
@@ -69,6 +70,7 @@ type trade struct {
|
||||
Type field.Int32
|
||||
CancelAt field.Field
|
||||
PaidAt field.Field
|
||||
PayURL field.String
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@@ -101,6 +103,7 @@ func (t *trade) updateTableName(table string) *trade {
|
||||
t.Type = field.NewInt32(table, "type")
|
||||
t.CancelAt = field.NewField(table, "cancel_at")
|
||||
t.PaidAt = field.NewField(table, "paid_at")
|
||||
t.PayURL = field.NewString(table, "pay_url")
|
||||
|
||||
t.fillFieldMap()
|
||||
|
||||
@@ -117,7 +120,7 @@ func (t *trade) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
func (t *trade) fillFieldMap() {
|
||||
t.fieldMap = make(map[string]field.Expr, 16)
|
||||
t.fieldMap = make(map[string]field.Expr, 17)
|
||||
t.fieldMap["id"] = t.ID
|
||||
t.fieldMap["user_id"] = t.UserID
|
||||
t.fieldMap["inner_no"] = t.InnerNo
|
||||
@@ -134,6 +137,7 @@ func (t *trade) fillFieldMap() {
|
||||
t.fieldMap["type"] = t.Type
|
||||
t.fieldMap["cancel_at"] = t.CancelAt
|
||||
t.fieldMap["paid_at"] = t.PaidAt
|
||||
t.fieldMap["pay_url"] = t.PayURL
|
||||
}
|
||||
|
||||
func (t trade) clone(db *gorm.DB) trade {
|
||||
|
||||
@@ -43,6 +43,10 @@ func ApplyRouters(app *fiber.App) {
|
||||
user.Post("/get/token", handlers.GetUserByToken)
|
||||
user.Post("/identify", handlers.Identify)
|
||||
user.Post("/identify/callback", handlers.IdentifyCallback)
|
||||
user.Post("/recharge/prepare/alipay", handlers.RechargePrepareAlipay)
|
||||
user.Post("/recharge/confirm/alipay", handlers.RechargeConfirmAlipay)
|
||||
user.Post("/recharge/prepare/wechat", handlers.RechargePrepareWechat)
|
||||
user.Post("/recharge/confirm/wechat", handlers.RechargeConfirmWechat)
|
||||
|
||||
// 账单
|
||||
bill := api.Group("/bill")
|
||||
|
||||
@@ -18,56 +18,37 @@ var Resource = &resourceService{}
|
||||
|
||||
type resourceService struct{}
|
||||
|
||||
func (s *resourceService) PrepareResource(ctx context.Context, data *CreateResourceData, uid int32, tradeNo string, method int32) error {
|
||||
func (s *resourceService) PrepareResource(ctx context.Context, data *CreateResourceData, uid int32, method TransactionMethod) (*TransactionPrepareResult, error) {
|
||||
amount := data.GetPrice()
|
||||
|
||||
// 保存到数据库
|
||||
var result *TransactionPrepareResult
|
||||
err := q.Q.Transaction(func(q *q.Query) error {
|
||||
// 创建交易订单
|
||||
var trade = m.Trade{
|
||||
UserID: uid,
|
||||
InnerNo: tradeNo,
|
||||
Subject: "购买套餐 - " + data.GetName(),
|
||||
Method: method,
|
||||
Type: 1,
|
||||
Status: 0,
|
||||
Amount: amount,
|
||||
}
|
||||
err := q.Trade.Create(&trade)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
|
||||
// 保存用户帐单
|
||||
bill := m.Bill{
|
||||
UserID: uid,
|
||||
TradeID: trade.ID,
|
||||
BillNo: ID.GenReadable("bil"),
|
||||
Info: "购买套餐 - " + data.GetName(),
|
||||
Type: 1,
|
||||
Amount: -amount,
|
||||
}
|
||||
err = q.Bill.
|
||||
Omit(q.Bill.ResourceID, q.Bill.RefundID).
|
||||
Create(&bill)
|
||||
// 生成交易订单
|
||||
result, err = Transaction.PrepareTransaction(ctx, q, uid, &TransactionPrepareData{
|
||||
Subject: "购买套餐 - " + data.GetName(),
|
||||
Amount: amount,
|
||||
ExpireAt: time.Now().Add(30 * time.Minute),
|
||||
Type: TransactionTypePurchase,
|
||||
Method: method,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 保存请求缓存
|
||||
cache := &CreateResourceCache{
|
||||
reqStr, err := json.Marshal(&CreateResourceCache{
|
||||
CreateResourceData: *data,
|
||||
Uid: uid,
|
||||
TradeId: trade.ID,
|
||||
BillId: bill.ID,
|
||||
}
|
||||
|
||||
reqStr, err := json.Marshal(cache)
|
||||
TradeId: result.Trade.ID,
|
||||
BillId: result.Bill.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rds.Client.Set(ctx, tradeNo, reqStr, 30*time.Minute).Err()
|
||||
err = rds.Client.Set(ctx, result.TradeNo, reqStr, 30*time.Minute).Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -75,20 +56,19 @@ func (s *resourceService) PrepareResource(ctx context.Context, data *CreateResou
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *resourceService) CreateResourcePrepared(ctx context.Context, tradeNo string, outerTradeNo string, payment float64, at time.Time) error {
|
||||
func (s *resourceService) CompleteResource(ctx context.Context, tradeNo string, rs *TransactionVerifyResult) error {
|
||||
|
||||
// 获取请求缓存
|
||||
reqStr, err := rds.Client.Get(ctx, tradeNo).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache := new(CreateResourceCache)
|
||||
if err := json.Unmarshal([]byte(reqStr), cache); err != nil {
|
||||
return err
|
||||
@@ -103,21 +83,7 @@ func (s *resourceService) CreateResourcePrepared(ctx context.Context, tradeNo st
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
_, err = q.Trade.Debug().
|
||||
Select(q.Trade.OuterNo, q.Trade.Payment, q.Trade.Status, q.Trade.PaidAt).
|
||||
Updates(&m.Trade{
|
||||
ID: cache.TradeId,
|
||||
OuterNo: outerTradeNo,
|
||||
Payment: payment,
|
||||
Status: 1,
|
||||
PaidAt: common.LocalDateTime(at),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新账单状态
|
||||
// 更新账单
|
||||
_, err = q.Bill.Debug().
|
||||
Select(q.Bill.ResourceID).
|
||||
Updates(&m.Bill{
|
||||
@@ -128,6 +94,15 @@ func (s *resourceService) CreateResourcePrepared(ctx context.Context, tradeNo st
|
||||
return err
|
||||
}
|
||||
|
||||
// 完成交易
|
||||
_, err = Transaction.CompleteTransaction(ctx, q, &TransactionCompleteData{
|
||||
TradeNo: tradeNo,
|
||||
TransactionVerifyResult: *rs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除缓存
|
||||
err = rds.Client.Del(ctx, tradeNo).Err()
|
||||
if err != nil {
|
||||
@@ -143,10 +118,24 @@ func (s *resourceService) CreateResourcePrepared(ctx context.Context, tradeNo st
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *resourceService) CreateResourceImmediately(data *CreateResourceData, uid int32) error {
|
||||
func (s *resourceService) CreateResource(data *CreateResourceData, uid int32) error {
|
||||
|
||||
// 保存交易信息
|
||||
err := q.Q.Transaction(func(q *q.Query) error {
|
||||
amount := data.GetPrice()
|
||||
|
||||
// 检查用户
|
||||
user, err := q.User.
|
||||
Where(q.User.ID.Eq(uid)).
|
||||
Take()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查余额
|
||||
if user.Balance < amount {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "余额不足")
|
||||
}
|
||||
|
||||
// 保存套餐
|
||||
resource, err := createResource(data, uid)
|
||||
@@ -170,6 +159,14 @@ func (s *resourceService) CreateResourceImmediately(data *CreateResourceData, ui
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新用户余额
|
||||
_, err = q.User.
|
||||
Where(q.User.ID.Eq(uid)).
|
||||
Update(q.User.Balance, user.Balance-amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -221,27 +218,10 @@ type CreateResourceCache struct {
|
||||
}
|
||||
|
||||
func createResource(data *CreateResourceData, uid int32) (*m.Resource, error) {
|
||||
amount := data.GetPrice()
|
||||
|
||||
// 检查用户
|
||||
user, err := q.User.
|
||||
Where(
|
||||
q.User.ID.Eq(uid),
|
||||
q.User.Status.Eq(1),
|
||||
).
|
||||
Take()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查余额
|
||||
if user.Balance < amount {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "余额不足")
|
||||
}
|
||||
|
||||
// 创建套餐
|
||||
resource := &m.Resource{
|
||||
UserID: user.ID,
|
||||
resource := m.Resource{
|
||||
UserID: uid,
|
||||
ResourceNo: ID.GenReadable("res"),
|
||||
Active: true,
|
||||
Type: 1,
|
||||
@@ -253,38 +233,29 @@ func createResource(data *CreateResourceData, uid int32) (*m.Resource, error) {
|
||||
DailyLimit: data.DailyLimit,
|
||||
},
|
||||
}
|
||||
err = q.Resource.Create(resource)
|
||||
err := q.Resource.Create(&resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 更新用户余额
|
||||
user.Balance -= amount
|
||||
_, err = q.User.
|
||||
Where(q.User.ID.Eq(uid)).
|
||||
Update(q.User.Balance, user.Balance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
return &resource, nil
|
||||
}
|
||||
|
||||
func (s *resourceService) CancelResource(ctx context.Context, tradeNo string, at time.Time) error {
|
||||
// 获取请求缓存
|
||||
func (s *resourceService) CancelResource(ctx context.Context, tradeNo string, at time.Time, method TransactionMethod) error {
|
||||
// 删除请求缓存
|
||||
_, err := rds.Client.Del(ctx, tradeNo).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 取消交易
|
||||
err = Transaction.RevokeTransaction(ctx, tradeNo, method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
_, err = q.Trade.
|
||||
Where(q.Trade.InnerNo.Eq(tradeNo)).
|
||||
Select(q.Trade.Status, q.Trade.CancelAt).
|
||||
Updates(m.Trade{
|
||||
Status: 2,
|
||||
CancelAt: common.LocalDateTime(at),
|
||||
})
|
||||
err = Transaction.FinishTransaction(ctx, q.Q, tradeNo, at)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
324
web/services/transaction.go
Normal file
324
web/services/transaction.go
Normal file
@@ -0,0 +1,324 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"platform/web/common"
|
||||
g "platform/web/globals"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
|
||||
)
|
||||
|
||||
var Transaction = &transactionService{}
|
||||
|
||||
type transactionService struct {
|
||||
}
|
||||
|
||||
func (s *transactionService) PrepareTransaction(ctx context.Context, q *q.Query, uid int32, data *TransactionPrepareData) (*TransactionPrepareResult, error) {
|
||||
var subject = data.Subject
|
||||
var amount = data.Amount
|
||||
var expire = data.ExpireAt
|
||||
var tType = data.Type
|
||||
var method = data.Method
|
||||
|
||||
// 生成订单号
|
||||
tradeNo, err := ID.GenSerial(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建支付订单
|
||||
var payUrl string
|
||||
switch method {
|
||||
|
||||
// 调用支付宝支付接口
|
||||
case TransactionMethodAlipay:
|
||||
resp, err := g.Alipay.TradePagePay(alipay.TradePagePay{
|
||||
QRPayMode: "4",
|
||||
Trade: alipay.Trade{
|
||||
ProductCode: "FAST_INSTANT_TRADE_PAY",
|
||||
OutTradeNo: tradeNo,
|
||||
Subject: subject,
|
||||
TotalAmount: strconv.FormatFloat(amount, 'f', 2, 64),
|
||||
TimeExpire: expire.Format("2006-01-02 15:04:05"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payUrl = resp.String()
|
||||
|
||||
// 调用微信支付接口
|
||||
case TransactionMethodWeChat:
|
||||
resp, _, err := g.WechatPay.Native.Prepay(ctx, native.PrepayRequest{
|
||||
Appid: &env.WechatPayAppId,
|
||||
Mchid: &env.WechatPayMchId,
|
||||
OutTradeNo: &tradeNo,
|
||||
Description: &subject,
|
||||
TimeExpire: &expire,
|
||||
NotifyUrl: &env.WechatPayCallbackUrl,
|
||||
Amount: &native.Amount{
|
||||
Total: u.P(int64(amount * 100)),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payUrl = *resp.CodeUrl
|
||||
|
||||
// 不支持的支付方式
|
||||
default:
|
||||
return nil, errors.New("不支持的支付方式")
|
||||
}
|
||||
|
||||
// 保存交易订单
|
||||
var trade = m.Trade{
|
||||
UserID: uid,
|
||||
InnerNo: tradeNo,
|
||||
Subject: subject,
|
||||
Method: int32(method),
|
||||
Type: int32(tType),
|
||||
Amount: amount,
|
||||
Status: 0, // 0-待支付
|
||||
PayURL: payUrl,
|
||||
}
|
||||
err = q.Trade.Create(&trade)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存用户帐单
|
||||
var bill = m.Bill{
|
||||
BillNo: ID.GenReadable("bil"),
|
||||
UserID: uid,
|
||||
TradeID: trade.ID,
|
||||
Info: subject,
|
||||
Type: int32(tType),
|
||||
Amount: amount,
|
||||
}
|
||||
err = q.Bill.
|
||||
Omit(q.Bill.ResourceID, q.Bill.RefundID).
|
||||
Create(&bill)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TransactionPrepareResult{
|
||||
TradeNo: tradeNo,
|
||||
PayURL: payUrl,
|
||||
Bill: &bill,
|
||||
Trade: &trade,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *transactionService) VerifyTransaction(ctx context.Context, data *TransactionVerifyData) (*TransactionVerifyResult, error) {
|
||||
var tradeNo = data.TradeNo
|
||||
var method = data.Method
|
||||
|
||||
// 检查交易号是否存在
|
||||
var transId string
|
||||
var paidAt time.Time
|
||||
var payment float64
|
||||
switch method {
|
||||
|
||||
// 检查支付宝交易
|
||||
case TransactionMethodAlipay:
|
||||
resp, err := g.Alipay.TradeQuery(ctx, alipay.TradeQuery{
|
||||
OutTradeNo: tradeNo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != alipay.CodeSuccess {
|
||||
slog.Warn("支付宝交易取消失败", "code", resp.Code, "sub_code", resp.SubCode, "msg", resp.Msg)
|
||||
return nil, errors.New("交易查询失败")
|
||||
}
|
||||
if resp.TradeStatus != alipay.TradeStatusSuccess {
|
||||
return nil, errors.New("交易未完成")
|
||||
}
|
||||
|
||||
transId = resp.TradeNo
|
||||
payment, err = strconv.ParseFloat(resp.TotalAmount, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paidAt, err = time.Parse("2006-01-02 15:04:05", resp.SendPayDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查微信交易
|
||||
case TransactionMethodWeChat:
|
||||
resp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
|
||||
OutTradeNo: &tradeNo,
|
||||
Mchid: &env.WechatPayMchId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if *resp.TradeState != "SUCCESS" {
|
||||
return nil, errors.New("交易未完成")
|
||||
}
|
||||
|
||||
transId = *resp.TransactionId
|
||||
payment = float64(*resp.Amount.PayerTotal) / 100
|
||||
paidAt, err = time.Parse(time.RFC3339, *resp.SuccessTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 不支持的支付方式
|
||||
default:
|
||||
return nil, errors.New("不支持的支付方式")
|
||||
}
|
||||
|
||||
return &TransactionVerifyResult{
|
||||
TransId: transId,
|
||||
Payment: payment,
|
||||
Time: paidAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *transactionService) CompleteTransaction(ctx context.Context, q *q.Query, data *TransactionCompleteData) (*TransactionCompleteResult, error) {
|
||||
var transId = data.TransId
|
||||
var tradeNo = data.TradeNo
|
||||
var payment = data.Payment
|
||||
var paidAt = data.Time
|
||||
|
||||
// 获取交易信息
|
||||
trade, err := q.Trade.WithContext(ctx).
|
||||
Where(q.Trade.InnerNo.Eq(tradeNo)).
|
||||
First()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查交易状态
|
||||
if trade.Status != 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 更新交易状态
|
||||
trade.Status = 1
|
||||
trade.OuterNo = transId
|
||||
trade.Payment = payment
|
||||
trade.PaidAt = common.LocalDateTime(paidAt)
|
||||
trade.PayURL = ""
|
||||
_, err = q.Trade.WithContext(ctx).Updates(trade)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TransactionCompleteResult{
|
||||
Trade: trade,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *transactionService) RevokeTransaction(ctx context.Context, tradeNo string, method TransactionMethod) error {
|
||||
|
||||
switch method {
|
||||
|
||||
case TransactionMethodAlipay:
|
||||
resp, err := g.Alipay.TradeCancel(ctx, alipay.TradeCancel{
|
||||
OutTradeNo: tradeNo,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Code != alipay.CodeSuccess {
|
||||
slog.Warn("支付宝交易取消失败", "code", resp.Code, "sub_code", resp.SubCode, "msg", resp.Msg)
|
||||
return errors.New("交易取消失败")
|
||||
}
|
||||
|
||||
case TransactionMethodWeChat:
|
||||
resp, err := g.WechatPay.Native.CloseOrder(ctx, native.CloseOrderRequest{
|
||||
Mchid: &env.WechatPayMchId,
|
||||
OutTradeNo: &tradeNo,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Response.StatusCode != http.StatusNoContent {
|
||||
body, _ := io.ReadAll(resp.Response.Body)
|
||||
slog.Warn("微信交易取消失败", "code", resp.Response.StatusCode, "body", string(body))
|
||||
return errors.New("交易取消失败")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *transactionService) FinishTransaction(ctx context.Context, q *q.Query, tradeNo string, time time.Time) error {
|
||||
_, err := q.Trade.
|
||||
Where(q.Trade.InnerNo.Eq(tradeNo)).
|
||||
Select(q.Trade.Status, q.Trade.CancelAt, q.Trade.PayURL).
|
||||
Updates(m.Trade{
|
||||
Status: 2,
|
||||
CancelAt: common.LocalDateTime(time),
|
||||
PayURL: "",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type TransactionType int32
|
||||
|
||||
const (
|
||||
TransactionTypeRecharge TransactionType = iota
|
||||
TransactionTypePurchase
|
||||
)
|
||||
|
||||
type TransactionMethod int32
|
||||
|
||||
const (
|
||||
TransactionMethodAlipay TransactionMethod = iota
|
||||
TransactionMethodWeChat
|
||||
)
|
||||
|
||||
type TransactionPrepareData struct {
|
||||
Subject string
|
||||
Amount float64
|
||||
ExpireAt time.Time
|
||||
Type TransactionType
|
||||
Method TransactionMethod
|
||||
}
|
||||
|
||||
type TransactionPrepareResult struct {
|
||||
TradeNo string
|
||||
PayURL string
|
||||
Bill *m.Bill
|
||||
Trade *m.Trade
|
||||
}
|
||||
|
||||
type TransactionVerifyData struct {
|
||||
TradeNo string
|
||||
Method TransactionMethod
|
||||
}
|
||||
|
||||
type TransactionVerifyResult struct {
|
||||
TransId string
|
||||
Payment float64
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
type TransactionCompleteData struct {
|
||||
TradeNo string
|
||||
TransactionVerifyResult
|
||||
}
|
||||
|
||||
type TransactionCompleteResult struct {
|
||||
Trade *m.Trade
|
||||
}
|
||||
46
web/services/user.go
Normal file
46
web/services/user.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
q "platform/web/queries"
|
||||
)
|
||||
|
||||
var User = &userService{}
|
||||
|
||||
type userService struct{}
|
||||
|
||||
func (s *userService) RechargeConfirm(ctx context.Context, tradeNo string, verified *TransactionVerifyResult) error {
|
||||
|
||||
err := q.Q.Transaction(func(tx *q.Query) error {
|
||||
|
||||
// 更新交易状态
|
||||
result, err := Transaction.CompleteTransaction(ctx, tx, &TransactionCompleteData{
|
||||
TradeNo: tradeNo,
|
||||
TransactionVerifyResult: *verified,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新用户余额
|
||||
user, err := tx.User.
|
||||
Where(tx.User.ID.Eq(result.Trade.UserID)).Take()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.User.
|
||||
Where(tx.User.ID.Eq(user.ID)).
|
||||
Update(tx.User.Balance, user.Balance+result.Trade.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user