726 lines
20 KiB
Go
726 lines
20 KiB
Go
package handlers
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"sale/internal/models"
|
|
"sale/internal/utils"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type PaymentHandler struct{}
|
|
|
|
func NewPaymentHandler() *PaymentHandler {
|
|
return &PaymentHandler{}
|
|
}
|
|
|
|
type BepUsdtCreateRequest struct {
|
|
TradeType string `json:"trade_type,omitempty"`
|
|
OrderID string `json:"order_id"`
|
|
Amount float64 `json:"amount"`
|
|
NotifyURL string `json:"notify_url"`
|
|
RedirectURL string `json:"redirect_url,omitempty"`
|
|
Signature string `json:"signature"`
|
|
}
|
|
|
|
func (h *PaymentHandler) CreatePayment(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
orderID, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
var order models.Order
|
|
if err := utils.DB.Preload("OrderItems").Where("id = ? AND user_id = ?", orderID, userID).First(&order).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
|
|
return
|
|
}
|
|
|
|
if order.Status != models.OrderStatusPendingPayment {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Order is not pending payment"})
|
|
return
|
|
}
|
|
|
|
if order.PaymentExpiresAt != nil && time.Now().After(*order.PaymentExpiresAt) {
|
|
tx := utils.DB.Begin()
|
|
tx.Model(&order).Update("status", models.OrderStatusCancelled)
|
|
for _, item := range order.OrderItems {
|
|
var inventory models.Inventory
|
|
if err := tx.Where("product_id = ?", item.ProductID).First(&inventory).Error; err == nil {
|
|
tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity+item.Quantity)
|
|
}
|
|
}
|
|
tx.Commit()
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "支付已超时,订单已取消"})
|
|
return
|
|
}
|
|
|
|
// 根据订单支付方式获取对应的支付通道
|
|
paymentType := order.PaymentMethod
|
|
if paymentType == "" {
|
|
paymentType = "bepusdt" // 默认使用 bepusdt
|
|
}
|
|
|
|
// 如果是 hashpay,调用 HashPay 支付
|
|
if paymentType == "hashpay" {
|
|
h.CreateHashPayPayment(c)
|
|
return
|
|
}
|
|
|
|
// BepUsdt 支付流程
|
|
var channels []models.PaymentChannel
|
|
utils.DB.Where("is_enabled = ? AND type = ?", true, paymentType).Order("sort_order ASC").Find(&channels)
|
|
if len(channels) == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "No available payment channel"})
|
|
return
|
|
}
|
|
|
|
channel := channels[0]
|
|
config := parseChannelConfig(channel.Config)
|
|
|
|
apiURL := strings.TrimSuffix(config["bepusdt_api_url"], "/")
|
|
apiToken := config["bepusdt_api_token"]
|
|
tradeType := config["bepusdt_trade_type"]
|
|
if tradeType == "" {
|
|
tradeType = "usdt.trc20"
|
|
}
|
|
|
|
scheme := "http"
|
|
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
|
scheme = "https"
|
|
}
|
|
host := c.Request.Host
|
|
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
|
|
|
notifyURL := fmt.Sprintf("%s/api/payment/bepusdt/notify", baseURL)
|
|
redirectURL := fmt.Sprintf("%s/orders/%d", baseURL, order.ID)
|
|
|
|
params := map[string]string{
|
|
"order_id": strconv.Itoa(int(order.ID)),
|
|
"amount": fmt.Sprint(order.TotalAmount),
|
|
"notify_url": notifyURL,
|
|
"redirect_url": redirectURL,
|
|
}
|
|
if tradeType != "" {
|
|
params["trade_type"] = tradeType
|
|
}
|
|
|
|
signature := signBepUsdt(params, apiToken)
|
|
|
|
log.Printf("[BepUsdt] signature params: %v", params)
|
|
log.Printf("[BepUsdt] signature: %s, token: %s", signature, apiToken)
|
|
|
|
reqBody := BepUsdtCreateRequest{
|
|
TradeType: tradeType,
|
|
OrderID: strconv.Itoa(int(order.ID)),
|
|
Amount: order.TotalAmount,
|
|
NotifyURL: notifyURL,
|
|
RedirectURL: redirectURL,
|
|
Signature: signature,
|
|
}
|
|
|
|
body, _ := json.Marshal(reqBody)
|
|
log.Printf("[BepUsdt] request body: %s", string(body))
|
|
|
|
req, err := http.NewRequest("POST", apiURL+"/api/v1/order/create-transaction", strings.NewReader(string(body)))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"})
|
|
return
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect payment gateway"})
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
log.Printf("[BepUsdt] response: %s", string(respBody))
|
|
|
|
var result map[string]interface{}
|
|
json.Unmarshal(respBody, &result)
|
|
|
|
if statusCode, ok := result["status_code"].(float64); !ok || int(statusCode) != 200 {
|
|
errMsg := "Payment creation failed"
|
|
if msg, ok := result["message"].(string); ok {
|
|
errMsg = msg
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg})
|
|
return
|
|
}
|
|
|
|
data, ok := result["data"].(map[string]interface{})
|
|
if !ok {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid response from payment gateway"})
|
|
return
|
|
}
|
|
|
|
tradeID, _ := data["trade_id"].(string)
|
|
token, _ := data["token"].(string)
|
|
paymentURL, _ := data["payment_url"].(string)
|
|
|
|
// 钱包地址在token字段中
|
|
walletAddress := token
|
|
|
|
log.Printf("[BepUsdt] payment data: trade_id=%s, token=%s, payment_url=%s, wallet_address=%s", tradeID, token, paymentURL, walletAddress)
|
|
|
|
var amount float64
|
|
if v, ok := data["amount"].(float64); ok {
|
|
amount = v
|
|
} else if v, ok := data["amount"].(string); ok {
|
|
amount, _ = strconv.ParseFloat(v, 64)
|
|
}
|
|
|
|
var actualAmount float64
|
|
if v, ok := data["actual_amount"].(float64); ok {
|
|
actualAmount = v
|
|
} else if v, ok := data["actual_amount"].(string); ok {
|
|
actualAmount, _ = strconv.ParseFloat(v, 64)
|
|
}
|
|
if actualAmount == 0 {
|
|
actualAmount = amount
|
|
}
|
|
|
|
var expiration int64
|
|
if v, ok := data["expiration_time"].(float64); ok {
|
|
expiration = int64(v)
|
|
}
|
|
if expiration > 1e12 {
|
|
expiration = expiration / 1000
|
|
}
|
|
|
|
var remainingSeconds int64
|
|
var expiresAt *time.Time
|
|
if expiration > 0 {
|
|
remainingSeconds = expiration - time.Now().Unix()
|
|
if remainingSeconds < 0 {
|
|
remainingSeconds = 0
|
|
}
|
|
if remainingSeconds > 0 {
|
|
t := time.Now().Add(time.Duration(remainingSeconds) * time.Second)
|
|
expiresAt = &t
|
|
}
|
|
}
|
|
|
|
utils.DB.Model(&order).Updates(map[string]interface{}{
|
|
"payment_method": "bepusdt",
|
|
"payment_expires_at": expiresAt,
|
|
})
|
|
|
|
paymentInfo := map[string]interface{}{
|
|
"trade_id": tradeID,
|
|
"order_id": order.ID,
|
|
"amount": amount,
|
|
"actual_amount": actualAmount,
|
|
"token": token,
|
|
"payment_url": paymentURL,
|
|
"wallet_address": walletAddress,
|
|
"expiration_time": remainingSeconds,
|
|
"trade_type": tradeType,
|
|
"channel_id": channel.ID,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": paymentInfo})
|
|
}
|
|
|
|
func (h *PaymentHandler) BepUsdtNotify(c *gin.Context) {
|
|
body, _ := io.ReadAll(c.Request.Body)
|
|
|
|
var params map[string]interface{}
|
|
json.Unmarshal(body, ¶ms)
|
|
|
|
orderIDStr, _ := params["order_id"].(string)
|
|
status, _ := params["status"].(float64)
|
|
signature, _ := params["signature"].(string)
|
|
|
|
orderID, err := strconv.Atoi(orderIDStr)
|
|
if err != nil {
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
var order models.Order
|
|
if err := utils.DB.First(&order, orderID).Error; err != nil {
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
var channels []models.PaymentChannel
|
|
utils.DB.Where("type = ?", "bepusdt").Find(&channels)
|
|
if len(channels) == 0 {
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
channel := channels[0]
|
|
config := parseChannelConfig(channel.Config)
|
|
apiToken := config["bepusdt_api_token"]
|
|
|
|
stringParams := make(map[string]string)
|
|
for k, v := range params {
|
|
if k != "signature" {
|
|
stringParams[k] = fmt.Sprintf("%v", v)
|
|
}
|
|
}
|
|
expectedSign := signBepUsdt(stringParams, apiToken)
|
|
|
|
if signature != expectedSign {
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
if int(status) == 2 {
|
|
utils.DB.Model(&order).Update("status", models.OrderStatusPendingConfirm)
|
|
} else if int(status) == 3 {
|
|
utils.DB.Model(&order).Update("status", models.OrderStatusCancelled)
|
|
}
|
|
|
|
c.String(http.StatusOK, "ok")
|
|
}
|
|
|
|
func (h *PaymentHandler) CheckPaymentStatus(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
orderID, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
var order models.Order
|
|
if err := utils.DB.Where("id = ? AND user_id = ?", orderID, userID).First(&order).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": gin.H{
|
|
"status": order.Status,
|
|
}})
|
|
}
|
|
|
|
func signBepUsdt(params map[string]string, token string) string {
|
|
keys := make([]string, 0, len(params))
|
|
for k := range params {
|
|
if params[k] != "" && k != "signature" && k != "sign_type" {
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
var parts []string
|
|
for _, k := range keys {
|
|
parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
|
|
}
|
|
str := strings.Join(parts, "&") + token
|
|
|
|
hash := md5.Sum([]byte(str))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
func parseChannelConfig(configStr string) map[string]string {
|
|
result := make(map[string]string)
|
|
if configStr == "" {
|
|
return result
|
|
}
|
|
var raw map[string]interface{}
|
|
if err := json.Unmarshal([]byte(configStr), &raw); err != nil {
|
|
var rawStr map[string]string
|
|
if err2 := json.Unmarshal([]byte(configStr), &rawStr); err2 == nil {
|
|
return rawStr
|
|
}
|
|
log.Printf("[Payment] failed to parse channel config: %v", err)
|
|
return result
|
|
}
|
|
for k, v := range raw {
|
|
if v == nil {
|
|
continue
|
|
}
|
|
switch val := v.(type) {
|
|
case string:
|
|
result[k] = val
|
|
case float64:
|
|
if val == float64(int64(val)) {
|
|
result[k] = strconv.FormatInt(int64(val), 10)
|
|
} else {
|
|
result[k] = strconv.FormatFloat(val, 'f', -1, 64)
|
|
}
|
|
case bool:
|
|
result[k] = strconv.FormatBool(val)
|
|
default:
|
|
result[k] = fmt.Sprintf("%v", val)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ===================== HashPay 支付 =====================
|
|
|
|
// HashPay 创建订单请求
|
|
type HashPayCreateRequest struct {
|
|
MerchantNo string `json:"merchantNo"`
|
|
Amount float64 `json:"amount"`
|
|
Currency string `json:"currency,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
ReturnURL string `json:"return_url,omitempty"`
|
|
}
|
|
|
|
// HashPay 创建订单响应
|
|
type HashPayCreateResponse struct {
|
|
CheckoutURL string `json:"checkoutUrl"`
|
|
Order struct {
|
|
ID string `json:"id"`
|
|
Amount float64 `json:"amount"`
|
|
Currency string `json:"currency"`
|
|
ExpiresAt int64 `json:"expiresAt"`
|
|
Status string `json:"status"`
|
|
} `json:"order"`
|
|
Reused bool `json:"reused"`
|
|
}
|
|
|
|
// CreateHashPayPayment 创建 HashPay 支付订单
|
|
func (h *PaymentHandler) CreateHashPayPayment(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
orderID, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
var order models.Order
|
|
if err := utils.DB.Preload("OrderItems").Where("id = ? AND user_id = ?", orderID, userID).First(&order).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
|
|
return
|
|
}
|
|
|
|
if order.Status != models.OrderStatusPendingPayment {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Order is not pending payment"})
|
|
return
|
|
}
|
|
|
|
if order.PaymentExpiresAt != nil && time.Now().After(*order.PaymentExpiresAt) {
|
|
tx := utils.DB.Begin()
|
|
tx.Model(&order).Update("status", models.OrderStatusCancelled)
|
|
for _, item := range order.OrderItems {
|
|
var inventory models.Inventory
|
|
if err := tx.Where("product_id = ?", item.ProductID).First(&inventory).Error; err == nil {
|
|
tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity+item.Quantity)
|
|
}
|
|
}
|
|
tx.Commit()
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "支付已超时,订单已取消"})
|
|
return
|
|
}
|
|
|
|
var channels []models.PaymentChannel
|
|
utils.DB.Where("is_enabled = ? AND type = ?", true, "hashpay").Order("sort_order ASC").Find(&channels)
|
|
if len(channels) == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "No available HashPay payment channel"})
|
|
return
|
|
}
|
|
|
|
channel := channels[0]
|
|
config := parseChannelConfig(channel.Config)
|
|
|
|
baseURL := config["hashpay_base_url"]
|
|
merchantID := config["hashpay_merchant_id"]
|
|
privateKeyPEM := config["hashpay_private_key"]
|
|
|
|
if baseURL == "" || merchantID == "" || privateKeyPEM == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "HashPay channel not configured properly"})
|
|
return
|
|
}
|
|
|
|
// 解析私钥
|
|
privateKey, err := parseRSAPrivateKey(privateKeyPEM)
|
|
if err != nil {
|
|
log.Printf("[HashPay] failed to parse private key: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid private key configuration"})
|
|
return
|
|
}
|
|
|
|
scheme := "http"
|
|
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
|
scheme = "https"
|
|
}
|
|
host := c.Request.Host
|
|
apiBaseURL := fmt.Sprintf("%s://%s", scheme, host)
|
|
|
|
merchantNo := fmt.Sprintf("ORDER-%d", order.ID)
|
|
returnURL := fmt.Sprintf("%s/orders/%d", apiBaseURL, order.ID)
|
|
|
|
reqBody := HashPayCreateRequest{
|
|
MerchantNo: merchantNo,
|
|
Amount: order.TotalAmount,
|
|
Currency: "USD",
|
|
Description: fmt.Sprintf("Order #%d", order.ID),
|
|
ReturnURL: returnURL,
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
|
|
|
// 生成签名
|
|
signature, err := signHashPay("POST", "/api/merchant/new", timestamp, string(bodyBytes), privateKey)
|
|
if err != nil {
|
|
log.Printf("[HashPay] failed to sign request: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sign request"})
|
|
return
|
|
}
|
|
|
|
// 发送请求
|
|
req, err := http.NewRequest("POST", baseURL+"/api/merchant/new", strings.NewReader(string(bodyBytes)))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"})
|
|
return
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Merchant-Id", merchantID)
|
|
req.Header.Set("X-Timestamp", timestamp)
|
|
req.Header.Set("X-Signature", signature)
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
log.Printf("[HashPay] request failed: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect to HashPay"})
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
log.Printf("[HashPay] response: %s", string(respBody))
|
|
|
|
var result HashPayCreateResponse
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid response from HashPay"})
|
|
return
|
|
}
|
|
|
|
if result.CheckoutURL == "" {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "No checkout URL returned"})
|
|
return
|
|
}
|
|
|
|
// 更新订单支付方式
|
|
utils.DB.Model(&order).Updates(map[string]interface{}{
|
|
"payment_method": "hashpay",
|
|
})
|
|
|
|
// 计算过期时间
|
|
var expiresAt *time.Time
|
|
if result.Order.ExpiresAt > 0 {
|
|
t := time.Unix(result.Order.ExpiresAt, 0)
|
|
expiresAt = &t
|
|
}
|
|
|
|
paymentInfo := map[string]interface{}{
|
|
"order_id": order.ID,
|
|
"checkout_url": result.CheckoutURL,
|
|
"hashpay_id": result.Order.ID,
|
|
"amount": result.Order.Amount,
|
|
"currency": result.Order.Currency,
|
|
"expires_at": expiresAt,
|
|
"channel_id": channel.ID,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": paymentInfo})
|
|
}
|
|
|
|
// HashPayNotify 处理 HashPay 回调通知
|
|
func (h *PaymentHandler) HashPayNotify(c *gin.Context) {
|
|
// 添加 CORS 头
|
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
c.Header("Access-Control-Allow-Headers", "Content-Type")
|
|
|
|
body, _ := io.ReadAll(c.Request.Body)
|
|
|
|
log.Printf("[HashPay] notify body: %s", string(body))
|
|
|
|
var envelope struct {
|
|
Alg string `json:"alg"`
|
|
Key string `json:"key"`
|
|
IV string `json:"iv"`
|
|
Data string `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &envelope); err != nil {
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
// 获取商户私钥进行解密
|
|
var channels []models.PaymentChannel
|
|
utils.DB.Where("type = ?", "hashpay").Find(&channels)
|
|
if len(channels) == 0 {
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
channel := channels[0]
|
|
config := parseChannelConfig(channel.Config)
|
|
privateKeyPEM := config["hashpay_private_key"]
|
|
|
|
privateKey, err := parseRSAPrivateKey(privateKeyPEM)
|
|
if err != nil {
|
|
log.Printf("[HashPay] failed to parse private key for decryption: %v", err)
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
// 解密回调数据
|
|
payload, err := decryptHashPayNotify(envelope.Key, envelope.IV, envelope.Data, privateKey)
|
|
if err != nil {
|
|
log.Printf("[HashPay] failed to decrypt notify: %v", err)
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
log.Printf("[HashPay] decrypted payload: %+v", payload)
|
|
|
|
// 解析订单号
|
|
merchantNo := payload.MerchantNo
|
|
if merchantNo == "" {
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
// 从 merchantNo 中提取订单ID (格式: ORDER-123)
|
|
orderIDStr := strings.TrimPrefix(merchantNo, "ORDER-")
|
|
orderID, err := strconv.Atoi(orderIDStr)
|
|
if err != nil {
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
var order models.Order
|
|
if err := utils.DB.First(&order, orderID).Error; err != nil {
|
|
c.String(http.StatusOK, "ok")
|
|
return
|
|
}
|
|
|
|
// 更新订单状态
|
|
if payload.Status == "paid" {
|
|
utils.DB.Model(&order).Update("status", models.OrderStatusPendingConfirm)
|
|
} else if payload.Status == "expired" || payload.Status == "cancelled" {
|
|
utils.DB.Model(&order).Update("status", models.OrderStatusCancelled)
|
|
}
|
|
|
|
c.String(http.StatusOK, "ok")
|
|
}
|
|
|
|
// HashPayNotifyPayload 回调解密后的数据结构
|
|
type HashPayNotifyPayload struct {
|
|
Timestamp int64 `json:"timestamp"`
|
|
MerchantNo string `json:"merchantNo"`
|
|
OrderID string `json:"orderId"`
|
|
Amount float64 `json:"amount"`
|
|
Currency string `json:"currency"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// parseRSAPrivateKey 解析 RSA 私钥
|
|
func parseRSAPrivateKey(pemStr string) (*rsa.PrivateKey, error) {
|
|
block, _ := pem.Decode([]byte(pemStr))
|
|
if block == nil {
|
|
return nil, fmt.Errorf("failed to parse PEM block")
|
|
}
|
|
|
|
// 尝试 PKCS1
|
|
if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
|
|
return key, nil
|
|
}
|
|
|
|
// 尝试 PKCS8
|
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rsaKey, ok := key.(*rsa.PrivateKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("not an RSA private key")
|
|
}
|
|
return rsaKey, nil
|
|
}
|
|
|
|
// signHashPay 生成 HashPay 请求签名
|
|
func signHashPay(method, path, timestamp, body string, privateKey *rsa.PrivateKey) (string, error) {
|
|
// 签名原文: method + "\n" + path + "\n" + timestamp + "\n" + body
|
|
message := fmt.Sprintf("%s\n%s\n%s\n%s", method, path, timestamp, body)
|
|
|
|
hashed := sha256.Sum256([]byte(message))
|
|
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(signature), nil
|
|
}
|
|
|
|
// decryptHashPayNotify 解密 HashPay 回调通知
|
|
func decryptHashPayNotify(encryptedKey, iv, encryptedData string, privateKey *rsa.PrivateKey) (*HashPayNotifyPayload, error) {
|
|
// 解码 Base64
|
|
keyBytes, err := base64.StdEncoding.DecodeString(encryptedKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode key: %v", err)
|
|
}
|
|
|
|
ivBytes, err := base64.StdEncoding.DecodeString(iv)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode iv: %v", err)
|
|
}
|
|
|
|
dataBytes, err := base64.StdEncoding.DecodeString(encryptedData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode data: %v", err)
|
|
}
|
|
|
|
// 使用 RSA-OAEP 解密 AES 密钥
|
|
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, keyBytes, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt AES key: %v", err)
|
|
}
|
|
|
|
// 使用 AES-256-GCM 解密数据
|
|
// Go 标准库的 AES-GCM 实现
|
|
block, err := aes.NewCipher(aesKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create cipher: %v", err)
|
|
}
|
|
|
|
aesgcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM: %v", err)
|
|
}
|
|
|
|
// GCM 的 nonce 就是 IV,密文最后是 tag
|
|
// dataBytes = ciphertext + tag
|
|
tagSize := aesgcm.Overhead()
|
|
if len(dataBytes) < tagSize {
|
|
return nil, fmt.Errorf("ciphertext too short")
|
|
}
|
|
|
|
plaintext, err := aesgcm.Open(nil, ivBytes, dataBytes, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt data: %v", err)
|
|
}
|
|
|
|
var payload HashPayNotifyPayload
|
|
if err := json.Unmarshal(plaintext, &payload); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal payload: %v", err)
|
|
}
|
|
|
|
return &payload, nil
|
|
}
|