Files
platform/web/services/trade.go

732 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/core"
coupon2 "platform/web/domains/coupon"
trade2 "platform/web/domains/trade"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"platform/web/tasks"
"time"
"github.com/shopspring/decimal"
wecahtpaycore "github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/smartwalle/alipay/v3"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
"gorm.io/gorm"
)
var Trade = &tradeService{}
type tradeService struct {
}
func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeData) (*CreateTradeResult, error) {
platform := data.Platform
method := data.Method
tType := data.Product.GetType()
subject := data.Product.GetSubject()
amount := data.Product.GetAmount()
expire := time.Now().Add(30 * time.Minute)
// 实际支付金额,只在创建真实订单时使用
var amountReal = data.Product.GetAmount()
if env.RunMode == "debug" {
amountReal = decimal.NewFromFloat(0.01)
}
// 附加优惠券
if data.CouponCode != nil {
coupon, err := q.Coupon.
Where(
q.Coupon.Code.Eq(*data.CouponCode),
q.Coupon.Status.Eq(int32(coupon2.StatusUnused)),
).
Take()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("优惠券不存在或已失效")
}
return nil, err
}
var expireAt = time.Time(u.Z(coupon.ExpireAt))
if !expireAt.IsZero() && expireAt.Before(now) {
_, err = q.Coupon.
Where(q.Coupon.ID.Eq(coupon.ID)).
Update(q.Coupon.Status, coupon2.StatusExpired)
if err != nil {
return nil, err
}
return nil, errors.New("优惠券已过期")
}
if amount.Cmp(coupon.MinAmount) < 0 {
return nil, errors.New("订单金额未达到使用优惠券的条件")
}
if coupon.UserID != nil {
switch *coupon.UserID {
// 指定用户的优惠券
case uid:
amount = amount.Sub(coupon.Amount)
if expireAt.IsZero() {
_, err = q.Coupon.
Where(q.Coupon.ID.Eq(coupon.ID)).
Update(q.Coupon.Status, int32(coupon2.StatusUsed))
if err != nil {
return nil, err
}
}
// 该优惠券不属于当前用户
default:
return nil, errors.New("优惠券不属于当前用户")
}
} else {
// 公开优惠券
amount = amount.Sub(coupon.Amount)
}
}
// 生成订单号
var tradeNo, err = ID.GenSerial()
if err != nil {
return nil, core.NewServErr("生成订单号失败", err)
}
// 提交支付订单
var paymentUrl string
switch {
// 支付宝 + 电脑网站
case method == trade2.MethodAlipay && platform == trade2.PlatformDesktop:
resp, err := g.Alipay.TradePagePay(alipay.TradePagePay{
QRPayMode: "4",
QRCodeWidth: "196", // 二维码宽度需要-4支付宝页面布局有问题
Trade: alipay.Trade{
ProductCode: "FAST_INSTANT_TRADE_PAY",
OutTradeNo: tradeNo,
Subject: subject,
TotalAmount: amountReal.StringFixed(2),
TimeExpire: expire.Format("2006-01-02 15:04:05"),
},
})
if err != nil {
return nil, err
}
paymentUrl = resp.String()
// 微信 + 电脑网站
case method == trade2.MethodWeChat && platform == trade2.PlatformDesktop:
resp, _, err := g.WechatPay.Native.Prepay(context.Background(), native.PrepayRequest{
Appid: &env.WechatPayAppId,
Mchid: &env.WechatPayMchId,
OutTradeNo: &tradeNo,
Description: &subject,
TimeExpire: &expire,
NotifyUrl: &env.WechatPayCallbackUrl,
Amount: &native.Amount{
Total: u.P(amountReal.Mul(decimal.NewFromInt(100)).Round(0).IntPart()),
},
})
if err != nil {
return nil, err
}
paymentUrl = *resp.CodeUrl
// 商福通 + 电脑网站
case
method == trade2.MethodSftAlipay && platform == trade2.PlatformDesktop,
method == trade2.MethodSftWeChat && platform == trade2.PlatformDesktop:
var payType g.SftPayType
switch method {
case trade2.MethodSftAlipay:
payType = g.SftAlipay
case trade2.MethodSftWeChat:
payType = g.SftWeChat
default:
panic("unhandled default case")
}
resp, err := g.SFTPay.PaymentScanPay(&g.PaymentScanPayReq{
MchOrderNo: tradeNo,
Subject: subject,
Body: subject,
Amount: amountReal.Mul(decimal.NewFromInt(100)).Round(0).IntPart(),
PayType: payType,
Currency: "cny",
ClientIp: "123.52.74.23",
OrderTimeout: u.P(expire.Format("2006-01-02 15:04:05")),
})
if err != nil {
return nil, err
}
paymentUrl = u.Z(u.Z(resp.PayInfo).QrCodeUrl)
// 商福通 + 手机网站
case
method == trade2.MethodSftAlipay && platform == trade2.PlatformMobile,
method == trade2.MethodSftWeChat && platform == trade2.PlatformMobile:
var payType g.SftPayType
switch method {
case trade2.MethodSftAlipay:
payType = g.SftAlipay
case trade2.MethodSftWeChat:
payType = g.SftWeChat
default:
panic("unhandled default case")
}
resp, err := g.SFTPay.PaymentH5Pay(&g.PaymentH5PayReq{
MchOrderNo: tradeNo,
Subject: subject,
Body: subject,
Amount: amountReal.Mul(decimal.NewFromInt(100)).Round(0).IntPart(),
PayType: payType,
Currency: "cny",
ClientIp: "123.52.74.23",
OrderTimeout: u.P(expire.Format("2006-01-02 15:04:05")),
})
if err != nil {
return nil, err
}
paymentUrl = u.Z(u.Z(resp.PayInfo).PayUrl)
// 不支持的支付方式
default:
slog.Warn(ErrTransactionNotSupported.Error(), "method", method, "platform", platform)
return nil, ErrTransactionNotSupported
}
// 保存订单
err = q.Trade.Create(&m.Trade{
UserID: uid,
InnerNo: tradeNo,
Type: int32(tType),
Subject: subject,
Amount: amount,
Method: int32(method),
Platform: int32(platform),
PaymentURL: &paymentUrl,
})
if err != nil {
return nil, core.NewServErr("保存交易订单失败", err)
}
// 缓存产品数据
serialized, err := data.Product.Serialize()
if err != nil {
return nil, core.NewServErr("序列化产品信息失败", err)
}
err = g.Redis.Set(
context.Background(),
tradeProductKey(tradeNo),
serialized,
time.Duration(env.TradeExpire+10)*time.Second,
).Err()
if err != nil {
return nil, core.NewServErr("保存购买信息失败", err)
}
// 提交异步关闭事件
_, err = g.Asynq.Enqueue(tasks.NewCancelTrade(tasks.CancelTradeData{
TradeNo: tradeNo,
Method: method,
}))
if err != nil {
return nil, core.NewServErr("提交异步关闭事件失败", err)
}
return &CreateTradeResult{
PaymentUrl: paymentUrl,
TradeNo: tradeNo,
}, nil
}
func (s *tradeService) CompleteTrade(data *CheckTradeData) error {
return g.Redsync.WithLock(tradeLockKey(data.TradeNo), func() error {
// 检查订单状态
result, err := s.ConfirmTradeCompleted(data)
if err != nil {
return core.NewServErr("确认交易状态失败", err)
}
// 更新交易状态
trade, err := completeTrade(&OnTradeCompletedData{data.TradeNo, *result})
if err != nil {
return core.NewServErr("处理交易失败", err)
}
// 处理交易完成事件
err = afterTradeComplete(trade)
if err != nil {
return core.NewServErr("处理交易完成事件失败", err)
}
return nil
})
}
func (s *tradeService) OnTradeCompleted(data *OnTradeCompletedData) error {
return g.Redsync.WithLock(tradeLockKey(data.TradeNo), func() error {
// 更新交易状态
trade, err := completeTrade(data)
if err != nil {
return core.NewServErr("处理交易失败", err)
}
// 处理交易完成事件
err = afterTradeComplete(trade)
if err != nil {
return core.NewServErr("处理交易完成事件失败", err)
}
return nil
})
}
func completeTrade(data *OnTradeCompletedData) (*m.Trade, error) {
var trade = new(m.Trade)
var err = q.Q.Transaction(func(tx *q.Query) error {
var tradeNo = data.TradeNo
var transId = data.TransId
var payment = data.Payment
var acquirer = data.Acquirer
var paidAt = data.Time
// 获取交易信息
trade, err := q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo)).
Take()
if err != nil {
return core.NewBizErr("获取交易信息失败", err)
}
// 检查交易状态
switch trade2.Status(trade.Status) {
case trade2.StatusCanceled:
return core.NewBizErr("交易已取消")
case trade2.StatusSuccess:
return core.NewBizErr("交易已完成")
case trade2.StatusPending:
}
// 更新交易信息
trade.Status = int32(trade2.StatusSuccess)
trade.OuterNo = &transId
trade.Payment = payment
trade.Acquirer = u.P(int32(acquirer))
trade.CompletedAt = u.P(orm.LocalDateTime(paidAt))
_, err = q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo)).
Updates(trade)
if err != nil {
return core.NewServErr("更新交易信息失败", err)
}
return nil
})
return trade, err
}
func afterTradeComplete(trade *m.Trade) error {
// 恢复购买信息
productData, err := g.Redis.Get(context.Background(), tradeProductKey(trade.InnerNo)).Result()
if err != nil {
return core.NewServErr("恢复购买信息失败", err)
}
// 执行资源创建
var ComplementEvents = []trade2.CompleteEvent{
ResourceOnTradeComplete{},
UserOnTradeComplete{},
}
for _, event := range ComplementEvents {
info, ok := event.Check(trade2.Type(trade.Type))
if !ok {
continue
}
err = info.Deserialize(productData)
if err != nil {
return core.NewServErr("反序列化购买信息失败", err)
}
err = event.OnTradeComplete(info, trade)
if err != nil {
return core.NewServErr("处理交易完成事件失败", err)
}
}
return nil
}
func (s *tradeService) CancelTrade(tradeNo string, method trade2.Method, now time.Time) error {
return g.Redsync.WithLock(tradeLockKey(tradeNo), func() error {
switch method {
case trade2.MethodAlipay:
resp, err := g.Alipay.TradeCancel(context.Background(), alipay.TradeCancel{
OutTradeNo: tradeNo,
})
if err != nil {
return core.NewServErr("上游取消交易失败", err)
}
if resp.Code != alipay.CodeSuccess {
slog.Error("支付宝交易取消失败", "code", resp.Code, "sub_code", resp.SubCode, "msg", resp.Msg)
return errors.New("上游取消交易失败")
}
case trade2.MethodWeChat:
resp, err := g.WechatPay.Native.CloseOrder(context.Background(), native.CloseOrderRequest{
Mchid: &env.WechatPayMchId,
OutTradeNo: &tradeNo,
})
if err != nil {
return core.NewServErr("上游取消交易失败", err)
}
if resp.Response.StatusCode != http.StatusNoContent {
body, err := io.ReadAll(resp.Response.Body)
if err != nil {
slog.Error("读取微信交易取消响应失败", "error", err)
return core.NewServErr("上游取消交易失败", err)
}
slog.Error("微信交易取消失败", "code", resp.Response.StatusCode, "body", string(body))
return errors.New("上游取消交易失败")
}
case trade2.MethodSft, trade2.MethodSftAlipay, trade2.MethodSftWeChat:
_, err := g.SFTPay.OrderClose(&g.OrderCloseReq{
MchOrderNo: &tradeNo,
})
if err != nil {
slog.Debug(fmt.Sprintf("订单无需关闭:%s", err.Error()))
return nil
}
default:
return ErrTransactionNotSupported
}
err := cancelTrade(tradeNo, now)
if err != nil {
return err
}
return nil
})
}
func (s *tradeService) OnTradeCanceled(tradeNo string, now time.Time) error {
err := g.Redsync.WithLock(tradeLockKey(tradeNo), func() error {
return cancelTrade(tradeNo, now)
})
if err != nil {
return core.NewServErr("处理交易取消失败", err)
}
return nil
}
func cancelTrade(tradeNo string, now time.Time) error {
return q.Q.Transaction(func(q *q.Query) error {
// 获取交易信息
var status trade2.Status
err := q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo)).
Select(q.Trade.Status).
Scan(&status)
if err != nil {
return core.NewBizErr("获取交易信息失败", err)
}
// 检查交易状态
switch status {
case trade2.StatusCanceled:
return core.NewBizErr("交易已取消")
case trade2.StatusSuccess:
return core.NewBizErr("交易已完成")
case trade2.StatusPending:
}
// 更新交易状态
_, err = q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo)).
UpdateSimple(
q.Trade.Status.Value(int32(trade2.StatusCanceled)),
q.Trade.CanceledAt.Value(orm.LocalDateTime(now)),
)
if err != nil {
return core.NewServErr("更新交易状态失败", err)
}
return nil
})
}
func (s *tradeService) RefundTrade(tradeNo string, method trade2.Method) error {
panic("todo")
}
func (s *tradeService) OnTradeRefunded(q *q.Query, tradeNo string, now time.Time) error {
panic("todo")
}
func (s *tradeService) CheckTrade(data *CheckTradeData) (*CheckTradeResult, error) {
var tradeNo = data.TradeNo
var method = data.Method
// 检查交易号是否存在
var result = new(CheckTradeResult)
switch method {
// 支付宝
case trade2.MethodAlipay:
// 查询交易状态
resp, err := g.Alipay.TradeQuery(context.Background(), 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("交易查询失败")
}
// 填充返回值
result.TransId = resp.TradeNo
switch resp.TradeStatus {
case alipay.TradeStatusWaitBuyerPay:
result.Status = trade2.StatusPending
case alipay.TradeStatusClosed:
result.Status = trade2.StatusCanceled
case alipay.TradeStatusSuccess, alipay.TradeStatusFinished:
result.Status = trade2.StatusSuccess
result.Success = &TradeSuccessResult{}
result.Success.Acquirer = trade2.AcquirerAlipay
result.Success.Payment, err = decimal.NewFromString(resp.TotalAmount)
if err != nil {
return nil, err
}
result.Success.Time, err = time.Parse("2006-01-02 15:04:05", resp.SendPayDate)
if err != nil {
return nil, err
}
}
// 微信
case trade2.MethodWeChat:
// 查询交易状态
resp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(context.Background(), native.QueryOrderByOutTradeNoRequest{
OutTradeNo: &tradeNo,
Mchid: &env.WechatPayMchId,
})
if err != nil {
var apiErr *wecahtpaycore.APIError
if errors.As(err, &apiErr) {
if apiErr.Code == "ORDER_NOT_EXIST" {
return nil, core.NewBizErr("订单不存在")
}
return nil, core.NewServErr(
fmt.Sprintf("微信上游接口异常code=%vmessage=%v", apiErr.Code, apiErr.Message),
apiErr,
)
}
return nil, core.NewServErr(fmt.Sprintf("微信上游支付接口异常:%s", err.Error()))
}
// 填充返回值
result.TransId = *resp.TransactionId
switch *resp.TradeState {
case "NOTPAY":
result.Status = trade2.StatusPending
case "CLOSED":
result.Status = trade2.StatusCanceled
case "SUCCESS", "REFUND":
result.Status = trade2.StatusSuccess
result.Success = &TradeSuccessResult{}
result.Success.Acquirer = trade2.AcquirerWeChat
result.Success.Payment = decimal.NewFromInt(*resp.Amount.PayerTotal).Div(decimal.NewFromInt(100))
result.Success.Time, err = time.Parse(time.RFC3339, *resp.SuccessTime)
if err != nil {
return nil, err
}
}
// 商福通
case trade2.MethodSft, trade2.MethodSftAlipay, trade2.MethodSftWeChat:
// 查询交易状态
resp, err := g.SFTPay.QueryTrade(&g.QueryTradeReq{
MchOrderNo: &tradeNo,
})
if err != nil {
return nil, err
}
if resp.PayOrderId == nil {
return nil, errors.New("商福通交易号不存在")
}
// 填充返回值
result.TransId = *resp.PayOrderId
switch resp.State {
case g.SftInit, g.SftTradeAwait, g.SftTradeFail:
result.Status = trade2.StatusPending
case g.SftTradeClosed, g.SftTradeCancel:
result.Status = trade2.StatusCanceled
case g.SftTradeSuccess, g.SftTradeRefund, g.SftRefundIng:
result.Status = trade2.StatusSuccess
result.Success = &TradeSuccessResult{}
switch resp.PayType {
case "WECHAT":
result.Success.Acquirer = trade2.AcquirerWeChat
case "ALIPAY":
result.Success.Acquirer = trade2.AcquirerAlipay
case "UNIONPAY":
result.Success.Acquirer = trade2.AcquirerUnionPay
}
result.Success.Payment = decimal.NewFromInt(resp.Amount).Div(decimal.NewFromInt(100))
result.Success.Time, err = time.Parse("2006-01-02 15:04:05", *resp.PayTime)
if err != nil {
return nil, err
}
}
// 不支持的支付方式
default:
return nil, ErrTransactionNotSupported
}
return result, nil
}
func (s *tradeService) ConfirmTradeCompleted(data *CheckTradeData) (*TradeSuccessResult, error) {
rs, err := Trade.CheckTrade(&CheckTradeData{
TradeNo: data.TradeNo,
Method: data.Method,
})
if err != nil {
return nil, err
}
switch rs.Status {
case trade2.StatusPending:
return nil, core.NewBizErr("订单未支付")
case trade2.StatusCanceled:
return nil, core.NewBizErr("订单已关闭")
case trade2.StatusSuccess:
}
return rs.Success, nil
}
func (s *tradeService) ConfirmTradeCanceled(data *CheckTradeData) error {
rs, err := Trade.CheckTrade(&CheckTradeData{
TradeNo: data.TradeNo,
Method: data.Method,
})
if err != nil {
return err
}
switch rs.Status {
case trade2.StatusPending:
return core.NewBizErr("订单未支付")
case trade2.StatusSuccess:
return core.NewBizErr("订单已关闭")
case trade2.StatusCanceled:
}
return nil
}
func (s *tradeService) ConfirmTradeRefunded(data *CheckTradeData) error {
rs, err := Trade.CheckTrade(&CheckTradeData{
TradeNo: data.TradeNo,
Method: data.Method,
})
if err != nil {
return err
}
switch rs.Status {
case trade2.StatusPending:
return core.NewBizErr("订单未支付")
case trade2.StatusCanceled:
return core.NewBizErr("订单已关闭")
case trade2.StatusSuccess:
}
return core.NewBizErr("订单状态异常")
}
func tradeProductKey(no string) string {
return fmt.Sprintf("trade:%s:product", no)
}
func tradeLockKey(no string) string {
return fmt.Sprintf("trade:%s:lock", no)
}
type CreateTradeData struct {
Platform trade2.Platform `json:"platform" validate:"required"`
Method trade2.Method `json:"method" validate:"required"`
CouponCode *string `json:"coupon_code"`
Product trade2.ProductInfo
}
type CreateTradeResult struct {
TradeNo string
PaymentUrl string
}
type CheckTradeData struct {
TradeNo string
Method trade2.Method
}
type CheckTradeResult struct {
TransId string
Status trade2.Status
Success *TradeSuccessResult
}
type TradeSuccessResult struct {
TransId string
Acquirer trade2.Acquirer
Payment decimal.Decimal
Time time.Time
}
type OnTradeCompletedData struct {
TradeNo string
TradeSuccessResult
}
type TradeErr string
func (e TradeErr) Error() string {
return string(e)
}
var (
ErrTransactionNotSupported = core.NewBizErr("不支持的支付方式")
)