From 692106ae5c40e875785b11bad3734983185d1328 Mon Sep 17 00:00:00 2001 From: luorijun Date: Thu, 5 Jun 2025 12:59:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=95=86=E7=A6=8F=E9=80=9A?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=8A=A0=E8=A7=A3=E5=AF=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E4=BA=A4=E6=98=93=E8=A1=A8=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=94=B6=E5=8D=95=E6=9C=BA=E6=9E=84=E5=AD=97=E6=AE=B5=E7=94=A8?= =?UTF-8?q?=E6=9D=A5=E4=BF=9D=E5=AD=98=E5=AE=9E=E9=99=85=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=EF=BC=8C=E5=8F=96=E6=B6=88=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/env/env.go | 19 +- scripts/sql/init.sql | 4 +- web/domains/trade/types.go | 15 +- web/globals/shangfutong.go | 174 +++++++++++------ web/handlers/user.go | 4 +- web/models/trade.gen.go | 3 +- web/queries/trade.gen.go | 8 +- web/services/resource.go | 4 +- web/services/trade.go | 391 ++++++++++++++++++++++++++++--------- 9 files changed, 452 insertions(+), 170 deletions(-) diff --git a/pkg/env/env.go b/pkg/env/env.go index 217a453..439d966 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -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() } diff --git a/scripts/sql/init.sql b/scripts/sql/init.sql index fc4eb35..7f54bce 100644 --- a/scripts/sql/init.sql +++ b/scripts/sql/init.sql @@ -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 '支付时间'; diff --git a/web/domains/trade/types.go b/web/domains/trade/types.go index 6d55fda..35416be 100644 --- a/web/domains/trade/types.go +++ b/web/domains/trade/types.go @@ -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 diff --git a/web/globals/shangfutong.go b/web/globals/shangfutong.go index ecd0f6a..eb50cee 100644 --- a/web/globals/shangfutong.go +++ b/web/globals/shangfutong.go @@ -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" +) diff --git a/web/handlers/user.go b/web/handlers/user.go index 1d6a0eb..3e18945 100644 --- a/web/handlers/user.go +++ b/web/handlers/user.go @@ -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, }) diff --git a/web/models/trade.gen.go b/web/models/trade.gen.go index d16c175..6f650a3 100644 --- a/web/models/trade.gen.go +++ b/web/models/trade.gen.go @@ -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 diff --git a/web/queries/trade.gen.go b/web/queries/trade.gen.go index 50a4b14..33dc15c 100644 --- a/web/queries/trade.gen.go +++ b/web/queries/trade.gen.go @@ -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 { diff --git a/web/services/resource.go b/web/services/resource.go index 5d117e2..04e80b9 100644 --- a/web/services/resource.go +++ b/web/services/resource.go @@ -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 } diff --git a/web/services/trade.go b/web/services/trade.go index 5b40ddc..98a72a7 100644 --- a/web/services/trade.go +++ b/web/services/trade.go @@ -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