Files
soul-yongping/soul-api/internal/wechat/transfer.go

224 lines
6.7 KiB
Go
Raw Normal View History

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("转账回调需要完整实现")
}