224 lines
6.7 KiB
Go
224 lines
6.7 KiB
Go
package wechat
|
||
|
||
import (
|
||
"context"
|
||
"crypto/rsa"
|
||
"crypto/x509"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"soul-api/internal/config"
|
||
|
||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||
"github.com/wechatpay-apiv3/wechatpay-go/services/transferbatch"
|
||
)
|
||
|
||
var (
|
||
transferClient *core.Client
|
||
transferCfg *config.Config
|
||
)
|
||
|
||
// InitTransfer 初始化转账客户端
|
||
func InitTransfer(c *config.Config) error {
|
||
transferCfg = c
|
||
|
||
// 加载商户私钥
|
||
privateKey, err := loadPrivateKey(c.WechatKeyPath)
|
||
if err != nil {
|
||
return fmt.Errorf("加载商户私钥失败: %w", err)
|
||
}
|
||
|
||
// 初始化客户端
|
||
opts := []core.ClientOption{
|
||
option.WithWechatPayAutoAuthCipher(c.WechatMchID, c.WechatSerialNo, privateKey, c.WechatAPIv3Key),
|
||
}
|
||
|
||
client, err := core.NewClient(context.Background(), opts...)
|
||
if err != nil {
|
||
return fmt.Errorf("初始化微信支付客户端失败: %w", err)
|
||
}
|
||
|
||
transferClient = client
|
||
return nil
|
||
}
|
||
|
||
// loadPrivateKey 加载商户私钥。path 支持本地路径或 http(s) 链接(如 OSS 地址)。
|
||
func loadPrivateKey(path string) (*rsa.PrivateKey, error) {
|
||
var privateKeyBytes []byte
|
||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||
resp, err := http.Get(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("从链接获取私钥失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("获取私钥返回异常状态: %d", resp.StatusCode)
|
||
}
|
||
privateKeyBytes, err = io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取私钥内容失败: %w", err)
|
||
}
|
||
} else {
|
||
var err error
|
||
privateKeyBytes, err = os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取私钥文件失败: %w", err)
|
||
}
|
||
}
|
||
|
||
block, _ := pem.Decode(privateKeyBytes)
|
||
if block == nil {
|
||
return nil, fmt.Errorf("解析私钥失败:无效的 PEM 格式")
|
||
}
|
||
|
||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||
if err != nil {
|
||
// 尝试 PKCS8 格式
|
||
key, err2 := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||
if err2 != nil {
|
||
return nil, fmt.Errorf("解析私钥失败: %w", err)
|
||
}
|
||
var ok bool
|
||
privateKey, ok = key.(*rsa.PrivateKey)
|
||
if !ok {
|
||
return nil, fmt.Errorf("私钥不是 RSA 格式")
|
||
}
|
||
}
|
||
|
||
return privateKey, nil
|
||
}
|
||
|
||
// TransferParams 转账参数
|
||
type TransferParams struct {
|
||
OutBatchNo string // 商家批次单号(唯一)
|
||
OutDetailNo string // 商家明细单号(唯一)
|
||
OpenID string // 收款用户 openid
|
||
Amount int // 转账金额(分)
|
||
UserName string // 收款用户姓名(可选,用于实名校验)
|
||
Remark string // 转账备注
|
||
BatchName string // 批次名称(如"提现")
|
||
BatchRemark string // 批次备注
|
||
}
|
||
|
||
// TransferResult 转账结果
|
||
type TransferResult struct {
|
||
BatchID string // 微信批次单号
|
||
OutBatchNo string // 商家批次单号
|
||
CreateTime time.Time // 批次创建时间
|
||
BatchStatus string // 批次状态:ACCEPTED-已受理, PROCESSING-处理中, FINISHED-已完成, CLOSED-已关闭
|
||
}
|
||
|
||
// InitiateTransfer 发起转账
|
||
func InitiateTransfer(params TransferParams) (*TransferResult, error) {
|
||
if transferClient == nil {
|
||
return nil, fmt.Errorf("转账客户端未初始化")
|
||
}
|
||
|
||
svc := transferbatch.TransferBatchApiService{Client: transferClient}
|
||
|
||
// 构建转账明细
|
||
details := []transferbatch.TransferDetailInput{
|
||
{
|
||
OutDetailNo: core.String(params.OutDetailNo),
|
||
TransferAmount: core.Int64(int64(params.Amount)),
|
||
TransferRemark: core.String(params.Remark),
|
||
Openid: core.String(params.OpenID),
|
||
},
|
||
}
|
||
|
||
// 如果提供了姓名,添加实名校验
|
||
if params.UserName != "" {
|
||
details[0].UserName = core.String(params.UserName)
|
||
}
|
||
|
||
// 发起转账请求
|
||
req := transferbatch.InitiateBatchTransferRequest{
|
||
Appid: core.String(transferCfg.WechatAppID),
|
||
OutBatchNo: core.String(params.OutBatchNo),
|
||
BatchName: core.String(params.BatchName),
|
||
BatchRemark: core.String(params.BatchRemark),
|
||
TotalAmount: core.Int64(int64(params.Amount)),
|
||
TotalNum: core.Int64(1),
|
||
TransferDetailList: details,
|
||
}
|
||
|
||
resp, result, err := svc.InitiateBatchTransfer(context.Background(), req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("发起转账失败: %w", err)
|
||
}
|
||
|
||
if result.Response.StatusCode != 200 {
|
||
return nil, fmt.Errorf("转账请求失败,状态码: %d", result.Response.StatusCode)
|
||
}
|
||
|
||
return &TransferResult{
|
||
BatchID: *resp.BatchId,
|
||
OutBatchNo: *resp.OutBatchNo,
|
||
CreateTime: *resp.CreateTime,
|
||
BatchStatus: "ACCEPTED",
|
||
}, nil
|
||
}
|
||
|
||
// QueryTransfer 查询转账结果(暂不实现,转账状态通过回调获取)
|
||
func QueryTransfer(outBatchNo, outDetailNo string) (map[string]interface{}, error) {
|
||
// TODO: 实现查询转账结果
|
||
// 微信转账采用异步模式,通过回调通知最终结果
|
||
return map[string]interface{}{
|
||
"out_batch_no": outBatchNo,
|
||
"out_detail_no": outDetailNo,
|
||
"status": "processing",
|
||
"message": "转账处理中,请等待回调通知",
|
||
}, nil
|
||
}
|
||
|
||
// GenerateTransferBatchNo 生成转账批次单号
|
||
func GenerateTransferBatchNo() string {
|
||
now := time.Now()
|
||
timestamp := now.Format("20060102150405")
|
||
random := now.UnixNano() % 1000000
|
||
return fmt.Sprintf("WD%s%06d", timestamp, random)
|
||
}
|
||
|
||
// GenerateTransferDetailNo 生成转账明细单号
|
||
func GenerateTransferDetailNo() string {
|
||
now := time.Now()
|
||
timestamp := now.Format("20060102150405")
|
||
random := now.UnixNano() % 1000000
|
||
return fmt.Sprintf("WDD%s%06d", timestamp, random)
|
||
}
|
||
|
||
// TransferNotifyResult 转账回调结果
|
||
type TransferNotifyResult struct {
|
||
MchID *string `json:"mchid"`
|
||
OutBatchNo *string `json:"out_batch_no"`
|
||
BatchID *string `json:"batch_id"`
|
||
AppID *string `json:"appid"`
|
||
OutDetailNo *string `json:"out_detail_no"`
|
||
DetailID *string `json:"detail_id"`
|
||
DetailStatus *string `json:"detail_status"`
|
||
TransferAmount *int64 `json:"transfer_amount"`
|
||
OpenID *string `json:"openid"`
|
||
UserName *string `json:"user_name"`
|
||
InitiateTime *string `json:"initiate_time"`
|
||
UpdateTime *string `json:"update_time"`
|
||
FailReason *string `json:"fail_reason"`
|
||
}
|
||
|
||
// VerifyTransferNotify 验证转账回调签名(使用 notify handler)
|
||
func VerifyTransferNotify(ctx context.Context, request interface{}) (*TransferNotifyResult, error) {
|
||
// 微信官方 SDK 的回调处理
|
||
// 实际使用时,微信会 POST JSON 数据,包含加密信息
|
||
// 这里暂时返回简化版本,实际项目中需要完整实现签名验证
|
||
|
||
// TODO: 完整实现回调验证
|
||
// 需要解析请求体中的 resource.ciphertext,使用 APIv3 密钥解密
|
||
|
||
return &TransferNotifyResult{}, fmt.Errorf("转账回调需要完整实现")
|
||
}
|