bindbox-game/internal/api/wechat/miniprogram_login.go
2025-10-30 15:25:43 +08:00

252 lines
7.5 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 (
"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
}