- 移除微信用户信息解密相关代码,改为系统自动生成用户名和头像 - 添加用户信息存储功能,使用openID作为用户ID - 集成govatar和namegenerator库生成用户头像和随机用户名 - 添加token生成功能,返回给客户端用于后续认证 - 更新swagger文档,反映接口变更
283 lines
7.9 KiB
Go
283 lines
7.9 KiB
Go
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
|
||
}
|