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

224 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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