231 lines
7.6 KiB
Go
231 lines
7.6 KiB
Go
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
|
||
}
|