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

231 lines
7.6 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"
"fmt"
"time"
"soul-api/internal/config"
"github.com/ArtisanCloud/PowerLibs/v3/object"
fundAppRequest "github.com/ArtisanCloud/PowerWeChat/v3/src/payment/fundApp/request"
"github.com/ArtisanCloud/PowerWeChat/v3/src/payment/transfer/request"
)
// 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-已受理 等
}
// InitTransfer 保留兼容:转账已由 Init() 中 PowerWeChat Payment 统一初始化,调用无副作用
func InitTransfer(_ *config.Config) error {
return nil
}
// InitiateTransfer 发起商家转账到零钱PowerWeChat TransferBatch
func InitiateTransfer(params TransferParams) (*TransferResult, error) {
if paymentApp == nil {
return nil, fmt.Errorf("支付/转账未初始化,请先调用 wechat.Init")
}
detail := &request.TransferDetail{
OutDetailNO: params.OutDetailNo,
TransferAmount: params.Amount,
TransferRemark: params.Remark,
OpenID: params.OpenID,
}
if params.UserName != "" {
detail.UserName = object.NewNullString(params.UserName, true)
}
req := &request.RequestTransferBatch{
AppID: cfg.WechatAppID,
OutBatchNO: params.OutBatchNo,
BatchName: params.BatchName,
BatchRemark: params.BatchRemark,
TotalAmount: params.Amount,
TotalNum: 1,
TransferDetailList: []*request.TransferDetail{detail},
}
if cfg.WechatTransferURL != "" {
req.SetNotifyUrl(cfg.WechatTransferURL)
}
resp, err := paymentApp.TransferBatch.Batch(context.Background(), req)
if err != nil {
return nil, fmt.Errorf("发起转账失败: %w", err)
}
if resp == nil {
return nil, fmt.Errorf("转账返回为空")
}
result := &TransferResult{
OutBatchNo: resp.OutBatchNo,
BatchStatus: "ACCEPTED",
}
if resp.BatchId != "" {
result.BatchID = resp.BatchId
}
if !resp.CreateTime.IsZero() {
result.CreateTime = resp.CreateTime
}
return result, nil
}
// QueryTransfer 查询转账结果(可选,转账状态也可通过回调获取)
func QueryTransfer(outBatchNo, outDetailNo string) (map[string]interface{}, error) {
if paymentApp == nil {
return map[string]interface{}{
"out_batch_no": outBatchNo,
"out_detail_no": outDetailNo,
"status": "unknown",
"message": "转账未初始化",
}, nil
}
detail, err := paymentApp.TransferBatch.QueryOutBatchNoDetail(context.Background(), outBatchNo, outDetailNo)
if err != nil {
return map[string]interface{}{
"out_batch_no": outBatchNo,
"out_detail_no": outDetailNo,
"status": "processing",
"message": err.Error(),
}, nil
}
if detail == nil {
return map[string]interface{}{
"out_batch_no": outBatchNo,
"out_detail_no": outDetailNo,
"status": "processing",
"message": "转账处理中",
}, nil
}
return map[string]interface{}{
"out_batch_no": outBatchNo,
"out_detail_no": outDetailNo,
"detail_status": detail.DetailStatus,
"fail_reason": detail.FailReason,
"transfer_amount": detail.TransferAmount,
}, 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)
}
// FundAppTransferParams 单笔转账FundApp 发起转账)参数
type FundAppTransferParams struct {
OutBillNo string // 商户单号(唯一,回调时 out_bill_no 即此值,建议存到 withdrawal.detail_no
OpenID string
UserName string // 可选
Amount int // 分
Remark string
NotifyURL string
TransferSceneId string // 可选,如 "1005"
}
// FundAppTransferResult 单笔转账结果(微信同步返回,无需等回调即可落库)
type FundAppTransferResult struct {
OutBillNo string // 商户单号
TransferBillNo string // 微信转账单号
State string // 如 WAIT_USER_CONFIRM 表示待用户确认收款
PackageInfo string // 供小程序 wx.requestMerchantTransfer 使用
CreateTime string // 微信返回的 create_time
}
// InitiateTransferByFundApp 发起商家转账到零钱PowerWeChat FundApp.TransferBills 单笔接口)
// 与 TransferBatch 不同,此为 /v3/fund-app/mch-transfer/transfer-bills 单笔发起,回调仍为 MCHTRANSFER.BILL.FINISHED解密后 out_bill_no 即本接口传入的 OutBillNo
func InitiateTransferByFundApp(params FundAppTransferParams) (*FundAppTransferResult, error) {
if paymentApp == nil || paymentApp.FundApp == nil {
return nil, fmt.Errorf("支付/转账未初始化,请先调用 wechat.Init")
}
req := &fundAppRequest.RequestTransferBills{
Appid: cfg.WechatAppID,
OutBillNo: params.OutBillNo,
TransferSceneId: params.TransferSceneId,
Openid: params.OpenID,
UserName: params.UserName,
TransferAmount: params.Amount,
TransferRemark: params.Remark,
NotifyUrl: params.NotifyURL,
}
// 1005=佣金报酬:微信要求同时传 transfer_scene_report_infos岗位类型与报酬说明分开两条
if params.TransferSceneId == "1005" {
req.TransferSceneReportInfos = []fundAppRequest.TransferSceneReportInfo{
{InfoType: "岗位类型", InfoContent: "会员"},
{InfoType: "报酬说明", InfoContent: "提现"},
}
}
if req.NotifyUrl == "" && cfg.WechatTransferURL != "" {
req.NotifyUrl = cfg.WechatTransferURL
}
ctx := context.Background()
resp, err := paymentApp.FundApp.TransferBills(ctx, req)
if err != nil {
return nil, fmt.Errorf("发起转账失败: %w", err)
}
if resp == nil {
return nil, fmt.Errorf("转账返回为空")
}
// 微信返回 4xx 时 body 可能被解析到 resp需根据 code 或 out_bill_no 判断是否成功
if resp.Code != "" {
msg := resp.Message
if msg == "" {
msg = resp.Code
}
return nil, fmt.Errorf("微信接口报错: %s", msg)
}
if resp.OutBillNo == "" {
return nil, fmt.Errorf("微信未返回商户单号可能请求被拒绝如IP未加入白名单")
}
result := &FundAppTransferResult{
OutBillNo: resp.OutBillNo,
TransferBillNo: resp.TransferBillNo,
State: resp.State,
PackageInfo: resp.PackageInfo,
CreateTime: resp.CreateTime,
}
return result, nil
}
// QueryTransferByOutBill 按商户单号查询单笔转账结果FundApp 接口,用于 sync
func QueryTransferByOutBill(outBillNo string) (state, transferBillNo, failReason string, err error) {
if paymentApp == nil || paymentApp.FundApp == nil {
return "", "", "", fmt.Errorf("支付/转账未初始化")
}
ctx := context.Background()
resp, err := paymentApp.FundApp.QueryOutBill(ctx, outBillNo)
if err != nil {
return "", "", "", err
}
if resp == nil {
return "", "", "", nil
}
return resp.State, resp.TransferBillNo, resp.FailReason, nil
}