This commit is contained in:
12600k-rog-d4
2025-12-09 20:33:57 +08:00
parent 144ed3cbdf
commit 8f4d399cfd
1498 changed files with 3778 additions and 3654 deletions

View File

@@ -0,0 +1,621 @@
package services
import (
"bbs-go/internal/models/constants"
"bbs-go/internal/pkg/bbsurls"
"bbs-go/internal/pkg/email"
"bbs-go/internal/pkg/errs"
"bbs-go/internal/pkg/validate"
"errors"
"log/slog"
"net/http"
"strconv"
"strings"
"time"
"github.com/mlogclub/simple/common/dates"
"github.com/mlogclub/simple/common/passwd"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
"github.com/mlogclub/simple/web"
"github.com/mlogclub/simple/web/params"
"gorm.io/gorm"
"bbs-go/internal/cache"
"bbs-go/internal/models"
"bbs-go/internal/repositories"
)
// 邮箱验证邮件有效期(小时)
const emailVerifyExpireHour = 24
var UserService = newUserService()
func newUserService() *userService {
return &userService{}
}
type userService struct {
}
func (s *userService) Get(id int64) *models.User {
return repositories.UserRepository.Get(sqls.DB(), id)
}
func (s *userService) Take(where ...interface{}) *models.User {
return repositories.UserRepository.Take(sqls.DB(), where...)
}
func (s *userService) Find(cnd *sqls.Cnd) []models.User {
return repositories.UserRepository.Find(sqls.DB(), cnd)
}
func (s *userService) FindOne(cnd *sqls.Cnd) *models.User {
return repositories.UserRepository.FindOne(sqls.DB(), cnd)
}
func (s *userService) FindPageByParams(params *params.QueryParams) (list []models.User, paging *sqls.Paging) {
return repositories.UserRepository.FindPageByParams(sqls.DB(), params)
}
func (s *userService) FindPageByCnd(cnd *sqls.Cnd) (list []models.User, paging *sqls.Paging) {
return repositories.UserRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *userService) Create(t *models.User) error {
err := repositories.UserRepository.Create(sqls.DB(), t)
if err == nil {
cache.UserCache.Invalidate(t.Id)
}
return nil
}
func (s *userService) Update(t *models.User) error {
err := repositories.UserRepository.Update(sqls.DB(), t)
cache.UserCache.Invalidate(t.Id)
return err
}
func (s *userService) Updates(id int64, columns map[string]interface{}) error {
err := repositories.UserRepository.Updates(sqls.DB(), id, columns)
cache.UserCache.Invalidate(id)
return err
}
func (s *userService) UpdateColumn(id int64, name string, value interface{}) error {
err := repositories.UserRepository.UpdateColumn(sqls.DB(), id, name, value)
cache.UserCache.Invalidate(id)
return err
}
func (s *userService) Delete(id int64) {
repositories.UserRepository.Delete(sqls.DB(), id)
cache.UserCache.Invalidate(id)
}
// Scan 扫描
func (s *userService) Scan(callback func(users []models.User)) {
var cursor int64
for {
list := repositories.UserRepository.Find(sqls.DB(), sqls.NewCnd().Where("id > ?", cursor).Asc("id").Limit(100))
if len(list) == 0 {
break
}
cursor = list[len(list)-1].Id
callback(list)
}
}
// Forbidden 禁言
func (s *userService) Forbidden(operatorId, userId int64, days int, reason string, r *http.Request) error {
var forbiddenEndTime int64
if days == -1 { // 永久禁言
forbiddenEndTime = -1
} else if days > 0 {
forbiddenEndTime = dates.Timestamp(time.Now().Add(time.Hour * 24 * time.Duration(days)))
} else {
return errors.New("禁言时间错误")
}
if repositories.UserRepository.UpdateColumn(sqls.DB(), userId, "forbidden_end_time", forbiddenEndTime) == nil {
cache.UserCache.Invalidate(userId)
description := ""
if strs.IsNotBlank(reason) {
description = "禁言原因:" + reason
}
OperateLogService.AddOperateLog(operatorId, constants.OpTypeForbidden, constants.EntityUser, userId,
description, r)
// 永久禁言
if days == -1 {
user := cache.UserCache.Get(userId)
_ = s.DecrScore(userId, user.Score, constants.EntityUser, strconv.FormatInt(operatorId, 10), "永久禁言")
go func() {
// 删除话题
TopicService.ScanByUser(userId, func(topics []models.Topic) {
for _, topic := range topics {
if topic.Status != constants.StatusDeleted {
_ = TopicService.Delete(topic.Id, operatorId, nil)
}
}
})
// 删除文章
ArticleService.ScanByUser(userId, func(articles []models.Article) {
for _, article := range articles {
if article.Status != constants.StatusDeleted {
_ = ArticleService.Delete(article.Id)
}
}
})
// 删除评论
CommentService.ScanByUser(userId, func(comments []models.Comment) {
for _, comment := range comments {
if comment.Status != constants.StatusDeleted {
_ = CommentService.Delete(comment.Id)
}
}
})
}()
}
}
return nil
}
// RemoveForbidden 移除禁言
func (s *userService) RemoveForbidden(operatorId, userId int64, r *http.Request) {
user := s.Get(userId)
if user == nil || !user.IsForbidden() {
return
}
if repositories.UserRepository.UpdateColumn(sqls.DB(), userId, "forbidden_end_time", 0) == nil {
cache.UserCache.Invalidate(user.Id)
OperateLogService.AddOperateLog(operatorId, constants.OpTypeRemoveForbidden, constants.EntityUser, userId, "", r)
}
}
// GetByEmail 根据邮箱查找
func (s *userService) GetByEmail(email string) *models.User {
return repositories.UserRepository.GetByEmail(sqls.DB(), email)
}
// GetByUsername 根据用户名查找
func (s *userService) GetByUsername(username string) *models.User {
return repositories.UserRepository.GetByUsername(sqls.DB(), username)
}
// SignUp 注册
func (s *userService) SignUp(username, email, nickname, password, rePassword string) (*models.User, error) {
username = strings.TrimSpace(username)
email = strings.TrimSpace(email)
nickname = strings.TrimSpace(nickname)
// 验证昵称
if len(nickname) == 0 {
return nil, errors.New("昵称不能为空")
}
// 验证密码
err := validate.IsValidPassword(password, rePassword)
if err != nil {
return nil, err
}
// 验证邮箱
if len(email) > 0 {
if err := validate.IsEmail(email); err != nil {
return nil, err
}
if s.GetByEmail(email) != nil {
return nil, errors.New("邮箱:" + email + " 已被占用")
}
} else {
return nil, errors.New("请输入邮箱")
}
// 验证用户名
if len(username) > 0 {
if err := validate.IsUsername(username); err != nil {
return nil, err
}
if s.isUsernameExists(username) {
return nil, errors.New("用户名:" + username + " 已被占用")
}
}
user := &models.User{
Username: sqls.SqlNullString(username),
Email: sqls.SqlNullString(email),
Nickname: nickname,
Password: passwd.EncodePassword(password),
Status: constants.StatusOk,
CreateTime: dates.NowTimestamp(),
UpdateTime: dates.NowTimestamp(),
}
err = repositories.UserRepository.Create(sqls.DB(), user)
if err != nil {
return nil, err
}
return user, nil
}
// SignIn 登录
func (s *userService) SignIn(username, password string) (*models.User, error) {
if strs.IsBlank(username) {
return nil, errors.New("用户名/邮箱不能为空")
}
if strs.IsBlank(password) {
return nil, errors.New("密码不能为空")
}
if err := validate.IsPassword(password); err != nil {
return nil, err
}
var user *models.User = nil
if err := validate.IsEmail(username); err == nil { // 如果用户输入的是邮箱
user = s.GetByEmail(username)
} else {
user = s.GetByUsername(username)
}
if user == nil || user.Status != constants.StatusOk {
return nil, errors.New("用户名或密码错误")
}
if !passwd.ValidatePassword(user.Password, password) {
return nil, errors.New("用户名或密码错误")
}
return user, nil
}
// isEmailExists 邮箱是否存在
func (s *userService) isEmailExists(email string) bool {
if len(email) == 0 { // 如果邮箱为空,那么就认为是不存在
return false
}
return s.GetByEmail(email) != nil
}
// isUsernameExists 用户名是否存在
func (s *userService) isUsernameExists(username string) bool {
return s.GetByUsername(username) != nil
}
// UpdateAvatar 更新头像
func (s *userService) UpdateAvatar(userId int64, avatar string) error {
return s.UpdateColumn(userId, "avatar", avatar)
}
// UpdateNickname 更新昵称
func (s *userService) UpdateNickname(userId int64, nickname string) error {
return s.UpdateColumn(userId, "nickname", nickname)
}
// UpdateDescription 更新简介
func (s *userService) UpdateDescription(userId int64, description string) error {
return s.UpdateColumn(userId, "description", description)
}
// UpdateGender 修改性别
func (s *userService) UpdateGender(userId int64, gender string) error {
if strs.IsBlank(gender) {
return s.UpdateColumn(userId, "gender", "")
} else {
if gender != string(constants.GenderMale) && gender != string(constants.GenderFemale) {
return errors.New("invalidate gender value")
}
return s.UpdateColumn(userId, "gender", gender)
}
}
// UpdateBirthday 修改生日
func (s *userService) UpdateBirthday(userId int64, birthdayStr string) error {
if strs.IsBlank(birthdayStr) {
return s.UpdateColumn(userId, "birthday", "")
} else {
birthday, err := dates.Parse(birthdayStr, dates.FmtDate)
if err != nil {
return err
}
return s.UpdateColumn(userId, "birthday", birthday)
}
}
// UpdateBackgroundImage 修改背景图
func (s *userService) UpdateBackgroundImage(userId int64, backgroundImage string) error {
return s.UpdateColumn(userId, "background_image", backgroundImage)
}
// SetUsername 设置用户名
func (s *userService) SetUsername(userId int64, username string) error {
username = strings.TrimSpace(username)
if err := validate.IsUsername(username); err != nil {
return err
}
user := s.Get(userId)
if len(user.Username.String) > 0 {
return errors.New("你已设置了用户名,无法重复设置。")
}
if s.isUsernameExists(username) {
return errors.New("用户名:" + username + " 已被占用")
}
return s.UpdateColumn(userId, "username", username)
}
// SetEmail 设置密码
func (s *userService) SetEmail(userId int64, email string) error {
email = strings.TrimSpace(email)
if err := validate.IsEmail(email); err != nil {
return err
}
user := s.Get(userId)
if user == nil {
return errors.New("用户不存在")
}
if user.Email.String == email {
// 用户邮箱没做变更
return nil
}
if s.isEmailExists(email) {
return errors.New("邮箱:" + email + " 已被占用")
}
return s.Updates(userId, map[string]interface{}{
"email": email,
"email_verified": false,
})
}
// SetPassword 设置密码
func (s *userService) SetPassword(userId int64, password, rePassword string) error {
if err := validate.IsValidPassword(password, rePassword); err != nil {
return err
}
user := s.Get(userId)
if len(user.Password) > 0 {
return errors.New("你已设置了密码,如需修改请前往修改页面。")
}
password = passwd.EncodePassword(password)
return s.UpdateColumn(userId, "password", password)
}
// UpdatePassword 修改密码
func (s *userService) UpdatePassword(userId int64, oldPassword, password, rePassword string) error {
if err := validate.IsValidPassword(password, rePassword); err != nil {
return err
}
user := s.Get(userId)
if len(user.Password) == 0 {
return errors.New("你没设置密码,请先设置密码")
}
if !passwd.ValidatePassword(user.Password, oldPassword) {
return errors.New("旧密码验证失败")
}
return s.UpdateColumn(userId, "password", passwd.EncodePassword(password))
}
// IncrTopicCount topic_count + 1
func (s *userService) IncrTopicCount(tx *gorm.DB, userId int64) error {
if err := repositories.UserRepository.UpdateColumn(sqls.DB(), userId, "topic_count", gorm.Expr("topic_count + 1")); err != nil {
slog.Error(err.Error(), slog.Any("err", err))
return err
}
cache.UserCache.Invalidate(userId)
return nil
}
// IncrCommentCount comment_count + 1
func (s *userService) IncrCommentCount(userId int64) int {
t := repositories.UserRepository.Get(sqls.DB(), userId)
if t == nil {
return 0
}
commentCount := t.CommentCount + 1
if err := repositories.UserRepository.UpdateColumn(sqls.DB(), userId, "comment_count", commentCount); err != nil {
slog.Error(err.Error(), slog.Any("err", err))
} else {
cache.UserCache.Invalidate(userId)
}
return commentCount
}
// SyncUserCount 同步用户计数
func (s *userService) SyncUserCount() {
s.Scan(func(users []models.User) {
for _, user := range users {
topicCount := repositories.TopicRepository.Count(sqls.DB(), sqls.NewCnd().Eq("user_id", user.Id).Eq("status", constants.StatusOk))
commentCount := repositories.CommentRepository.Count(sqls.DB(), sqls.NewCnd().Eq("user_id", user.Id).Eq("status", constants.StatusOk))
_ = repositories.UserRepository.UpdateColumn(sqls.DB(), user.Id, "topic_count", topicCount)
_ = repositories.UserRepository.UpdateColumn(sqls.DB(), user.Id, "comment_count", commentCount)
cache.UserCache.Invalidate(user.Id)
}
})
}
// SendEmailVerifyEmail 发送邮箱验证邮件
func (s *userService) SendEmailVerifyEmail(userId int64) error {
user := s.Get(userId)
if user == nil {
return errors.New("用户不存在")
}
if user.EmailVerified {
return errors.New("用户邮箱已验证")
}
if err := validate.IsEmail(user.Email.String); err != nil {
return err
}
// 如果设置了邮箱白名单
if emailWhitelist := SysConfigService.GetEmailWhitelist(); len(emailWhitelist) > 0 {
isInWhitelist := false
for _, whitelist := range emailWhitelist {
if strings.Contains(strings.ToLower(user.Email.String), strings.ToLower(whitelist)) {
isInWhitelist = true
break
}
}
if !isInWhitelist {
// 直接返回,也不抛出异常了,就是不发邮件
slog.Error("不支持使用该邮箱进行验证.", slog.String("email", user.Email.String))
return errors.New("不支持该类型邮箱")
}
}
var (
token = strs.UUID()
url = bbsurls.AbsUrl("/user/email/verify?token=" + token)
link = &models.ActionLink{Title: "点击这里验证邮箱>>", Url: url}
siteTitle = cache.SysConfigCache.GetValue(constants.SysConfigSiteTitle)
subject = "邮箱验证 - " + siteTitle
title = "邮箱验证 - " + siteTitle
content = "该邮件用于验证你在 " + siteTitle + " 中设置邮箱的正确性,请在" + strconv.Itoa(emailVerifyExpireHour) + "小时内完成验证。验证链接:" + url
)
return sqls.DB().Transaction(func(tx *gorm.DB) error {
if err := repositories.EmailCodeRepository.Create(tx, &models.EmailCode{
Model: models.Model{},
UserId: userId,
Email: user.Email.String,
Code: "",
Token: token,
Title: title,
Content: content,
Used: false,
CreateTime: dates.NowTimestamp(),
}); err != nil {
return nil
}
if err := email.SendTemplateEmail(nil, user.Email.String, subject, title, content, "", link); err != nil {
return err
}
return nil
})
}
// VerifyEmail 验证邮箱
func (s *userService) VerifyEmail(token string) (string, error) {
emailCode := EmailCodeService.FindOne(sqls.NewCnd().Eq("token", token))
if emailCode == nil || emailCode.Used {
return "", errors.New("非法请求")
}
user := s.Get(emailCode.UserId)
if user == nil || emailCode.Email != user.Email.String {
return "", errors.New("验证码过期")
}
if dates.FromTimestamp(emailCode.CreateTime).Add(time.Hour * time.Duration(emailVerifyExpireHour)).Before(time.Now()) {
return "", errors.New("验证邮件已过期")
}
err := sqls.DB().Transaction(func(tx *gorm.DB) error {
if err := repositories.UserRepository.UpdateColumn(tx, emailCode.UserId, "email_verified", true); err != nil {
return err
}
cache.UserCache.Invalidate(emailCode.UserId)
return repositories.EmailCodeRepository.UpdateColumn(tx, emailCode.Id, "used", true)
})
if err != nil {
return "", err
}
return emailCode.Email, nil
}
// CheckPostStatus 用于在发表内容时检查用户状态
func (s *userService) CheckPostStatus(user *models.User) error {
if user == nil {
return errs.NotLogin
}
if user.Status != constants.StatusOk {
return errs.UserDisabled
}
if user.IsForbidden() {
return errs.ForbiddenError
}
observeSeconds := SysConfigService.GetInt(constants.SysConfigUserObserveSeconds, 0)
if user.InObservationPeriod(observeSeconds) {
return web.NewError(errs.InObservationPeriod.Code, "账号尚在观察期,观察期时长:"+strconv.Itoa(observeSeconds)+"秒,请稍后再试")
}
return nil
}
// IncrScoreForPostTopic 发帖获积分
func (s *userService) IncrScoreForPostTopic(topic *models.Topic) {
config := SysConfigService.GetConfig()
if config.ScoreConfig.PostTopicScore <= 0 {
slog.Info("请配置发帖积分")
return
}
err := s.addScore(topic.UserId, config.ScoreConfig.PostTopicScore, constants.EntityTopic,
strconv.FormatInt(topic.Id, 10), "发表话题")
if err != nil {
slog.Error(err.Error(), slog.Any("err", err))
}
}
// IncrScoreForPostComment 跟帖获积分
func (s *userService) IncrScoreForPostComment(comment *models.Comment) {
// 非话题跟帖,跳过
if comment.EntityType != constants.EntityTopic {
return
}
config := SysConfigService.GetConfig()
if config.ScoreConfig.PostCommentScore <= 0 {
slog.Info("请配置跟帖积分")
return
}
err := s.addScore(comment.UserId, config.ScoreConfig.PostCommentScore, constants.EntityComment,
strconv.FormatInt(comment.Id, 10), "发表跟帖")
if err != nil {
slog.Error(err.Error(), slog.Any("err", err))
}
}
// IncrScore 增加分数
func (s *userService) IncrScore(userId int64, score int, sourceType, sourceId, description string) error {
if score <= 0 {
return errors.New("分数必须为正数")
}
return s.addScore(userId, score, sourceType, sourceId, description)
}
// DecrScore 减少分数
func (s *userService) DecrScore(userId int64, score int, sourceType, sourceId, description string) error {
if score <= 0 {
return errors.New("分数必须为正数")
}
return s.addScore(userId, -score, sourceType, sourceId, description)
}
// addScore 加分数,也可以加负数
func (s *userService) addScore(userId int64, score int, sourceType, sourceId, description string) error {
if score == 0 {
return errors.New("分数不能为0")
}
user := s.Get(userId)
if user == nil {
return errors.New("用户不存在")
}
if err := s.Updates(userId, map[string]interface{}{
"score": gorm.Expr("score + ?", score),
"update_time": dates.NowTimestamp(),
}); err != nil {
return err
}
scoreType := constants.ScoreTypeIncr
if score < 0 {
scoreType = constants.ScoreTypeDecr
}
err := UserScoreLogService.Create(&models.UserScoreLog{
UserId: userId,
SourceType: sourceType,
SourceId: sourceId,
Description: description,
Type: scoreType,
Score: score,
CreateTime: dates.NowTimestamp(),
})
if err == nil {
cache.UserCache.Invalidate(userId)
}
return err
}