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