package controller import ( "context" "net/http" "strconv" "dubbo.apache.org/dubbo-go/v3/client" "dubbo.apache.org/dubbo-go/v3/common/constant" "github.com/gin-gonic/gin" "github.com/topfans/backend/gateway/dto" "github.com/topfans/backend/gateway/pkg/response" "github.com/topfans/backend/gateway/pkg/starcache" "github.com/topfans/backend/pkg/logger" "google.golang.org/grpc/codes" pb "github.com/topfans/backend/pkg/proto/user" "go.uber.org/zap" ) // AuthController 认证控制器 type AuthController struct { userServiceClient pb.UserSocialService starCache *starcache.Cache } // pbError 用于包装 proto 错误消息 type pbError struct { message string } func (e *pbError) Error() string { return e.message } // NewAuthController 创建认证控制器 // // starCache 用于 Register/Login 两个公开入口的 star 解析: // 取代原先每次都直接 RPC GetFanIdentities 拉一遍可选身份列表。 func NewAuthController(dubboClient *client.Client, starCache *starcache.Cache) (*AuthController, error) { svc, err := pb.NewUserSocialService(dubboClient) if err != nil { return nil, err } return &AuthController{ userServiceClient: svc, starCache: starCache, }, nil } // findStar 解析 starID 对应的 *pb.Star。失败仅为 warn,不阻断主流程 // (DTO 转换对 nil star 有防御,Register/Login 仍能成功)。 func (ctrl *AuthController) findStar(ctx context.Context, starID int64) *pb.Star { star, err := ctrl.starCache.GetStar(ctx, starID) if err != nil { logger.Logger.Warn("GetStar cache miss+RPC failed, continuing with nil star", zap.Int64("star_id", starID), zap.Error(err), ) return nil } return star } // Register 用户注册 // @Summary 用户注册 // @Description 用户注册接口,需要提供手机号、密码、选择明星身份 // @Tags auth // @Accept json // @Produce json // @Param request body pb.RegisterRequest true "注册请求" // @Success 200 {object} response.Response{data=dto.RegisterResponseDTO} // @Router /api/v1/auth/register [post] func (ctrl *AuthController) Register(c *gin.Context) { var req pb.RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { logger.Logger.Warn("Invalid register request", zap.Error(err)) response.BadRequest(c, "请求参数错误") return } logger.Logger.Info("Register request received", zap.String("mobile", req.Mobile), zap.Int64("star_id", req.StarId), ) // 调用 Dubbo 服务(无需 Attachments) ctx := context.Background() resp, err := ctrl.userServiceClient.Register(ctx, &req) if err != nil { logger.Logger.Error("Register failed", zap.Error(err)) response.HandleError(c, err) return } // 检查业务错误 if resp.Base != nil && resp.Base.Code != uint32(codes.OK) { response.HandleError(c, &pbError{message: resp.Base.Message}) return } star := ctrl.findStar(ctx, req.StarId) logger.Logger.Info("Register successful", zap.Int64("user_id", resp.User.Id), ) // 转换为 DTO 并返回 data := dto.ToRegisterResponseDTO( resp.AccessToken, resp.ExpiresIn, resp.User, resp.FanProfile, star, ) response.Success(c, data) } // Login 用户登录 // @Summary 用户登录 // @Description 用户登录接口,需要提供手机号和密码 // @Tags auth // @Accept json // @Produce json // @Param request body pb.LoginRequest true "登录请求" // @Success 200 {object} response.Response{data=dto.LoginResponseDTO} // @Router /api/v1/auth/login [post] func (ctrl *AuthController) Login(c *gin.Context) { var req pb.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { logger.Logger.Warn("Invalid login request", zap.Error(err)) response.BadRequest(c, "请求参数错误") return } logger.Logger.Info("Login request received", zap.String("mobile", req.Mobile), ) // 调用 Dubbo 服务(无需 Attachments) ctx := context.Background() resp, err := ctrl.userServiceClient.Login(ctx, &req) if err != nil { logger.Logger.Error("Login failed", zap.Error(err)) response.HandleError(c, err) return } // 检查业务错误 if resp.Base != nil && resp.Base.Code != uint32(codes.OK) { response.HandleError(c, &pbError{message: resp.Base.Message}) return } star := ctrl.findStar(ctx, resp.FanProfile.StarId) logger.Logger.Info("Login successful", zap.Int64("user_id", resp.User.Id), ) // 转换为 DTO 并返回 data := dto.ToLoginResponseDTO( resp.AccessToken, resp.ExpiresIn, "", // refresh_token 暂时为空 resp.User, resp.FanProfile, star, ) response.Success(c, data) } // RefreshToken 刷新 Token // @Summary 刷新访问令牌 // @Description 使用当前访问令牌刷新获取新的访问令牌 // @Tags auth // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response // @Router /api/v1/auth/refresh [post] func (ctrl *AuthController) RefreshToken(c *gin.Context) { // 从认证中间件获取用户信息 userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{ "code": "UNAUTHORIZED", "message": "user not authenticated", }) return } starID, _ := c.Get("star_id") logger.Logger.Info("RefreshToken request received", zap.Any("user_id", userID), zap.Any("star_id", starID), ) // 创建带 Attachments 的 context // 注意:Dubbo Attachments 的值必须是 string 或 []string ctx := context.Background() ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{ "user_id": strconv.FormatInt(userID.(int64), 10), "star_id": strconv.FormatInt(starID.(int64), 10), }) // 调用 Dubbo 服务 resp, err := ctrl.userServiceClient.RefreshToken(ctx, &pb.RefreshTokenRequest{}) if err != nil { logger.Logger.Error("RefreshToken failed", zap.Error(err)) response.InternalError(c, "刷新令牌失败") return } logger.Logger.Info("RefreshToken successful", zap.Any("user_id", userID), ) c.JSON(http.StatusOK, resp) } // Logout 用户登出 // @Summary 用户登出 // @Description 使用户访问令牌失效 // @Tags auth // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} response.Response // @Router /api/v1/auth/logout [post] func (ctrl *AuthController) Logout(c *gin.Context) { // 从认证中间件获取用户信息 userID, exists := c.Get("user_id") if !exists { response.Unauthorized(c, "请先登录") return } logger.Logger.Info("Logout request received", zap.Any("user_id", userID), ) // 创建带 Attachments 的 context // 注意:Dubbo Attachments 的值必须是 string 或 []string ctx := context.Background() ctx = context.WithValue(ctx, constant.AttachmentKey, map[string]interface{}{ "user_id": strconv.FormatInt(userID.(int64), 10), }) // 调用 Dubbo 服务 _, err := ctrl.userServiceClient.Logout(ctx, &pb.LogoutRequest{}) if err != nil { logger.Logger.Error("Logout failed", zap.Error(err)) response.HandleError(c, err) return } logger.Logger.Info("Logout successful", zap.Any("user_id", userID), ) // 返回空 data response.Success(c, gin.H{}) } // ValidateToken 验证 Token(用于客户端检查 Token 是否有效) // @Summary 验证访问令牌 // @Description 验证访问令牌的有效性 // @Tags auth // @Accept json // @Produce json // @Security BearerAuth // @Param request body pb.ValidateTokenRequest true "验证请求" // @Success 200 {object} response.Response // @Router /api/v1/auth/validate [post] // // ★ 本接口已被 AuthMiddleware 保护(鉴权边界审计 §四 P2): // 路由从公开 /auth 组移到 authProtected(router.go:179-191), // 调用方必须持有有效、未过期、未在黑名单的 JWT。 // 控制器内部无需再校验 Authorization 头,AuthMiddleware 已做。 func (ctrl *AuthController) ValidateToken(c *gin.Context) { var req pb.ValidateTokenRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "请求参数错误") return } // 调用 Dubbo 服务 ctx := context.Background() resp, err := ctrl.userServiceClient.ValidateToken(ctx, &req) if err != nil { logger.Logger.Error("ValidateToken failed", zap.Error(err)) response.InternalError(c, "令牌校验失败") return } c.JSON(http.StatusOK, resp) } // CheckNickname 检查昵称是否已被注册 // @Summary 检查昵称是否被注册 // @Description 检查指定昵称是否已被他人使用 // @Tags auth // @Accept json // @Produce json // @Param request body pb.CheckNicknameRequest true "检查昵称请求" // @Success 200 {object} response.Response{data=pb.CheckNicknameResponse} // @Router /api/v1/auth/check-nickname [post] func (ctrl *AuthController) CheckNickname(c *gin.Context) { var req pb.CheckNicknameRequest if err := c.ShouldBindJSON(&req); err != nil { logger.Logger.Warn("Invalid check nickname request", zap.Error(err)) response.BadRequest(c, "请求参数错误") return } // 校验 nickname 不能为空 if req.Nickname == "" { response.BadRequest(c, "昵称不能为空") return } logger.Logger.Info("CheckNickname request received", zap.String("nickname", req.Nickname), ) // 调用 Dubbo 服务 ctx := context.Background() resp, err := ctrl.userServiceClient.CheckNickname(ctx, &req) if err != nil { logger.Logger.Error("CheckNickname failed", zap.Error(err)) response.HandleError(c, err) return } // 检查业务错误 if resp.Base != nil && resp.Base.Code != uint32(codes.OK) { response.HandleError(c, &pbError{message: resp.Base.Message}) return } logger.Logger.Info("CheckNickname successful", zap.String("nickname", req.Nickname), zap.Bool("exists", resp.Exists), ) response.Success(c, gin.H{ "exists": resp.Exists, }) } // SendCode 发送验证码 // @Summary 发送验证码 // @Description 发送手机验证码,用于注册或重置密码 // @Tags auth // @Accept json // @Produce json // @Param request body dto.SendCodeRequest true "发送验证码请求" // @Success 200 {object} response.Response{data=dto.SendCodeResponse} // @Router /api/v1/auth/send-code [post] func (ctrl *AuthController) SendCode(c *gin.Context) { var req dto.SendCodeRequest if err := c.ShouldBindJSON(&req); err != nil { logger.Logger.Warn("Invalid send code request", zap.Error(err)) response.BadRequest(c, "参数错误") return } logger.Logger.Info("SendCode request received", zap.String("mobile", req.Mobile), zap.String("scene", req.Scene), ) // 调用 Dubbo 服务 ctx := context.Background() resp, err := ctrl.userServiceClient.SendCode(ctx, &pb.SendCodeRequest{ Mobile: req.Mobile, Scene: req.Scene, }) if err != nil { logger.Logger.Error("SendCode failed", zap.Error(err)) response.HandleError(c, err) return } // 检查业务错误 if resp.Base != nil && resp.Base.Code != uint32(codes.OK) { response.HandleError(c, &pbError{message: resp.Base.Message}) return } logger.Logger.Info("SendCode successful", zap.String("mobile", req.Mobile), ) response.Success(c, gin.H{ "expires_in": resp.ExpiresIn, }) } // VerifyCode 验证验证码 // @Summary 验证验证码 // @Description 验证手机验证码,验证成功后返回 verify_token // @Tags auth // @Accept json // @Produce json // @Param request body dto.VerifyCodeRequest true "验证验证码请求" // @Success 200 {object} response.Response{data=dto.VerifyCodeResponse} // @Router /api/v1/auth/verify-code [post] func (ctrl *AuthController) VerifyCode(c *gin.Context) { var req dto.VerifyCodeRequest if err := c.ShouldBindJSON(&req); err != nil { logger.Logger.Warn("Invalid verify code request", zap.Error(err)) response.BadRequest(c, "参数错误") return } logger.Logger.Info("VerifyCode request received", zap.String("mobile", req.Mobile), zap.String("scene", req.Scene), ) // 调用 Dubbo 服务 ctx := context.Background() resp, err := ctrl.userServiceClient.VerifyCode(ctx, &pb.VerifyCodeRequest{ Mobile: req.Mobile, Code: req.Code, Scene: req.Scene, }) if err != nil { logger.Logger.Error("VerifyCode failed", zap.Error(err)) response.HandleError(c, err) return } // 检查业务错误 if resp.Base != nil && resp.Base.Code != uint32(codes.OK) { response.HandleError(c, &pbError{message: resp.Base.Message}) return } logger.Logger.Info("VerifyCode successful", zap.String("mobile", req.Mobile), ) response.Success(c, gin.H{ "verified": resp.Verified, "verify_token": resp.VerifyToken, "expires_in": resp.ExpiresIn, }) } // ResetPassword 匿名重置密码(忘记密码场景) // @Summary 匿名重置密码 // @Description 通过手机号+短信验证码(scene=password)+新密码重置密码,无需登录态 // @Tags auth // @Accept json // @Produce json // @Param request body dto.ResetPasswordRequest true "重置密码请求" // @Success 200 {object} response.Response // @Router /api/v1/auth/reset-password [post] func (ctrl *AuthController) ResetPassword(c *gin.Context) { var req dto.ResetPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { logger.Logger.Warn("Invalid reset password request", zap.Error(err)) response.BadRequest(c, "参数错误") return } logger.Logger.Info("ResetPassword request received", zap.String("mobile", req.Mobile), ) // 调用 Dubbo 服务 ctx := context.Background() resp, err := ctrl.userServiceClient.ResetPassword(ctx, &pb.ResetPasswordRequest{ Mobile: req.Mobile, NewPassword: req.NewPassword, VerifyToken: req.VerifyToken, }) if err != nil { logger.Logger.Error("ResetPassword failed", zap.Error(err)) response.HandleError(c, err) return } // 检查业务错误 if resp.Base != nil && resp.Base.Code != uint32(codes.OK) { response.HandleError(c, &pbError{message: resp.Base.Message}) return } logger.Logger.Info("ResetPassword successful", zap.String("mobile", req.Mobile), ) response.Success(c, gin.H{}) } // CheckMobile 检查手机号是否已被注册 // @Summary 检查手机号是否被注册 // @Description 检查指定手机号是否已被他人使用 // @Tags auth // @Accept json // @Produce json // @Param request body pb.CheckMobileRequest true "检查手机号请求" // @Success 200 {object} response.Response{data=pb.CheckMobileResponse} // @Router /api/v1/auth/check-mobile [post] func (ctrl *AuthController) CheckMobile(c *gin.Context) { var req pb.CheckMobileRequest if err := c.ShouldBindJSON(&req); err != nil { logger.Logger.Warn("Invalid check mobile request", zap.Error(err)) response.BadRequest(c, "请求参数错误") return } // 校验 mobile 不能为空 if req.Mobile == "" { response.BadRequest(c, "手机号不能为空") return } logger.Logger.Info("CheckMobile request received", zap.String("mobile", req.Mobile), ) // 调用 Dubbo 服务 ctx := context.Background() resp, err := ctrl.userServiceClient.CheckMobile(ctx, &req) if err != nil { logger.Logger.Error("CheckMobile failed", zap.Error(err)) response.HandleError(c, err) return } // 检查业务错误 if resp.Base != nil && resp.Base.Code != uint32(codes.OK) { response.HandleError(c, &pbError{message: resp.Base.Message}) return } logger.Logger.Info("CheckMobile successful", zap.String("mobile", req.Mobile), zap.Bool("exists", resp.Exists), ) response.Success(c, gin.H{ "exists": resp.Exists, }) }