修复商福通客户端加解密逻辑,交易表新增收单机构字段用来保存实际支付方式,取消交易接口实现
This commit is contained in:
19
pkg/env/env.go
vendored
19
pkg/env/env.go
vendored
@@ -350,8 +350,8 @@ func loadAliyun() {
|
||||
// region 商福通
|
||||
|
||||
var (
|
||||
SftPayEnable = false
|
||||
SftPayAppId string
|
||||
SftPayAppSecret string
|
||||
SftPayAppPrivateKey string
|
||||
SftPayPublicKey string
|
||||
)
|
||||
@@ -359,6 +359,15 @@ var (
|
||||
func loadSftPay() {
|
||||
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")
|
||||
if value == "" {
|
||||
panic("环境变量 ALIYUN_SMS_TEMPLATE_LOGIN 的值不能为空")
|
||||
@@ -379,13 +388,6 @@ func loadSftPay() {
|
||||
} else {
|
||||
SftPayPublicKey = value
|
||||
}
|
||||
|
||||
value = os.Getenv("SFTPAY_APP_SECRET")
|
||||
if value == "" {
|
||||
panic("环境变量 SFTPAY_APP_SECRET 的值不能为空")
|
||||
} else {
|
||||
SftPayAppSecret = value
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -440,4 +442,5 @@ func Init() {
|
||||
loadAlipay()
|
||||
loadWechatPay()
|
||||
loadAliyun()
|
||||
// loadSftPay()
|
||||
}
|
||||
|
||||
@@ -805,6 +805,7 @@ create table trade (
|
||||
amount decimal(12, 2) not null default 0,
|
||||
payment decimal(12, 2) not null default 0,
|
||||
method int not null,
|
||||
acquirer int not null,
|
||||
status int not null default 0,
|
||||
pay_url text,
|
||||
paid_at timestamp,
|
||||
@@ -830,7 +831,8 @@ comment on column trade.subject is '订单主题';
|
||||
comment on column trade.remark is '订单备注';
|
||||
comment on column trade.amount 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.pay_url is '支付链接';
|
||||
comment on column trade.paid_at is '支付时间';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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×tamp=%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"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user