- Add OPPO category/notify_level support (2024.11.20 regulation: category required when notify_level present) - Add Honor importance field (NORMAL=service/LOW=marketing) for notification classification - Add vivo category field (IM/ACCOUNT) for message scenario identification - Set Xiaomi channel/template defaults in code (system=153609, chat=153608, templates P10761/M10289) - Add classification option to JPush push payloads (0=operation, 1=system) - Update xiaomi template keywords mapping to match actual template placeholders - Add keyword search support for GetFollowers and GetFollowing endpoints - Bind new config fields to environment variables for OPPO/Honor/vivo
1013 lines
28 KiB
Go
1013 lines
28 KiB
Go
package handler
|
||
|
||
import (
|
||
"cmp"
|
||
"context"
|
||
"crypto/sha256"
|
||
"fmt"
|
||
"strconv"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"with_you/internal/dto"
|
||
"with_you/internal/middleware"
|
||
"with_you/internal/model"
|
||
"with_you/internal/pkg/response"
|
||
"with_you/internal/service"
|
||
)
|
||
|
||
const (
|
||
maxPageSize = 100
|
||
defaultPageSize = 20
|
||
)
|
||
|
||
func normalizePagination(page, pageSize int) (int, int) {
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > maxPageSize {
|
||
pageSize = defaultPageSize
|
||
}
|
||
return page, pageSize
|
||
}
|
||
|
||
// UserHandler 用户处理器
|
||
type UserHandler struct {
|
||
userService service.UserService
|
||
activityService service.UserActivityService
|
||
profileAuditService service.UserProfileAuditService
|
||
jwtService service.JWTService
|
||
logService *service.LogService
|
||
}
|
||
|
||
func NewUserHandler(userService service.UserService, activityService service.UserActivityService, logService *service.LogService) *UserHandler {
|
||
return &UserHandler{
|
||
userService: userService,
|
||
activityService: activityService,
|
||
logService: logService,
|
||
}
|
||
}
|
||
|
||
func (h *UserHandler) SetProfileAuditService(profileAuditService service.UserProfileAuditService) {
|
||
h.profileAuditService = profileAuditService
|
||
}
|
||
|
||
// SetJWTService 设置JWT服务
|
||
func (h *UserHandler) SetJWTService(jwtService service.JWTService) {
|
||
h.jwtService = jwtService
|
||
}
|
||
|
||
// SetActivityService 设置活跃统计服务
|
||
func (h *UserHandler) SetActivityService(activityService service.UserActivityService) {
|
||
h.activityService = activityService
|
||
}
|
||
|
||
// generateTokenID 生成Token ID
|
||
func generateTokenID(token string) string {
|
||
h := sha256.New()
|
||
h.Write([]byte(token))
|
||
return fmt.Sprintf("%x", h.Sum(nil))[:16]
|
||
}
|
||
|
||
// Register 用户注册
|
||
func (h *UserHandler) Register(c *gin.Context) {
|
||
type RegisterRequest struct {
|
||
Username string `json:"username" binding:"required"`
|
||
Email string `json:"email" binding:"required,email"`
|
||
Password string `json:"password" binding:"required,min=6"`
|
||
Nickname string `json:"nickname" binding:"required"`
|
||
Phone string `json:"phone"`
|
||
VerificationCode string `json:"verification_code" binding:"required"`
|
||
}
|
||
|
||
var req RegisterRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
user, err := h.userService.Register(c.Request.Context(), req.Username, req.Email, req.Password, req.Nickname, req.Phone, req.VerificationCode)
|
||
if err != nil {
|
||
response.HandleError(c, err, "failed to register")
|
||
return
|
||
}
|
||
|
||
// 生成Token
|
||
accessToken, _ := h.jwtService.GenerateAccessToken(user.ID, user.Username)
|
||
refreshToken, _ := h.jwtService.GenerateRefreshToken(user.ID, user.Username)
|
||
|
||
ip := c.ClientIP()
|
||
userAgent := c.GetHeader("User-Agent")
|
||
|
||
// 记录用户活跃
|
||
if h.activityService != nil {
|
||
go func() {
|
||
ctx := context.Background()
|
||
loginType := model.LoginTypeLogin
|
||
h.activityService.RecordUserActive(ctx, user.ID, loginType, ip, userAgent)
|
||
}()
|
||
}
|
||
|
||
// 记录注册后首次登录
|
||
if h.logService != nil {
|
||
go func() {
|
||
h.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||
UserID: user.ID,
|
||
UserName: user.Username,
|
||
NickName: user.Nickname,
|
||
LoginType: string(model.LoginTypePassword),
|
||
Event: string(model.LoginEventLogin),
|
||
IP: ip,
|
||
UserAgent: userAgent,
|
||
Result: string(model.LoginResultSuccess),
|
||
TokenID: generateTokenID(refreshToken),
|
||
})
|
||
}()
|
||
}
|
||
|
||
response.Success(c, gin.H{
|
||
"user": dto.ConvertUserToSelfResponse(user, 0),
|
||
"token": accessToken,
|
||
"refresh_token": refreshToken,
|
||
})
|
||
}
|
||
|
||
// Login 用户登录
|
||
func (h *UserHandler) Login(c *gin.Context) {
|
||
type LoginRequest struct {
|
||
Username string `json:"username"`
|
||
Account string `json:"account"`
|
||
Password string `json:"password" binding:"required"`
|
||
}
|
||
|
||
var req LoginRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
account := cmp.Or(req.Account, req.Username)
|
||
if account == "" {
|
||
response.BadRequest(c, "username or account is required")
|
||
return
|
||
}
|
||
|
||
user, err := h.userService.Login(c.Request.Context(), account, req.Password)
|
||
if err != nil {
|
||
middleware.RecordLoginFailure(c.ClientIP())
|
||
response.HandleError(c, err, "failed to login")
|
||
return
|
||
}
|
||
|
||
middleware.ResetLoginFailures(c.ClientIP())
|
||
|
||
// 生成Token
|
||
accessToken, _ := h.jwtService.GenerateAccessToken(user.ID, user.Username)
|
||
refreshToken, _ := h.jwtService.GenerateRefreshToken(user.ID, user.Username)
|
||
|
||
ip := c.ClientIP()
|
||
userAgent := c.GetHeader("User-Agent")
|
||
|
||
// 记录用户活跃
|
||
if h.activityService != nil {
|
||
go func() {
|
||
ctx := context.Background()
|
||
loginType := model.LoginTypeLogin
|
||
h.activityService.RecordUserActive(ctx, user.ID, loginType, ip, userAgent)
|
||
}()
|
||
}
|
||
|
||
// 更新登录日志(补充IP和UserAgent)
|
||
if h.logService != nil {
|
||
ip := c.ClientIP()
|
||
userAgent := c.GetHeader("User-Agent")
|
||
go func() {
|
||
h.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||
UserID: user.ID,
|
||
UserName: user.Username,
|
||
NickName: user.Nickname,
|
||
LoginType: string(model.LoginTypePassword),
|
||
Event: string(model.LoginEventLogin),
|
||
IP: ip,
|
||
UserAgent: userAgent,
|
||
Result: string(model.LoginResultSuccess),
|
||
})
|
||
}()
|
||
}
|
||
|
||
response.Success(c, gin.H{
|
||
"user": dto.ConvertUserToSelfResponse(user, int(user.PostsCount)),
|
||
"token": accessToken,
|
||
"refresh_token": refreshToken,
|
||
})
|
||
}
|
||
|
||
// SendRegisterCode 发送注册验证码
|
||
func (h *UserHandler) SendRegisterCode(c *gin.Context) {
|
||
type SendCodeRequest struct {
|
||
Email string `json:"email" binding:"required,email"`
|
||
}
|
||
|
||
var req SendCodeRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
clientIP := c.ClientIP()
|
||
if err := h.userService.SendRegisterCode(c.Request.Context(), req.Email, clientIP); err != nil {
|
||
response.HandleError(c, err, "failed to send register verification code")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
func (h *UserHandler) SendPasswordResetCode(c *gin.Context) {
|
||
type SendCodeRequest struct {
|
||
Email string `json:"email" binding:"required,email"`
|
||
}
|
||
|
||
var req SendCodeRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
clientIP := c.ClientIP()
|
||
if err := h.userService.SendPasswordResetCode(c.Request.Context(), req.Email, clientIP); err != nil {
|
||
response.HandleError(c, err, "failed to send reset verification code")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// ResetPassword 找回密码并重置
|
||
func (h *UserHandler) ResetPassword(c *gin.Context) {
|
||
type ResetPasswordRequest struct {
|
||
Email string `json:"email" binding:"required,email"`
|
||
VerificationCode string `json:"verification_code" binding:"required"`
|
||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||
}
|
||
|
||
var req ResetPasswordRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
if err := h.userService.ResetPasswordByEmail(c.Request.Context(), req.Email, req.VerificationCode, req.NewPassword); err != nil {
|
||
response.HandleError(c, err, "failed to reset password")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// GetCurrentUser 获取当前用户
|
||
func (h *UserHandler) GetCurrentUser(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
user, err := h.userService.GetUserByID(c.Request.Context(), userID)
|
||
if err != nil {
|
||
response.NotFound(c, "user not found")
|
||
return
|
||
}
|
||
|
||
postsCount, err := h.userService.GetUserPostCount(c.Request.Context(), userID)
|
||
if err != nil {
|
||
postsCount = int64(user.PostsCount)
|
||
}
|
||
|
||
response.Success(c, dto.ConvertUserToSelfResponse(user, int(postsCount)))
|
||
}
|
||
|
||
// GetUserByID 获取指定用户
|
||
func (h *UserHandler) GetUserByID(c *gin.Context) {
|
||
id := c.Param("id")
|
||
currentUserID := c.GetString("user_id")
|
||
|
||
// 获取用户信息,包含双向关注状态
|
||
user, isFollowing, isFollowingMe, err := h.userService.GetUserByIDWithMutualFollowStatus(c.Request.Context(), id, currentUserID)
|
||
if err != nil {
|
||
response.NotFound(c, "user not found")
|
||
return
|
||
}
|
||
|
||
// 实时计算帖子数量
|
||
postsCount, err := h.userService.GetUserPostCount(c.Request.Context(), id)
|
||
if err != nil {
|
||
// 如果获取失败,使用数据库中的值
|
||
postsCount = int64(user.PostsCount)
|
||
}
|
||
|
||
// 转换为响应格式,包含双向关注状态和实时计算的帖子数量
|
||
userResponse := dto.ConvertUserToResponse(user, dto.WithFollowing(isFollowing, isFollowingMe), dto.WithPostsCount(int(postsCount)))
|
||
|
||
response.Success(c, userResponse)
|
||
}
|
||
|
||
// UpdateUser 更新用户
|
||
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
type UpdateRequest struct {
|
||
Nickname string `json:"nickname"`
|
||
Bio string `json:"bio"`
|
||
Website string `json:"website"`
|
||
Location string `json:"location"`
|
||
Avatar string `json:"avatar"`
|
||
CoverURL string `json:"cover_url"`
|
||
Phone *string `json:"phone"`
|
||
Email *string `json:"email"`
|
||
}
|
||
|
||
var req UpdateRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
user, err := h.userService.GetUserByID(c.Request.Context(), userID)
|
||
if err != nil {
|
||
response.NotFound(c, "user not found")
|
||
return
|
||
}
|
||
|
||
if req.Nickname != "" {
|
||
user.Nickname = req.Nickname
|
||
}
|
||
if req.Website != "" {
|
||
user.Website = req.Website
|
||
}
|
||
if req.Location != "" {
|
||
user.Location = req.Location
|
||
}
|
||
if req.Phone != nil {
|
||
user.Phone = req.Phone
|
||
}
|
||
if req.Email != nil {
|
||
if user.Email == nil || *user.Email != *req.Email {
|
||
user.EmailVerified = false
|
||
}
|
||
user.Email = req.Email
|
||
}
|
||
|
||
avatarNeedsAudit := req.Avatar != "" && req.Avatar != user.Avatar && h.profileAuditService != nil
|
||
coverNeedsAudit := req.CoverURL != "" && req.CoverURL != user.CoverURL && h.profileAuditService != nil
|
||
bioNeedsAudit := req.Bio != "" && req.Bio != user.Bio && h.profileAuditService != nil
|
||
|
||
if avatarNeedsAudit {
|
||
if _, err := h.profileAuditService.SubmitAvatarAudit(c.Request.Context(), userID, req.Avatar); err != nil {
|
||
avatarNeedsAudit = false
|
||
user.Avatar = req.Avatar
|
||
}
|
||
} else if req.Avatar != "" {
|
||
user.Avatar = req.Avatar
|
||
}
|
||
|
||
if coverNeedsAudit {
|
||
if _, err := h.profileAuditService.SubmitCoverAudit(c.Request.Context(), userID, req.CoverURL); err != nil {
|
||
coverNeedsAudit = false
|
||
user.CoverURL = req.CoverURL
|
||
}
|
||
} else if req.CoverURL != "" {
|
||
user.CoverURL = req.CoverURL
|
||
}
|
||
|
||
if bioNeedsAudit {
|
||
if _, err := h.profileAuditService.SubmitBioAudit(c.Request.Context(), userID, req.Bio); err != nil {
|
||
bioNeedsAudit = false
|
||
user.Bio = req.Bio
|
||
}
|
||
} else if req.Bio != "" {
|
||
user.Bio = req.Bio
|
||
}
|
||
|
||
err = h.userService.UpdateUser(c.Request.Context(), user)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to update user")
|
||
return
|
||
}
|
||
|
||
postsCount, err := h.userService.GetUserPostCount(c.Request.Context(), userID)
|
||
if err != nil {
|
||
postsCount = int64(user.PostsCount)
|
||
}
|
||
|
||
resp := gin.H{"user": dto.ConvertUserToDetailResponseWithPostsCount(user, int(postsCount))}
|
||
if avatarNeedsAudit || coverNeedsAudit || bioNeedsAudit {
|
||
resp["audit_pending"] = true
|
||
}
|
||
response.Success(c, resp)
|
||
}
|
||
|
||
// SendEmailVerifyCode 发送当前用户邮箱验证码
|
||
func (h *UserHandler) SendEmailVerifyCode(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
type SendCodeRequest struct {
|
||
Email string `json:"email" binding:"required,email"`
|
||
}
|
||
var req SendCodeRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
if err := h.userService.SendCurrentUserEmailVerifyCode(c.Request.Context(), userID, req.Email, c.ClientIP()); err != nil {
|
||
response.HandleError(c, err, "failed to send email verify code")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// VerifyEmail 验证当前用户邮箱
|
||
func (h *UserHandler) VerifyEmail(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
type VerifyEmailRequest struct {
|
||
Email string `json:"email" binding:"required,email"`
|
||
VerificationCode string `json:"verification_code" binding:"required"`
|
||
}
|
||
var req VerifyEmailRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
if err := h.userService.VerifyCurrentUserEmail(c.Request.Context(), userID, req.Email, req.VerificationCode); err != nil {
|
||
response.HandleError(c, err, "failed to verify email")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// RefreshToken 刷新Token
|
||
func (h *UserHandler) RefreshToken(c *gin.Context) {
|
||
type RefreshRequest struct {
|
||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||
}
|
||
|
||
var req RefreshRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
// 解析 refresh token
|
||
claims, err := h.jwtService.ParseToken(req.RefreshToken)
|
||
if err != nil {
|
||
response.Unauthorized(c, "invalid refresh token")
|
||
return
|
||
}
|
||
|
||
// 获取用户信息
|
||
user, err := h.userService.GetUserByID(c.Request.Context(), claims.UserID)
|
||
if err != nil {
|
||
response.InternalServerError(c, "user not found")
|
||
return
|
||
}
|
||
|
||
// 生成新 token
|
||
accessToken, _ := h.jwtService.GenerateAccessToken(user.ID, user.Username)
|
||
refreshToken, _ := h.jwtService.GenerateRefreshToken(user.ID, user.Username)
|
||
|
||
ip := c.ClientIP()
|
||
userAgent := c.GetHeader("User-Agent")
|
||
|
||
// 记录用户活跃
|
||
if h.activityService != nil {
|
||
go func() {
|
||
ctx := context.Background()
|
||
loginType := model.LoginTypeRefresh
|
||
h.activityService.RecordUserActive(ctx, user.ID, loginType, ip, userAgent)
|
||
}()
|
||
}
|
||
|
||
// 记录Token刷新日志
|
||
if h.logService != nil {
|
||
go func() {
|
||
h.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||
UserID: user.ID,
|
||
UserName: user.Username,
|
||
NickName: user.Nickname,
|
||
LoginType: string(model.LoginTypePassword),
|
||
Event: string(model.LoginEventRefresh),
|
||
IP: ip,
|
||
UserAgent: userAgent,
|
||
Result: string(model.LoginResultSuccess),
|
||
TokenID: generateTokenID(req.RefreshToken),
|
||
})
|
||
}()
|
||
}
|
||
|
||
response.Success(c, gin.H{
|
||
"token": accessToken,
|
||
"refresh_token": refreshToken,
|
||
})
|
||
}
|
||
|
||
// FollowUser 关注用户
|
||
func (h *UserHandler) FollowUser(c *gin.Context) {
|
||
userID := c.Param("id")
|
||
currentUserID := c.GetString("user_id")
|
||
|
||
if userID == currentUserID {
|
||
response.BadRequest(c, "cannot follow yourself")
|
||
return
|
||
}
|
||
|
||
err := h.userService.FollowUser(c.Request.Context(), currentUserID, userID)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to follow user")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// UnfollowUser 取消关注用户
|
||
func (h *UserHandler) UnfollowUser(c *gin.Context) {
|
||
userID := c.Param("id")
|
||
currentUserID := c.GetString("user_id")
|
||
|
||
err := h.userService.UnfollowUser(c.Request.Context(), currentUserID, userID)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to unfollow user")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// BlockUser 拉黑用户
|
||
func (h *UserHandler) BlockUser(c *gin.Context) {
|
||
targetUserID := c.Param("id")
|
||
currentUserID := c.GetString("user_id")
|
||
|
||
if targetUserID == currentUserID {
|
||
response.BadRequest(c, "cannot block yourself")
|
||
return
|
||
}
|
||
|
||
err := h.userService.BlockUser(c.Request.Context(), currentUserID, targetUserID)
|
||
if err != nil {
|
||
response.HandleError(c, err, "failed to block user")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// UnblockUser 取消拉黑
|
||
func (h *UserHandler) UnblockUser(c *gin.Context) {
|
||
targetUserID := c.Param("id")
|
||
currentUserID := c.GetString("user_id")
|
||
|
||
if targetUserID == currentUserID {
|
||
response.BadRequest(c, "cannot unblock yourself")
|
||
return
|
||
}
|
||
|
||
err := h.userService.UnblockUser(c.Request.Context(), currentUserID, targetUserID)
|
||
if err != nil {
|
||
response.HandleError(c, err, "failed to unblock user")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// GetBlockedUsers 获取黑名单列表
|
||
func (h *UserHandler) GetBlockedUsers(c *gin.Context) {
|
||
currentUserID := c.GetString("user_id")
|
||
if currentUserID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||
page, pageSize = normalizePagination(page, pageSize)
|
||
|
||
users, total, err := h.userService.GetBlockedUsers(c.Request.Context(), currentUserID, page, pageSize)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to get blocked users")
|
||
return
|
||
}
|
||
|
||
userIDs := make([]string, len(users))
|
||
for i, u := range users {
|
||
userIDs[i] = u.ID
|
||
}
|
||
postsCountMap, _ := h.userService.GetUserPostCountBatch(c.Request.Context(), userIDs)
|
||
userResponses := dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, nil, postsCountMap)
|
||
response.Paginated(c, userResponses, total, page, pageSize)
|
||
}
|
||
|
||
// GetBlockStatus 获取拉黑状态
|
||
func (h *UserHandler) GetBlockStatus(c *gin.Context) {
|
||
targetUserID := c.Param("id")
|
||
currentUserID := c.GetString("user_id")
|
||
if currentUserID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
if targetUserID == "" {
|
||
response.BadRequest(c, "target user id is required")
|
||
return
|
||
}
|
||
|
||
isBlocked, err := h.userService.IsBlocked(c.Request.Context(), currentUserID, targetUserID)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to get block status")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"is_blocked": isBlocked})
|
||
}
|
||
|
||
// GetFollowingList 获取关注列表
|
||
func (h *UserHandler) GetFollowingList(c *gin.Context) {
|
||
userID := c.Param("id")
|
||
currentUserID := c.GetString("user_id")
|
||
|
||
canView, err := h.userService.CanViewFollowing(c.Request.Context(), currentUserID, userID)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to check privacy settings")
|
||
return
|
||
}
|
||
if !canView {
|
||
response.Forbidden(c, "该用户的关注列表不对外公开")
|
||
return
|
||
}
|
||
|
||
page := c.DefaultQuery("page", "1")
|
||
pageSize := c.DefaultQuery("page_size", "20")
|
||
keyword := c.Query("keyword")
|
||
|
||
users, err := h.userService.GetFollowingList(c.Request.Context(), userID, page, pageSize, keyword)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to get following list")
|
||
return
|
||
}
|
||
|
||
var userResponses []*dto.UserResponse
|
||
if len(users) > 0 {
|
||
userIDs := make([]string, len(users))
|
||
for i, u := range users {
|
||
userIDs[i] = u.ID
|
||
}
|
||
postsCountMap, _ := h.userService.GetUserPostCountBatch(c.Request.Context(), userIDs)
|
||
if currentUserID != "" && userID == currentUserID {
|
||
statusMap, _ := h.userService.GetMutualFollowStatus(c.Request.Context(), currentUserID, userIDs)
|
||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, statusMap, postsCountMap)
|
||
} else {
|
||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, nil, postsCountMap)
|
||
}
|
||
} else {
|
||
userResponses = dto.ConvertUsersToResponse(users)
|
||
}
|
||
|
||
response.Success(c, gin.H{
|
||
"list": userResponses,
|
||
})
|
||
}
|
||
|
||
// GetFollowersList 获取粉丝列表
|
||
func (h *UserHandler) GetFollowersList(c *gin.Context) {
|
||
userID := c.Param("id")
|
||
currentUserID := c.GetString("user_id")
|
||
|
||
canView, err := h.userService.CanViewFollowers(c.Request.Context(), currentUserID, userID)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to check privacy settings")
|
||
return
|
||
}
|
||
if !canView {
|
||
response.Forbidden(c, "该用户的粉丝列表不对外公开")
|
||
return
|
||
}
|
||
|
||
page := c.DefaultQuery("page", "1")
|
||
pageSize := c.DefaultQuery("page_size", "20")
|
||
keyword := c.Query("keyword")
|
||
|
||
users, err := h.userService.GetFollowersList(c.Request.Context(), userID, page, pageSize, keyword)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to get followers list")
|
||
return
|
||
}
|
||
|
||
var userResponses []*dto.UserResponse
|
||
if len(users) > 0 {
|
||
userIDs := make([]string, len(users))
|
||
for i, u := range users {
|
||
userIDs[i] = u.ID
|
||
}
|
||
postsCountMap, _ := h.userService.GetUserPostCountBatch(c.Request.Context(), userIDs)
|
||
if currentUserID != "" && userID == currentUserID {
|
||
statusMap, _ := h.userService.GetMutualFollowStatus(c.Request.Context(), currentUserID, userIDs)
|
||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, statusMap, postsCountMap)
|
||
} else {
|
||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, nil, postsCountMap)
|
||
}
|
||
} else {
|
||
userResponses = dto.ConvertUsersToResponse(users)
|
||
}
|
||
|
||
response.Success(c, gin.H{
|
||
"list": userResponses,
|
||
})
|
||
}
|
||
|
||
// CheckUsername 检查用户名是否可用
|
||
func (h *UserHandler) CheckUsername(c *gin.Context) {
|
||
username := c.Query("username")
|
||
if username == "" {
|
||
response.BadRequest(c, "username is required")
|
||
return
|
||
}
|
||
|
||
available, err := h.userService.CheckUsernameAvailable(c.Request.Context(), username)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to check username")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"available": available})
|
||
}
|
||
|
||
// ChangePassword 修改密码
|
||
func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||
currentUserID := c.GetString("user_id")
|
||
|
||
type ChangePasswordRequest struct {
|
||
OldPassword string `json:"old_password" binding:"required"`
|
||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||
VerificationCode string `json:"verification_code" binding:"required"`
|
||
}
|
||
|
||
var req ChangePasswordRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
err := h.userService.ChangePassword(c.Request.Context(), currentUserID, req.OldPassword, req.NewPassword, req.VerificationCode)
|
||
if err != nil {
|
||
response.HandleError(c, err, "failed to change password")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// SendChangePasswordCode 发送修改密码验证码
|
||
func (h *UserHandler) SendChangePasswordCode(c *gin.Context) {
|
||
currentUserID := c.GetString("user_id")
|
||
if currentUserID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
err := h.userService.SendChangePasswordCode(c.Request.Context(), currentUserID, c.ClientIP())
|
||
if err != nil {
|
||
response.HandleError(c, err, "failed to send change password code")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// Logout 用户登出
|
||
func (h *UserHandler) Logout(c *gin.Context) {
|
||
currentUserID := c.GetString("user_id")
|
||
if currentUserID == "" {
|
||
response.Unauthorized(c, "not logged in")
|
||
return
|
||
}
|
||
|
||
ip := c.ClientIP()
|
||
userAgent := c.GetHeader("User-Agent")
|
||
|
||
// 获取用户信息
|
||
user, err := h.userService.GetUserByID(c.Request.Context(), currentUserID)
|
||
if err != nil {
|
||
response.InternalServerError(c, "user not found")
|
||
return
|
||
}
|
||
|
||
// 记录登出日志
|
||
if h.logService != nil {
|
||
h.logService.LoginLog.RecordLogin(&model.LoginLog{
|
||
UserID: user.ID,
|
||
UserName: user.Username,
|
||
NickName: user.Nickname,
|
||
Event: string(model.LoginEventLogout),
|
||
IP: ip,
|
||
UserAgent: userAgent,
|
||
Result: string(model.LoginResultSuccess),
|
||
})
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// Search 搜索用户
|
||
func (h *UserHandler) Search(c *gin.Context) {
|
||
keyword := c.Query("keyword")
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||
page, pageSize = normalizePagination(page, pageSize)
|
||
|
||
users, total, err := h.userService.Search(c.Request.Context(), keyword, page, pageSize)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to search users")
|
||
return
|
||
}
|
||
|
||
// 获取实时计算的帖子数量
|
||
var userResponses []*dto.UserResponse
|
||
if len(users) > 0 {
|
||
userIDs := make([]string, len(users))
|
||
for i, u := range users {
|
||
userIDs[i] = u.ID
|
||
}
|
||
postsCountMap, _ := h.userService.GetUserPostCountBatch(c.Request.Context(), userIDs)
|
||
userResponses = dto.ConvertUsersToResponseWithMutualFollowAndPostsCount(users, nil, postsCountMap)
|
||
} else {
|
||
userResponses = dto.ConvertUsersToResponse(users)
|
||
}
|
||
|
||
response.Paginated(c, userResponses, total, page, pageSize)
|
||
}
|
||
|
||
// GetPrivacySettings 获取隐私设置
|
||
func (h *UserHandler) GetPrivacySettings(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
settings, err := h.userService.GetPrivacySettings(c.Request.Context(), userID)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to get privacy settings")
|
||
return
|
||
}
|
||
|
||
response.Success(c, &dto.PrivacySettingsResponse{
|
||
FollowersVisibility: string(settings.FollowersVisibility),
|
||
FollowingVisibility: string(settings.FollowingVisibility),
|
||
PostsVisibility: string(settings.PostsVisibility),
|
||
FavoritesVisibility: string(settings.FavoritesVisibility),
|
||
})
|
||
}
|
||
|
||
// UpdatePrivacySettings 更新隐私设置
|
||
func (h *UserHandler) UpdatePrivacySettings(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
var req dto.PrivacySettingsRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
settings := &model.PrivacySettings{}
|
||
if req.FollowersVisibility != nil {
|
||
settings.FollowersVisibility = model.VisibilityLevel(*req.FollowersVisibility)
|
||
} else {
|
||
settings.FollowersVisibility = model.VisibilityEveryone
|
||
}
|
||
if req.FollowingVisibility != nil {
|
||
settings.FollowingVisibility = model.VisibilityLevel(*req.FollowingVisibility)
|
||
} else {
|
||
settings.FollowingVisibility = model.VisibilityEveryone
|
||
}
|
||
if req.PostsVisibility != nil {
|
||
settings.PostsVisibility = model.VisibilityLevel(*req.PostsVisibility)
|
||
} else {
|
||
settings.PostsVisibility = model.VisibilityEveryone
|
||
}
|
||
if req.FavoritesVisibility != nil {
|
||
settings.FavoritesVisibility = model.VisibilityLevel(*req.FavoritesVisibility)
|
||
} else {
|
||
settings.FavoritesVisibility = model.VisibilityEveryone
|
||
}
|
||
|
||
if err := h.userService.UpdatePrivacySettings(c.Request.Context(), userID, settings); err != nil {
|
||
response.InternalServerError(c, "failed to update privacy settings")
|
||
return
|
||
}
|
||
|
||
response.Success(c, &dto.PrivacySettingsResponse{
|
||
FollowersVisibility: string(settings.FollowersVisibility),
|
||
FollowingVisibility: string(settings.FollowingVisibility),
|
||
PostsVisibility: string(settings.PostsVisibility),
|
||
FavoritesVisibility: string(settings.FavoritesVisibility),
|
||
})
|
||
}
|
||
|
||
// RequestAccountDeletion 申请注销账号
|
||
func (h *UserHandler) RequestAccountDeletion(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
var req dto.RequestDeletionRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, err.Error())
|
||
return
|
||
}
|
||
|
||
if err := h.userService.RequestAccountDeletion(c.Request.Context(), userID, req.Password); err != nil {
|
||
response.HandleError(c, err, "failed to request account deletion")
|
||
return
|
||
}
|
||
|
||
isPending, requestedAt, daysLeft, err := h.userService.GetDeletionStatus(c.Request.Context(), userID)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to get deletion status")
|
||
return
|
||
}
|
||
|
||
resp := &dto.DeletionStatusResponse{
|
||
IsPendingDeletion: isPending,
|
||
CoolDownDays: daysLeft,
|
||
}
|
||
if requestedAt != nil {
|
||
resp.DeletionRequestedAt = dto.FormatTime(*requestedAt)
|
||
}
|
||
|
||
response.Success(c, resp)
|
||
}
|
||
|
||
// CancelAccountDeletion 取消注销
|
||
func (h *UserHandler) CancelAccountDeletion(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
if err := h.userService.CancelAccountDeletion(c.Request.Context(), userID); err != nil {
|
||
response.InternalServerError(c, "failed to cancel account deletion")
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"success": true})
|
||
}
|
||
|
||
// GetDeletionStatus 获取注销状态
|
||
func (h *UserHandler) GetDeletionStatus(c *gin.Context) {
|
||
userID := c.GetString("user_id")
|
||
if userID == "" {
|
||
response.Unauthorized(c, "")
|
||
return
|
||
}
|
||
|
||
isPending, requestedAt, daysLeft, err := h.userService.GetDeletionStatus(c.Request.Context(), userID)
|
||
if err != nil {
|
||
response.InternalServerError(c, "failed to get deletion status")
|
||
return
|
||
}
|
||
|
||
resp := &dto.DeletionStatusResponse{
|
||
IsPendingDeletion: isPending,
|
||
CoolDownDays: daysLeft,
|
||
}
|
||
if requestedAt != nil {
|
||
resp.DeletionRequestedAt = dto.FormatTime(*requestedAt)
|
||
}
|
||
|
||
response.Success(c, resp)
|
||
}
|