完善商福通支付接口,修复证书加载问题;数据库扩展支付平台字段并更新支付信息保存逻辑;日志中间件异步记录日志
This commit is contained in:
@@ -49,7 +49,7 @@ type PageResp struct {
|
||||
|
||||
// region error
|
||||
|
||||
type BizErr struct {
|
||||
type Err struct {
|
||||
msg string
|
||||
err error
|
||||
|
||||
@@ -58,18 +58,18 @@ type BizErr struct {
|
||||
errFunc string
|
||||
}
|
||||
|
||||
func (e *BizErr) Error() string {
|
||||
func (e *Err) Error() string {
|
||||
if e.err != nil {
|
||||
return e.msg + ":" + e.err.Error()
|
||||
}
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e *BizErr) Unwrap() error {
|
||||
func (e *Err) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e *BizErr) Source() *slog.Source {
|
||||
func (e *Err) Source() *slog.Source {
|
||||
return &slog.Source{
|
||||
Function: e.errFunc,
|
||||
File: e.errFile,
|
||||
@@ -77,25 +77,37 @@ func (e *BizErr) Source() *slog.Source {
|
||||
}
|
||||
}
|
||||
|
||||
func NewBizErr(msg string, err ...error) (biz *BizErr) {
|
||||
biz = &BizErr{
|
||||
func newErr(msg string, err ...error) Err {
|
||||
o := Err{
|
||||
msg: msg,
|
||||
}
|
||||
|
||||
if len(err) > 0 {
|
||||
biz.err = err[0]
|
||||
o.err = err[0]
|
||||
}
|
||||
|
||||
if env.RunMode == env.RunModeDev {
|
||||
pc, file, line, ok := runtime.Caller(1)
|
||||
pc, file, line, ok := runtime.Caller(2)
|
||||
if ok {
|
||||
biz.errFile = file
|
||||
biz.errLine = line
|
||||
biz.errFunc = runtime.FuncForPC(pc).Name()
|
||||
o.errFile = file
|
||||
o.errLine = line
|
||||
o.errFunc = runtime.FuncForPC(pc).Name()
|
||||
}
|
||||
}
|
||||
|
||||
return biz
|
||||
return o
|
||||
}
|
||||
|
||||
type BizErr struct{ Err }
|
||||
|
||||
func NewBizErr(msg string, err ...error) (biz *BizErr) {
|
||||
return &BizErr{newErr(msg, err...)}
|
||||
}
|
||||
|
||||
type ServErr struct{ Err }
|
||||
|
||||
func NewServErr(msg string, err ...error) *ServErr {
|
||||
return &ServErr{newErr(msg, err...)}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -17,6 +17,13 @@ const (
|
||||
MethodSftWeChat // 商福通渠道指定微信
|
||||
)
|
||||
|
||||
type Platform int32
|
||||
|
||||
const (
|
||||
PlatformDesktop Platform = iota + 1 // 桌面网站
|
||||
PlatformMobile // 手机网站
|
||||
)
|
||||
|
||||
type Acquirer int32
|
||||
|
||||
const (
|
||||
|
||||
@@ -46,7 +46,7 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
|
||||
|
||||
// 所有未手动声明的错误类型
|
||||
default:
|
||||
slog.Debug("未处理的异常", slog.String("type", reflect.TypeOf(err).Name()), slog.String("error", err.Error()))
|
||||
slog.Warn("未处理的异常", slog.String("type", reflect.TypeOf(err).Name()), slog.String("error", err.Error()))
|
||||
}
|
||||
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
var Asynq *asynq.Client
|
||||
|
||||
func InitAsynq() {
|
||||
func initAsynq() {
|
||||
var client = asynq.NewClientFromRedisClient(Redis)
|
||||
Asynq = client
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ func Init() {
|
||||
initRedis()
|
||||
initOrm()
|
||||
initProxy()
|
||||
InitAsynq()
|
||||
initAsynq()
|
||||
initSft()
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"platform/web/core"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -21,29 +23,31 @@ var SFTPay SftClient
|
||||
|
||||
type SftClient struct {
|
||||
appid string
|
||||
routeId string
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey *rsa.PublicKey
|
||||
}
|
||||
|
||||
func init() {
|
||||
func initSft() {
|
||||
if !env.SftPayEnable {
|
||||
return
|
||||
panic("商福通支付未启用,请检查环境变量 SFTPAY_ENABLE")
|
||||
}
|
||||
|
||||
SFTPay = SftClient{
|
||||
appid: env.SftPayAppId,
|
||||
appid: env.SftPayAppId,
|
||||
routeId: env.SftPayRouteId,
|
||||
}
|
||||
|
||||
// 加载私钥
|
||||
block, _ := pem.Decode([]byte(env.SftPayAppPrivateKey))
|
||||
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
||||
panic("加载商福通私钥失败")
|
||||
private, err := base64.StdEncoding.DecodeString(env.SftPayAppPrivateKey)
|
||||
if err != nil {
|
||||
panic("解析商福通私钥失败: " + err.Error())
|
||||
}
|
||||
|
||||
var privateKey *rsa.PrivateKey
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
privateKey, err = x509.ParsePKCS1PrivateKey(private)
|
||||
if err != nil {
|
||||
pkcs8, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
pkcs8, err := x509.ParsePKCS8PrivateKey(private)
|
||||
if err != nil {
|
||||
panic("解析商福通私钥失败: " + err.Error())
|
||||
}
|
||||
@@ -57,13 +61,13 @@ func init() {
|
||||
SFTPay.privateKey = privateKey
|
||||
|
||||
// 加载公钥
|
||||
block, _ = pem.Decode([]byte(env.SftPayPublicKey))
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
panic("加载商福通公钥失败")
|
||||
public, err := base64.StdEncoding.DecodeString(env.SftPayPublicKey)
|
||||
if err != nil {
|
||||
panic("解析商福通公钥失败: " + err.Error())
|
||||
}
|
||||
|
||||
var publicKey *rsa.PublicKey
|
||||
pkix, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
pkix, err := x509.ParsePKIXPublicKey(public)
|
||||
if err != nil {
|
||||
panic("解析商福通公钥失败: " + err.Error())
|
||||
}
|
||||
@@ -78,11 +82,13 @@ func init() {
|
||||
|
||||
func (s *SftClient) PaymentScanPay(req *PaymentScanPayReq) (*PaymentScanPayResp, error) {
|
||||
const url = "https://pay.rscygroup.com/api/open/payment/scanpay"
|
||||
req.RouteNo = u.P(s.routeId)
|
||||
return call[PaymentScanPayResp](s, url, req)
|
||||
}
|
||||
|
||||
func (s *SftClient) PaymentH5Pay(req *PaymentH5PayReq) (*PaymentH5PayResp, error) {
|
||||
const url = "https://pay.rscygroup.com/api/open/payment/h5pay"
|
||||
req.RouteNo = u.P(s.routeId)
|
||||
return call[PaymentH5PayResp](s, url, req)
|
||||
}
|
||||
|
||||
@@ -91,6 +97,11 @@ func (s *SftClient) OrderClose(req *OrderCloseReq) (*OrderCloseResp, error) {
|
||||
return call[OrderCloseResp](s, url, req)
|
||||
}
|
||||
|
||||
func (s *SftClient) QueryTrade(req *QueryTradeReq) (*QueryTradeResp, error) {
|
||||
const url = "https://pay.rscygroup.com/api/open/query/trade"
|
||||
return call[QueryTradeResp](s, url, req)
|
||||
}
|
||||
|
||||
type PaymentScanPayReq struct {
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
@@ -132,6 +143,11 @@ type PaymentH5PayReq struct {
|
||||
LimitPay *int `json:"limitPay"`
|
||||
}
|
||||
|
||||
type QueryTradeReq struct {
|
||||
PayOrderId *string `json:"payOrderId"`
|
||||
MchOrderNo *string `json:"mchOrderNo"`
|
||||
}
|
||||
|
||||
type OrderCloseReq struct {
|
||||
MchOrderNo *string `json:"mchOrderNo"`
|
||||
PayOrderId *string `json:"payOrderId"`
|
||||
@@ -197,6 +213,29 @@ type PaymentH5PayResp struct {
|
||||
SettlementType *string `json:"settlementType"`
|
||||
}
|
||||
|
||||
type QueryTradeResp struct {
|
||||
Amount int64 `json:"amount"`
|
||||
ChannelSendNo *string `json:"channelSendNo"`
|
||||
IfCode *string `json:"ifCode"`
|
||||
MercNo string `json:"mercNo"`
|
||||
MchOrderNo string `json:"mchOrderNo"`
|
||||
PayOrderId *string `json:"payOrderId"`
|
||||
PayType string `json:"payType"`
|
||||
ChannelTradeNo *string `json:"channelTradeNo"`
|
||||
State SftTradeState `json:"state"`
|
||||
RefundAmt *int64 `json:"refundAmt"`
|
||||
RefundState int32 `json:"refundState"`
|
||||
DrType *string `json:"drType"`
|
||||
ExtParam *string `json:"extParam"`
|
||||
PayTime *string `json:"payTime"`
|
||||
Subject string `json:"subject"`
|
||||
TradeFee *int64 `json:"tradeFee"`
|
||||
CashFee *int64 `json:"cashFee"`
|
||||
StoreId *string `json:"storeId"`
|
||||
UserId *string `json:"userId"`
|
||||
SettlementType *string `json:"settlementType"`
|
||||
}
|
||||
|
||||
type OrderCloseResp struct {
|
||||
MchOrderNo string `json:"mchOrderNo"`
|
||||
PayOrderId string `json:"payOrderId"`
|
||||
@@ -224,19 +263,30 @@ func call[T any](s *SftClient, url string, req any) (*T, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败:%w", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
reqDump, err := httputil.DumpRequest(request, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求内容转储失败:%w", err)
|
||||
}
|
||||
println(string(reqDump) + "\n\n")
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败:%w", err)
|
||||
}
|
||||
|
||||
respDump, err := httputil.DumpResponse(response, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("响应内容转储失败:%w", err)
|
||||
}
|
||||
println(string(respDump) + "\n\n")
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("请求响应失败:%d", response.StatusCode)
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
defer func(body io.ReadCloser) {
|
||||
_ = body.Close()
|
||||
}(response.Body)
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
@@ -248,15 +298,9 @@ func call[T any](s *SftClient, url string, req any) (*T, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解密响应内容失败:%w", err)
|
||||
}
|
||||
if decode.Code != "000000" {
|
||||
return nil, fmt.Errorf("请求业务响应失败:%s", u.Z(decode.Msg))
|
||||
}
|
||||
if decode.BizData == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var resp = new(T)
|
||||
err = json.Unmarshal([]byte(*decode.BizData), resp)
|
||||
err = json.Unmarshal([]byte(decode), resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("响应正文解析失败:%w", err)
|
||||
}
|
||||
@@ -290,24 +334,34 @@ func (s *SftClient) sign(msg any) (*request, error) {
|
||||
return &body, nil
|
||||
}
|
||||
|
||||
func (s *SftClient) verify(str []byte) (*response, error) {
|
||||
func (s *SftClient) verify(str []byte) (string, error) {
|
||||
|
||||
var resp = new(response)
|
||||
err := json.Unmarshal(str, resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析响应正文失败:%w", err)
|
||||
return "", fmt.Errorf("解析响应正文失败:%w", err)
|
||||
}
|
||||
|
||||
if resp.Sign != nil || resp.SignType != nil || resp.BizData != nil {
|
||||
|
||||
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)
|
||||
}
|
||||
if resp.Code != "000000" {
|
||||
return "", fmt.Errorf("请求业务响应失败:%s", u.Z(resp.Msg))
|
||||
}
|
||||
|
||||
return resp, err
|
||||
if resp.Sign == nil {
|
||||
return "", core.NewServErr("响应数据签名为空")
|
||||
}
|
||||
|
||||
ser, err := resp.String()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("格式化响应内容失败:%w", err)
|
||||
}
|
||||
|
||||
hashed := sha256.Sum256([]byte(ser))
|
||||
err = rsa.VerifyPKCS1v15(s.publicKey, crypto.SHA256, hashed[:], []byte(*resp.Sign))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("验签失败:%w", err)
|
||||
}
|
||||
|
||||
return *resp.BizData, nil
|
||||
}
|
||||
|
||||
type request struct {
|
||||
@@ -336,11 +390,17 @@ type response struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
func (r response) String() string {
|
||||
func (r response) String() (string, error) {
|
||||
if r.BizData == nil || r.Msg == nil || r.SignType == nil {
|
||||
return "", core.NewServErr(fmt.Sprintf(
|
||||
"上游数据返回有空值:BizData %v,Msg %v, SignType %v",
|
||||
r.BizData == nil, r.Msg == nil, r.SignType == nil,
|
||||
))
|
||||
}
|
||||
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,
|
||||
)
|
||||
), nil
|
||||
}
|
||||
|
||||
type SftPayType string
|
||||
@@ -350,3 +410,16 @@ const (
|
||||
SftWeChat SftPayType = "WECHAT"
|
||||
SftUnionPay SftPayType = "UNIONPAY"
|
||||
)
|
||||
|
||||
type SftTradeState string
|
||||
|
||||
const (
|
||||
SftInit SftTradeState = "INIT"
|
||||
SftTradeAWAIT SftTradeState = "TRADE_WAIT"
|
||||
SftTradeSuccess SftTradeState = "TRADE_SUCCESS"
|
||||
SftTradeFail SftTradeState = "TRADE_FAIL"
|
||||
SftTradeCancel SftTradeState = "TRADE_CANCEL"
|
||||
SftTradeRefund SftTradeState = "TRADE_REFUND"
|
||||
SftRefundIng SftTradeState = "REFUND_ING"
|
||||
SftTradeClosed SftTradeState = "TRADE_CLOSED"
|
||||
)
|
||||
|
||||
@@ -293,7 +293,7 @@ func PrepareCreateResource(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 准备创建套餐
|
||||
result, err := s.Resource.PrepareResource(authCtx.Payload.Id, time.Now(), req.Method, &req.CreateResourceSerializer)
|
||||
result, err := s.Resource.PrepareResource(authCtx.Payload.Id, time.Now(), &req.CreateResourceSerializer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shopspring/decimal"
|
||||
"platform/web/auth"
|
||||
"platform/web/core"
|
||||
trade2 "platform/web/domains/trade"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
@@ -143,7 +145,9 @@ func UpdatePassword(c *fiber.Ctx) error {
|
||||
// region /recharge
|
||||
|
||||
type RechargePrepareReq struct {
|
||||
Amount string `json:"amount" validate:"required,numeric"`
|
||||
Amount string `json:"amount" validate:"required,numeric"`
|
||||
Platform trade2.Platform `json:"platform" validate:"required"`
|
||||
Method trade2.Method `json:"method" validate:"required"`
|
||||
}
|
||||
|
||||
type RechargePrepareResp struct {
|
||||
@@ -159,7 +163,6 @@ type RechargeConfirmResp struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// RechargePrepareAlipay 通过支付宝充值
|
||||
func RechargePrepareAlipay(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
|
||||
@@ -181,12 +184,13 @@ func RechargePrepareAlipay(c *fiber.Ctx) error {
|
||||
}
|
||||
var result *s.TradeCreateResult
|
||||
err = q.Q.Transaction(func(tx *q.Query) error {
|
||||
result, err = s.Trade.SendCreateTradeByQrcode(tx, authContext.Payload.Id, now, &s.TradeCreateData{
|
||||
result, err = s.Trade.CreateTrade(tx, authContext.Payload.Id, now, &s.TradeCreateData{
|
||||
Subject: "账户充值 - " + amount.StringFixed(2) + "元",
|
||||
Amount: amount,
|
||||
ExpireAt: time.Now().Add(30 * time.Minute),
|
||||
Type: trade2.TypeRecharge,
|
||||
Method: trade2.MethodAlipay,
|
||||
Platform: req.Platform,
|
||||
})
|
||||
return err
|
||||
})
|
||||
@@ -215,7 +219,7 @@ func RechargeConfirmAlipay(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 验证支付结果
|
||||
result, err := s.Trade.VerifyCreateTrade(&s.TradeVerifyData{
|
||||
result, err := s.Trade.VerifyTrade(&s.TradeVerifyData{
|
||||
TradeNo: req.TradeNo,
|
||||
Method: trade2.MethodAlipay,
|
||||
})
|
||||
@@ -253,12 +257,13 @@ func RechargePrepareWechat(c *fiber.Ctx) error {
|
||||
}
|
||||
var result *s.TradeCreateResult
|
||||
err = q.Q.Transaction(func(tx *q.Query) error {
|
||||
result, err = s.Trade.SendCreateTradeByQrcode(tx, authContext.Payload.Id, now, &s.TradeCreateData{
|
||||
result, err = s.Trade.CreateTrade(tx, authContext.Payload.Id, now, &s.TradeCreateData{
|
||||
Subject: "账户充值 - " + amount.StringFixed(2) + "元",
|
||||
Amount: amount,
|
||||
ExpireAt: now.Add(30 * time.Minute),
|
||||
Type: trade2.TypeRecharge,
|
||||
Method: trade2.MethodWeChat,
|
||||
Platform: req.Platform,
|
||||
})
|
||||
return err
|
||||
})
|
||||
@@ -289,7 +294,82 @@ func RechargeConfirmWechat(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 验证支付结果
|
||||
result, err := s.Trade.VerifyCreateTrade(&s.TradeVerifyData{
|
||||
result, err := s.Trade.VerifyTrade(&s.TradeVerifyData{
|
||||
TradeNo: req.TradeNo,
|
||||
Method: trade2.MethodWeChat,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
err = s.User.RechargeConfirm(req.TradeNo, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"status": "success"})
|
||||
}
|
||||
|
||||
func RechargePrepare(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(RechargePrepareReq)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 保存交易信息
|
||||
var now = time.Now()
|
||||
amount, err := decimal.NewFromString(req.Amount)
|
||||
if err != nil {
|
||||
return core.NewBizErr(fmt.Sprintf("金额格式错误: %s", err.Error()))
|
||||
}
|
||||
var result *s.TradeCreateResult
|
||||
err = q.Q.Transaction(func(tx *q.Query) error {
|
||||
result, err = s.Trade.CreateTrade(tx, authContext.Payload.Id, now, &s.TradeCreateData{
|
||||
Subject: "账户充值 - " + amount.StringFixed(2) + "元",
|
||||
Amount: amount,
|
||||
ExpireAt: now.Add(30 * time.Minute),
|
||||
Type: trade2.TypeRecharge,
|
||||
Method: req.Method,
|
||||
Platform: req.Platform,
|
||||
})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
return c.JSON(RechargePrepareResp{
|
||||
TradeNo: result.TradeNo,
|
||||
PayURL: result.PayURL,
|
||||
})
|
||||
}
|
||||
|
||||
func RechargeConfirm(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(struct {
|
||||
TradeNo string `json:"trade_no" validate:"required"`
|
||||
})
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证支付结果
|
||||
result, err := s.Trade.VerifyTrade(&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-微信,3-商福通" json:"method"` // 支付方式:1-支付宝,2-微信,3-商福通
|
||||
Method int32 `gorm:"column:method;type:integer;not null;comment:支付方式:1-支付宝,2-微信,3-商福通渠道支付宝,4-商福通渠道微信" json:"method"` // 支付方式:1-支付宝,2-微信,3-商福通渠道支付宝,4-商福通渠道微信
|
||||
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"` // 支付时间
|
||||
@@ -33,6 +33,7 @@ type Trade struct {
|
||||
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-银联
|
||||
Platform int32 `gorm:"column:platform;type:integer;not null;comment:支付平台:1-电脑网站,2-手机网站" json:"platform"` // 支付平台:1-电脑网站,2-手机网站
|
||||
}
|
||||
|
||||
// TableName Trade's table name
|
||||
|
||||
@@ -45,6 +45,7 @@ func newTrade(db *gorm.DB, opts ...gen.DOOption) trade {
|
||||
_trade.UpdatedAt = field.NewField(tableName, "updated_at")
|
||||
_trade.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||
_trade.Acquirer = field.NewInt32(tableName, "acquirer")
|
||||
_trade.Platform = field.NewInt32(tableName, "platform")
|
||||
|
||||
_trade.fillFieldMap()
|
||||
|
||||
@@ -64,7 +65,7 @@ type trade struct {
|
||||
Remark field.String // 订单备注
|
||||
Amount field.Field // 订单总金额
|
||||
Payment field.Field // 支付金额
|
||||
Method field.Int32 // 支付方式:1-支付宝,2-微信,3-商福通
|
||||
Method field.Int32 // 支付方式:1-支付宝,2-微信,3-商福通渠道支付宝,4-商福通渠道微信
|
||||
Status field.Int32 // 订单状态:0-待支付,1-已支付,2-已取消,3-已退款
|
||||
PayURL field.String // 支付链接
|
||||
PaidAt field.Field // 支付时间
|
||||
@@ -73,6 +74,7 @@ type trade struct {
|
||||
UpdatedAt field.Field // 更新时间
|
||||
DeletedAt field.Field // 删除时间
|
||||
Acquirer field.Int32 // 收单机构:1-支付宝,2-微信,3-银联
|
||||
Platform field.Int32 // 支付平台:1-电脑网站,2-手机网站
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@@ -107,6 +109,7 @@ func (t *trade) updateTableName(table string) *trade {
|
||||
t.UpdatedAt = field.NewField(table, "updated_at")
|
||||
t.DeletedAt = field.NewField(table, "deleted_at")
|
||||
t.Acquirer = field.NewInt32(table, "acquirer")
|
||||
t.Platform = field.NewInt32(table, "platform")
|
||||
|
||||
t.fillFieldMap()
|
||||
|
||||
@@ -123,7 +126,7 @@ func (t *trade) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
func (t *trade) fillFieldMap() {
|
||||
t.fieldMap = make(map[string]field.Expr, 18)
|
||||
t.fieldMap = make(map[string]field.Expr, 19)
|
||||
t.fieldMap["id"] = t.ID
|
||||
t.fieldMap["user_id"] = t.UserID
|
||||
t.fieldMap["inner_no"] = t.InnerNo
|
||||
@@ -142,6 +145,7 @@ func (t *trade) fillFieldMap() {
|
||||
t.fieldMap["updated_at"] = t.UpdatedAt
|
||||
t.fieldMap["deleted_at"] = t.DeletedAt
|
||||
t.fieldMap["acquirer"] = t.Acquirer
|
||||
t.fieldMap["platform"] = t.Platform
|
||||
}
|
||||
|
||||
func (t trade) clone(db *gorm.DB) trade {
|
||||
|
||||
@@ -28,6 +28,8 @@ func ApplyRouters(app *fiber.App) {
|
||||
user.Post("/recharge/confirm/alipay", handlers.RechargeConfirmAlipay)
|
||||
user.Post("/recharge/prepare/wechat", handlers.RechargePrepareWechat)
|
||||
user.Post("/recharge/confirm/wechat", handlers.RechargeConfirmWechat)
|
||||
user.Post("/recharge/prepare", handlers.RechargePrepare)
|
||||
user.Post("/recharge/confirm", handlers.RechargeConfirm)
|
||||
|
||||
// 白名单
|
||||
whitelist := api.Group("/whitelist")
|
||||
|
||||
@@ -87,7 +87,7 @@ func (s *resourceService) CreateResource(uid int32, now time.Time, ser *CreateRe
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *resourceService) PrepareResource(uid int32, now time.Time, method trade2.Method, ser *CreateResourceSerializer) (*TradeCreateResult, error) {
|
||||
func (s *resourceService) PrepareResource(uid int32, now time.Time, ser *CreateResourceSerializer) (*TradeCreateResult, error) {
|
||||
|
||||
data, err := ser.ToData()
|
||||
if err != nil {
|
||||
@@ -97,18 +97,22 @@ func (s *resourceService) PrepareResource(uid int32, now time.Time, method trade
|
||||
name := data.GetName()
|
||||
amount := data.GetPrice()
|
||||
|
||||
method := ser.PaymentMethod
|
||||
platform := ser.PaymentPlatform
|
||||
|
||||
// 保存到数据库
|
||||
var result *TradeCreateResult
|
||||
err = q.Q.Transaction(func(q *q.Query) error {
|
||||
var err error
|
||||
|
||||
// 生成交易订单
|
||||
result, err = Trade.SendCreateTradeByQrcode(q, uid, now, &TradeCreateData{
|
||||
result, err = Trade.CreateTrade(q, uid, now, &TradeCreateData{
|
||||
Subject: "购买套餐 - " + name,
|
||||
Amount: amount,
|
||||
ExpireAt: time.Now().Add(30 * time.Minute),
|
||||
Type: trade2.TypeRecharge,
|
||||
Method: method,
|
||||
Platform: platform,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -124,7 +128,6 @@ func (s *resourceService) PrepareResource(uid int32, now time.Time, method trade
|
||||
Uid: uid,
|
||||
TradeId: result.Trade.ID,
|
||||
BillId: result.Bill.ID,
|
||||
Method: method,
|
||||
CreateResourceSerializer: resourceSerializer,
|
||||
}, 30*time.Minute).Err()
|
||||
if err != nil {
|
||||
@@ -158,9 +161,9 @@ func (s *resourceService) CompleteResource(tradeNo string, now time.Time, opResu
|
||||
rs = opResult[0]
|
||||
} else {
|
||||
var err error
|
||||
rs, err = Trade.VerifyCreateTrade(&TradeVerifyData{
|
||||
rs, err = Trade.VerifyTrade(&TradeVerifyData{
|
||||
TradeNo: tradeNo,
|
||||
Method: cache.Method,
|
||||
Method: cache.PaymentMethod,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -229,7 +232,7 @@ func (s *resourceService) CancelResource(tradeNo string, now time.Time, opRevoke
|
||||
|
||||
// 取消交易
|
||||
if len(opRevoked) <= 0 {
|
||||
err = Trade.CancelTrade(tradeNo, cache.Method)
|
||||
err = Trade.CancelTrade(tradeNo, cache.PaymentMethod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -403,9 +406,11 @@ func (data *CreateLongResourceData) GetPrice() decimal.Decimal {
|
||||
}
|
||||
|
||||
type CreateResourceSerializer struct {
|
||||
Type resource2.Type `json:"type" validate:"required"`
|
||||
Short *CreateShortResourceData `json:"short,omitempty"`
|
||||
Long *CreateLongResourceData `json:"long,omitempty"`
|
||||
Type resource2.Type `json:"type" validate:"required"`
|
||||
Short *CreateShortResourceData `json:"short,omitempty"`
|
||||
Long *CreateLongResourceData `json:"long,omitempty"`
|
||||
PaymentMethod trade2.Method `json:"payment_method" validate:"required"`
|
||||
PaymentPlatform trade2.Platform `json:"payment_platform" validate:"required"`
|
||||
}
|
||||
|
||||
func (s *CreateResourceSerializer) ToData() (CreateResourceData, error) {
|
||||
@@ -433,10 +438,9 @@ func (s *CreateResourceSerializer) ByData(data CreateResourceData) error {
|
||||
}
|
||||
|
||||
type CreateResourceCache struct {
|
||||
Uid int32 `json:"uid"`
|
||||
TradeId int32 `json:"trade_id"`
|
||||
BillId int32 `json:"bill_id"`
|
||||
Method trade2.Method `json:"method"`
|
||||
Uid int32 `json:"uid"`
|
||||
TradeId int32 `json:"trade_id"`
|
||||
BillId int32 `json:"bill_id"`
|
||||
*CreateResourceSerializer
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"platform/web/core"
|
||||
bill2 "platform/web/domains/bill"
|
||||
coupon2 "platform/web/domains/coupon"
|
||||
trade2 "platform/web/domains/trade"
|
||||
@@ -29,11 +30,12 @@ var Trade = &tradeService{}
|
||||
type tradeService struct {
|
||||
}
|
||||
|
||||
func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.Time, data *TradeCreateData) (*TradeCreateResult, error) {
|
||||
func (s *tradeService) CreateTrade(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 platform = data.Platform
|
||||
var amount = data.Amount
|
||||
|
||||
// 实际支付金额,只在创建真实订单时使用
|
||||
@@ -103,11 +105,10 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
|
||||
|
||||
// 创建支付订单
|
||||
var payUrl string
|
||||
var acquirer trade2.Acquirer
|
||||
switch method {
|
||||
switch {
|
||||
|
||||
// 调用支付宝支付接口
|
||||
case trade2.MethodAlipay:
|
||||
// 支付宝 + 电脑网站
|
||||
case method == trade2.MethodAlipay && platform == trade2.PlatformDesktop:
|
||||
resp, err := g.Alipay.TradePagePay(alipay.TradePagePay{
|
||||
QRPayMode: "4",
|
||||
QRCodeWidth: "196", // 二维码宽度需要-4,支付宝页面布局有问题
|
||||
@@ -123,10 +124,9 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
|
||||
return nil, err
|
||||
}
|
||||
payUrl = resp.String()
|
||||
acquirer = trade2.AcquirerAlipay
|
||||
|
||||
// 调用微信支付接口
|
||||
case trade2.MethodWeChat:
|
||||
// 微信 + 电脑网站
|
||||
case method == trade2.MethodWeChat && platform == trade2.PlatformDesktop:
|
||||
resp, _, err := g.WechatPay.Native.Prepay(context.Background(), native.PrepayRequest{
|
||||
Appid: &env.WechatPayAppId,
|
||||
Mchid: &env.WechatPayMchId,
|
||||
@@ -142,10 +142,9 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
|
||||
return nil, err
|
||||
}
|
||||
payUrl = *resp.CodeUrl
|
||||
acquirer = trade2.AcquirerWeChat
|
||||
|
||||
// 调用商福通接口
|
||||
case trade2.MethodSft:
|
||||
// 商福通 + 电脑网站
|
||||
case method == trade2.MethodSft && platform == trade2.PlatformDesktop:
|
||||
resp, err := g.SFTPay.PaymentScanPay(&g.PaymentScanPayReq{
|
||||
MchOrderNo: tradeNo,
|
||||
Subject: subject,
|
||||
@@ -159,155 +158,9 @@ func (s *tradeService) SendCreateTradeByQrcode(q *q.Query, uid int32, now time.T
|
||||
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:
|
||||
return nil, ErrTransactionNotSupported
|
||||
}
|
||||
|
||||
// 保存交易订单
|
||||
var billType bill2.Type
|
||||
switch tType {
|
||||
case trade2.TypeRecharge:
|
||||
billType = bill2.TypeRecharge
|
||||
case trade2.TypePurchase:
|
||||
billType = bill2.TypeConsume
|
||||
}
|
||||
|
||||
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 nil, err
|
||||
}
|
||||
|
||||
// 保存用户帐单
|
||||
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
|
||||
}
|
||||
|
||||
// 提交异步任务更新订单状态
|
||||
_, err = g.Asynq.Enqueue(tasks.NewUpdateTrade(tradeNo, method))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TradeCreateResult{
|
||||
TradeNo: tradeNo,
|
||||
PayURL: payUrl,
|
||||
Bill: &bill,
|
||||
Trade: &trade,
|
||||
}, nil
|
||||
}
|
||||
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
|
||||
|
||||
// 实际支付金额,只在创建真实订单时使用
|
||||
var amountReal = data.Amount
|
||||
if env.RunMode == "debug" {
|
||||
amountReal = decimal.NewFromFloat(0.01)
|
||||
}
|
||||
|
||||
// 附加优惠券
|
||||
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
|
||||
}
|
||||
|
||||
// 创建支付订单
|
||||
var payUrl string
|
||||
var acquirer trade2.Acquirer
|
||||
switch method {
|
||||
|
||||
// 调用商福通接口
|
||||
case trade2.MethodSftAlipay, trade2.MethodSftWeChat:
|
||||
// 商福通 + 手机网站
|
||||
case (method == trade2.MethodSftAlipay || method == trade2.MethodSftWeChat) && platform == trade2.PlatformMobile:
|
||||
var payType g.SftPayType
|
||||
if method == trade2.MethodSftAlipay {
|
||||
payType = g.SftAlipay
|
||||
@@ -328,24 +181,31 @@ func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time
|
||||
return nil, err
|
||||
}
|
||||
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:
|
||||
slog.Warn(ErrTransactionNotSupported.Error(), "method", method, "platform", platform)
|
||||
return nil, ErrTransactionNotSupported
|
||||
}
|
||||
|
||||
// 保存交易订单
|
||||
var trade = m.Trade{
|
||||
UserID: uid,
|
||||
InnerNo: tradeNo,
|
||||
Subject: subject,
|
||||
Type: int32(tType),
|
||||
Method: int32(method),
|
||||
Platform: int32(platform),
|
||||
Amount: amount,
|
||||
PayURL: &payUrl,
|
||||
}
|
||||
|
||||
err = q.Trade.Create(&trade)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存用户帐单
|
||||
var billType bill2.Type
|
||||
switch tType {
|
||||
case trade2.TypeRecharge:
|
||||
@@ -354,22 +214,6 @@ func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time
|
||||
billType = bill2.TypeConsume
|
||||
}
|
||||
|
||||
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 nil, err
|
||||
}
|
||||
|
||||
// 保存用户帐单
|
||||
var bill = m.Bill{
|
||||
BillNo: ID.GenReadable("bil"),
|
||||
UserID: uid,
|
||||
@@ -378,6 +222,7 @@ func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time
|
||||
Type: int32(billType),
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
err = q.Bill.
|
||||
Omit(q.Bill.ResourceID, q.Bill.RefundID).
|
||||
Create(&bill)
|
||||
@@ -398,78 +243,12 @@ func (s *tradeService) SendCreateTradeByRedirect(q *q.Query, uid int32, now time
|
||||
Trade: &trade,
|
||||
}, nil
|
||||
}
|
||||
func (s *tradeService) VerifyCreateTrade(data *TradeVerifyData) (*TradeSuccessResult, error) {
|
||||
var tradeNo = data.TradeNo
|
||||
var method = data.Method
|
||||
|
||||
// 检查交易号是否存在
|
||||
var transId string
|
||||
var paidAt time.Time
|
||||
var payment decimal.Decimal
|
||||
switch method {
|
||||
|
||||
// 检查支付宝交易
|
||||
case trade2.MethodAlipay:
|
||||
resp, err := g.Alipay.TradeQuery(context.Background(), alipay.TradeQuery{
|
||||
OutTradeNo: tradeNo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != alipay.CodeSuccess {
|
||||
slog.Warn("支付宝交易查询失败", "code", resp.Code, "sub_code", resp.SubCode, "msg", resp.Msg)
|
||||
return nil, errors.New("交易查询失败")
|
||||
}
|
||||
if resp.TradeStatus != alipay.TradeStatusSuccess {
|
||||
return nil, ErrTransactionNotPaid
|
||||
}
|
||||
|
||||
transId = resp.TradeNo
|
||||
payment, err = decimal.NewFromString(resp.TotalAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paidAt, err = time.Parse("2006-01-02 15:04:05", resp.SendPayDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查微信交易
|
||||
case trade2.MethodWeChat:
|
||||
resp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(context.Background(), native.QueryOrderByOutTradeNoRequest{
|
||||
OutTradeNo: &tradeNo,
|
||||
Mchid: &env.WechatPayMchId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if *resp.TradeState != "SUCCESS" {
|
||||
return nil, ErrTransactionNotPaid
|
||||
}
|
||||
|
||||
transId = *resp.TransactionId
|
||||
payment = decimal.NewFromInt(*resp.Amount.PayerTotal).Div(decimal.NewFromInt(100))
|
||||
paidAt, err = time.Parse(time.RFC3339, *resp.SuccessTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 不支持的支付方式
|
||||
default:
|
||||
return nil, ErrTransactionNotSupported
|
||||
}
|
||||
|
||||
return &TradeSuccessResult{
|
||||
TransId: transId,
|
||||
Payment: payment,
|
||||
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
|
||||
var acquirer = data.Acquirer
|
||||
|
||||
// 获取交易信息
|
||||
trade, err := q.Trade.
|
||||
@@ -491,6 +270,7 @@ func (s *tradeService) OnTradeCreated(q *q.Query, data *OnTradeCreateData) (*m.T
|
||||
trade.Status = int32(trade2.StatusSuccess)
|
||||
trade.OuterNo = &transId
|
||||
trade.Payment = payment
|
||||
trade.Acquirer = int32(acquirer)
|
||||
trade.PaidAt = u.P(orm.LocalDateTime(paidAt))
|
||||
trade.PayURL = u.P("")
|
||||
_, err = q.Trade.Updates(trade)
|
||||
@@ -574,12 +354,106 @@ func (s *tradeService) OnTradeRefunded(q *q.Query, tradeNo string, now time.Time
|
||||
panic("todo")
|
||||
}
|
||||
|
||||
func (s *tradeService) VerifyTrade(data *TradeVerifyData) (*TradeSuccessResult, error) {
|
||||
var tradeNo = data.TradeNo
|
||||
var method = data.Method
|
||||
|
||||
// 检查交易号是否存在
|
||||
var transId string
|
||||
var paidAt time.Time
|
||||
var payment decimal.Decimal
|
||||
switch method {
|
||||
|
||||
// 检查支付宝交易
|
||||
case trade2.MethodAlipay:
|
||||
resp, err := g.Alipay.TradeQuery(context.Background(), alipay.TradeQuery{
|
||||
OutTradeNo: tradeNo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Code != alipay.CodeSuccess {
|
||||
slog.Warn("支付宝交易查询失败", "code", resp.Code, "sub_code", resp.SubCode, "msg", resp.Msg)
|
||||
return nil, errors.New("交易查询失败")
|
||||
}
|
||||
if resp.TradeStatus != alipay.TradeStatusSuccess {
|
||||
return nil, ErrTransactionNotPaid
|
||||
}
|
||||
|
||||
transId = resp.TradeNo
|
||||
payment, err = decimal.NewFromString(resp.TotalAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paidAt, err = time.Parse("2006-01-02 15:04:05", resp.SendPayDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查微信交易
|
||||
case trade2.MethodWeChat:
|
||||
resp, _, err := g.WechatPay.Native.QueryOrderByOutTradeNo(context.Background(), native.QueryOrderByOutTradeNoRequest{
|
||||
OutTradeNo: &tradeNo,
|
||||
Mchid: &env.WechatPayMchId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if *resp.TradeState != "SUCCESS" {
|
||||
return nil, ErrTransactionNotPaid
|
||||
}
|
||||
|
||||
transId = *resp.TransactionId
|
||||
payment = decimal.NewFromInt(*resp.Amount.PayerTotal).Div(decimal.NewFromInt(100))
|
||||
paidAt, err = time.Parse(time.RFC3339, *resp.SuccessTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查商福通交易
|
||||
case trade2.MethodSft, trade2.MethodSftAlipay, trade2.MethodSftWeChat:
|
||||
resp, err := g.SFTPay.QueryTrade(&g.QueryTradeReq{
|
||||
MchOrderNo: &tradeNo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.State != g.SftTradeSuccess {
|
||||
return nil, ErrTransactionNotPaid
|
||||
}
|
||||
|
||||
if resp.PayOrderId == nil {
|
||||
return nil, errors.New("商福通交易号不存在")
|
||||
}
|
||||
if resp.PayTime == nil {
|
||||
return nil, errors.New("商福通交易时间不存在")
|
||||
}
|
||||
transId = *resp.PayOrderId
|
||||
payment = decimal.NewFromInt(resp.Amount).Div(decimal.NewFromInt(100))
|
||||
paidAt, err = time.Parse("2006-01-02 15:04:05", *resp.PayTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 不支持的支付方式
|
||||
default:
|
||||
return nil, ErrTransactionNotSupported
|
||||
}
|
||||
|
||||
return &TradeSuccessResult{
|
||||
TransId: transId,
|
||||
Payment: payment,
|
||||
Time: paidAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type TradeCreateData struct {
|
||||
Subject string
|
||||
Amount decimal.Decimal
|
||||
ExpireAt time.Time
|
||||
Type trade2.Type
|
||||
Method trade2.Method
|
||||
Platform trade2.Platform
|
||||
CouponCode string
|
||||
}
|
||||
|
||||
@@ -596,9 +470,10 @@ type TradeVerifyData struct {
|
||||
}
|
||||
|
||||
type TradeSuccessResult struct {
|
||||
TransId string
|
||||
Payment decimal.Decimal
|
||||
Time time.Time
|
||||
TransId string
|
||||
Payment decimal.Decimal
|
||||
Time time.Time
|
||||
Acquirer trade2.Acquirer
|
||||
}
|
||||
|
||||
type OnTradeCreateData struct {
|
||||
@@ -606,13 +481,6 @@ type OnTradeCreateData struct {
|
||||
TradeSuccessResult
|
||||
}
|
||||
|
||||
type TradePlatform int
|
||||
|
||||
const (
|
||||
TradePlatformDesktop TradePlatform = iota + 1 // 桌面端
|
||||
TradePlatformMobile // 移动端
|
||||
)
|
||||
|
||||
type TradeErr string
|
||||
|
||||
func (e TradeErr) Error() string {
|
||||
@@ -620,6 +488,6 @@ func (e TradeErr) Error() string {
|
||||
}
|
||||
|
||||
var (
|
||||
ErrTransactionNotPaid = TradeErr("交易未支付")
|
||||
ErrTransactionNotSupported = TradeErr("不支持的支付方式")
|
||||
ErrTransactionNotPaid = core.NewBizErr("交易未支付")
|
||||
ErrTransactionNotSupported = core.NewBizErr("不支持的支付方式")
|
||||
)
|
||||
|
||||
78
web/web.go
78
web/web.go
@@ -126,50 +126,52 @@ func newLogger() fiber.Handler {
|
||||
return false
|
||||
},
|
||||
Done: func(c *fiber.Ctx, logBytes []byte) {
|
||||
var logStr = strings.TrimPrefix(string(logBytes), "🚀")
|
||||
var logVars = strings.Split(logStr, "|")
|
||||
go func(ip, ua, method, path string, status int, logBytes []byte) {
|
||||
var logStr = strings.TrimPrefix(string(logBytes), "🚀")
|
||||
var logVars = strings.Split(logStr, "|")
|
||||
|
||||
var reqTimeStr = strings.TrimSpace(logVars[0])
|
||||
reqTime, err := time.ParseInLocation("2006-01-02 15:04:05", reqTimeStr, time.Local)
|
||||
if err != nil {
|
||||
slog.Error("时间解析错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
var reqTimeStr = strings.TrimSpace(logVars[0])
|
||||
reqTime, err := time.ParseInLocation("2006-01-02 15:04:05", reqTimeStr, time.Local)
|
||||
if err != nil {
|
||||
slog.Error("时间解析错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
var authInfo = strings.Split(strings.TrimSpace(logVars[1]), " ")
|
||||
var authType = auth.PayloadTypeFromStr(strings.TrimSpace(authInfo[0]))
|
||||
authID, err := strconv.Atoi(strings.TrimSpace(authInfo[1]))
|
||||
if err != nil {
|
||||
slog.Error("负载ID解析错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
var authInfo = strings.Split(strings.TrimSpace(logVars[1]), " ")
|
||||
var authType = auth.PayloadTypeFromStr(strings.TrimSpace(authInfo[0]))
|
||||
authID, err := strconv.Atoi(strings.TrimSpace(authInfo[1]))
|
||||
if err != nil {
|
||||
slog.Error("负载ID解析错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
var latency = strings.TrimSpace(logVars[4])
|
||||
var latency = strings.TrimSpace(logVars[4])
|
||||
|
||||
var errStr = strings.TrimSpace(logVars[5])
|
||||
var errStr = strings.TrimSpace(logVars[5])
|
||||
|
||||
var item = &m.LogsRequest{
|
||||
IP: c.IP(),
|
||||
Ua: u.P(c.Get("User-Agent")),
|
||||
Method: c.Method(),
|
||||
Path: c.Path(),
|
||||
Latency: &latency,
|
||||
Status: int32(c.Response().StatusCode()),
|
||||
Error: &errStr,
|
||||
Time: u.P(orm.LocalDateTime(reqTime)),
|
||||
}
|
||||
if authType != auth.PayloadNone {
|
||||
item.Identity = u.P(int32(authType))
|
||||
}
|
||||
if authID != 0 {
|
||||
item.Visitor = u.P(int32(authID))
|
||||
}
|
||||
var item = &m.LogsRequest{
|
||||
IP: ip,
|
||||
Ua: u.P(ua),
|
||||
Method: method,
|
||||
Path: path,
|
||||
Latency: &latency,
|
||||
Status: int32(status),
|
||||
Error: &errStr,
|
||||
Time: u.P(orm.LocalDateTime(reqTime)),
|
||||
}
|
||||
if authType != auth.PayloadNone {
|
||||
item.Identity = u.P(int32(authType))
|
||||
}
|
||||
if authID != 0 {
|
||||
item.Visitor = u.P(int32(authID))
|
||||
}
|
||||
|
||||
err = q.LogsRequest.Create(item)
|
||||
if err != nil {
|
||||
slog.Error("日志记录错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
err = q.LogsRequest.Create(item)
|
||||
if err != nil {
|
||||
slog.Error("日志记录错误", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
}(c.IP(), c.Get("User-Agent"), c.Method(), c.Path(), c.Response().StatusCode(), logBytes)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user