173 lines
5.6 KiB
Go
173 lines
5.6 KiB
Go
package wechat
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
|
||
"mini-chat/internal/code"
|
||
"mini-chat/internal/pkg/core"
|
||
"mini-chat/internal/pkg/httpclient"
|
||
"mini-chat/internal/pkg/validation"
|
||
"mini-chat/internal/pkg/wechat"
|
||
)
|
||
|
||
type miniprogramLoginRequest struct {
|
||
AppID string `json:"app_id" binding:"required"` // 小程序AppID
|
||
JSCode string `json:"js_code" binding:"required"` // 登录时获取的code
|
||
EncryptedData string `json:"encrypted_data,omitempty"` // 加密数据(可选)
|
||
IV string `json:"iv,omitempty"` // 初始向量(可选)
|
||
RawData string `json:"raw_data,omitempty"` // 原始数据(可选,用于签名验证)
|
||
Signature string `json:"signature,omitempty"` // 签名(可选,用于验证数据完整性)
|
||
}
|
||
|
||
type miniprogramLoginResponse struct {
|
||
Success bool `json:"success"`
|
||
Message string `json:"message"`
|
||
OpenID string `json:"openid,omitempty"` // 用户唯一标识
|
||
SessionKey string `json:"session_key,omitempty"` // 会话密钥
|
||
UnionID string `json:"unionid,omitempty"` // 用户在开放平台的唯一标识符
|
||
DecryptedData *wechat.DecryptedUserInfo `json:"decrypted_data,omitempty"` // 解密后的用户信息(可选)
|
||
}
|
||
|
||
// Code2SessionResponse 微信code2Session接口响应
|
||
type Code2SessionResponse struct {
|
||
OpenID string `json:"openid"` // 用户唯一标识
|
||
SessionKey string `json:"session_key"` // 会话密钥
|
||
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符
|
||
ErrCode int `json:"errcode"` // 错误码
|
||
ErrMsg string `json:"errmsg"` // 错误信息
|
||
}
|
||
|
||
// MiniprogramLogin 小程序登录
|
||
// @Summary 小程序登录
|
||
// @Description 通过AppID和code获取用户的openid和session_key
|
||
// @Tags 微信
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Param request body miniprogramLoginRequest true "请求参数"
|
||
// @Success 200 {object} miniprogramLoginResponse
|
||
// @Failure 400 {object} code.Failure
|
||
// @Failure 500 {object} code.Failure
|
||
// @Router /api/wechat/miniprogram/login [post]
|
||
func (h *handler) MiniprogramLogin() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
req := new(miniprogramLoginRequest)
|
||
res := new(miniprogramLoginResponse)
|
||
|
||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||
ctx.AbortWithError(core.Error(
|
||
http.StatusBadRequest,
|
||
code.ParamBindError,
|
||
validation.Error(err),
|
||
))
|
||
return
|
||
}
|
||
|
||
// 根据AppID查询小程序信息获取AppSecret
|
||
miniProgram, err := h.readDB.MiniProgram.WithContext(ctx.RequestContext()).
|
||
Where(h.readDB.MiniProgram.AppID.Eq(req.AppID)).
|
||
First()
|
||
if err != nil {
|
||
h.logger.Error(fmt.Sprintf("查询小程序信息失败: %s", err.Error()))
|
||
ctx.AbortWithError(core.Error(
|
||
http.StatusBadRequest,
|
||
code.ServerError,
|
||
"小程序不存在或查询失败",
|
||
))
|
||
return
|
||
}
|
||
|
||
// 调用微信code2Session接口
|
||
openID, sessionKey, unionID, err := h.callCode2Session(ctx, req.AppID, miniProgram.AppSecret, req.JSCode)
|
||
if err != nil {
|
||
h.logger.Error(fmt.Sprintf("调用微信code2Session接口失败: %s", err.Error()))
|
||
ctx.AbortWithError(core.Error(
|
||
http.StatusInternalServerError,
|
||
code.ServerError,
|
||
err.Error(),
|
||
))
|
||
return
|
||
}
|
||
|
||
res.Success = true
|
||
res.Message = "登录成功"
|
||
res.OpenID = openID
|
||
res.SessionKey = sessionKey
|
||
res.UnionID = unionID
|
||
|
||
// 如果提供了加密数据,则进行解密
|
||
if req.EncryptedData != "" && req.IV != "" {
|
||
// 如果提供了签名验证数据,先验证签名
|
||
if req.RawData != "" && req.Signature != "" {
|
||
if !wechat.VerifySignature(req.RawData, req.Signature, sessionKey) {
|
||
h.logger.Warn("数据签名验证失败")
|
||
ctx.AbortWithError(core.Error(
|
||
http.StatusBadRequest,
|
||
code.ParamBindError,
|
||
"数据签名验证失败",
|
||
))
|
||
return
|
||
}
|
||
}
|
||
|
||
// 解密用户数据
|
||
decryptedUserInfo, err := wechat.DecryptUserInfo(sessionKey, req.EncryptedData, req.IV)
|
||
if err != nil {
|
||
h.logger.Error(fmt.Sprintf("解密用户数据失败: %s", err.Error()))
|
||
ctx.AbortWithError(core.Error(
|
||
http.StatusBadRequest,
|
||
code.ServerError,
|
||
"解密用户数据失败",
|
||
))
|
||
return
|
||
}
|
||
|
||
res.DecryptedData = decryptedUserInfo
|
||
}
|
||
|
||
ctx.Payload(res)
|
||
}
|
||
}
|
||
|
||
// callCode2Session 调用微信code2Session接口
|
||
func (h *handler) callCode2Session(ctx core.Context, appID, appSecret, jsCode string) (string, string, string, error) {
|
||
// 构建请求URL
|
||
url := "https://api.weixin.qq.com/sns/jscode2session"
|
||
|
||
// 发送HTTP请求
|
||
client := httpclient.GetHttpClientWithContext(ctx.RequestContext())
|
||
resp, err := client.R().
|
||
SetQueryParams(map[string]string{
|
||
"appid": appID,
|
||
"secret": appSecret,
|
||
"js_code": jsCode,
|
||
"grant_type": "authorization_code",
|
||
}).
|
||
Get(url)
|
||
|
||
if err != nil {
|
||
return "", "", "", fmt.Errorf("HTTP请求失败: %v", err)
|
||
}
|
||
|
||
if resp.StatusCode() != http.StatusOK {
|
||
return "", "", "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode())
|
||
}
|
||
|
||
var result Code2SessionResponse
|
||
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
||
return "", "", "", fmt.Errorf("解析响应失败: %v", err)
|
||
}
|
||
|
||
// 检查微信API返回的错误码
|
||
if result.ErrCode != 0 {
|
||
return "", "", "", fmt.Errorf("微信API错误: errcode=%d, errmsg=%s", result.ErrCode, result.ErrMsg)
|
||
}
|
||
|
||
if result.OpenID == "" {
|
||
return "", "", "", fmt.Errorf("获取到的openid为空")
|
||
}
|
||
|
||
return result.OpenID, result.SessionKey, result.UnionID, nil
|
||
}
|