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

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

19
pkg/env/env.go vendored
View File

@@ -350,8 +350,8 @@ func loadAliyun() {
// region 商福通 // region 商福通
var ( var (
SftPayEnable = false
SftPayAppId string SftPayAppId string
SftPayAppSecret string
SftPayAppPrivateKey string SftPayAppPrivateKey string
SftPayPublicKey string SftPayPublicKey string
) )
@@ -359,6 +359,15 @@ var (
func loadSftPay() { func loadSftPay() {
var value string var value string
value = os.Getenv("SFTPAY_ENABLE")
if value != "" {
enabled, err := strconv.ParseBool(value)
if err != nil {
panic("环境变量 SFTPAY_ENABLE 的值不是布尔值")
}
SftPayEnable = enabled
}
value = os.Getenv("SFTPAY_APP_ID") value = os.Getenv("SFTPAY_APP_ID")
if value == "" { if value == "" {
panic("环境变量 ALIYUN_SMS_TEMPLATE_LOGIN 的值不能为空") panic("环境变量 ALIYUN_SMS_TEMPLATE_LOGIN 的值不能为空")
@@ -379,13 +388,6 @@ func loadSftPay() {
} else { } else {
SftPayPublicKey = value SftPayPublicKey = value
} }
value = os.Getenv("SFTPAY_APP_SECRET")
if value == "" {
panic("环境变量 SFTPAY_APP_SECRET 的值不能为空")
} else {
SftPayAppSecret = value
}
} }
// endregion // endregion
@@ -440,4 +442,5 @@ func Init() {
loadAlipay() loadAlipay()
loadWechatPay() loadWechatPay()
loadAliyun() loadAliyun()
// loadSftPay()
} }

View File

@@ -805,6 +805,7 @@ create table trade (
amount decimal(12, 2) not null default 0, amount decimal(12, 2) not null default 0,
payment decimal(12, 2) not null default 0, payment decimal(12, 2) not null default 0,
method int not null, method int not null,
acquirer int not null,
status int not null default 0, status int not null default 0,
pay_url text, pay_url text,
paid_at timestamp, paid_at timestamp,
@@ -830,7 +831,8 @@ comment on column trade.subject is '订单主题';
comment on column trade.remark is '订单备注'; comment on column trade.remark is '订单备注';
comment on column trade.amount is '订单总金额'; comment on column trade.amount is '订单总金额';
comment on column trade.payment is '支付金额'; comment on column trade.payment is '支付金额';
comment on column trade.method is '支付方式1-支付宝2-微信'; comment on column trade.method is '支付方式1-支付宝2-微信3-商福通渠道支付宝4-商福通渠道微信';
comment on column trade.acquirer is '收单机构1-支付宝2-微信3-银联';
comment on column trade.status is '订单状态0-待支付1-已支付2-已取消3-已退款'; comment on column trade.status is '订单状态0-待支付1-已支付2-已取消3-已退款';
comment on column trade.pay_url is '支付链接'; comment on column trade.pay_url is '支付链接';
comment on column trade.paid_at is '支付时间'; comment on column trade.paid_at is '支付时间';

View File

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

View File

@@ -4,6 +4,7 @@ import (
"crypto" "crypto"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
@@ -20,15 +21,17 @@ var SFTPay SftClient
type SftClient struct { type SftClient struct {
appid string appid string
appSecret string
privateKey *rsa.PrivateKey privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey publicKey *rsa.PublicKey
} }
func init() { func init() {
if !env.SftPayEnable {
return
}
SFTPay = SftClient{ SFTPay = SftClient{
appid: env.SftPayAppId, appid: env.SftPayAppId,
appSecret: env.SftPayAppSecret,
} }
// 加载私钥 // 加载私钥
@@ -73,6 +76,21 @@ func init() {
SFTPay.publicKey = publicKey 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 { type PaymentScanPayReq struct {
Subject string `json:"subject"` Subject string `json:"subject"`
Body string `json:"body"` Body string `json:"body"`
@@ -93,18 +111,56 @@ type PaymentScanPayReq struct {
LimitPay *int `json:"limitPay"` 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 { type PaymentScanPayResp struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
MchOrderNo string `json:"mchOrderNo"` MchOrderNo string `json:"mchOrderNo"`
PayOrderId string `json:"payOrderId"` PayOrderId string `json:"payOrderId"`
MercNo string `json:"mercNo"` MercNo string `json:"mercNo"`
ChannelSendNo *string `json:"channelSendNo"` ChannelSendNo *string `json:"channelSendNo"`
ChannelTradeNo *string `json:"channelTradeNo"` ChannelTradeNo *string `json:"channelTradeNo"`
State string `json:"state"` State string `json:"state"`
PayType string `json:"payType"` PayType SftPayType `json:"payType"`
IfCode string `json:"ifCode"` IfCode string `json:"ifCode"`
ExtParam *string `json:"extParam"` ExtParam *string `json:"extParam"`
PayInfo *string `json:"payInfo"` PayInfo *struct {
QrCodeUrl *string `json:"qrCodeUrl"`
} `json:"payInfo"`
Note *string `json:"note"` Note *string `json:"note"`
TradeFee *int64 `json:"tradeFee"` TradeFee *int64 `json:"tradeFee"`
StoreId *string `json:"storeId"` StoreId *string `json:"storeId"`
@@ -116,44 +172,20 @@ type PaymentScanPayResp struct {
SettlementType *string `json:"settlementType"` 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 { type PaymentH5PayResp struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
MchOrderNo string `json:"mchOrderNo"` MchOrderNo string `json:"mchOrderNo"`
PayOrderId string `json:"payOrderId"` PayOrderId string `json:"payOrderId"`
MercNo string `json:"mercNo"` MercNo string `json:"mercNo"`
ChannelSendNo *string `json:"channelSendNo"` ChannelSendNo *string `json:"channelSendNo"`
ChannelTradeNo *string `json:"channelTradeNo"` ChannelTradeNo *string `json:"channelTradeNo"`
State string `json:"state"` State string `json:"state"`
PayType string `json:"payType"` PayType SftPayType `json:"payType"`
IfCode string `json:"ifCode"` IfCode string `json:"ifCode"`
ExtParam *string `json:"extParam"` ExtParam *string `json:"extParam"`
PayInfo *string `json:"payInfo"` PayInfo *struct {
PayUrl *string `json:"payUrl"`
} `json:"payInfo"`
Note *string `json:"note"` Note *string `json:"note"`
TradeFee *int64 `json:"tradeFee"` TradeFee *int64 `json:"tradeFee"`
StoreId *string `json:"storeId"` StoreId *string `json:"storeId"`
@@ -165,9 +197,12 @@ type PaymentH5PayResp struct {
SettlementType *string `json:"settlementType"` SettlementType *string `json:"settlementType"`
} }
func (s *SftClient) CreateOrderByRedirect(req *PaymentH5PayReq) (*PaymentH5PayResp, error) { type OrderCloseResp struct {
const url = "https://pay.rscygroup.com/api/open/payment/h5pay" MchOrderNo string `json:"mchOrderNo"`
return call[PaymentH5PayResp](s, url, req) 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) { 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) { func (s *SftClient) sign(msg any) (*request, error) {
// 处理请求正文
bytes, err := json.Marshal(msg) bytes, err := json.Marshal(msg)
if err != nil { if err != nil {
return nil, fmt.Errorf("格式化加密正文失败:%w", err) return nil, fmt.Errorf("格式化加密正文失败:%w", err)
@@ -246,12 +280,13 @@ func (s *SftClient) sign(msg any) (*request, error) {
BizData: string(bytes), 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 { if err != nil {
return nil, fmt.Errorf("签名失败:%w", err) return nil, fmt.Errorf("签名失败:%w", err)
} }
body.Sign = string(encrypted) body.Sign = string(signature)
return &body, nil 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 { 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 { if err != nil {
return nil, fmt.Errorf("验签失败:%w", err) return nil, fmt.Errorf("验签失败:%w", err)
} }
@@ -283,10 +320,10 @@ type request struct {
BizData string `json:"bizData"` BizData string `json:"bizData"`
} }
func (r request) String(secret string) string { func (r request) String() string {
return fmt.Sprintf( return fmt.Sprintf(
"appId=%s&bizData=%s&reqId=%s&reqTime=%s&signType=%s&version=%s&appSecret=%s", "appId=%s&bizData=%s&reqId=%s&reqTime=%s&signType=%s&version=%s",
r.AppId, r.BizData, r.ReqId, r.ReqTime, r.SignType, r.Version, secret, r.AppId, r.BizData, r.ReqId, r.ReqTime, r.SignType, r.Version,
) )
} }
@@ -298,3 +335,18 @@ type response struct {
SignType *string `json:"signType"` SignType *string `json:"signType"`
Timestamp string `json:"timestamp"` 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, TradeNo: req.TradeNo,
Method: trade2.MethodAlipay, 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, TradeNo: req.TradeNo,
Method: trade2.MethodWeChat, Method: trade2.MethodWeChat,
}) })

View File

@@ -24,7 +24,7 @@ type Trade struct {
Remark *string `gorm:"column:remark;type:character varying(255);comment:订单备注" json:"remark"` // 订单备注 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"` // 订单总金额 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"` // 支付金额 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-已退款 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"` // 支付链接 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"` // 支付时间 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"` // 创建时间 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"` // 更新时间 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"` // 删除时间 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 // 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.CreatedAt = field.NewField(tableName, "created_at")
_trade.UpdatedAt = field.NewField(tableName, "updated_at") _trade.UpdatedAt = field.NewField(tableName, "updated_at")
_trade.DeletedAt = field.NewField(tableName, "deleted_at") _trade.DeletedAt = field.NewField(tableName, "deleted_at")
_trade.Acquirer = field.NewInt32(tableName, "acquirer")
_trade.fillFieldMap() _trade.fillFieldMap()
@@ -63,7 +64,7 @@ type trade struct {
Remark field.String // 订单备注 Remark field.String // 订单备注
Amount field.Field // 订单总金额 Amount field.Field // 订单总金额
Payment field.Field // 支付金额 Payment field.Field // 支付金额
Method field.Int32 // 支付方式1-支付宝2-微信 Method field.Int32 // 支付方式1-支付宝2-微信3-商福通
Status field.Int32 // 订单状态0-待支付1-已支付2-已取消3-已退款 Status field.Int32 // 订单状态0-待支付1-已支付2-已取消3-已退款
PayURL field.String // 支付链接 PayURL field.String // 支付链接
PaidAt field.Field // 支付时间 PaidAt field.Field // 支付时间
@@ -71,6 +72,7 @@ type trade struct {
CreatedAt field.Field // 创建时间 CreatedAt field.Field // 创建时间
UpdatedAt field.Field // 更新时间 UpdatedAt field.Field // 更新时间
DeletedAt field.Field // 删除时间 DeletedAt field.Field // 删除时间
Acquirer field.Int32 // 收单机构1-支付宝2-微信3-银联
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@@ -104,6 +106,7 @@ func (t *trade) updateTableName(table string) *trade {
t.CreatedAt = field.NewField(table, "created_at") t.CreatedAt = field.NewField(table, "created_at")
t.UpdatedAt = field.NewField(table, "updated_at") t.UpdatedAt = field.NewField(table, "updated_at")
t.DeletedAt = field.NewField(table, "deleted_at") t.DeletedAt = field.NewField(table, "deleted_at")
t.Acquirer = field.NewInt32(table, "acquirer")
t.fillFieldMap() t.fillFieldMap()
@@ -120,7 +123,7 @@ func (t *trade) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (t *trade) fillFieldMap() { 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["id"] = t.ID
t.fieldMap["user_id"] = t.UserID t.fieldMap["user_id"] = t.UserID
t.fieldMap["inner_no"] = t.InnerNo t.fieldMap["inner_no"] = t.InnerNo
@@ -138,6 +141,7 @@ func (t *trade) fillFieldMap() {
t.fieldMap["created_at"] = t.CreatedAt t.fieldMap["created_at"] = t.CreatedAt
t.fieldMap["updated_at"] = t.UpdatedAt t.fieldMap["updated_at"] = t.UpdatedAt
t.fieldMap["deleted_at"] = t.DeletedAt t.fieldMap["deleted_at"] = t.DeletedAt
t.fieldMap["acquirer"] = t.Acquirer
} }
func (t trade) clone(db *gorm.DB) trade { 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] rs = opResult[0]
} else { } else {
var err error var err error
rs, err = Trade.VerifyTrade(&TradeVerifyData{ rs, err = Trade.VerifyCreateTrade(&TradeVerifyData{
TradeNo: tradeNo, TradeNo: tradeNo,
Method: cache.Method, Method: cache.Method,
}) })
@@ -229,7 +229,7 @@ func (s *resourceService) CancelResource(tradeNo string, now time.Time, opRevoke
// 取消交易 // 取消交易
if len(opRevoked) <= 0 { if len(opRevoked) <= 0 {
err = Trade.SendCancelTrade(tradeNo, cache.Method) err = Trade.CancelTrade(tradeNo, cache.Method)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -103,6 +103,7 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
// 创建支付订单 // 创建支付订单
var payUrl string var payUrl string
var acquirer trade2.Acquirer
switch method { switch method {
// 调用支付宝支付接口 // 调用支付宝支付接口
@@ -122,6 +123,7 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
return nil, err return nil, err
} }
payUrl = resp.String() payUrl = resp.String()
acquirer = trade2.AcquirerAlipay
// 调用微信支付接口 // 调用微信支付接口
case trade2.MethodWeChat: case trade2.MethodWeChat:
@@ -140,6 +142,34 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
return nil, err return nil, err
} }
payUrl = *resp.CodeUrl 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: default:
@@ -156,13 +186,14 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
} }
var trade = m.Trade{ var trade = m.Trade{
UserID: uid, UserID: uid,
InnerNo: tradeNo, InnerNo: tradeNo,
Subject: subject, Subject: subject,
Method: int32(method), Method: int32(method),
Type: int32(tType), Type: int32(tType),
Amount: amount, Amount: amount,
PayURL: &payUrl, PayURL: &payUrl,
Acquirer: int32(acquirer),
} }
err = q.Trade.Create(&trade) err = q.Trade.Create(&trade)
if err != nil { if err != nil {
@@ -198,105 +229,176 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
Trade: &trade, Trade: &trade,
}, nil }, nil
} }
func (s *tradeService) SendCreateTradeByRedirect() { func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time.Time, data *TradeCreateData) (*TradeCreateResult, error) {
panic("todo") 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 amountReal = data.Amount
var tradeNo = data.TradeNo if env.RunMode == "debug" {
var payment = data.Payment amountReal = decimal.NewFromFloat(0.01)
var paidAt = data.Time }
// 获取交易信息 // 附加优惠券
trade, err := q.Trade. if data.CouponCode != "" {
Where(q.Trade.InnerNo.Eq(tradeNo)). coupon, err := q.Coupon.
First() 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 { if err != nil {
return nil, err return nil, err
} }
// 检查交易状态 // 创建支付订单
switch trade2.Status(trade.Status) { var payUrl string
var acquirer trade2.Acquirer
switch method {
// 如果已退款或取消,则返回错误 // 调用商福通接口
case trade2.StatusCanceled, trade2.StatusRefunded: case trade2.MethodSftAlipay, trade2.MethodSftWeChat:
return nil, errors.New("交易已取消或已退款") var payType g.SftPayType
if method == trade2.MethodSftAlipay {
// 如果是未支付,则更新支付状态 payType = g.SftAlipay
case trade2.StatusPending: } else {
trade.Status = int32(trade2.StatusSuccess) payType = g.SftWeChat
trade.OuterNo = &transId }
trade.Payment = payment resp, err := g.SFTPay.PaymentH5Pay(&g.PaymentH5PayReq{
trade.PaidAt = u.P(orm.LocalDateTime(paidAt)) MchOrderNo: tradeNo,
trade.PayURL = u.P("") Subject: subject,
_, err = q.Trade.Updates(trade) 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 { if err != nil {
return nil, err 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 // 保存交易订单
} var billType bill2.Type
switch tType {
func (s *tradeService) SendCancelTrade(tradeNo string, method trade2.Method) error { case trade2.TypeRecharge:
billType = bill2.TypeRecharge
switch method { case trade2.TypePurchase:
billType = bill2.TypeConsume
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("交易取消失败")
}
} }
return nil var trade = m.Trade{
} UserID: uid,
func (s *tradeService) OnTradeCanceled(q *q.Query, tradeNo string, now time.Time) error { InnerNo: tradeNo,
_, err := q.Trade. Subject: subject,
Where(q.Trade.InnerNo.Eq(tradeNo)). Method: int32(method),
Select(q.Trade.Status, q.Trade.CancelAt, q.Trade.PayURL). Type: int32(tType),
Updates(m.Trade{ Amount: amount,
Status: int32(trade2.StatusCanceled), PayURL: &payUrl,
CancelAt: u.P(orm.LocalDateTime(now)), Acquirer: int32(acquirer),
PayURL: u.P(""), }
}) err = q.Trade.Create(&trade)
if err != nil { 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") _, err = g.Asynq.Enqueue(tasks.NewUpdateTrade(tradeNo, method))
} if err != nil {
func (s *tradeService) OnTradeRefunded(q *q.Query, tradeNo string, now time.Time) error { return nil, err
panic("todo") }
}
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 tradeNo = data.TradeNo
var method = data.Method var method = data.Method
@@ -363,6 +465,114 @@ func (s *tradeService) VerifyTrade(data *TradeVerifyData) (*TradeSuccessResult,
Time: paidAt, Time: paidAt,
}, nil }, 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 { type TradeCreateData struct {
Subject string Subject string
@@ -396,12 +606,11 @@ type OnTradeCreateData struct {
TradeSuccessResult TradeSuccessResult
} }
type TradeResult int type TradePlatform int
const ( const (
TradeSuccess TradeResult = iota + 1 TradePlatformDesktop TradePlatform = iota + 1 // 桌面端
TradeCanceled TradePlatformMobile // 移动端
TradeClosed
) )
type TradeErr string type TradeErr string