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 }