Files
jiang13-forum/service/user.go
freefire 7cc574f9a1 feat: 优化首页帖子列表并新增右侧栏最新注册组件
首页列表精简 meta 与统计展示,板块色标前移并淡化;有回复时显示最后回复人。右侧栏新增最新注册(4 列头像网格),友链改为标签块并排换行。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 18:50:09 +08:00

314 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"errors"
"fmt"
"mime/multipart"
"strconv"
"strings"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
)
type UserService struct {
filter *SensitiveFilter
settings *ForumSettingsService
}
func NewUserService(filter *SensitiveFilter, settings *ForumSettingsService) *UserService {
return &UserService{filter: filter, settings: settings}
}
// GetByID 获取用户信息
func (s *UserService) GetByID(id uint) (*model.User, error) {
var user model.User
if err := model.DB.First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
}
// UserActivityStats 个人主页活动统计
type UserActivityStats struct {
PostCount int64 `json:"post_count"`
CommentCount int64 `json:"comment_count"`
FavoriteCount int64 `json:"favorite_count"`
LikeReceived int64 `json:"like_received"`
}
// ActivityStats 统计用户发帖、评论、收藏与帖子获赞
func (s *UserService) ActivityStats(userID uint) (UserActivityStats, error) {
var st UserActivityStats
if userID == 0 {
return st, errors.New("无效用户")
}
if err := model.DB.Model(&model.Post{}).Where("user_id = ?", userID).Count(&st.PostCount).Error; err != nil {
return st, err
}
if err := model.DB.Model(&model.Comment{}).Where("user_id = ?", userID).Count(&st.CommentCount).Error; err != nil {
return st, err
}
if err := model.DB.Model(&model.PostFavorite{}).Where("user_id = ?", userID).Count(&st.FavoriteCount).Error; err != nil {
return st, err
}
var likeSum int64
if err := model.DB.Model(&model.Post{}).
Select("COALESCE(SUM(like_count), 0)").
Where("user_id = ?", userID).
Scan(&likeSum).Error; err != nil {
return st, err
}
st.LikeReceived = likeSum
return st, nil
}
// GetByUsername 按用户名查询
func (s *UserService) GetByUsername(username string) (*model.User, error) {
var user model.User
if err := model.DB.Where("username = ?", username).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
// GetByEmail 按邮箱查询
func (s *UserService) GetByEmail(email string) (*model.User, error) {
email = NormalizeEmail(email)
var user model.User
if err := model.DB.Where("email = ?", email).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
// ResetPasswordByEmail 通过邮箱重置密码(已通过验证码校验)
func (s *UserService) ResetPasswordByEmail(email, newPass string) error {
if err := ValidatePassword(newPass, s.settings.PasswordMinLen()); err != nil {
return err
}
user, err := s.GetByEmail(email)
if err != nil {
return errors.New("用户不存在")
}
hash, err := HashPassword(newPass)
if err != nil {
return err
}
return model.DB.Model(&model.User{}).Where("id = ?", user.ID).Update("password", hash).Error
}
// SearchUsersBrief 公开用户搜索(@补全):匹配用户名/昵称,不含邮箱
func (s *UserService) SearchUsersBrief(keyword string, limit int) ([]model.User, error) {
keyword = strings.TrimSpace(keyword)
if keyword == "" {
return []model.User{}, nil
}
if limit <= 0 || limit > 20 {
limit = 8
}
like := "%" + keyword + "%"
var users []model.User
err := model.DB.Select("id", "username", "nickname", "avatar", "role", "verified").
Where("username LIKE ? OR nickname LIKE ?", like, like).
Order("username ASC").
Limit(limit).
Find(&users).Error
if err != nil {
return nil, err
}
if users == nil {
users = []model.User{}
}
return users, nil
}
// RecentUserItem 右栏「最新注册」条目
type RecentUserItem struct {
ID uint `json:"id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
CreatedAt string `json:"created_at"`
}
// ListRecentRegistered 前台最新注册用户(排除封禁)
func (s *UserService) ListRecentRegistered(limit int) ([]RecentUserItem, error) {
if limit < 1 {
limit = 8
}
var users []model.User
err := model.DB.Select("id", "username", "nickname", "avatar", "created_at").
Where("banned = ?", false).
Order("created_at DESC, id DESC").
Limit(limit).
Find(&users).Error
if err != nil {
return nil, err
}
out := make([]RecentUserItem, 0, len(users))
for _, u := range users {
nick := strings.TrimSpace(u.Nickname)
if nick == "" {
nick = u.Username
}
out = append(out, RecentUserItem{
ID: u.ID,
Nickname: nick,
Avatar: u.Avatar,
CreatedAt: u.CreatedAt.UTC().Format(time.RFC3339),
})
}
return out, nil
}
// UpdateNickname 修改昵称
func (s *UserService) UpdateNickname(userID uint, nickname string) error {
nickname = strings.TrimSpace(nickname)
if nickname == "" {
return errors.New("昵称不能为空")
}
nickname = s.filter.Filter(nickname)
return model.DB.Model(&model.User{}).Where("id = ?", userID).Update("nickname", nickname).Error
}
// UpdateSignature 修改个人签名
func (s *UserService) UpdateSignature(userID uint, signature string) error {
signature = strings.TrimSpace(signature)
maxLen := s.settings.SignatureMax()
if maxLen > 0 {
runes := []rune(signature)
if len(runes) > maxLen {
return fmt.Errorf("签名不能超过 %d 字", maxLen)
}
}
if signature != "" {
signature = s.filter.Filter(signature)
}
return model.DB.Model(&model.User{}).Where("id = ?", userID).Update("signature", signature).Error
}
// UpdatePassword 修改密码
func (s *UserService) UpdatePassword(userID uint, oldPass, newPass string) error {
if err := ValidatePassword(newPass, s.settings.PasswordMinLen()); err != nil {
return err
}
var user model.User
if err := model.DB.First(&user, userID).Error; err != nil {
return err
}
if !CheckPassword(user.Password, oldPass) {
return errors.New("原密码错误")
}
hash, err := HashPassword(newPass)
if err != nil {
return err
}
return model.DB.Model(&user).Update("password", hash).Error
}
// UploadAvatar 上传头像;成功后删除用户旧头像文件,避免磁盘/对象存储堆积
func (s *UserService) UploadAvatar(userID uint, file *multipart.FileHeader, store *UploadStore) (string, error) {
var user model.User
if err := model.DB.Select("id", "avatar").First(&user, userID).Error; err != nil {
return "", err
}
url, err := SaveUploadedImage(store, file, UploadCategoryAvatars, fmt.Sprintf("%d", userID))
if err != nil {
return "", err
}
if err := model.DB.Model(&model.User{}).Where("id = ?", userID).Update("avatar", url).Error; err != nil {
return "", err
}
if old := strings.TrimSpace(user.Avatar); old != "" && old != url {
store.DeleteByURL(old)
}
return url, nil
}
// ListUsers 管理员列出用户
// UserListQuery 后台用户列表筛选
type UserListQuery struct {
Page int
Size int
Keyword string // 匹配用户名/昵称/邮箱
Filter string // all | verified | banned | admin
}
func (s *UserService) ListUsers(q UserListQuery) ([]model.User, int64, error) {
if q.Page < 1 {
q.Page = 1
}
if q.Size < 1 {
q.Size = 20
}
if q.Size > 100 {
q.Size = 100
}
db := model.DB.Model(&model.User{})
kw := strings.TrimSpace(q.Keyword)
if kw != "" {
like := "%" + kw + "%"
if id, err := strconv.ParseUint(kw, 10, 64); err == nil {
db = db.Where("id = ? OR username LIKE ? OR nickname LIKE ? OR email LIKE ?", id, like, like, like)
} else {
db = db.Where("username LIKE ? OR nickname LIKE ? OR email LIKE ?", like, like, like)
}
}
switch strings.TrimSpace(q.Filter) {
case "verified":
db = db.Where("verified = ? AND role <> ?", true, model.RoleAdmin)
case "banned":
db = db.Where("banned = ?", true)
case "admin":
db = db.Where("role = ?", model.RoleAdmin)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var users []model.User
offset := (q.Page - 1) * q.Size
err := db.Order("id desc").Offset(offset).Limit(q.Size).Find(&users).Error
return users, total, err
}
// BanUser 禁言用户
func (s *UserService) BanUser(userID uint, banned bool) error {
var user model.User
if err := model.DB.First(&user, userID).Error; err != nil {
return errors.New("用户不存在")
}
if user.Role == model.RoleAdmin {
return errors.New("不能禁言管理员账号")
}
now := time.Now()
updates := map[string]interface{}{"banned": banned}
if banned {
updates["banned_at"] = &now
}
return model.DB.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error
}
// SitemapUser 站点地图用的轻量用户字段
type SitemapUser struct {
ID uint
UpdatedAt time.Time
}
// ListSitemap 列出未禁言用户(供 sitemap
func (s *UserService) ListSitemap(limit int) ([]SitemapUser, error) {
if limit <= 0 {
limit = 5000
}
var rows []SitemapUser
err := model.DB.Model(&model.User{}).
Select("id, updated_at").
Where("banned = ?", false).
Order("updated_at desc, id desc").
Limit(limit).
Find(&rows).Error
return rows, err
}