init: 1.0.0

This commit is contained in:
Chang lue Tsen
2025-04-25 12:08:29 +09:00
commit 8addcc584b
1031 changed files with 76472 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
package alipay
import (
"context"
"net/url"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/pkg/errors"
"github.com/smartwalle/alipay/v3"
)
type Config struct {
AppId string
PrivateKey string
PublicKey string
InvoiceName string
NotifyURL string
Sandbox bool
}
type Notification struct {
OrderNo string
Amount int64
Status Status
}
type Status string
const (
Success Status = "TRADE_SUCCESS"
Pending Status = "WAIT_BUYER_PAY"
Closed Status = "TRADE_CLOSED"
Finished Status = "TRADE_FINISHED"
Error Status = "TRADE_ERROR"
)
type Client struct {
Config
client *alipay.Client
}
type Order struct {
OrderNo string
Amount int64
}
func NewClient(c Config) *Client {
client, err := alipay.New(c.AppId, c.PrivateKey, c.Sandbox)
if err != nil {
logger.Error("[Alipay] NewClient failed: ", logger.Field("errors", err), logger.Field("config", c))
return nil
}
err = client.LoadAliPayPublicKey(c.PublicKey)
if err != nil {
logger.Error("[Alipay] NewClient failed: ", logger.Field("errors", err), logger.Field("config", c))
}
return &Client{
Config: c,
client: client,
}
}
func (c *Client) PreCreateTrade(ctx context.Context, order Order) (string, error) {
amountString := tool.FormatFloat(float64(order.Amount)/float64(100), 2)
trade, err := c.client.TradePreCreate(ctx, alipay.TradePreCreate{
Trade: alipay.Trade{
OutTradeNo: order.OrderNo,
TotalAmount: amountString,
Subject: c.InvoiceName,
NotifyURL: c.NotifyURL,
},
})
if err != nil {
return "", err
}
if trade.Code != alipay.CodeSuccess {
return "", errors.New("PreCreateTrade failed: " + trade.Msg)
}
return trade.QRCode, nil
}
func (c *Client) QueryTrade(ctx context.Context, orderNo string) (Status, error) {
trade, err := c.client.TradeQuery(ctx, alipay.TradeQuery{
OutTradeNo: orderNo,
})
if err != nil {
return Error, err
}
switch trade.TradeStatus {
case alipay.TradeStatusSuccess:
return Success, nil
case alipay.TradeStatusWaitBuyerPay:
return Pending, nil
case alipay.TradeStatusClosed:
return Closed, nil
case alipay.TradeStatusFinished:
return Finished, nil
default:
return Error, errors.New("QueryTrade failed: " + trade.Msg)
}
}
func (c *Client) DecodeNotification(form url.Values) (*Notification, error) {
notify, err := c.client.DecodeNotification(form)
if err != nil {
return nil, err
}
return &Notification{
OrderNo: notify.OutTradeNo,
Amount: int64(tool.FormatStringToFloat(notify.TotalAmount) * 100),
Status: Status(notify.TradeStatus),
}, nil
}
+25
View File
@@ -0,0 +1,25 @@
package alipay
import (
"context"
"testing"
)
func TestClientPreCreateTrade(t *testing.T) {
t.Skipf("Skip TestClientPreCreateTrade")
cfg := Config{
InvoiceName: "XrayR",
NotifyURL: "https://example.com/alipay/notify",
Sandbox: true,
}
c := NewClient(cfg)
order := Order{
OrderNo: "20210701000001",
Amount: 100,
}
qr, err := c.PreCreateTrade(context.Background(), order)
if err != nil {
t.Fatal(err)
}
t.Log(qr)
}
+122
View File
@@ -0,0 +1,122 @@
package epay
import (
"encoding/json"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
type Client struct {
Pid string
Url string
Key string
}
type Order struct {
Name string
OrderNo string
Amount float64
SignType string
NotifyUrl string
ReturnUrl string
}
type queryOrderStatusResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
TradeNo string `json:"trade_no"`
OutTradeNo string `json:"out_trade_no"`
Type string `json:"type"`
Status int `json:"status"`
}
func NewClient(pid, url, key string) *Client {
return &Client{
Pid: pid,
Url: url,
Key: key,
}
}
func (c *Client) CreatePayUrl(order Order) string {
// Prepare URL values
params := url.Values{}
params.Set("name", order.Name)
params.Set("money", tool.FormatFloat(order.Amount, 2))
params.Set("notify_url", order.NotifyUrl)
params.Set("out_trade_no", order.OrderNo)
params.Set("pid", c.Pid)
params.Set("return_url", order.ReturnUrl)
// Generate the sign using the CreateSign function
sign := c.createSign(c.structToMap(order))
params.Set("sign", sign)
// Add sign_type manually
params.Set("sign_type", "MD5")
return c.Url + "/submit.php?" + params.Encode()
}
func (c *Client) createSign(params map[string]string) string {
keys := make([]string, 0, len(params))
for k := range params {
if params[k] != "" && k != "sign" && k != "sign_type" {
keys = append(keys, k)
}
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
parts = append(parts, k+"="+params[k])
}
queryString := strings.Join(parts, "&")
text := queryString + c.Key
return tool.Md5Encode(text, false)
}
func (c *Client) VerifySign(params map[string]string) bool {
return c.createSign(params) == params["sign"]
}
func (c *Client) QueryOrderStatus(orderNo string) bool {
client := http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get(c.Url + "/api.php" + "?act=order" + "&pid=" + c.Pid + "&key=" + c.Key + "&out_trade_no=" + orderNo)
if err != nil {
logger.Error("[Epay] QueryOrderStatus error", logger.Field("orderNo", orderNo), logger.Field("error", err.Error()))
return false
}
defer resp.Body.Close()
value, err := io.ReadAll(resp.Body)
if err != nil {
logger.Error("[Epay] QueryOrderStatus error", logger.Field("orderNo", orderNo), logger.Field("error", err.Error()))
return false
}
var response queryOrderStatusResponse
err = json.Unmarshal(value, &response)
if err != nil {
logger.Error("[Epay] QueryOrderStatus error", logger.Field("orderNo", orderNo), logger.Field("error", err.Error()))
return false
}
return response.Status == 1
}
// StructToMap converts a struct to map[string]string
func (c *Client) structToMap(order Order) map[string]string {
result := make(map[string]string)
result["money"] = tool.FormatFloat(order.Amount, 2)
result["name"] = order.Name
result["notify_url"] = order.NotifyUrl
result["out_trade_no"] = order.OrderNo
result["pid"] = c.Pid
result["return_url"] = order.ReturnUrl
return result
}
+49
View File
@@ -0,0 +1,49 @@
package epay
import "testing"
func TestEpay(t *testing.T) {
client := NewClient("", "http://127.0.0.1", "")
order := Order{
Name: "测试",
OrderNo: "123456789",
Amount: 1000,
SignType: "md5",
NotifyUrl: "http://127.0.0.1",
ReturnUrl: "http://127.0.0.1",
}
url := client.CreatePayUrl(order)
t.Logf("PayUrl: %s\n", url)
}
func TestQueryOrderStatus(t *testing.T) {
t.Skipf("Skip TestQueryOrderStatus test")
client := NewClient("Pid", "Url", "Key")
orderNo := "123456789"
status := client.QueryOrderStatus(orderNo)
t.Logf("OrderNo: %s, Status: %v\n", orderNo, status)
}
func TestVerifySign(t *testing.T) {
t.Skipf("Skip TestVerifySign test")
params := map[string]string{
"pid": "1654",
"trade_no": "2024121521150860990",
"out_trade_no": "202412152115078262977262254",
"type": "alipay",
"name": "product",
"money": "10",
"trade_status": "TRADE_SUCCESS",
"sign": "d3181f18ebdf9821f0ab6ee93faa82d1",
"sign_type": "MD5",
}
key := "LbTabbB580zWyhXhyyww7wwvy5u8k0wl"
c := NewClient("Pid", "Url", key)
if c.VerifySign(params) {
t.Logf("Sign verification success!")
} else {
t.Error("Sign verification failed!")
}
}
+21
View File
@@ -0,0 +1,21 @@
package payment
import (
"net/http"
"testing"
"github.com/gin-gonic/gin"
)
func TestHttp(t *testing.T) {
t.Skipf("Skip TestHttp test")
router := gin.Default()
router.LoadHTMLGlob("./*")
router.GET("/stripe", func(c *gin.Context) {
c.HTML(http.StatusOK, "stripe.html", gin.H{
"title": "Gin HTML Example",
"message": "Hello, Gin!",
})
})
_ = router.Run(":8989")
}
+72
View File
@@ -0,0 +1,72 @@
package payment
import "github.com/perfect-panel/ppanel-server/internal/types"
type Platform int
const (
Stripe Platform = iota
AlipayF2F
EPay
Balance
UNSUPPORTED
)
var platformNames = map[string]Platform{
"Stripe": Stripe,
"AlipayF2F": AlipayF2F,
"EPay": EPay,
"balance": Balance,
"unsupported": UNSUPPORTED,
}
func (p Platform) String() string {
for k, v := range platformNames {
if v == p {
return k
}
}
return "unsupported"
}
func ParsePlatform(s string) Platform {
if p, ok := platformNames[s]; ok {
return p
}
return UNSUPPORTED
}
func GetSupportedPlatforms() []types.PlatformInfo {
return []types.PlatformInfo{
{
Platform: Stripe.String(),
PlatformUrl: "https://stripe.com",
PlatformFieldDescription: map[string]string{
"public_key": "Publishable key",
"secret_key": "Secret key",
"webhook_secret": "Webhook secret",
"payment": "Payment Method, only supported card/alipay/wechat_pay",
},
},
{
Platform: AlipayF2F.String(),
PlatformUrl: "https://alipay.com",
PlatformFieldDescription: map[string]string{
"app_id": "App ID",
"private_key": "Private Key",
"public_key": "Public Key",
"invoice_name": "Invoice Name",
"sandbox": "Sandbox Mode",
},
},
{
Platform: EPay.String(),
PlatformUrl: "",
PlatformFieldDescription: map[string]string{
"pid": "PID",
"url": "URL",
"key": "Key",
},
},
}
}
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://js.stripe.com/v3/"></script>
</head>
<body>
<button id="payment">提交</button>
<script>
const stripe = Stripe('pk_test_51L4gYuHqdC7TVcHRMpjoxpdlNM3MwlcWHFYHmSSUjkjzE9x0IlKqxcYvSxnPXD5ahxdxSxVf6kJf7AEwpwmTqATx005M6v5QDe');
const btn = document.getElementById('payment');
btn.addEventListener('click',async ()=>{
try {
// wechatpi_3Q8kM5HqdC7TVcHR10ESkDJD_secret_sEJxtzWruoYEPjLbpKIC4Z0Ae
// alipaypi_3Q8kTxHqdC7TVcHR08NCtdE5_secret_wpc54Pa1EXxoccznERFjKvxHr
const {error, paymentIntent}= await stripe.confirmAlipayPayment('',
{
payment_method_options: {
wechat_pay: {
client: 'web',
},
},
},
{
handleActions: false,
}
)
if (error) {
console.error(error)
} else {
console.log(paymentIntent)
}
}catch (e) {
console.log(e)
}
})
</script>
</body>
</html>
+210
View File
@@ -0,0 +1,210 @@
package stripe
import (
"encoding/json"
"fmt"
"strconv"
"github.com/stripe/stripe-go/v81/webhookendpoint"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/stripe/stripe-go/v81"
"github.com/stripe/stripe-go/v81/customer"
"github.com/stripe/stripe-go/v81/ephemeralkey"
"github.com/stripe/stripe-go/v81/paymentintent"
"github.com/stripe/stripe-go/v81/paymentmethod"
"github.com/stripe/stripe-go/v81/webhook"
)
const APIVersion = "2024-04-10"
type Config struct {
PublicKey string
SecretKey string
WebhookSecret string
}
type User struct {
UserId int64
Email string
}
type NotifyResult struct {
EventType string
OrderNo string
TradeNo string
Method string
UserId int64
Amount int64
}
type Order struct {
OrderNo string
Subscribe string
Amount int64
Currency string
Payment string
}
type Client struct {
Config
}
type PaymentSheet struct {
ClientSecret string
EphemeralKey string
Customer string
PublishableKey string
TradeNo string
}
func NewClient(config Config) *Client {
return &Client{
Config: config,
}
}
func (c *Client) CreatePaymentSheet(order *Order, user *User) (*PaymentSheet, error) {
stripe.Key = c.SecretKey
// Create a new Stripe customer if it does not exist
customerDataRes, err := c.SearchStripeCustomer(user)
if err != nil {
return nil, err
}
if customerDataRes == nil {
customerDataRes, err = c.CreateCustomer(user)
if err != nil {
return nil, err
}
}
// Create Ephemeral Key
ekParams := &stripe.EphemeralKeyParams{
Customer: stripe.String(customerDataRes.ID),
StripeVersion: stripe.String(APIVersion),
}
ek, err := ephemeralkey.New(ekParams)
if err != nil {
return nil, err
}
// Create Payment Intent
params := &stripe.PaymentIntentParams{
Amount: stripe.Int64(order.Amount),
Customer: stripe.String(customerDataRes.ID),
Currency: stripe.String(order.Currency),
PaymentMethodTypes: []*string{
stripe.String(order.Payment),
},
Metadata: map[string]string{
"order_no": order.OrderNo,
"user_id": strconv.FormatInt(user.UserId, 10),
"subscribe": order.Subscribe,
},
}
result, err := paymentintent.New(params)
if err != nil {
return nil, err
}
return &PaymentSheet{
ClientSecret: result.ClientSecret,
EphemeralKey: ek.Secret,
Customer: customerDataRes.ID,
PublishableKey: c.PublicKey,
TradeNo: result.ID,
}, nil
}
// SearchStripeCustomer Search for a Stripe customer by email or user ID
func (c *Client) SearchStripeCustomer(user *User) (*stripe.Customer, error) {
stripe.Key = c.SecretKey
params := &stripe.CustomerSearchParams{}
if user.Email != "" {
params.SearchParams.Query = fmt.Sprintf("email:'%s'", user.Email)
} else {
params.SearchParams.Query = fmt.Sprintf("metadata['user_id']:'%d'", user.UserId)
}
result := customer.Search(params)
if result.Err() != nil {
fmt.Printf("Error: %v\n", result.Err().Error())
return nil, result.Err()
}
if len(result.CustomerSearchResult().Data) != 0 {
return result.CustomerSearchResult().Data[0], nil
}
return nil, nil
}
// CreateCustomer Create a new Stripe customer
func (c *Client) CreateCustomer(user *User) (*stripe.Customer, error) {
stripe.Key = c.SecretKey
customerData := &stripe.CustomerParams{}
if user.Email != "" {
customerData.Email = &user.Email
}
customerData.AddMetadata("user_id", strconv.FormatInt(user.UserId, 10))
return customer.New(customerData)
}
// QueryOrderStatus Query the status of the order
func (c *Client) QueryOrderStatus(orderNo string) (bool, error) {
stripe.Key = c.SecretKey
intent, err := paymentintent.Get(orderNo, nil)
if err != nil {
return false, err
}
return intent.Status == "succeeded", err
}
// ParseNotify
func (c *Client) ParseNotify(payload []byte, signature string) (*NotifyResult, error) {
event, err := webhook.ConstructEvent(payload, signature, c.Config.WebhookSecret)
if err != nil {
return nil, err
}
var paymentIntent stripe.PaymentIntent
err = json.Unmarshal(event.Data.Raw, &paymentIntent)
if err != nil {
logger.Error("Failed to unmarshal payment intent", logger.Field("error", err.Error()))
return nil, err
}
orderNo := paymentIntent.Metadata["order_no"]
userId := paymentIntent.Metadata["user_id"]
var method string
if paymentIntent.PaymentMethod != nil && paymentIntent.PaymentMethod.ID != "" {
fmt.Println("paymentMethod:", paymentIntent.PaymentMethod.ID)
result, err := c.RetrievePaymentMethod(paymentIntent.PaymentMethod.ID)
if err != nil {
logger.Error("[stripe] Payment callback query payment method error", logger.Field("errors", err.Error()))
}
if result != nil {
method = string(result.Type)
}
}
// userId string 转 int64
uid, _ := strconv.ParseInt(userId, 10, 64)
return &NotifyResult{
EventType: string(event.Type),
OrderNo: orderNo,
TradeNo: paymentIntent.ID,
UserId: uid,
Method: method,
Amount: paymentIntent.Amount,
}, nil
}
// RetrievePaymentMethod 查询支付方式
func (c *Client) RetrievePaymentMethod(id string) (*stripe.PaymentMethod, error) {
stripe.Key = c.SecretKey
return paymentmethod.Get(id, nil)
}
// CreateWebhookEndpoint 创建 webhook endpoint
func (c *Client) CreateWebhookEndpoint(url string) (*stripe.WebhookEndpoint, error) {
stripe.Key = c.SecretKey
params := &stripe.WebhookEndpointParams{
URL: stripe.String(url),
EnabledEvents: []*string{
stripe.String("payment_intent.succeeded"),
stripe.String("payment_intent.payment_failed"),
},
}
return webhookendpoint.New(params)
}
+55
View File
@@ -0,0 +1,55 @@
package stripe
import (
"testing"
"github.com/stripe/stripe-go/v81"
)
func TestStripeAlipay(t *testing.T) {
t.Skipf("Skip TestStripeAlipay test")
client := NewClient(Config{
WebhookSecret: "",
})
order := Order{
OrderNo: "JS20210719123456789",
Subscribe: "测试",
Amount: 100,
Currency: string(stripe.CurrencyGBP),
Payment: "alipay",
}
user := User{
UserId: 1,
Email: "tension@ppanel.dev",
}
result, err := client.CreatePaymentSheet(&order, &user)
if err != nil {
t.Error(err.Error())
}
t.Logf("TradeNo: %s\n", result.ClientSecret)
}
func TestStripeWechat(t *testing.T) {
t.Skipf("Skip TestStripeWechat test")
client := NewClient(Config{
SecretKey: "SecretKey",
PublicKey: "PublicKey",
WebhookSecret: "",
})
order := Order{
OrderNo: "JS20210719123456789",
Subscribe: "测试",
Amount: 100,
Currency: string(stripe.CurrencyGBP),
Payment: "wechat_pay",
}
user := User{
UserId: 1,
Email: "tension@ppanel.dev",
}
result, err := client.CreatePaymentSheet(&order, &user)
if err != nil {
t.Error(err.Error())
}
t.Logf("TradeNo: %s\n", result.ClientSecret)
}