go-zero之支付服務二

charliecen發表於2021-10-27

由於程式碼太多,所以分兩章來寫。
go-zero之支付服務一

API介面

編寫api檔案
syntax = "v1"

info(
    title: "訂單支付"
    desc: "訂單支付"
    author: "charlie"
    email: "cenhuqing@163.com"
    version: "1.0"
)

type (
    createOrderReq {
        Amount          float64    `form:"amount"`
        PayType            int64     `form:"pay_type"`
        AutoRenewal        int64   `form:"auto_renewal"`
        Bit                int64   `form:"bit"`
    }

    orderResp {
        Code int               `json:"code"`
        Msg  string            `json:"msg"`
        Data map[string]string `json:"data"`
    }
    generateQrcodeReq {
        OrderId string `form:"order_id"`        // 訂單id
        Description string `form:"description"`    // 商品描述
    }

    wxNotifyResp {
        Code string `json:"code"`
        Message string `json:"message"`
    }

    wxNotifyRequest {}

    findOrderPayRequest {
        OrderId string `form:"order_id"`
    }

    aliPayNotifyRequest {}

    aliPayNotifyResponse {
        Message string `json:"message"`
    }
)



@server(
    group: order
    jwt: Auth
)

service api {
    // 建立訂單
    @handler CreateOrderHandle
    post /order/create(createOrderReq) returns(orderResp)

    // 生成二維碼
    @handler GenerateQrcodeHandle
    post /order/qrcode(generateQrcodeReq) returns(orderResp)

    // 查詢支付訂單資訊
    @handler FindOrderPayHandler
    post /order/find(findOrderPayRequest) returns(orderResp)
}

@server (
    group: order
)

service api {
    // 微信支付回撥
    @handler WxNotifyHandle
    post /order/notify(wxNotifyRequest) returns(wxNotifyResp)

    // 支付寶回撥
    @handler AliPayNotifyHandle
    post /order/alipay_notify() returns(orderResp)
}
生成檔案
> goctl.exe api go -api api.api -dir .
Done.
修改配置檔案
Name: api
Host: 0.0.0.0
Port: 8888
User:
  Etcd:
    Hosts:
      - $IP:23790
    Key: user.rpc
Auth:
  AccessSecret: 1231313
  AccessExpire: 72000
SmsRpc:
  Etcd:
    Hosts:
      - $IP:23790
    Key: sms.rpc
Order:
  Etcd:
    Hosts:
      - $IP:23790
    Key: order.rpc
# 微信支付
WxPay:
  # 商戶號
  MchId: "1111111111111111111"
  # 應用ID
  AppId: "wx22222222222222222"
  # 商戶證照序列號
  MCSNum: "FAKFJDAKFJKJAKDFEKJKDJAKFKDAJFKDAJKFJALF"
  # api金鑰
  ApiSecret: "kdjfkjafj23kjkdk23r23423423kjd"
  # apiv3金鑰
  ApiV3Secret: "12312kdjf4jkkjfkj32jljlfjel2jkj23jkjkj"
  # 證照路徑
  CPath: "/certificate/wx_apiclient_key.pem"
  # 回撥url
  NotifyUrl: "https://test.example.com/order/notify"
# 支付寶
AliPay:
  # 應用ID
  AppId: "11111333333333111"
  # 私鑰
  ApiPrivateKey: "DFJKDJAKFJKEdjkdkajkdjf112312421jkjkdkaf"
  # 應用公鑰證照
  AppPublicKeyCertPath: "/certificate/appCertPublicKey_2021002172619121.crt"
  # 支付寶根證照
  AliPayRootCertPath: "/certificate/alipayRootCert.crt"
  # 支付寶公鑰證照
  AliPayCertPublicKeyPath: "/certificate/alipayCertPublicKey_RSA2.crt"
  # 回撥url,支付成功後,支付寶回撥此url,修改訂單狀態
  NotifyUrl: "https://test.example.com/order/alipay_notify"
  # 跳轉url,支付成功後跳轉到此url
  RedirectUrl: "https://test.example.com"
宣告配置型別
package config

import (
    "github.com/tal-tech/go-zero/rest"
    "github.com/tal-tech/go-zero/zrpc"
)

type Config struct {
    rest.RestConf
    User zrpc.RpcClientConf
    Auth struct {
        AccessSecret string
        AccessExpire int64
    }
    SmsRpc zrpc.RpcClientConf
    Order zrpc.RpcClientConf
    WxPay WxPayConf
    AliPay AliPayConf
}

type WxPayConf struct{
    MchId string
    AppId string
    MCSNum string
    ApiSecret string
    ApiV3Secret string
    CPath string
    NotifyUrl string
}

