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

393 lines
9.4 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 (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
"soul-api/internal/config"
"github.com/ArtisanCloud/PowerWeChat/v3/src/miniProgram"
"github.com/ArtisanCloud/PowerWeChat/v3/src/payment"
)
var (
miniProgramApp *miniProgram.MiniProgram
paymentApp *payment.Payment
cfg *config.Config
)
// Init 初始化微信客户端
func Init(c *config.Config) error {
cfg = c
// 初始化小程序
var err error
miniProgramApp, err = miniProgram.NewMiniProgram(&miniProgram.UserConfig{
AppID: cfg.WechatAppID,
Secret: cfg.WechatAppSecret,
HttpDebug: cfg.Mode == "debug",
})
if err != nil {
return fmt.Errorf("初始化小程序失败: %w", err)
}
// 初始化支付v2
paymentApp, err = payment.NewPayment(&payment.UserConfig{
AppID: cfg.WechatAppID,
MchID: cfg.WechatMchID,
Key: cfg.WechatMchKey,
HttpDebug: cfg.Mode == "debug",
})
if err != nil {
return fmt.Errorf("初始化支付失败: %w", err)
}
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
}
// PayV2UnifiedOrder 微信支付 v2 统一下单
func PayV2UnifiedOrder(params map[string]string) (map[string]string, error) {
// 添加必要参数
params["appid"] = cfg.WechatAppID
params["mch_id"] = cfg.WechatMchID
params["nonce_str"] = generateNonceStr()
params["sign_type"] = "MD5"
// 生成签名
params["sign"] = generateSign(params, cfg.WechatMchKey)
// 转换为 XML
xmlData := mapToXML(params)
// 发送请求
resp, err := http.Post("https://api.mch.weixin.qq.com/pay/unifiedorder", "application/xml", bytes.NewReader([]byte(xmlData)))
if err != nil {
return nil, fmt.Errorf("请求统一下单接口失败: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
result := xmlToMap(string(body))
if result["return_code"] != "SUCCESS" {
return nil, fmt.Errorf("统一下单失败: %s", result["return_msg"])
}
if result["result_code"] != "SUCCESS" {
return nil, fmt.Errorf("下单失败: %s", result["err_code_des"])
}
return result, nil
}
// PayV2OrderQuery 微信支付 v2 订单查询
func PayV2OrderQuery(outTradeNo string) (map[string]string, error) {
params := map[string]string{
"appid": cfg.WechatAppID,
"mch_id": cfg.WechatMchID,
"out_trade_no": outTradeNo,
"nonce_str": generateNonceStr(),
}
params["sign"] = generateSign(params, cfg.WechatMchKey)
xmlData := mapToXML(params)
resp, err := http.Post("https://api.mch.weixin.qq.com/pay/orderquery", "application/xml", bytes.NewReader([]byte(xmlData)))
if err != nil {
return nil, fmt.Errorf("请求订单查询接口失败: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
result := xmlToMap(string(body))
return result, nil
}
// VerifyPayNotify 验证支付回调签名
func VerifyPayNotify(data map[string]string) bool {
receivedSign := data["sign"]
if receivedSign == "" {
return false
}
delete(data, "sign")
calculatedSign := generateSign(data, cfg.WechatMchKey)
return receivedSign == calculatedSign
}
// GenerateJSAPIPayParams 生成小程序支付参数
func GenerateJSAPIPayParams(prepayID string) map[string]string {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
nonceStr := generateNonceStr()
params := map[string]string{
"appId": cfg.WechatAppID,
"timeStamp": timestamp,
"nonceStr": nonceStr,
"package": fmt.Sprintf("prepay_id=%s", prepayID),
"signType": "MD5",
}
params["paySign"] = generateSign(params, cfg.WechatMchKey)
return params
}
// === 辅助函数 ===
func generateNonceStr() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
func generateSign(params map[string]string, key string) string {
// 按字典序排序
var keys []string
for k := range params {
if k != "sign" && params[k] != "" {
keys = append(keys, k)
}
}
// 简单冒泡排序
for i := 0; i < len(keys); i++ {
for j := i + 1; j < len(keys); j++ {
if keys[i] > keys[j] {
keys[i], keys[j] = keys[j], keys[i]
}
}
}
// 拼接字符串
var signStr string
for _, k := range keys {
signStr += fmt.Sprintf("%s=%s&", k, params[k])
}
signStr += fmt.Sprintf("key=%s", key)
// MD5
hash := md5.Sum([]byte(signStr))
return fmt.Sprintf("%X", hash) // 大写
}
func mapToXML(data map[string]string) string {
xml := "<xml>"
for k, v := range data {
xml += fmt.Sprintf("<%s><![CDATA[%s]]></%s>", k, v, k)
}
xml += "</xml>"
return xml
}
func xmlToMap(xmlStr string) map[string]string {
result := make(map[string]string)
// 简单的 XML 解析(仅支持 <key><![CDATA[value]]></key> 和 <key>value</key> 格式)
var key, value string
inCDATA := false
inTag := false
isClosing := false
for i := 0; i < len(xmlStr); i++ {
ch := xmlStr[i]
if ch == '<' {
if i+1 < len(xmlStr) && xmlStr[i+1] == '/' {
isClosing = true
i++ // skip '/'
} else if i+8 < len(xmlStr) && xmlStr[i:i+9] == "<![CDATA[" {
inCDATA = true
i += 8 // skip "![CDATA["
continue
}
inTag = true
key = ""
continue
}
if ch == '>' {
inTag = false
if isClosing {
if key != "" && key != "xml" {
result[key] = value
}
key = ""
value = ""
isClosing = false
}
continue
}
if inCDATA && i+2 < len(xmlStr) && xmlStr[i:i+3] == "]]>" {
inCDATA = false
i += 2
continue
}
if inTag {
key += string(ch)
} else if !isClosing {
value += string(ch)
}
}
return result
}
// XMLToMap 导出供外部使用
func XMLToMap(xmlStr string) map[string]string {
return xmlToMap(xmlStr)
}
// GenerateOrderSn 生成订单号
func GenerateOrderSn() string {
now := time.Now()
timestamp := now.Format("20060102150405")
random := now.UnixNano() % 1000000
return fmt.Sprintf("MP%s%06d", timestamp, random)
}