bindbox-game/internal/api/wechat/miniprogram_login.go
邹方成 a4e532c6b6 feat(wechat): 重构小程序登录接口,实现自动生成用户信息和头像
- 移除微信用户信息解密相关代码,改为系统自动生成用户名和头像
- 添加用户信息存储功能,使用openID作为用户ID
- 集成govatar和namegenerator库生成用户头像和随机用户名
- 添加token生成功能,返回给客户端用于后续认证
- 更新swagger文档,反映接口变更
2025-10-19 00:34:02 +08:00

283 lines
7.9 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"
"image/png"
"net/http"
"os"
"time"
"github.com/goombaio/namegenerator"
"github.com/o1egl/govatar"
"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"
)
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
}
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
}
// 用户不存在,创建新用户
// 生成随机用户名
seed := time.Now().UTC().UnixNano()
nameGen := namegenerator.NewNameGenerator(seed)
username := nameGen.Generate()
// 生成头像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 生成头像
func (h *handler) generateAvatar(seed string) (string, error) {
// 根据seed生成随机性别
gender := govatar.MALE
if len(seed)%2 == 0 {
gender = govatar.FEMALE
}
// 生成头像文件名基于seed的hash
filename := fmt.Sprintf("%x", seed)
if len(filename) > 16 {
filename = filename[:16]
}
// 生成头像文件路径
avatarFilename := fmt.Sprintf("%s_%d.png", filename, gender)
avatarPath := fmt.Sprintf("static/avatars/%s", avatarFilename)
avatarURL := fmt.Sprintf("/static/avatars/%s", avatarFilename)
// 检查文件是否已存在
if _, err := os.Stat(avatarPath); err == nil {
// 文件已存在直接返回URL
return avatarURL, nil
}
// 生成头像图片
img, err := govatar.GenerateForUsername(gender, seed)
if err != nil {
return "", fmt.Errorf("生成头像失败: %v", err)
}
// 创建文件
file, err := os.Create(avatarPath)
if err != nil {
return "", fmt.Errorf("创建头像文件失败: %v", err)
}
defer file.Close()
// 保存PNG图片
err = png.Encode(file, img)
if err != nil {
return "", fmt.Errorf("保存头像文件失败: %v", err)
}
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
}