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 := "" for k, v := range data { xml += fmt.Sprintf("<%s>", k, v, k) } xml += "" return xml } func xmlToMap(xmlStr string) map[string]string { result := make(map[string]string) // 简单的 XML 解析(仅支持 value 格式) 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] == "' { 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) }