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 }