type AliPayConf struct {
    AppId                     string
    ApiPrivateKey             string
    AppPublicKeyCertPath     string
    AliPayRootCertPath        string
    AliPayCertPublicKeyPath    string
    NotifyUrl                 string
    RedirectUrl                string
}

const (
    NoPay     int64         = 0        // 未支付
    Paying     int64         = 1        // 正在支付
    Paid     int64         = 2        // 已支付
    PaidFailed int64     = 3        // 支付失敗

    WxPay    int64         = 1        // 微信支付
    AliPay    int64         = 2        // 支付寶支付
)


填充依賴
package svc

import (
    "app/api/internal/config"
    "app/rpc/order/orderclient"
    "app/rpc/sms/smsclient"
    "app/rpc/user/userclient"
    "github.com/tal-tech/go-zero/zrpc"
)

type ServiceContext struct {
    Config config.Config
    Userclient userclient.User
    SmsClient smsclient.Sms
    OrderClient orderclient.Order
}

func NewServiceContext(c config.Config) *ServiceContext {
    return  &ServiceContext{
        Config: c,
        Userclient: userclient.NewUser(zrpc.MustNewClient(c.User)),
        SmsClient: smsclient.NewSms(zrpc.MustNewClient(c.SmsRpc)),
        OrderClient: orderclient.NewOrder(zrpc.MustNewClient(c.Order)),
    }
}
填充建立訂單邏輯
package order

import (
    "app/api/internal/svc"
    "app/api/internal/types"
    "app/rpc/order/order"
    "context"
    "encoding/json"
    "github.com/tal-tech/go-zero/core/logx"
)

type CreateOrderHandleLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewCreateOrderHandleLogic(ctx context.Context, svcCtx *svc.ServiceContext) CreateOrderHandleLogic {
    return CreateOrderHandleLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

// 建立訂單
func (l *CreateOrderHandleLogic) CreateOrderHandle(req types.CreateOrderReq) (*types.Resp, error) {
    userId, _ := l.ctx.Value("userId").(json.Number).Int64()
    // 金額float64轉int64
    amount := int64(req.Amount * 100)
    result, err := l.svcCtx.OrderClient.CreateOrder(l.ctx, &order.OrderRequest{
        OrderId: "",
        Amount:  amount,
        Status:  0,
        PayType:    req.PayType,
        Bit:    req.Bit,
        BitValue: 0,
        Data:    "",
        UserId:  userId,
    })
    if err != nil {
        return &types.Resp{
            Code: 201,
            Msg: result.Msg,
            Data: result.Data,
        }, nil
    }
    if result.Code != 200 {
        return &types.Resp{
            Code: 201,
            Msg: result.Msg,
            Data: result.Data,
        }, nil
    }
    return &types.Resp{
        Code: 200,
        Msg: result.Msg,
        Data: result.Data,
    }, nil
}
填充生成url邏輯
package order

import (
    "app/api/internal/svc"
    "app/api/internal/types"
    "app/rpc/order/order"
    "context"
    "encoding/json"
    "github.com/tal-tech/go-zero/core/logx"
)

type CreateOrderHandleLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewCreateOrderHandleLogic(ctx context.Context, svcCtx *svc.ServiceContext) CreateOrderHandleLogic {
    return CreateOrderHandleLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

// 建立訂單
func (l *CreateOrderHandleLogic) CreateOrderHandle(req types.CreateOrderReq) (*types.Resp, error) {
    userId, _ := l.ctx.Value("userId").(json.Number).Int64()
    // 金額float64轉int64
    amount := int64(req.Amount * 100)
    result, err := l.svcCtx.OrderClient.CreateOrder(l.ctx, &order.OrderRequest{
        OrderId: "",
        Amount:  amount,
        Status:  0,
        PayType:    req.PayType,
        Bit:    req.Bit,
        BitValue: 0,
        Data:    "",
        UserId:  userId,
    })
    if err != nil {
        return &types.Resp{
            Code: 201,
            Msg: result.Msg,
            Data: result.Data,
        }, nil
    }
    if result.Code != 200 {
        return &types.Resp{
            Code: 201,
            Msg: result.Msg,
            Data: result.Data,
        }, nil
    }
    return &types.Resp{
        Code: 200,
        Msg: result.Msg,
        Data: result.Data,
    }, nil
}
微信回撥介面
package order

import (
    "app/api/internal/config"
    "app/api/internal/svc"
    "app/api/internal/types"
    "app/library/tool"
    "app/rpc/order/orderclient"
    "context"
    "fmt"
    "github.com/tal-tech/go-zero/core/logx"
    "github.com/wechatpay-apiv3/wechatpay-go/core"
    "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
    "github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
    "github.com/wechatpay-apiv3/wechatpay-go/core/notify"
    "github.com/wechatpay-apiv3/wechatpay-go/core/option"
    "github.com/wechatpay-apiv3/wechatpay-go/services/payments"
    "github.com/wechatpay-apiv3/wechatpay-go/utils"
    "net/http"
    "time"
)

type WxNotifyHandleLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewWxNotifyHandleLogic(ctx context.Context, svcCtx *svc.ServiceContext) WxNotifyHandleLogic {
    return WxNotifyHandleLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

func (l *WxNotifyHandleLogic) WxNotifyHandle(req *http.Request) (*types.WxNotifyResp, error) {
    logx.Info("微信支付回撥")
    mchAPIv3Key := l.svcCtx.Config.WxPay.ApiV3Secret
    mcsNum := l.svcCtx.Config.WxPay.MCSNum
    mchId := l.svcCtx.Config.WxPay.MchId
    cPath := l.svcCtx.Config.WxPay.CPath
    privateKey := tool.GetFilePath(cPath)
    // 通過路徑載入私鑰
    mchPrivateKey, err := utils.LoadPrivateKeyWithPath(privateKey)
    if err != nil {
        logx.Error("載入商戶私鑰錯誤")
        return  &types.WxNotifyResp{
            Code: "FAILED",
            Message:  err.Error(),
        },nil
    }
    ctx := context.Background()
    // 使用商戶私鑰等初始化 client,並使它具有自動定時獲取微信支付平臺證照的能力
    opts := []core.ClientOption{
        option.WithWechatPayAutoAuthCipher(mchId, mcsNum, mchPrivateKey, mchAPIv3Key),
    }
    _, err = core.NewClient(ctx, opts...)
    if err != nil {
        logx.Error("例項化微信支付客戶端錯誤: %s", err)
        return  &types.WxNotifyResp{
            Code: "FAILED",
            Message:  err.Error(),
        },nil
    }
    //獲取平臺證照訪問器
    certVisitor := downloader.MgrInstance().GetCertificateVisitor(mchId)
    handler := notify.NewNotifyHandler(mchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certVisitor))
    logx.Info("驗證簽名")
    // 解密
    transaction := new(payments.Transaction)
    notifyReq, err := handler.ParseNotifyRequest(context.Background(), req, transaction)

    if err != nil {
        logx.Error("簽名或解密失敗", err)
        return &types.WxNotifyResp{
            Code: "FAILED",
            Message:  err.Error(),
        },nil
    }
    if notifyReq.Summary != "支付成功" {
        logx.Error("支付失敗", err)
        return  &types.WxNotifyResp{
            Code: "FAILED",
            Message:  err.Error(),
        },nil
    }
    logx.Info("返回通知資料: ", transaction)
    fmt.Println("返回通知資料: ", transaction)
    // 修改訂單
    go l.updateOrder(transaction)


    return &types.WxNotifyResp{
        Code: "SUCCESS",
        Message:  "成功",
    }, nil
}

// 更新訂單
func (l *WxNotifyHandleLogic) updateOrder(transaction *payments.Transaction) {
    ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
    defer cancel()
    _, err := l.svcCtx.OrderClient.UpdateOrder(ctx, &orderclient.UpdateOrderRequest{
        OrderId: *transaction.OutTradeNo,
        Status: config.Paid,
    })
    if err != nil {
        logx.Error("支付寶回撥更新訂單失敗", err)
    }
}
支付寶回撥介面
package order

import (
    "app/api/internal/config"
    "app/library/tool"
    "app/rpc/order/orderclient"
    "context"
    "github.com/smartwalle/alipay/v3"
    "net/http"
    "time"

    "app/api/internal/svc"
    "app/api/internal/types"

    "github.com/tal-tech/go-zero/core/logx"
)

type AliPayNotifyHandleLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewAliPayNotifyHandleLogic(ctx context.Context, svcCtx *svc.ServiceContext) AliPayNotifyHandleLogic {
    return AliPayNotifyHandleLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

// 支付寶回撥
func (l *AliPayNotifyHandleLogic) AliPayNotifyHandle(req *http.Request, resp http.ResponseWriter) (*types.Resp, error) {
    var data = make(map[string]string, 0)
    client, err := l.newAliPayClient()
    if err != nil {
        logx.Error("支付寶例項化錯誤:", err)
        return &types.Resp{
            Code: 201,
            Msg: "支付寶例項化錯誤",
            Data: data,
        }, nil
    }
    notify, err := client.GetTradeNotification(req)
    if err != nil {
        logx.Error("支付寶回撥驗證錯誤:", err)
        return &types.Resp{
            Code: 201,
            Msg: "支付寶回撥驗證錯誤",
            Data: data,
        }, nil
    }

    if notify.TradeStatus != "TRADE_SUCCESS" {
        logx.Error("支付寶交易失敗:", err)
        return &types.Resp{
            Code: 201,
            Msg: "支付寶交易失敗",
            Data: data,
        }, nil
    }

    logx.Info("交易狀態為:", notify.TradeStatus)
    logx.Info("返回通知資料: ", notify)
    // 非同步更新訂單狀態
    go l.updateOrder(notify)
    // 確認收到通知訊息
    alipay.AckNotification(resp)
    return &types.Resp{
        Code: 200,
        Msg: "success",
        Data: data,
    }, nil
}

// 更新訂單
func (l *AliPayNotifyHandleLogic) updateOrder(notify *alipay.TradeNotification) {
    ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
    defer cancel()
    _, err := l.svcCtx.OrderClient.UpdateOrder(ctx, &orderclient.UpdateOrderRequest{
        OrderId: notify.OutTradeNo,
        Status: config.Paid,
    })
    if err != nil {
        logx.Error("支付寶回撥更新訂單失敗", err)
    }
}


// 例項化支付寶客戶端
func (l *AliPayNotifyHandleLogic) newAliPayClient() (*alipay.Client, error){
    appId := l.svcCtx.Config.AliPay.AppId
    apiPrivateKey := l.svcCtx.Config.AliPay.ApiPrivateKey
    appPublicKeyCertPath := tool.GetFilePath(l.svcCtx.Config.AliPay.AppPublicKeyCertPath)
    aliPayRootCertPath := tool.GetFilePath(l.svcCtx.Config.AliPay.AliPayRootCertPath)
    aliPayCertPublicKeyPath := tool.GetFilePath(l.svcCtx.Config.AliPay.AliPayCertPublicKeyPath)

    // 初始化
    var client, err = alipay.New(appId, apiPrivateKey, true)
    if err != nil {
        logx.Error("支付寶初始化錯誤:", err)
        return nil, err
    }

    _ = client.LoadAppPublicCertFromFile(appPublicKeyCertPath)       // 載入應用公鑰證照
    _ = client.LoadAliPayRootCertFromFile(aliPayRootCertPath)        // 載入支付寶根證照
    _ = client.LoadAliPayPublicCertFromFile(aliPayCertPublicKeyPath) // 載入支付寶公鑰證照
    return client, err
}
啟動服務
❯ go run .\api.go
Starting api server at 0.0.0.0:8888...
測試
# 生成訂單
curl --location --request POST 'http://localhost:8888/order/create' \
--header 'User-Agent: Apipost client Runtime/+https://www.apipost.cn/' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUzODQ3NjUsImlhdCI6MTYzNTMxMjc2NSwidXNlcklkIjo5fQ.sgw9XFon0w0FU3kcLafafkejfkeaf' \
--form 'amount=0.01' \
--form 'bit=1' \
--form 'pay_type=2' \
--form 'auto_renewal=0'

# 結果
{
    "code": 200,
    "msg": "success",
    "data": {
        "amount": "1",    # 金額,單位是分
        "order_id": "P20211027101632027405"    # 訂單號
    }
}


# 獲取微信二維碼
curl --location --request POST 'https://localhost:8888/order/qrcode' \
--header 'User-Agent: Apipost client Runtime/+https://www.apipost.cn/' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUzODQ3NjUsImlhdCI6MTYzNTMxMjc2NSwidXNlcklkIjo5fQ.sgw9XFon0w0FU3kcLafafkejfkeaf' \
--form 'order_id=P20211027103535964140' \
--form 'description=卡飯包月'

# 結果
{
    "code": 200,
    "msg": "success",
    "data": {
        "url": "weixin://wxpay/bizpayurl?pr=e11vomAbc"
    }
}

通過工具來轉成二維碼,手機微信掃碼支付

# 支付寶獲取連結
curl --location --request POST 'https://cleaner.kfsafe.cn/order/qrcode' \
--header 'User-Agent: Apipost client Runtime/+https://www.apipost.cn/' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUzODQ3NjUsImlhdCI6MTYzNTMxMjc2NSwidXNlcklkIjo5fQ.sgw9XFon0w0FU3kcLafafkejfkeaf' \
--form 'order_id=P20211027103958985656' \
--form 'description=卡飯包月'

# 結果
{
    "code": 200,
    "msg": "success",
    "data": {
        "url": "https://openapi.alipay.com/gateway.do?alipay_root_cert_sn=..."
    }
}

複製url地址到瀏覽器

支付完成後,可以檢視訂單狀態,是否修改成功,如果不成功則檢視日誌,除錯

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章