修复商福通客户端加解密逻辑,交易表新增收单机构字段用来保存实际支付方式,取消交易接口实现

This commit is contained in:
2025-06-05 12:59:07 +08:00
parent 392e404d68
commit 692106ae5c
9 changed files with 452 additions and 170 deletions

View File

@@ -10,8 +10,19 @@ const (
type Method int32
const (
MethodAlipay Method = iota + 1 // 支付宝
MethodWeChat // 微信
MethodAlipay Method = iota + 1 // 支付宝
MethodWeChat // 微信
MethodSft // 商福通
MethodSftAlipay // 商福通渠道指定支付宝
MethodSftWeChat // 商福通渠道指定微信
)
type Acquirer int32
const (
AcquirerAlipay Acquirer = iota + 1 // 支付宝
AcquirerWeChat // 微信
AcquirerUnionPay // 银联
)
type Status int32

View File

@@ -4,6 +4,7 @@ import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/json"
"encoding/pem"
@@ -20,15 +21,17 @@ var SFTPay SftClient
type SftClient struct {
appid string
appSecret string
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
}
func init() {
if !env.SftPayEnable {
return
}
SFTPay = SftClient{
appid: env.SftPayAppId,
appSecret: env.SftPayAppSecret,
appid: env.SftPayAppId,
}
// 加载私钥
@@ -73,6 +76,21 @@ func init() {
SFTPay.publicKey = publicKey
}
func (s *SftClient) PaymentScanPay(req *PaymentScanPayReq) (*PaymentScanPayResp, error) {
const url = "https://pay.rscygroup.com/api/open/payment/scanpay"
return call[PaymentScanPayResp](s, url, req)
}
func (s *SftClient) PaymentH5Pay(req *PaymentH5PayReq) (*PaymentH5PayResp, error) {
const url = "https://pay.rscygroup.com/api/open/payment/h5pay"
return call[PaymentH5PayResp](s, url, req)
}
func (s *SftClient) OrderClose(req *OrderCloseReq) (*OrderCloseResp, error) {
const url = "https://pay.rscygroup.com/api/open/order/close"
return call[OrderCloseResp](s, url, req)
}
type PaymentScanPayReq struct {
Subject string `json:"subject"`
Body string `json:"body"`
@@ -93,18 +111,56 @@ type PaymentScanPayReq struct {
LimitPay *int `json:"limitPay"`
}
type PaymentH5PayReq struct {
Subject string `json:"subject"`
Body string `json:"body"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
PayType SftPayType `json:"payType"`
ClientIp string `json:"clientIp"`
MchOrderNo string `json:"mchOrderNo"`
StoreId *string `json:"storeId"`
RouteNo *string `json:"routeNo"`
HbFqNum *int `json:"hbFqNum"`
HbFqPercent *int `json:"hbFqPercent"`
BuyerRemark *string `json:"buyerRemark"`
NotifyUrl *string `json:"notifyUrl"`
ReturnUrl *string `json:"returnUrl"`
ExpiredTime *int `json:"expiredTime"`
OrderTimeout *string `json:"orderTimeout"`
ExtParam *string `json:"extParam"`
LimitPay *int `json:"limitPay"`
}
type OrderCloseReq struct {
MchOrderNo *string `json:"mchOrderNo"`
PayOrderId *string `json:"payOrderId"`
}
// type OrderRefundReq struct {
// mchRefundNo
// payOrderId
// mchOrderNo
// refundReason
// refundAmount
// notifyUrl
// extParam
// }
type PaymentScanPayResp struct {
Amount int64 `json:"amount"`
MchOrderNo string `json:"mchOrderNo"`
PayOrderId string `json:"payOrderId"`
MercNo string `json:"mercNo"`
ChannelSendNo *string `json:"channelSendNo"`
ChannelTradeNo *string `json:"channelTradeNo"`
State string `json:"state"`
PayType string `json:"payType"`
IfCode string `json:"ifCode"`
ExtParam *string `json:"extParam"`
PayInfo *string `json:"payInfo"`
Amount int64 `json:"amount"`
MchOrderNo string `json:"mchOrderNo"`
PayOrderId string `json:"payOrderId"`
MercNo string `json:"mercNo"`
ChannelSendNo *string `json:"channelSendNo"`
ChannelTradeNo *string `json:"channelTradeNo"`
State string `json:"state"`
PayType SftPayType `json:"payType"`
IfCode string `json:"ifCode"`
ExtParam *string `json:"extParam"`
PayInfo *struct {
QrCodeUrl *string `json:"qrCodeUrl"`
} `json:"payInfo"`
Note *string `json:"note"`
TradeFee *int64 `json:"tradeFee"`
StoreId *string `json:"storeId"`
@@ -116,44 +172,20 @@ type PaymentScanPayResp struct {
SettlementType *string `json:"settlementType"`
}
func (s *SftClient) PaymentScanPay(req *PaymentScanPayResp) (*PaymentScanPayResp, error) {
const url = "https://pay.rscygroup.com/api/open/payment/scanpay"
return call[PaymentScanPayResp](s, url, req)
}
type PaymentH5PayReq struct {
Subject string `json:"subject"`
Body string `json:"body"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
PayType string `json:"payType"`
ClientIp string `json:"clientIp"`
MchOrderNo string `json:"mchOrderNo"`
StoreId *string `json:"storeId"`
RouteNo *string `json:"routeNo"`
HbFqNum *int `json:"hbFqNum"`
HbFqPercent *int `json:"hbFqPercent"`
BuyerRemark *string `json:"buyerRemark"`
NotifyUrl *string `json:"notifyUrl"`
ReturnUrl *string `json:"returnUrl"`
ExpiredTime *int `json:"expiredTime"`
OrderTimeout *string `json:"orderTimeout"`
ExtParam *string `json:"extParam"`
LimitPay *int `json:"limitPay"`
}
type PaymentH5PayResp struct {
Amount int64 `json:"amount"`
MchOrderNo string `json:"mchOrderNo"`
PayOrderId string `json:"payOrderId"`
MercNo string `json:"mercNo"`
ChannelSendNo *string `json:"channelSendNo"`
ChannelTradeNo *string `json:"channelTradeNo"`
State string `json:"state"`
PayType string `json:"payType"`
IfCode string `json:"ifCode"`
ExtParam *string `json:"extParam"`
PayInfo *string `json:"payInfo"`
Amount int64 `json:"amount"`
MchOrderNo string `json:"mchOrderNo"`
PayOrderId string `json:"payOrderId"`
MercNo string `json:"mercNo"`
ChannelSendNo *string `json:"channelSendNo"`
ChannelTradeNo *string `json:"channelTradeNo"`
State string `json:"state"`
PayType SftPayType `json:"payType"`
IfCode string `json:"ifCode"`
ExtParam *string `json:"extParam"`
PayInfo *struct {
PayUrl *string `json:"payUrl"`
} `json:"payInfo"`
Note *string `json:"note"`
TradeFee *int64 `json:"tradeFee"`
StoreId *string `json:"storeId"`
@@ -165,9 +197,12 @@ type PaymentH5PayResp struct {
SettlementType *string `json:"settlementType"`
}
func (s *SftClient) CreateOrderByRedirect(req *PaymentH5PayReq) (*PaymentH5PayResp, error) {
const url = "https://pay.rscygroup.com/api/open/payment/h5pay"
return call[PaymentH5PayResp](s, url, req)
type OrderCloseResp struct {
MchOrderNo string `json:"mchOrderNo"`
PayOrderId string `json:"payOrderId"`
MercNo string `json:"mercNo"`
Amount int64 `json:"amount"`
State string `json:"state"`
}
func call[T any](s *SftClient, url string, req any) (*T, error) {
@@ -231,7 +266,6 @@ func call[T any](s *SftClient, url string, req any) (*T, error) {
func (s *SftClient) sign(msg any) (*request, error) {
// 处理请求正文
bytes, err := json.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("格式化加密正文失败:%w", err)
@@ -246,12 +280,13 @@ func (s *SftClient) sign(msg any) (*request, error) {
BizData: string(bytes),
}
encrypted, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, []byte(body.String(s.appSecret)))
hashed := sha256.Sum256([]byte(body.String()))
signature, err := rsa.SignPKCS1v15(nil, s.privateKey, crypto.SHA256, hashed[:])
if err != nil {
return nil, fmt.Errorf("签名失败:%w", err)
}
body.Sign = string(encrypted)
body.Sign = string(signature)
return &body, nil
}
@@ -264,7 +299,9 @@ func (s *SftClient) verify(str []byte) (*response, error) {
}
if resp.Sign != nil || resp.SignType != nil || resp.BizData != nil {
err := rsa.VerifyPKCS1v15(s.publicKey, crypto.SHA256, str, []byte(s.appSecret))
hashed := sha256.Sum256([]byte(resp.String()))
err := rsa.VerifyPKCS1v15(s.publicKey, crypto.SHA256, hashed[:], []byte(*resp.Sign))
if err != nil {
return nil, fmt.Errorf("验签失败:%w", err)
}
@@ -283,10 +320,10 @@ type request struct {
BizData string `json:"bizData"`
}
func (r request) String(secret string) string {
func (r request) String() string {
return fmt.Sprintf(
"appId=%s&bizData=%s&reqId=%s&reqTime=%s&signType=%s&version=%s&appSecret=%s",
r.AppId, r.BizData, r.ReqId, r.ReqTime, r.SignType, r.Version, secret,
"appId=%s&bizData=%s&reqId=%s&reqTime=%s&signType=%s&version=%s",
r.AppId, r.BizData, r.ReqId, r.ReqTime, r.SignType, r.Version,
)
}
@@ -298,3 +335,18 @@ type response struct {
SignType *string `json:"signType"`
Timestamp string `json:"timestamp"`
}
func (r response) String() string {
return fmt.Sprintf(
"bizData=%s&code=%s&msg=%s&signType=%s&timestamp=%s",
u.Z(r.BizData), r.Code, u.Z(r.Msg), u.Z(r.SignType), r.Timestamp,
)
}
type SftPayType string
const (
SftAlipay SftPayType = "ALIPAY"
SftWeChat SftPayType = "WECHAT"
SftUnionPay SftPayType = "UNIONPAY"
)

View File

@@ -215,7 +215,7 @@ func RechargeConfirmAlipay(c *fiber.Ctx) error {
}
// 验证支付结果
result, err := s.Trade.VerifyTrade(&s.TradeVerifyData{
result, err := s.Trade.VerifyCreateTrade(&s.TradeVerifyData{
TradeNo: req.TradeNo,
Method: trade2.MethodAlipay,
})
@@ -289,7 +289,7 @@ func RechargeConfirmWechat(c *fiber.Ctx) error {
}
// 验证支付结果
result, err := s.Trade.VerifyTrade(&s.TradeVerifyData{
result, err := s.Trade.VerifyCreateTrade(&s.TradeVerifyData{
TradeNo: req.TradeNo,
Method: trade2.MethodWeChat,
})

View File

@@ -24,7 +24,7 @@ type Trade struct {
Remark *string `gorm:"column:remark;type:character varying(255);comment:订单备注" json:"remark"` // 订单备注
Amount decimal.Decimal `gorm:"column:amount;type:numeric(12,2);not null;comment:订单总金额" json:"amount"` // 订单总金额
Payment decimal.Decimal `gorm:"column:payment;type:numeric(12,2);not null;comment:支付金额" json:"payment"` // 支付金额
Method int32 `gorm:"column:method;type:integer;not null;comment:支付方式1-支付宝2-微信" json:"method"` // 支付方式1-支付宝2-微信
Method int32 `gorm:"column:method;type:integer;not null;comment:支付方式1-支付宝2-微信3-商福通" json:"method"` // 支付方式1-支付宝2-微信3-商福通
Status int32 `gorm:"column:status;type:integer;not null;comment:订单状态0-待支付1-已支付2-已取消3-已退款" json:"status"` // 订单状态0-待支付1-已支付2-已取消3-已退款
PayURL *string `gorm:"column:pay_url;type:text;comment:支付链接" json:"pay_url"` // 支付链接
PaidAt *orm.LocalDateTime `gorm:"column:paid_at;type:timestamp without time zone;comment:支付时间" json:"paid_at"` // 支付时间
@@ -32,6 +32,7 @@ type Trade struct {
CreatedAt *orm.LocalDateTime `gorm:"column:created_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt *orm.LocalDateTime `gorm:"column:updated_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp without time zone;comment:删除时间" json:"deleted_at"` // 删除时间
Acquirer int32 `gorm:"column:acquirer;type:integer;not null;comment:收单机构1-支付宝2-微信3-银联" json:"acquirer"` // 收单机构1-支付宝2-微信3-银联
}
// TableName Trade's table name

View File

@@ -44,6 +44,7 @@ func newTrade(db *gorm.DB, opts ...gen.DOOption) trade {
_trade.CreatedAt = field.NewField(tableName, "created_at")
_trade.UpdatedAt = field.NewField(tableName, "updated_at")
_trade.DeletedAt = field.NewField(tableName, "deleted_at")
_trade.Acquirer = field.NewInt32(tableName, "acquirer")
_trade.fillFieldMap()
@@ -63,7 +64,7 @@ type trade struct {
Remark field.String // 订单备注
Amount field.Field // 订单总金额
Payment field.Field // 支付金额
Method field.Int32 // 支付方式1-支付宝2-微信
Method field.Int32 // 支付方式1-支付宝2-微信3-商福通
Status field.Int32 // 订单状态0-待支付1-已支付2-已取消3-已退款
PayURL field.String // 支付链接
PaidAt field.Field // 支付时间
@@ -71,6 +72,7 @@ type trade struct {
CreatedAt field.Field // 创建时间
UpdatedAt field.Field // 更新时间
DeletedAt field.Field // 删除时间
Acquirer field.Int32 // 收单机构1-支付宝2-微信3-银联
fieldMap map[string]field.Expr
}
@@ -104,6 +106,7 @@ func (t *trade) updateTableName(table string) *trade {
t.CreatedAt = field.NewField(table, "created_at")
t.UpdatedAt = field.NewField(table, "updated_at")
t.DeletedAt = field.NewField(table, "deleted_at")
t.Acquirer = field.NewInt32(table, "acquirer")
t.fillFieldMap()
@@ -120,7 +123,7 @@ func (t *trade) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (t *trade) fillFieldMap() {
t.fieldMap = make(map[string]field.Expr, 17)
t.fieldMap = make(map[string]field.Expr, 18)
t.fieldMap["id"] = t.ID
t.fieldMap["user_id"] = t.UserID
t.fieldMap["inner_no"] = t.InnerNo
@@ -138,6 +141,7 @@ func (t *trade) fillFieldMap() {
t.fieldMap["created_at"] = t.CreatedAt
t.fieldMap["updated_at"] = t.UpdatedAt
t.fieldMap["deleted_at"] = t.DeletedAt
t.fieldMap["acquirer"] = t.Acquirer
}
func (t trade) clone(db *gorm.DB) trade {

View File

@@ -158,7 +158,7 @@ func (s *resourceService) CompleteResource(tradeNo string, now time.Time, opResu
rs = opResult[0]
} else {
var err error
rs, err = Trade.VerifyTrade(&TradeVerifyData{
rs, err = Trade.VerifyCreateTrade(&TradeVerifyData{
TradeNo: tradeNo,
Method: cache.Method,
})
@@ -229,7 +229,7 @@ func (s *resourceService) CancelResource(tradeNo string, now time.Time, opRevoke
// 取消交易
if len(opRevoked) <= 0 {
err = Trade.SendCancelTrade(tradeNo, cache.Method)
err = Trade.CancelTrade(tradeNo, cache.Method)
if err != nil {
return err
}

View File

@@ -103,6 +103,7 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
// 创建支付订单
var payUrl string
var acquirer trade2.Acquirer
switch method {
// 调用支付宝支付接口
@@ -122,6 +123,7 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
return nil, err
}
payUrl = resp.String()
acquirer = trade2.AcquirerAlipay
// 调用微信支付接口
case trade2.MethodWeChat:
@@ -140,6 +142,34 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
return nil, err
}
payUrl = *resp.CodeUrl
acquirer = trade2.AcquirerWeChat
// 调用商福通接口
case trade2.MethodSft:
resp, err := g.SFTPay.PaymentScanPay(&g.PaymentScanPayReq{
MchOrderNo: tradeNo,
Subject: subject,
Body: subject,
Amount: amountReal.Mul(decimal.NewFromInt(100)).Round(0).IntPart(),
Currency: "cny",
ClientIp: "",
OrderTimeout: u.P(expire.Format("2006-01-02 15:04:05")),
})
if err != nil {
return nil, err
}
payUrl = u.Z(u.Z(resp.PayInfo).QrCodeUrl)
if payUrl == "" {
return nil, errors.New("支付接口未返回正确的二维码地址")
}
switch resp.PayType {
case g.SftAlipay:
acquirer = trade2.AcquirerAlipay
case g.SftWeChat:
acquirer = trade2.AcquirerWeChat
case g.SftUnionPay:
acquirer = trade2.AcquirerUnionPay
}
// 不支持的支付方式
default:
@@ -156,13 +186,14 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
}
var trade = m.Trade{
UserID: uid,
InnerNo: tradeNo,
Subject: subject,
Method: int32(method),
Type: int32(tType),
Amount: amount,
PayURL: &payUrl,
UserID: uid,
InnerNo: tradeNo,
Subject: subject,
Method: int32(method),
Type: int32(tType),
Amount: amount,
PayURL: &payUrl,
Acquirer: int32(acquirer),
}
err = q.Trade.Create(&trade)
if err != nil {
@@ -198,105 +229,176 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
Trade: &trade,
}, nil
}
func (s *tradeService) SendCreateTradeByRedirect() {
panic("todo")
}
func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time.Time, data *TradeCreateData) (*TradeCreateResult, error) {
var subject = data.Subject
var expire = data.ExpireAt
var tType = data.Type
var method = data.Method
var amount = data.Amount
func (s *tradeService) OnTradeCreated(q *q.Query, data *OnTradeCreateData) (*m.Trade, error) {
var transId = data.TransId
var tradeNo = data.TradeNo
var payment = data.Payment
var paidAt = data.Time
// 实际支付金额,只在创建真实订单时使用
var amountReal = data.Amount
if env.RunMode == "debug" {
amountReal = decimal.NewFromFloat(0.01)
}
// 获取交易信息
trade, err := q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo)).
First()
// 附加优惠券
if data.CouponCode != "" {
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)
}
}
// 生成订单号
tradeNo, err := ID.GenSerial(context.Background())
if err != nil {
return nil, err
}
// 检查交易状态
switch trade2.Status(trade.Status) {
// 创建支付订单
var payUrl string
var acquirer trade2.Acquirer
switch method {
// 如果已退款或取消,则返回错误
case trade2.StatusCanceled, trade2.StatusRefunded:
return nil, errors.New("交易已取消或已退款")
// 如果是未支付,则更新支付状态
case trade2.StatusPending:
trade.Status = int32(trade2.StatusSuccess)
trade.OuterNo = &transId
trade.Payment = payment
trade.PaidAt = u.P(orm.LocalDateTime(paidAt))
trade.PayURL = u.P("")
_, err = q.Trade.Updates(trade)
// 调用商福通接口
case trade2.MethodSftAlipay, trade2.MethodSftWeChat:
var payType g.SftPayType
if method == trade2.MethodSftAlipay {
payType = g.SftAlipay
} else {
payType = g.SftWeChat
}
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: "",
OrderTimeout: u.P(expire.Format("2006-01-02 15:04:05")),
})
if err != nil {
return nil, err
}
case trade2.StatusSuccess:
payUrl = u.Z(u.Z(resp.PayInfo).PayUrl)
if payUrl == "" {
return nil, errors.New("支付接口未返回正确的二维码地址")
}
switch resp.PayType {
case g.SftAlipay:
acquirer = trade2.AcquirerAlipay
case g.SftWeChat:
acquirer = trade2.AcquirerWeChat
case g.SftUnionPay:
acquirer = trade2.AcquirerUnionPay
}
// 不支持的支付方式
default:
return nil, ErrTransactionNotSupported
}
return trade, nil
}
func (s *tradeService) SendCancelTrade(tradeNo string, method trade2.Method) error {
switch method {
case trade2.MethodAlipay:
resp, err := g.Alipay.TradeCancel(context.Background(), 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 trade2.MethodWeChat:
resp, err := g.WechatPay.Native.CloseOrder(context.Background(), 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("交易取消失败")
}
// 保存交易订单
var billType bill2.Type
switch tType {
case trade2.TypeRecharge:
billType = bill2.TypeRecharge
case trade2.TypePurchase:
billType = bill2.TypeConsume
}
return nil
}
func (s *tradeService) OnTradeCanceled(q *q.Query, tradeNo string, now 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: int32(trade2.StatusCanceled),
CancelAt: u.P(orm.LocalDateTime(now)),
PayURL: u.P(""),
})
var trade = m.Trade{
UserID: uid,
InnerNo: tradeNo,
Subject: subject,
Method: int32(method),
Type: int32(tType),
Amount: amount,
PayURL: &payUrl,
Acquirer: int32(acquirer),
}
err = q.Trade.Create(&trade)
if err != nil {
return err
return nil, err
}
return nil
}
// 保存用户帐单
var bill = m.Bill{
BillNo: ID.GenReadable("bil"),
UserID: uid,
TradeID: &trade.ID,
Info: &subject,
Type: int32(billType),
Amount: amount,
}
err = q.Bill.
Omit(q.Bill.ResourceID, q.Bill.RefundID).
Create(&bill)
if err != nil {
return nil, err
}
func (s *tradeService) SendRefundTrade(tradeNo string, method trade2.Method) error {
panic("todo")
}
func (s *tradeService) OnTradeRefunded(q *q.Query, tradeNo string, now time.Time) error {
panic("todo")
}
// 提交异步任务更新订单状态
_, err = g.Asynq.Enqueue(tasks.NewUpdateTrade(tradeNo, method))
if err != nil {
return nil, err
}
func (s *tradeService) VerifyTrade(data *TradeVerifyData) (*TradeSuccessResult, error) {
return &TradeCreateResult{
TradeNo: tradeNo,
PayURL: payUrl,
Bill: &bill,
Trade: &trade,
}, nil
}
func (s *tradeService) VerifyCreateTrade(data *TradeVerifyData) (*TradeSuccessResult, error) {
var tradeNo = data.TradeNo
var method = data.Method
@@ -363,6 +465,114 @@ func (s *tradeService) VerifyTrade(data *TradeVerifyData) (*TradeSuccessResult,
Time: paidAt,
}, nil
}
func (s *tradeService) OnTradeCreated(q *q.Query, data *OnTradeCreateData) (*m.Trade, error) {
var transId = data.TransId
var tradeNo = data.TradeNo
var payment = data.Payment
var paidAt = data.Time
// 获取交易信息
trade, err := q.Trade.
Where(q.Trade.InnerNo.Eq(tradeNo)).
First()
if err != nil {
return nil, err
}
// 检查交易状态
switch trade2.Status(trade.Status) {
// 如果已退款或取消,则返回错误
case trade2.StatusCanceled, trade2.StatusRefunded:
return nil, errors.New("交易已取消或已退款")
// 如果是未支付,则更新支付状态
case trade2.StatusPending:
trade.Status = int32(trade2.StatusSuccess)
trade.OuterNo = &transId
trade.Payment = payment
trade.PaidAt = u.P(orm.LocalDateTime(paidAt))
trade.PayURL = u.P("")
_, err = q.Trade.Updates(trade)
if err != nil {
return nil, err
}
case trade2.StatusSuccess:
}
return trade, nil
}
func (s *tradeService) CancelTrade(tradeNo string, method trade2.Method) error {
switch method {
case trade2.MethodAlipay:
resp, err := g.Alipay.TradeCancel(context.Background(), 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 trade2.MethodWeChat:
resp, err := g.WechatPay.Native.CloseOrder(context.Background(), 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("交易取消失败")
}
case trade2.MethodSft:
resp, err := g.SFTPay.OrderClose(&g.OrderCloseReq{
MchOrderNo: &tradeNo,
})
if err != nil {
return err
}
if resp.State != "TRADE_CLOSE" {
slog.Warn("商福通交易取消失败", "state", resp.State)
return errors.New("交易取消失败")
}
default:
return ErrTransactionNotSupported
}
return nil
}
func (s *tradeService) OnTradeCanceled(q *q.Query, tradeNo string, now 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: int32(trade2.StatusCanceled),
CancelAt: u.P(orm.LocalDateTime(now)),
PayURL: u.P(""),
})
if err != nil {
return err
}
return nil
}
func (s *tradeService) SendRefundTrade(tradeNo string, method trade2.Method) error {
panic("todo")
}
func (s *tradeService) OnTradeRefunded(q *q.Query, tradeNo string, now time.Time) error {
panic("todo")
}
type TradeCreateData struct {
Subject string
@@ -396,12 +606,11 @@ type OnTradeCreateData struct {
TradeSuccessResult
}
type TradeResult int
type TradePlatform int
const (
TradeSuccess TradeResult = iota + 1
TradeCanceled
TradeClosed
TradePlatformDesktop TradePlatform = iota + 1 // 桌面端
TradePlatformMobile // 移动端
)
type TradeErr string