2026-02-09 18:19:12 +08:00
|
|
|
|
package wechat
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"io"
|
|
|
|
|
|
"net/http"
|
2026-02-09 21:29:52 +08:00
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"strings"
|
2026-02-09 18:19:12 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"soul-api/internal/config"
|
|
|
|
|
|
|
2026-02-09 21:29:52 +08:00
|
|
|
|
"github.com/ArtisanCloud/PowerLibs/v3/object"
|
|
|
|
|
|
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel/models"
|
2026-02-11 09:56:57 +08:00
|
|
|
|
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel/power"
|
|
|
|
|
|
"github.com/ArtisanCloud/PowerWeChat/v3/src/miniProgram"
|
2026-02-09 18:19:12 +08:00
|
|
|
|
"github.com/ArtisanCloud/PowerWeChat/v3/src/payment"
|
2026-02-09 21:29:52 +08:00
|
|
|
|
notifyrequest "github.com/ArtisanCloud/PowerWeChat/v3/src/payment/notify/request"
|
|
|
|
|
|
"github.com/ArtisanCloud/PowerWeChat/v3/src/payment/order/request"
|
2026-02-11 09:56:57 +08:00
|
|
|
|
subrequest "github.com/ArtisanCloud/PowerWeChat/v3/src/basicService/subscribeMessage/request"
|
2026-02-09 18:19:12 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
miniProgramApp *miniProgram.MiniProgram
|
|
|
|
|
|
paymentApp *payment.Payment
|
|
|
|
|
|
cfg *config.Config
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-09 21:29:52 +08:00
|
|
|
|
// resolveCertPaths 若证书/私钥路径为 URL 则下载到临时文件并返回本地路径
|
|
|
|
|
|
func resolveCertPaths(c *config.Config) (certPath, keyPath string, err error) {
|
|
|
|
|
|
certPath = c.WechatCertPath
|
|
|
|
|
|
keyPath = c.WechatKeyPath
|
|
|
|
|
|
if certPath == "" || keyPath == "" {
|
|
|
|
|
|
return certPath, keyPath, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.HasPrefix(keyPath, "http://") || strings.HasPrefix(keyPath, "https://") {
|
|
|
|
|
|
dir, e := os.MkdirTemp("", "wechat_cert_*")
|
|
|
|
|
|
if e != nil {
|
|
|
|
|
|
return "", "", fmt.Errorf("创建临时目录失败: %w", e)
|
|
|
|
|
|
}
|
|
|
|
|
|
keyPath, e = downloadToFile(keyPath, filepath.Join(dir, "apiclient_key.pem"))
|
|
|
|
|
|
if e != nil {
|
|
|
|
|
|
return "", "", e
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.HasPrefix(certPath, "http://") || strings.HasPrefix(certPath, "https://") {
|
|
|
|
|
|
certPath, e = downloadToFile(certPath, filepath.Join(dir, "apiclient_cert.pem"))
|
|
|
|
|
|
if e != nil {
|
|
|
|
|
|
return "", "", e
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// cert 是本地路径,只下载了 key
|
|
|
|
|
|
certPath = c.WechatCertPath
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return certPath, keyPath, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func downloadToFile(url, filePath string) (string, error) {
|
|
|
|
|
|
resp, err := http.Get(url)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("下载文件失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
|
return "", fmt.Errorf("下载返回状态: %d", resp.StatusCode)
|
|
|
|
|
|
}
|
|
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("读取内容失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := os.WriteFile(filePath, data, 0600); err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("写入临时文件失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return filePath, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Init 初始化微信客户端(小程序 + 支付 v3 + 转账均使用 PowerWeChat)
|
2026-02-09 18:19:12 +08:00
|
|
|
|
func Init(c *config.Config) error {
|
|
|
|
|
|
cfg = c
|
2026-02-09 21:29:52 +08:00
|
|
|
|
|
2026-02-09 18:19:12 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
miniProgramApp, err = miniProgram.NewMiniProgram(&miniProgram.UserConfig{
|
2026-02-09 21:29:52 +08:00
|
|
|
|
AppID: cfg.WechatAppID,
|
|
|
|
|
|
Secret: cfg.WechatAppSecret,
|
2026-02-09 18:19:12 +08:00
|
|
|
|
HttpDebug: cfg.Mode == "debug",
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("初始化小程序失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 21:29:52 +08:00
|
|
|
|
certPath, keyPath, err := resolveCertPaths(cfg)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("解析证书路径: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
paymentConfig := &payment.UserConfig{
|
|
|
|
|
|
AppID: cfg.WechatAppID,
|
|
|
|
|
|
MchID: cfg.WechatMchID,
|
|
|
|
|
|
MchApiV3Key: cfg.WechatAPIv3Key,
|
|
|
|
|
|
Key: cfg.WechatMchKey,
|
|
|
|
|
|
CertPath: certPath,
|
|
|
|
|
|
KeyPath: keyPath,
|
|
|
|
|
|
SerialNo: cfg.WechatSerialNo,
|
|
|
|
|
|
NotifyURL: cfg.WechatNotifyURL,
|
|
|
|
|
|
HttpDebug: cfg.Mode == "debug",
|
|
|
|
|
|
}
|
|
|
|
|
|
paymentApp, err = payment.NewPayment(paymentConfig)
|
2026-02-09 18:19:12 +08:00
|
|
|
|
if err != nil {
|
2026-02-09 21:29:52 +08:00
|
|
|
|
return fmt.Errorf("初始化支付(v3)失败: %w", err)
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Code2Session 小程序登录
|
|
|
|
|
|
func Code2Session(code string) (openID, sessionKey, unionID string, err error) {
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
response, err := miniProgramApp.Auth.Session(ctx, code)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", "", "", fmt.Errorf("code2Session失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PowerWeChat v3 返回的是 *object.HashMap
|
|
|
|
|
|
if response.ErrCode != 0 {
|
|
|
|
|
|
return "", "", "", fmt.Errorf("微信返回错误: %d - %s", response.ErrCode, response.ErrMsg)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
openID = response.OpenID
|
|
|
|
|
|
sessionKey = response.SessionKey
|
|
|
|
|
|
unionID = response.UnionID
|
|
|
|
|
|
|
|
|
|
|
|
return openID, sessionKey, unionID, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetAccessToken 获取小程序 access_token(用于手机号解密、小程序码生成)
|
|
|
|
|
|
func GetAccessToken() (string, error) {
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
tokenResp, err := miniProgramApp.AccessToken.GetToken(ctx, false)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("获取access_token失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return tokenResp.AccessToken, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetPhoneNumber 获取用户手机号
|
|
|
|
|
|
func GetPhoneNumber(code string) (phoneNumber, countryCode string, err error) {
|
|
|
|
|
|
token, err := GetAccessToken()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
url := fmt.Sprintf("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s", token)
|
|
|
|
|
|
|
|
|
|
|
|
reqBody := map[string]string{"code": code}
|
|
|
|
|
|
jsonData, _ := json.Marshal(reqBody)
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := http.Post(url, "application/json", bytes.NewReader(jsonData))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", "", fmt.Errorf("请求微信接口失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
|
ErrCode int `json:"errcode"`
|
|
|
|
|
|
ErrMsg string `json:"errmsg"`
|
|
|
|
|
|
PhoneInfo struct {
|
|
|
|
|
|
PhoneNumber string `json:"phoneNumber"`
|
|
|
|
|
|
PurePhoneNumber string `json:"purePhoneNumber"`
|
|
|
|
|
|
CountryCode string `json:"countryCode"`
|
|
|
|
|
|
} `json:"phone_info"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
|
|
|
|
return "", "", fmt.Errorf("解析微信返回失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if result.ErrCode != 0 {
|
|
|
|
|
|
return "", "", fmt.Errorf("微信返回错误: %d - %s", result.ErrCode, result.ErrMsg)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
phoneNumber = result.PhoneInfo.PhoneNumber
|
|
|
|
|
|
if phoneNumber == "" {
|
|
|
|
|
|
phoneNumber = result.PhoneInfo.PurePhoneNumber
|
|
|
|
|
|
}
|
|
|
|
|
|
countryCode = result.PhoneInfo.CountryCode
|
|
|
|
|
|
if countryCode == "" {
|
|
|
|
|
|
countryCode = "86"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return phoneNumber, countryCode, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GenerateMiniProgramCode 生成小程序码
|
|
|
|
|
|
func GenerateMiniProgramCode(scene, page string, width int) ([]byte, error) {
|
|
|
|
|
|
token, err := GetAccessToken()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
url := fmt.Sprintf("https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s", token)
|
|
|
|
|
|
|
|
|
|
|
|
if width <= 0 || width > 430 {
|
|
|
|
|
|
width = 280
|
|
|
|
|
|
}
|
|
|
|
|
|
if page == "" {
|
|
|
|
|
|
page = "pages/index/index"
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(scene) > 32 {
|
|
|
|
|
|
scene = scene[:32]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
reqBody := map[string]interface{}{
|
|
|
|
|
|
"scene": scene,
|
|
|
|
|
|
"page": page,
|
|
|
|
|
|
"width": width,
|
|
|
|
|
|
"auto_color": false,
|
|
|
|
|
|
"line_color": map[string]int{"r": 0, "g": 206, "b": 209},
|
|
|
|
|
|
"is_hyaline": false,
|
|
|
|
|
|
"env_version": "trial", // 体验版,正式发布后改为 release
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
jsonData, _ := json.Marshal(reqBody)
|
|
|
|
|
|
resp, err := http.Post(url, "application/json", bytes.NewReader(jsonData))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("请求微信接口失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否是 JSON 错误返回
|
|
|
|
|
|
if resp.Header.Get("Content-Type") == "application/json" {
|
|
|
|
|
|
var errResult struct {
|
|
|
|
|
|
ErrCode int `json:"errcode"`
|
|
|
|
|
|
ErrMsg string `json:"errmsg"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := json.Unmarshal(body, &errResult); err == nil && errResult.ErrCode != 0 {
|
|
|
|
|
|
return nil, fmt.Errorf("生成小程序码失败: %d - %s", errResult.ErrCode, errResult.ErrMsg)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(body) < 1000 {
|
|
|
|
|
|
return nil, fmt.Errorf("返回的图片数据异常(太小)")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return body, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 21:29:52 +08:00
|
|
|
|
// GetPayNotifyURL 返回支付回调地址(与商户平台配置一致)
|
|
|
|
|
|
func GetPayNotifyURL() string {
|
|
|
|
|
|
if cfg != nil && cfg.WechatNotifyURL != "" {
|
|
|
|
|
|
return cfg.WechatNotifyURL
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
return "https://soul.quwanzhi.com/api/miniprogram/pay/notify"
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 21:29:52 +08:00
|
|
|
|
// PayJSAPIOrder 微信支付 v3 小程序 JSAPI 统一下单,返回 prepay_id
|
|
|
|
|
|
func PayJSAPIOrder(ctx context.Context, openID, orderSn string, amountCents int, description, attach string) (prepayID string, err error) {
|
|
|
|
|
|
if paymentApp == nil {
|
|
|
|
|
|
return "", fmt.Errorf("支付未初始化")
|
|
|
|
|
|
}
|
|
|
|
|
|
req := &request.RequestJSAPIPrepay{
|
|
|
|
|
|
PrepayBase: request.PrepayBase{
|
|
|
|
|
|
AppID: cfg.WechatAppID,
|
|
|
|
|
|
MchID: cfg.WechatMchID,
|
|
|
|
|
|
NotifyUrl: GetPayNotifyURL(),
|
|
|
|
|
|
},
|
|
|
|
|
|
Description: description,
|
|
|
|
|
|
OutTradeNo: orderSn,
|
|
|
|
|
|
Amount: &request.JSAPIAmount{
|
|
|
|
|
|
Total: amountCents,
|
|
|
|
|
|
Currency: "CNY",
|
|
|
|
|
|
},
|
|
|
|
|
|
Payer: &request.JSAPIPayer{OpenID: openID},
|
|
|
|
|
|
Attach: attach,
|
|
|
|
|
|
}
|
|
|
|
|
|
res, err := paymentApp.Order.JSAPITransaction(ctx, req)
|
2026-02-09 18:19:12 +08:00
|
|
|
|
if err != nil {
|
2026-02-09 21:29:52 +08:00
|
|
|
|
return "", err
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
if res == nil || res.PrepayID == "" {
|
|
|
|
|
|
return "", fmt.Errorf("微信返回 prepay_id 为空")
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
return res.PrepayID, nil
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 21:29:52 +08:00
|
|
|
|
// GetJSAPIPayParams 根据 prepay_id 生成小程序 wx.requestPayment 所需参数(v3 签名)
|
|
|
|
|
|
func GetJSAPIPayParams(prepayID string) (map[string]string, error) {
|
|
|
|
|
|
if paymentApp == nil {
|
|
|
|
|
|
return nil, fmt.Errorf("支付未初始化")
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
cfgMap, err := paymentApp.JSSDK.BridgeConfig(prepayID, false)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
out := make(map[string]string)
|
|
|
|
|
|
if m, ok := cfgMap.(*object.StringMap); ok && m != nil {
|
|
|
|
|
|
for k, v := range *m {
|
|
|
|
|
|
out[k] = v
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
if len(out) == 0 && cfgMap != nil {
|
|
|
|
|
|
if ms, ok := cfgMap.(map[string]interface{}); ok {
|
|
|
|
|
|
for k, v := range ms {
|
|
|
|
|
|
if s, ok := v.(string); ok {
|
|
|
|
|
|
out[k] = s
|
|
|
|
|
|
}
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
return out, nil
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 21:29:52 +08:00
|
|
|
|
// QueryOrderByOutTradeNo 根据商户订单号查询订单状态(v3)
|
|
|
|
|
|
func QueryOrderByOutTradeNo(ctx context.Context, outTradeNo string) (tradeState, transactionID string, totalFee int, err error) {
|
|
|
|
|
|
if paymentApp == nil {
|
|
|
|
|
|
return "", "", 0, fmt.Errorf("支付未初始化")
|
|
|
|
|
|
}
|
|
|
|
|
|
res, err := paymentApp.Order.QueryByOutTradeNumber(ctx, outTradeNo)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", "", 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if res == nil {
|
|
|
|
|
|
return "", "", 0, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
tradeState = res.TradeState
|
|
|
|
|
|
transactionID = res.TransactionID
|
|
|
|
|
|
if res.Amount != nil {
|
|
|
|
|
|
totalFee = int(res.Amount.Total)
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
return tradeState, transactionID, totalFee, nil
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 21:29:52 +08:00
|
|
|
|
// HandlePayNotify 处理 v3 支付回调:验签并解密后调用 handler,返回应写回微信的 HTTP 响应
|
|
|
|
|
|
// handler 参数:orderSn, transactionID, totalFee(分), attach(JSON), openID
|
|
|
|
|
|
func HandlePayNotify(req *http.Request, handler func(orderSn, transactionID string, totalFee int, attach, openID string) error) (*http.Response, error) {
|
|
|
|
|
|
if paymentApp == nil {
|
|
|
|
|
|
return nil, fmt.Errorf("支付未初始化")
|
|
|
|
|
|
}
|
|
|
|
|
|
return paymentApp.HandlePaidNotify(req, func(_ *notifyrequest.RequestNotify, transaction *models.Transaction, fail func(string)) interface{} {
|
|
|
|
|
|
if transaction == nil {
|
|
|
|
|
|
fail("transaction is nil")
|
|
|
|
|
|
return nil
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
orderSn := transaction.OutTradeNo
|
|
|
|
|
|
transactionID := transaction.TransactionID
|
|
|
|
|
|
totalFee := 0
|
|
|
|
|
|
if transaction.Amount != nil {
|
|
|
|
|
|
totalFee = int(transaction.Amount.Total)
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
attach := transaction.Attach
|
|
|
|
|
|
openID := ""
|
|
|
|
|
|
if transaction.Payer != nil {
|
|
|
|
|
|
openID = transaction.Payer.OpenID
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
if err := handler(orderSn, transactionID, totalFee, attach, openID); err != nil {
|
|
|
|
|
|
fail(err.Error())
|
|
|
|
|
|
return nil
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
2026-02-09 21:29:52 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
})
|
2026-02-09 18:19:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 21:48:26 +08:00
|
|
|
|
// HandleTransferNotify 处理商家转账结果回调:验签并解密后调用 handler,返回应写回微信的 HTTP 响应
|
|
|
|
|
|
// handler 参数:outBillNo(商户单号/即我们存的 detail_no)、transferBillNo、state(SUCCESS/FAIL/CANCELLED)、failReason
|
|
|
|
|
|
func HandleTransferNotify(req *http.Request, handler func(outBillNo, transferBillNo, state, failReason string) error) (*http.Response, error) {
|
|
|
|
|
|
if paymentApp == nil {
|
|
|
|
|
|
return nil, fmt.Errorf("支付/转账未初始化")
|
|
|
|
|
|
}
|
|
|
|
|
|
return paymentApp.HandleTransferBillsNotify(req, func(_ *notifyrequest.RequestNotify, bill *models.TransferBills, fail func(string)) interface{} {
|
|
|
|
|
|
if bill == nil {
|
|
|
|
|
|
fail("bill is nil")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
outBillNo := bill.OutBillNo
|
|
|
|
|
|
transferBillNo := bill.TransferBillNo
|
|
|
|
|
|
state := bill.State
|
|
|
|
|
|
failReason := bill.FailReason
|
|
|
|
|
|
if err := handler(outBillNo, transferBillNo, state, failReason); err != nil {
|
|
|
|
|
|
fail(err.Error())
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 18:19:12 +08:00
|
|
|
|
// GenerateOrderSn 生成订单号
|
|
|
|
|
|
func GenerateOrderSn() string {
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
timestamp := now.Format("20060102150405")
|
|
|
|
|
|
random := now.UnixNano() % 1000000
|
|
|
|
|
|
return fmt.Sprintf("MP%s%06d", timestamp, random)
|
|
|
|
|
|
}
|
2026-02-11 09:56:57 +08:00
|
|
|
|
|
|
|
|
|
|
// WithdrawSubscribeTemplateID 提现结果订阅消息模板 ID(与小程序 app.js withdrawSubscribeTmplId 一致)
|
|
|
|
|
|
const WithdrawSubscribeTemplateID = "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE"
|
|
|
|
|
|
|
|
|
|
|
|
// SendWithdrawSubscribeMessage 发起转账成功后发订阅消息(提现成功/待确认收款)
|
|
|
|
|
|
// openID 为接收人 openid,amount 为提现金额(元),success 为 true 表示打款已受理
|
|
|
|
|
|
func SendWithdrawSubscribeMessage(ctx context.Context, openID string, amount float64, success bool) error {
|
|
|
|
|
|
if miniProgramApp == nil {
|
|
|
|
|
|
return fmt.Errorf("小程序未初始化")
|
|
|
|
|
|
}
|
|
|
|
|
|
phrase := "提现成功"
|
|
|
|
|
|
thing8 := "微信打款成功,请点击查收"
|
|
|
|
|
|
if !success {
|
|
|
|
|
|
phrase = "提现失败"
|
|
|
|
|
|
thing8 = "请联系官方客服"
|
|
|
|
|
|
}
|
|
|
|
|
|
amountStr := fmt.Sprintf("¥%.2f", amount)
|
|
|
|
|
|
data := &power.HashMap{
|
|
|
|
|
|
"phrase4": object.HashMap{"value": phrase},
|
|
|
|
|
|
"amount5": object.HashMap{"value": amountStr},
|
|
|
|
|
|
"thing8": object.HashMap{"value": thing8},
|
|
|
|
|
|
}
|
|
|
|
|
|
state := "formal"
|
2026-02-11 12:05:54 +08:00
|
|
|
|
if cfg != nil && cfg.WechatMiniProgramState != "" {
|
|
|
|
|
|
state = cfg.WechatMiniProgramState
|
2026-02-11 09:56:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
_, err := miniProgramApp.SubscribeMessage.Send(ctx, &subrequest.RequestSubscribeMessageSend{
|
|
|
|
|
|
ToUser: openID,
|
|
|
|
|
|
TemplateID: WithdrawSubscribeTemplateID,
|
|
|
|
|
|
Page: "/pages/my/my",
|
|
|
|
|
|
MiniProgramState: state,
|
|
|
|
|
|
Lang: "zh_CN",
|
|
|
|
|
|
Data: data,
|
|
|
|
|
|
})
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|