252 lines
7.5 KiB
Go
252 lines
7.5 KiB
Go
package wechat
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"time"
|
||
|
||
"mini-chat/internal/code"
|
||
"mini-chat/internal/pkg/core"
|
||
"mini-chat/internal/pkg/httpclient"
|
||
"mini-chat/internal/pkg/validation"
|
||
"mini-chat/internal/repository/mysql/model"
|
||
|
||
"github.com/DanPlayer/randomname"
|
||
)
|
||
|
||
type miniprogramLoginRequest struct {
|
||
AppID string `json:"app_id" binding:"required"` // 小程序AppID
|
||
JSCode string `json:"js_code" binding:"required"` // 登录时获取的code
|
||
}
|
||
|
||
type miniprogramLoginResponse struct {
|
||
Success bool `json:"success"`
|
||
Message string `json:"message"`
|
||
Token string `json:"token"` // 登录token
|
||
UserID string `json:"user_id"` // 用户ID
|
||
UserName string `json:"user_name"` // 用户昵称
|
||
Avatar string `json:"user_avatar"` // 用户头像
|
||
OpenID string `json:"openid,omitempty"` // 用户唯一标识
|
||
UnionID string `json:"unionid,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,系统自动生成用户名和头像
|
||
// @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
|
||
}
|
||
|
||
// 查询或创建用户
|
||
user, err := h.getOrCreateUser(ctx, req.AppID, openID, unionID)
|
||
if err != nil {
|
||
h.logger.Error(fmt.Sprintf("获取或创建用户失败: %s", err.Error()))
|
||
ctx.AbortWithError(core.Error(
|
||
http.StatusInternalServerError,
|
||
code.ServerError,
|
||
"用户创建失败",
|
||
))
|
||
return
|
||
}
|
||
|
||
// 生成登录token
|
||
token, err := h.generateToken(user.UserID, sessionKey)
|
||
if err != nil {
|
||
h.logger.Error(fmt.Sprintf("生成token失败: %s", err.Error()))
|
||
ctx.AbortWithError(core.Error(
|
||
http.StatusInternalServerError,
|
||
code.ServerError,
|
||
"token生成失败",
|
||
))
|
||
return
|
||
}
|
||
|
||
// 授权成功,主动发消息
|
||
createData := new(model.AppMessageLog)
|
||
createData.AppID = req.AppID
|
||
createData.SenderID = "888888"
|
||
createData.SenderName = "平台"
|
||
createData.Content = `{"messages":"您好,欢迎开启专属体验之旅!"}`
|
||
createData.ReceiverID = openID
|
||
createData.MsgType = 1
|
||
createData.SendTime = time.Now()
|
||
createData.CreatedAt = time.Now()
|
||
if err := h.writeDB.AppMessageLog.WithContext(ctx.RequestContext()).Create(createData); err != nil {
|
||
h.logger.Error(fmt.Sprintf("授权成功,主动发消息失败: %s", err.Error()))
|
||
}
|
||
|
||
res.Success = true
|
||
res.Message = "登录成功"
|
||
res.Token = token
|
||
res.UserID = user.UserID
|
||
res.UserName = user.UserName
|
||
res.Avatar = user.UserAvatar
|
||
res.OpenID = openID
|
||
res.UnionID = unionID
|
||
|
||
ctx.Payload(res)
|
||
}
|
||
}
|
||
|
||
// getOrCreateUser 获取或创建用户
|
||
func (h *handler) getOrCreateUser(ctx core.Context, appID, openID, unionID string) (*model.AppUser, error) {
|
||
// 先查询用户是否存在(使用openID作为用户ID)
|
||
user, err := h.readDB.AppUser.WithContext(ctx.RequestContext()).
|
||
Where(h.readDB.AppUser.AppID.Eq(appID)).
|
||
Where(h.readDB.AppUser.UserID.Eq(openID)).
|
||
First()
|
||
|
||
if err == nil {
|
||
// 用户已存在,直接返回
|
||
return user, nil
|
||
}
|
||
|
||
username := randomname.GenerateName()
|
||
|
||
// 生成头像URL
|
||
avatarURL, err := h.generateAvatar(openID)
|
||
if err != nil {
|
||
h.logger.Warn(fmt.Sprintf("生成头像失败: %s,使用默认头像", err.Error()))
|
||
avatarURL = "/static/avatars/default.svg"
|
||
}
|
||
|
||
// 创建新用户
|
||
newUser := &model.AppUser{
|
||
AppID: appID,
|
||
UserID: openID, // 使用openID作为用户ID
|
||
UserName: username,
|
||
UserMobile: "", // 暂时为空
|
||
UserAvatar: avatarURL,
|
||
CreatedAt: time.Now(),
|
||
}
|
||
|
||
err = h.writeDB.AppUser.WithContext(ctx.RequestContext()).Create(newUser)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("创建用户失败: %v", err)
|
||
}
|
||
|
||
return newUser, nil
|
||
}
|
||
|
||
// generateAvatar 生成头像URL
|
||
func (h *handler) generateAvatar(seed string) (string, error) {
|
||
// 使用dicebear API生成头像
|
||
// 直接使用seed作为头像种子,确保每个用户有不同的头像
|
||
avatarURL := fmt.Sprintf("https://api.dicebear.com/7.x/avataaars/svg?seed=%s", seed)
|
||
fmt.Printf("生成头像URL: %s\n", avatarURL)
|
||
return avatarURL, nil
|
||
}
|
||
|
||
// generateToken 生成登录token
|
||
func (h *handler) generateToken(userID, sessionKey string) (string, error) {
|
||
// 生成32字节的随机token
|
||
bytes := make([]byte, 32)
|
||
if _, err := rand.Read(bytes); err != nil {
|
||
return "", err
|
||
}
|
||
|
||
token := hex.EncodeToString(bytes)
|
||
|
||
// TODO: 这里应该将token保存到缓存或数据库中,并设置过期时间
|
||
// 可以结合sessionKey一起存储,用于后续的用户身份验证
|
||
|
||
return token, nil
|
||
}
|
||
|
||
// 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
|
||
}
|