feat: payment pay
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package alipay
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
pkgpay "lone-services/pkg/payment"
|
||||
)
|
||||
|
||||
// Channel is a placeholder for Alipay (to be implemented later).
|
||||
type Channel struct{}
|
||||
|
||||
func NewChannel() *Channel {
|
||||
return &Channel{}
|
||||
}
|
||||
|
||||
func (c *Channel) Name() string {
|
||||
return pkgpay.ChannelAlipay
|
||||
}
|
||||
|
||||
func (c *Channel) Prepay(context.Context, string, *pkgpay.PrepayRequest) (*pkgpay.PrepayResult, error) {
|
||||
return nil, pkgpay.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (c *Channel) Refund(context.Context, *pkgpay.RefundRequest) (*pkgpay.RefundResult, error) {
|
||||
return nil, pkgpay.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (c *Channel) ParsePayNotify(context.Context, map[string]string, []byte) (*pkgpay.NotifyResult, error) {
|
||||
return nil, pkgpay.ErrNotImplemented
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package bank
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
pkgpay "lone-services/pkg/payment"
|
||||
)
|
||||
|
||||
// Channel is a placeholder for bank-card / unionpay (to be implemented later).
|
||||
type Channel struct{}
|
||||
|
||||
func NewChannel() *Channel {
|
||||
return &Channel{}
|
||||
}
|
||||
|
||||
func (c *Channel) Name() string {
|
||||
return pkgpay.ChannelBank
|
||||
}
|
||||
|
||||
func (c *Channel) Prepay(context.Context, string, *pkgpay.PrepayRequest) (*pkgpay.PrepayResult, error) {
|
||||
return nil, pkgpay.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (c *Channel) Refund(context.Context, *pkgpay.RefundRequest) (*pkgpay.RefundResult, error) {
|
||||
return nil, pkgpay.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (c *Channel) ParsePayNotify(context.Context, map[string]string, []byte) (*pkgpay.NotifyResult, error) {
|
||||
return nil, pkgpay.ErrNotImplemented
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Channel codes used in API body `channel`.
|
||||
const (
|
||||
ChannelWechat = "wechat"
|
||||
ChannelAlipay = "alipay"
|
||||
ChannelBank = "bank"
|
||||
)
|
||||
|
||||
// Client scenes used in API body `client`.
|
||||
const (
|
||||
ClientMiniProgram = "mini_program"
|
||||
ClientApp = "app"
|
||||
ClientH5 = "h5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupportedChannel = errors.New("unsupported payment channel")
|
||||
ErrUnsupportedClient = errors.New("unsupported payment client for channel")
|
||||
ErrNotImplemented = errors.New("payment channel not implemented")
|
||||
)
|
||||
|
||||
// PrepayRequest is channel-agnostic prepay input.
|
||||
type PrepayRequest struct {
|
||||
OutTradeNo string
|
||||
Description string
|
||||
AmountFen int64
|
||||
Currency string
|
||||
NotifyURL string
|
||||
Attach string
|
||||
// Payer identity: WeChat uses OpenID; Alipay uses BuyerID.
|
||||
OpenID string
|
||||
BuyerID string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
// PrepayResult holds client-side invoke params (JSON-serializable).
|
||||
type PrepayResult struct {
|
||||
PrepayID string `json:"prepay_id,omitempty"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
}
|
||||
|
||||
// RefundRequest is channel-agnostic refund input.
|
||||
type RefundRequest struct {
|
||||
OutTradeNo string
|
||||
TransactionID string
|
||||
OutRefundNo string
|
||||
Reason string
|
||||
NotifyURL string
|
||||
RefundFen int64
|
||||
TotalFen int64
|
||||
Currency string
|
||||
}
|
||||
|
||||
// RefundResult is channel-agnostic refund output.
|
||||
type RefundResult struct {
|
||||
RefundID string `json:"refund_id"`
|
||||
OutRefundNo string `json:"out_refund_no"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// NotifyResult is a normalized pay-success notify.
|
||||
type NotifyResult struct {
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
TradeState string `json:"trade_state"`
|
||||
AmountFen int64 `json:"amount_fen"`
|
||||
Success bool `json:"success"`
|
||||
Attach string `json:"attach,omitempty"`
|
||||
}
|
||||
|
||||
// Channel abstracts WeChat / Alipay / bank card providers.
|
||||
type Channel interface {
|
||||
Name() string
|
||||
Prepay(ctx context.Context, client string, req *PrepayRequest) (*PrepayResult, error)
|
||||
Refund(ctx context.Context, req *RefundRequest) (*RefundResult, error)
|
||||
ParsePayNotify(ctx context.Context, headers map[string]string, body []byte) (*NotifyResult, error)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
pkgpay "lone-services/pkg/payment"
|
||||
wxpay "lone-services/pkg/wechat/pay"
|
||||
)
|
||||
|
||||
// Channel wraps WeChat Pay APIv3 client.
|
||||
type Channel struct {
|
||||
client *wxpay.Client
|
||||
}
|
||||
|
||||
func NewChannel(cfg wxpay.Config) (*Channel, error) {
|
||||
c, err := wxpay.NewClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Channel{client: c}, nil
|
||||
}
|
||||
|
||||
func (c *Channel) Name() string {
|
||||
return pkgpay.ChannelWechat
|
||||
}
|
||||
|
||||
func (c *Channel) Prepay(ctx context.Context, client string, req *pkgpay.PrepayRequest) (*pkgpay.PrepayResult, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("prepay request is required")
|
||||
}
|
||||
if client == "" {
|
||||
client = pkgpay.ClientMiniProgram
|
||||
}
|
||||
|
||||
switch client {
|
||||
case pkgpay.ClientMiniProgram:
|
||||
return c.miniProgramPrepay(ctx, req)
|
||||
default:
|
||||
return nil, pkgpay.ErrUnsupportedClient
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Channel) miniProgramPrepay(ctx context.Context, req *pkgpay.PrepayRequest) (*pkgpay.PrepayResult, error) {
|
||||
currency := req.Currency
|
||||
if currency == "" {
|
||||
currency = "CNY"
|
||||
}
|
||||
result, err := c.client.MiniProgramPay(ctx, &wxpay.JsapiPrepayRequest{
|
||||
Description: req.Description,
|
||||
OutTradeNo: req.OutTradeNo,
|
||||
Attach: req.Attach,
|
||||
NotifyURL: req.NotifyURL,
|
||||
Amount: wxpay.Amount{
|
||||
Total: req.AmountFen,
|
||||
Currency: currency,
|
||||
},
|
||||
Payer: wxpay.Payer{OpenID: req.OpenID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload := map[string]any{}
|
||||
if result.Pay != nil {
|
||||
payload["timeStamp"] = result.Pay.TimeStamp
|
||||
payload["nonceStr"] = result.Pay.NonceStr
|
||||
payload["package"] = result.Pay.Package
|
||||
payload["signType"] = result.Pay.SignType
|
||||
payload["paySign"] = result.Pay.PaySign
|
||||
}
|
||||
return &pkgpay.PrepayResult{
|
||||
PrepayID: result.PrepayID,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Channel) Refund(ctx context.Context, req *pkgpay.RefundRequest) (*pkgpay.RefundResult, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("refund request is required")
|
||||
}
|
||||
currency := req.Currency
|
||||
if currency == "" {
|
||||
currency = "CNY"
|
||||
}
|
||||
result, err := c.client.Refund(ctx, &wxpay.RefundRequest{
|
||||
OutTradeNo: req.OutTradeNo,
|
||||
TransactionID: req.TransactionID,
|
||||
OutRefundNo: req.OutRefundNo,
|
||||
Reason: req.Reason,
|
||||
NotifyURL: req.NotifyURL,
|
||||
Amount: wxpay.RefundAmount{
|
||||
Refund: req.RefundFen,
|
||||
Total: req.TotalFen,
|
||||
Currency: currency,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pkgpay.RefundResult{
|
||||
RefundID: result.RefundID,
|
||||
OutRefundNo: result.OutRefundNo,
|
||||
TransactionID: result.TransactionID,
|
||||
OutTradeNo: result.OutTradeNo,
|
||||
Status: result.Status,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Channel) ParsePayNotify(_ context.Context, headers map[string]string, body []byte) (*pkgpay.NotifyResult, error) {
|
||||
httpReq, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range headers {
|
||||
httpReq.Header.Set(k, v)
|
||||
}
|
||||
|
||||
notify, err := c.client.ParseNotify(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if notify.EventType != "TRANSACTION.SUCCESS" {
|
||||
return &pkgpay.NotifyResult{Success: false, TradeState: notify.EventType}, nil
|
||||
}
|
||||
|
||||
tx, err := c.client.DecryptNotify(notify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pkgpay.NotifyResult{
|
||||
OutTradeNo: tx.OutTradeNo,
|
||||
TransactionID: tx.TransactionID,
|
||||
TradeState: tx.TradeState,
|
||||
AmountFen: tx.Amount.Total,
|
||||
Success: tx.TradeState == "SUCCESS",
|
||||
Attach: tx.Attach,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user