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

231 lines
7.6 KiB
Go
Raw Normal View History

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
}