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 } 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 }