feat: payment pay

This commit is contained in:
zzw
2026-09-11 16:08:17 +08:00
parent f8c7419964
commit d9529a219d
33 changed files with 3108 additions and 89 deletions
+30
View File
@@ -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
}
+30
View File
@@ -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
}
+85
View File
@@ -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)
}
+141
View File
@@ -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
}