fix: 完成 Gitea 目录改组收尾(import、构建与 LICENSE)

同步包路径与路由,去掉 SPA 构建步骤,对齐 Gitea 式 LICENSE,并更新规格/规则与占位 SSR。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 04:01:20 +08:00
parent 9fe299a45f
commit 3f50316ad0
93 changed files with 1472 additions and 1522 deletions

View File

@@ -1,4 +1,4 @@
package service
package services
import "testing"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"errors"
@@ -6,7 +6,7 @@ import (
"time"
"github.com/golang-jwt/jwt/v5"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
// 最近访问写入节流,避免每次 API 都打库
@@ -19,7 +19,7 @@ const TokenExpire = 7 * 24 * time.Hour
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Role model.Role `json:"role"`
Role models.Role `json:"role"`
jwt.RegisteredClaims
}
@@ -36,12 +36,12 @@ func NewAuthService(jwtSecret string, filter *SensitiveFilter, settings *ForumSe
// UserCount 当前用户数
func (s *AuthService) UserCount() int64 {
var n int64
model.DB.Model(&model.User{}).Count(&n)
models.DB.Model(&models.User{}).Count(&n)
return n
}
// Register 用户注册
func (s *AuthService) Register(username, password, nickname, email string) (*model.User, error) {
func (s *AuthService) Register(username, password, nickname, email string) (*models.User, error) {
if err := ValidateUsername(username); err != nil {
return nil, err
}
@@ -53,11 +53,11 @@ func (s *AuthService) Register(username, password, nickname, email string) (*mod
return nil, err
}
var exist model.User
if err := model.DB.Where("username = ?", username).First(&exist).Error; err == nil {
var exist models.User
if err := models.DB.Where("username = ?", username).First(&exist).Error; err == nil {
return nil, ErrUserExists
}
if err := model.DB.Where("email = ?", email).First(&exist).Error; err == nil {
if err := models.DB.Where("email = ?", email).First(&exist).Error; err == nil {
return nil, ErrEmailExists
}
@@ -71,28 +71,28 @@ func (s *AuthService) Register(username, password, nickname, email string) (*mod
nickname = s.filter.Filter(nickname)
// 首个注册用户自动成为管理员
role := model.RoleUser
role := models.RoleUser
if s.UserCount() == 0 {
role = model.RoleAdmin
role = models.RoleAdmin
}
user := &model.User{
user := &models.User{
Username: username,
Email: email,
Password: hash,
Nickname: nickname,
Role: role,
}
if err := model.DB.Create(user).Error; err != nil {
if err := models.DB.Create(user).Error; err != nil {
return nil, err
}
return user, nil
}
// Login 用户登录,返回 JWT tokenclientIP 写入上次登录记录
func (s *AuthService) Login(username, password, clientIP string) (string, *model.User, error) {
var user model.User
if err := model.DB.Where("username = ?", username).First(&user).Error; err != nil {
func (s *AuthService) Login(username, password, clientIP string) (string, *models.User, error) {
var user models.User
if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil {
return "", nil, ErrInvalidCred
}
if user.Banned {
@@ -107,13 +107,13 @@ func (s *AuthService) Login(username, password, clientIP string) (string, *model
}
// recordLogin 记录上次登录时间与 IP登录同时视为一次访问失败不影响登录
func (s *AuthService) recordLogin(user *model.User, clientIP string) {
func (s *AuthService) recordLogin(user *models.User, clientIP string) {
now := time.Now()
ip := clientIP
if len(ip) > 45 {
ip = ip[:45]
}
_ = model.DB.Model(user).Updates(map[string]interface{}{
_ = models.DB.Model(user).Updates(map[string]interface{}{
"last_login_at": now,
"last_login_ip": ip,
"last_access_at": now,
@@ -136,11 +136,11 @@ func (s *AuthService) TouchLastAccess(userID uint) {
}
}
lastAccessTouchCache.Store(userID, now)
_ = model.DB.Model(&model.User{}).Where("id = ?", userID).Update("last_access_at", now).Error
_ = models.DB.Model(&models.User{}).Where("id = ?", userID).Update("last_access_at", now).Error
}
// GenerateToken 生成 JWT
func (s *AuthService) GenerateToken(user *model.User) (string, error) {
func (s *AuthService) GenerateToken(user *models.User) (string, error) {
claims := Claims{
UserID: user.ID,
Username: user.Username,

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"fmt"

View File

@@ -1,10 +1,10 @@
package service
package services
import (
"errors"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
@@ -14,34 +14,34 @@ type BadgeService struct{}
func NewBadgeService() *BadgeService { return &BadgeService{} }
// ListDefs 列出徽章定义
func (s *BadgeService) ListDefs(includeDisabled bool) ([]model.BadgeDef, error) {
q := model.DB.Order("sort_order asc, id asc")
func (s *BadgeService) ListDefs(includeDisabled bool) ([]models.BadgeDef, error) {
q := models.DB.Order("sort_order asc, id asc")
if !includeDisabled {
q = q.Where("enabled = ?", true)
}
var rows []model.BadgeDef
var rows []models.BadgeDef
err := q.Find(&rows).Error
return rows, err
}
// UpsertDef 创建或更新徽章定义(按 code
func (s *BadgeService) UpsertDef(def *model.BadgeDef) error {
func (s *BadgeService) UpsertDef(def *models.BadgeDef) error {
if def.Code == "" || def.Name == "" {
return errors.New("徽章代码与名称不能为空")
}
if def.Kind != model.BadgeKindAuto && def.Kind != model.BadgeKindLimited {
if def.Kind != models.BadgeKindAuto && def.Kind != models.BadgeKindLimited {
return errors.New("无效的徽章类型")
}
var existing model.BadgeDef
err := model.DB.Where("code = ?", def.Code).Limit(1).Find(&existing).Error
var existing models.BadgeDef
err := models.DB.Where("code = ?", def.Code).Limit(1).Find(&existing).Error
if err != nil {
return err
}
if existing.ID == 0 {
return model.DB.Create(def).Error
return models.DB.Create(def).Error
}
def.ID = existing.ID
return model.DB.Model(&existing).Updates(map[string]interface{}{
return models.DB.Model(&existing).Updates(map[string]interface{}{
"name": def.Name,
"description": def.Description,
"icon": def.Icon,
@@ -55,22 +55,22 @@ func (s *BadgeService) UpsertDef(def *model.BadgeDef) error {
// AwardLimited 站长颁发限定徽章
func (s *BadgeService) AwardLimited(userID, badgeID, adminID uint) error {
var def model.BadgeDef
if err := model.DB.First(&def, badgeID).Error; err != nil {
var def models.BadgeDef
if err := models.DB.First(&def, badgeID).Error; err != nil {
return errors.New("徽章不存在")
}
if def.Kind != model.BadgeKindLimited {
if def.Kind != models.BadgeKindLimited {
return errors.New("仅可颁发限定徽章")
}
if !def.Enabled {
return errors.New("徽章已停用")
}
var n int64
model.DB.Model(&model.UserBadge{}).Where("user_id = ? AND badge_id = ?", userID, badgeID).Count(&n)
models.DB.Model(&models.UserBadge{}).Where("user_id = ? AND badge_id = ?", userID, badgeID).Count(&n)
if n > 0 {
return errors.New("用户已拥有该徽章")
}
return model.DB.Create(&model.UserBadge{
return models.DB.Create(&models.UserBadge{
UserID: userID,
BadgeID: badgeID,
AwardedAt: time.Now(),
@@ -80,7 +80,7 @@ func (s *BadgeService) AwardLimited(userID, badgeID, adminID uint) error {
// Revoke 收回徽章
func (s *BadgeService) Revoke(userID, badgeID uint) error {
res := model.DB.Where("user_id = ? AND badge_id = ?", userID, badgeID).Delete(&model.UserBadge{})
res := models.DB.Where("user_id = ? AND badge_id = ?", userID, badgeID).Delete(&models.UserBadge{})
if res.Error != nil {
return res.Error
}
@@ -91,21 +91,21 @@ func (s *BadgeService) Revoke(userID, badgeID uint) error {
}
// ListUserBadges 用户已获徽章(含定义)
func (s *BadgeService) ListUserBadges(userID uint) ([]model.UserBadge, error) {
var rows []model.UserBadge
err := model.DB.Preload("Badge").Where("user_id = ?", userID).
func (s *BadgeService) ListUserBadges(userID uint) ([]models.UserBadge, error) {
var rows []models.UserBadge
err := models.DB.Preload("Badge").Where("user_id = ?", userID).
Order("awarded_at desc").Find(&rows).Error
return rows, err
}
// BadgeViews 转为展示视图(最多 limit 枚0=全部)
func BadgeViews(rows []model.UserBadge, limit int) []model.UserBadgeView {
out := make([]model.UserBadgeView, 0, len(rows))
func BadgeViews(rows []models.UserBadge, limit int) []models.UserBadgeView {
out := make([]models.UserBadgeView, 0, len(rows))
for _, r := range rows {
if r.Badge.ID == 0 || !r.Badge.Enabled {
continue
}
out = append(out, model.UserBadgeView{
out = append(out, models.UserBadgeView{
Code: r.Badge.Code,
Name: r.Badge.Name,
Description: r.Badge.Description,
@@ -121,25 +121,25 @@ func BadgeViews(rows []model.UserBadge, limit int) []model.UserBadgeView {
// EvaluateAuto 检查并授予符合条件的自动徽章
func (s *BadgeService) EvaluateAuto(userID uint) error {
var user model.User
if err := model.DB.First(&user, userID).Error; err != nil {
var user models.User
if err := models.DB.First(&user, userID).Error; err != nil {
return err
}
var defs []model.BadgeDef
if err := model.DB.Where("kind = ? AND enabled = ?", model.BadgeKindAuto, true).Find(&defs).Error; err != nil {
var defs []models.BadgeDef
if err := models.DB.Where("kind = ? AND enabled = ?", models.BadgeKindAuto, true).Find(&defs).Error; err != nil {
return err
}
tenureDays := int(time.Since(user.CreatedAt).Hours() / 24)
var likes int64
_ = model.DB.Model(&model.Post{}).
_ = models.DB.Model(&models.Post{}).
Select("COALESCE(SUM(like_count), 0)").
Where("user_id = ? AND status = ?", userID, model.ContentStatusPublished).
Where("user_id = ? AND status = ?", userID, models.ContentStatusPublished).
Scan(&likes).Error
income := user.CreatorIncomeTotal
owned := map[uint]bool{}
var existing []model.UserBadge
_ = model.DB.Where("user_id = ?", userID).Find(&existing).Error
var existing []models.UserBadge
_ = models.DB.Where("user_id = ?", userID).Find(&existing).Error
for _, e := range existing {
owned[e.BadgeID] = true
}
@@ -150,17 +150,17 @@ func (s *BadgeService) EvaluateAuto(userID uint) error {
}
ok := false
switch d.Metric {
case model.BadgeMetricTenureDays:
case models.BadgeMetricTenureDays:
ok = tenureDays >= d.Threshold
case model.BadgeMetricLikesReceived:
case models.BadgeMetricLikesReceived:
ok = int(likes) >= d.Threshold
case model.BadgeMetricCreatorIncome:
case models.BadgeMetricCreatorIncome:
ok = income >= d.Threshold
}
if !ok {
continue
}
_ = model.DB.Create(&model.UserBadge{
_ = models.DB.Create(&models.UserBadge{
UserID: userID,
BadgeID: d.ID,
AwardedAt: time.Now(),
@@ -171,7 +171,7 @@ func (s *BadgeService) EvaluateAuto(userID uint) error {
}
// AttachBadgeSummaries 批量为用户填充展示用徽章(最多 perUser 枚)
func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) {
func (s *BadgeService) AttachBadgeSummaries(users []*models.User, perUser int) {
if len(users) == 0 {
return
}
@@ -184,7 +184,7 @@ func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) {
if u == nil || u.ID == 0 {
continue
}
u.Level = model.LevelFromExp(u.Exp)
u.Level = models.LevelFromExp(u.Exp)
if !seen[u.ID] {
seen[u.ID] = true
ids = append(ids, u.ID)
@@ -193,10 +193,10 @@ func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) {
if len(ids) == 0 {
return
}
var rows []model.UserBadge
_ = model.DB.Preload("Badge").Where("user_id IN ?", ids).
var rows []models.UserBadge
_ = models.DB.Preload("Badge").Where("user_id IN ?", ids).
Order("awarded_at desc").Find(&rows).Error
grouped := map[uint][]model.UserBadgeView{}
grouped := map[uint][]models.UserBadgeView{}
for _, r := range rows {
if r.Badge.ID == 0 || !r.Badge.Enabled {
continue
@@ -205,7 +205,7 @@ func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) {
if len(list) >= perUser {
continue
}
list = append(list, model.UserBadgeView{
list = append(list, models.UserBadgeView{
Code: r.Badge.Code,
Name: r.Badge.Name,
Description: r.Badge.Description,
@@ -223,8 +223,8 @@ func (s *BadgeService) AttachBadgeSummaries(users []*model.User, perUser int) {
}
// AttachBadgeSummariesOnPosts 给帖子作者填充徽章摘要
func (s *BadgeService) AttachBadgeSummariesOnPosts(posts []model.Post, perUser int) {
users := make([]*model.User, 0, len(posts))
func (s *BadgeService) AttachBadgeSummariesOnPosts(posts []models.Post, perUser int) {
users := make([]*models.User, 0, len(posts))
for i := range posts {
if posts[i].User.ID > 0 {
users = append(users, &posts[i].User)
@@ -234,8 +234,8 @@ func (s *BadgeService) AttachBadgeSummariesOnPosts(posts []model.Post, perUser i
}
// AttachBadgeSummariesOnComments 给评论作者填充徽章摘要
func (s *BadgeService) AttachBadgeSummariesOnComments(comments []model.Comment, perUser int) {
users := make([]*model.User, 0, len(comments))
func (s *BadgeService) AttachBadgeSummariesOnComments(comments []models.Comment, perUser int) {
users := make([]*models.User, 0, len(comments))
for i := range comments {
if comments[i].User.ID > 0 {
users = append(users, &comments[i].User)
@@ -249,24 +249,24 @@ func AddExp(userID uint, delta int) {
if userID == 0 || delta <= 0 {
return
}
_ = model.DB.Model(&model.User{}).Where("id = ?", userID).
_ = models.DB.Model(&models.User{}).Where("id = ?", userID).
UpdateColumn("exp", gorm.Expr("exp + ?", delta)).Error
}
// SetUserLevel 站长设等级(调整 Exp 到门槛)
func SetUserLevel(userID uint, level int) error {
if level < 1 || level > model.MaxLevel() {
if level < 1 || level > models.MaxLevel() {
return errors.New("等级须在 110")
}
exp := model.ExpForLevel(level)
return model.DB.Model(&model.User{}).Where("id = ?", userID).Update("exp", exp).Error
exp := models.ExpForLevel(level)
return models.DB.Model(&models.User{}).Where("id = ?", userID).Update("exp", exp).Error
}
// SetVerified 设置认证
func SetVerified(userID uint, verified bool) error {
var user model.User
if err := model.DB.First(&user, userID).Error; err != nil {
var user models.User
if err := models.DB.First(&user, userID).Error; err != nil {
return errors.New("用户不存在")
}
return model.DB.Model(&user).Update("verified", verified).Error
return models.DB.Model(&user).Update("verified", verified).Error
}

View File

@@ -1,9 +1,9 @@
package service
package services
import (
"errors"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
type BoardService struct{}
@@ -14,13 +14,13 @@ func NewBoardService() *BoardService {
// BoardWithStats 板块及帖子数量
type BoardWithStats struct {
model.Board
models.Board
PostCount int `json:"post_count"`
}
func (s *BoardService) List() ([]model.Board, error) {
var boards []model.Board
err := model.DB.Order("sort_order asc, id asc").Find(&boards).Error
func (s *BoardService) List() ([]models.Board, error) {
var boards []models.Board
err := models.DB.Order("sort_order asc, id asc").Find(&boards).Error
return boards, err
}
@@ -32,34 +32,34 @@ func (s *BoardService) ListWithStats() ([]BoardWithStats, error) {
result := make([]BoardWithStats, len(boards))
for i, b := range boards {
var count int64
model.DB.Model(&model.Post{}).
Where("board_id = ? AND status = ?", b.ID, model.ContentStatusPublished).
models.DB.Model(&models.Post{}).
Where("board_id = ? AND status = ?", b.ID, models.ContentStatusPublished).
Count(&count)
result[i] = BoardWithStats{Board: b, PostCount: int(count)}
}
return result, nil
}
func (s *BoardService) GetByID(id uint) (*model.Board, error) {
var board model.Board
if err := model.DB.First(&board, id).Error; err != nil {
func (s *BoardService) GetByID(id uint) (*models.Board, error) {
var board models.Board
if err := models.DB.First(&board, id).Error; err != nil {
return nil, ErrBoardNotFound
}
return &board, nil
}
func (s *BoardService) Create(name, desc, icon string, colorIndex, sortOrder int) (*model.Board, error) {
board := &model.Board{
func (s *BoardService) Create(name, desc, icon string, colorIndex, sortOrder int) (*models.Board, error) {
board := &models.Board{
Name: name,
Description: desc,
Icon: NormalizeBoardIcon(icon),
ColorIndex: NormalizeBoardColorIndex(colorIndex),
SortOrder: sortOrder,
}
return board, model.DB.Create(board).Error
return board, models.DB.Create(board).Error
}
func (s *BoardService) Update(id uint, name, desc, icon string, colorIndex, sortOrder int) error {
return model.DB.Model(&model.Board{}).Where("id = ?", id).Updates(map[string]interface{}{
return models.DB.Model(&models.Board{}).Where("id = ?", id).Updates(map[string]interface{}{
"name": name,
"description": desc,
"icon": NormalizeBoardIcon(icon),
@@ -70,17 +70,17 @@ func (s *BoardService) Update(id uint, name, desc, icon string, colorIndex, sort
func (s *BoardService) Delete(id uint) error {
var count int64
model.DB.Model(&model.Post{}).Where("board_id = ?", id).Count(&count)
models.DB.Model(&models.Post{}).Where("board_id = ?", id).Count(&count)
if count > 0 {
return errors.New("该板块下还有帖子,无法删除")
}
return model.DB.Delete(&model.Board{}, id).Error
return models.DB.Delete(&models.Board{}, id).Error
}
// EnsureDefaultBoard 若尚无板块则创建默认「综合讨论」,便于全新安装后直接发帖
func (s *BoardService) EnsureDefaultBoard() {
var n int64
if err := model.DB.Model(&model.Board{}).Count(&n).Error; err != nil || n > 0 {
if err := models.DB.Model(&models.Board{}).Count(&n).Error; err != nil || n > 0 {
return
}
_, _ = s.Create("综合讨论", "默认板块,欢迎发帖交流", "message-square", 0, 0)

View File

@@ -1,4 +1,4 @@
package service
package services
import "strings"

View File

@@ -1,9 +1,9 @@
package service
package services
import (
"errors"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
@@ -19,27 +19,27 @@ const bountyRefundBlockReason = "已有用户回复,无法自行取消悬赏
// CountEligibleBountyReplies 统计他人已发布的有效回复数(不含楼主)
func CountEligibleBountyReplies(db *gorm.DB, postID, authorID uint) (int64, error) {
if db == nil {
db = model.DB
db = models.DB
}
var n int64
err := db.Model(&model.Comment{}).
Where("post_id = ? AND status = ? AND user_id != ?", postID, model.ContentStatusPublished, authorID).
err := db.Model(&models.Comment{}).
Where("post_id = ? AND status = ? AND user_id != ?", postID, models.ContentStatusPublished, authorID).
Count(&n).Error
return n, err
}
// CanRefundBounty 当前查看者是否可取消悬赏(管理员始终可强制取消)
func CanRefundBounty(post *model.Post, viewerIsAdmin bool) (bool, string) {
if post == nil || post.PostType != model.PostTypeBounty {
func CanRefundBounty(post *models.Post, viewerIsAdmin bool) (bool, string) {
if post == nil || post.PostType != models.PostTypeBounty {
return false, ""
}
if post.BountyStatus != model.BountyStatusOpen || post.BountyPoints < 1 {
if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 {
return false, ""
}
if viewerIsAdmin {
return true, ""
}
n, err := CountEligibleBountyReplies(model.DB, post.ID, post.UserID)
n, err := CountEligibleBountyReplies(models.DB, post.ID, post.UserID)
if err != nil {
return false, ""
}
@@ -54,42 +54,42 @@ func EscrowBounty(tx *gorm.DB, userID, postID uint, points int) error {
if points < 1 {
return ErrBountyInvalidPoint
}
_, err := AdjustPointsTx(tx, userID, -points, model.PointReasonBountyEscrow, "post", postID, "发布悬赏帖")
_, err := AdjustPointsTx(tx, userID, -points, models.PointReasonBountyEscrow, "post", postID, "发布悬赏帖")
return err
}
// AwardBounty 采纳评论并发放悬赏
func AwardBounty(postID, operatorID uint, isAdmin bool, commentID uint) error {
var post model.Post
if err := model.DB.First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.First(&post, postID).Error; err != nil {
return ErrPostNotFound
}
if post.PostType != model.PostTypeBounty {
if post.PostType != models.PostTypeBounty {
return errors.New("非悬赏帖")
}
if !isAdmin && post.UserID != operatorID {
return ErrPermissionDenied
}
if post.BountyStatus != model.BountyStatusOpen || post.BountyPoints < 1 {
if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 {
return ErrBountyNotOpen
}
var comment model.Comment
if err := model.DB.First(&comment, commentID).Error; err != nil {
var comment models.Comment
if err := models.DB.First(&comment, commentID).Error; err != nil {
return errors.New("评论不存在")
}
if comment.PostID != postID || comment.Status != model.ContentStatusPublished {
if comment.PostID != postID || comment.Status != models.ContentStatusPublished {
return errors.New("评论无效")
}
if comment.UserID == post.UserID {
return ErrBountySelfAward
}
points := post.BountyPoints
return model.DB.Transaction(func(tx *gorm.DB) error {
if _, err := AdjustPointsTx(tx, comment.UserID, points, model.PointReasonBountyAward, "post", postID, "悬赏采纳"); err != nil {
return models.DB.Transaction(func(tx *gorm.DB) error {
if _, err := AdjustPointsTx(tx, comment.UserID, points, models.PointReasonBountyAward, "post", postID, "悬赏采纳"); err != nil {
return err
}
return tx.Model(&post).Updates(map[string]interface{}{
"bounty_status": model.BountyStatusAwarded,
"bounty_status": models.BountyStatusAwarded,
"bounty_comment_id": commentID,
}).Error
})
@@ -97,21 +97,21 @@ func AwardBounty(postID, operatorID uint, isAdmin bool, commentID uint) error {
// RefundBounty 取消悬赏并退回积分
func RefundBounty(postID, operatorID uint, isAdmin bool) error {
var post model.Post
if err := model.DB.First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.First(&post, postID).Error; err != nil {
return ErrPostNotFound
}
if post.PostType != model.PostTypeBounty {
if post.PostType != models.PostTypeBounty {
return errors.New("非悬赏帖")
}
if !isAdmin && post.UserID != operatorID {
return ErrPermissionDenied
}
if post.BountyStatus != model.BountyStatusOpen || post.BountyPoints < 1 {
if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 {
return ErrBountyNotOpen
}
if !isAdmin {
n, err := CountEligibleBountyReplies(model.DB, post.ID, post.UserID)
n, err := CountEligibleBountyReplies(models.DB, post.ID, post.UserID)
if err != nil {
return err
}
@@ -120,31 +120,31 @@ func RefundBounty(postID, operatorID uint, isAdmin bool) error {
}
}
points := post.BountyPoints
return model.DB.Transaction(func(tx *gorm.DB) error {
if _, err := AdjustPointsTx(tx, post.UserID, points, model.PointReasonBountyRefund, "post", postID, "悬赏退回"); err != nil {
return models.DB.Transaction(func(tx *gorm.DB) error {
if _, err := AdjustPointsTx(tx, post.UserID, points, models.PointReasonBountyRefund, "post", postID, "悬赏退回"); err != nil {
return err
}
return tx.Model(&post).Updates(map[string]interface{}{
"bounty_status": model.BountyStatusRefunded,
"bounty_status": models.BountyStatusRefunded,
"bounty_points": 0,
}).Error
})
}
// RefundBountyIfOpen 删帖时自动退回未采纳悬赏
func RefundBountyIfOpen(tx *gorm.DB, post *model.Post) error {
if post == nil || post.PostType != model.PostTypeBounty {
func RefundBountyIfOpen(tx *gorm.DB, post *models.Post) error {
if post == nil || post.PostType != models.PostTypeBounty {
return nil
}
if post.BountyStatus != model.BountyStatusOpen || post.BountyPoints < 1 {
if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 {
return nil
}
points := post.BountyPoints
if _, err := AdjustPointsTx(tx, post.UserID, points, model.PointReasonBountyRefund, "post", post.ID, "删帖退回悬赏"); err != nil {
if _, err := AdjustPointsTx(tx, post.UserID, points, models.PointReasonBountyRefund, "post", post.ID, "删帖退回悬赏"); err != nil {
return err
}
return tx.Model(post).Updates(map[string]interface{}{
"bounty_status": model.BountyStatusRefunded,
"bounty_status": models.BountyStatusRefunded,
"bounty_points": 0,
}).Error
}

View File

@@ -1,11 +1,11 @@
package service
package services
import (
"errors"
"testing"
"github.com/glebarez/sqlite"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
@@ -15,26 +15,26 @@ func setupBountyTestDB(t *testing.T) *gorm.DB {
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.User{}, &model.Post{}, &model.Comment{}, &model.PointLedger{}); err != nil {
if err := db.AutoMigrate(&models.User{}, &models.Post{}, &models.Comment{}, &models.PointLedger{}); err != nil {
t.Fatal(err)
}
prev := model.DB
model.DB = db
t.Cleanup(func() { model.DB = prev })
prev := models.DB
models.DB = db
t.Cleanup(func() { models.DB = prev })
return db
}
func seedBountyPost(t *testing.T, db *gorm.DB, authorID uint, points int) model.Post {
func seedBountyPost(t *testing.T, db *gorm.DB, authorID uint, points int) models.Post {
t.Helper()
post := model.Post{
post := models.Post{
UserID: authorID,
BoardID: 1,
Title: "悬赏测试",
Content: "内容",
PostType: model.PostTypeBounty,
PostType: models.PostTypeBounty,
BountyPoints: points,
BountyStatus: model.BountyStatusOpen,
Status: model.ContentStatusPublished,
BountyStatus: models.BountyStatusOpen,
Status: models.ContentStatusPublished,
}
if err := db.Create(&post).Error; err != nil {
t.Fatal(err)
@@ -44,7 +44,7 @@ func seedBountyPost(t *testing.T, db *gorm.DB, authorID uint, points int) model.
func seedUser(t *testing.T, db *gorm.DB, id uint, points int) {
t.Helper()
u := model.User{
u := models.User{
ID: id,
Username: "user" + string(rune('0'+id)),
Password: "hash",
@@ -58,7 +58,7 @@ func seedUser(t *testing.T, db *gorm.DB, id uint, points int) {
func seedComment(t *testing.T, db *gorm.DB, postID, userID uint, floor int, status string) {
t.Helper()
c := model.Comment{
c := models.Comment{
PostID: postID,
UserID: userID,
Floor: floor,
@@ -79,25 +79,25 @@ func TestCountEligibleBountyReplies(t *testing.T) {
t.Fatalf("无回复时期望 0得到 %d err=%v", n, err)
}
seedComment(t, db, post.ID, 1, 1, model.ContentStatusPublished)
seedComment(t, db, post.ID, 1, 1, models.ContentStatusPublished)
n, err = CountEligibleBountyReplies(db, post.ID, 1)
if err != nil || n != 0 {
t.Fatalf("楼主自己的回复不应计入,得到 %d", n)
}
seedComment(t, db, post.ID, 2, 2, model.ContentStatusPublished)
seedComment(t, db, post.ID, 2, 2, models.ContentStatusPublished)
n, err = CountEligibleBountyReplies(db, post.ID, 1)
if err != nil || n != 1 {
t.Fatalf("他人 published 回复期望 1得到 %d", n)
}
seedComment(t, db, post.ID, 3, 3, model.ContentStatusPending)
seedComment(t, db, post.ID, 3, 3, models.ContentStatusPending)
n, err = CountEligibleBountyReplies(db, post.ID, 1)
if err != nil || n != 1 {
t.Fatalf("pending 回复不应增加计数,得到 %d", n)
}
seedComment(t, db, post.ID, 0, 4, model.ContentStatusPublished)
seedComment(t, db, post.ID, 0, 4, models.ContentStatusPublished)
n, err = CountEligibleBountyReplies(db, post.ID, 1)
if err != nil || n != 2 {
t.Fatalf("游客回复应计入,得到 %d", n)
@@ -113,7 +113,7 @@ func TestCanRefundBounty(t *testing.T) {
t.Fatalf("无回复时楼主应可退can=%v reason=%q", can, reason)
}
seedComment(t, db, post.ID, 2, 1, model.ContentStatusPublished)
seedComment(t, db, post.ID, 2, 1, models.ContentStatusPublished)
can, reason = CanRefundBounty(&post, false)
if can || reason != bountyRefundBlockReason {
t.Fatalf("有他人回复时楼主不可退can=%v reason=%q", can, reason)
@@ -130,7 +130,7 @@ func TestRefundBountyBlockedForAuthorWithReplies(t *testing.T) {
seedUser(t, db, 1, 0)
seedUser(t, db, 2, 0)
post := seedBountyPost(t, db, 1, 8)
seedComment(t, db, post.ID, 2, 1, model.ContentStatusPublished)
seedComment(t, db, post.ID, 2, 1, models.ContentStatusPublished)
err := RefundBounty(post.ID, 1, false)
if !errors.Is(err, ErrBountyRefundBlocked) {
@@ -146,14 +146,14 @@ func TestRefundBountyAllowedWithoutReplies(t *testing.T) {
if err := RefundBounty(post.ID, 1, false); err != nil {
t.Fatalf("无回复时楼主应可退回err=%v", err)
}
var updated model.Post
var updated models.Post
if err := db.First(&updated, post.ID).Error; err != nil {
t.Fatal(err)
}
if updated.BountyStatus != model.BountyStatusRefunded || updated.BountyPoints != 0 {
if updated.BountyStatus != models.BountyStatusRefunded || updated.BountyPoints != 0 {
t.Fatalf("状态应为 refunded 且积分为 0得到 status=%s points=%d", updated.BountyStatus, updated.BountyPoints)
}
var author model.User
var author models.User
if err := db.First(&author, 1).Error; err != nil {
t.Fatal(err)
}
@@ -167,7 +167,7 @@ func TestRefundBountyAdminBypassWithReplies(t *testing.T) {
seedUser(t, db, 1, 0)
seedUser(t, db, 2, 0)
post := seedBountyPost(t, db, 1, 4)
seedComment(t, db, post.ID, 2, 1, model.ContentStatusPublished)
seedComment(t, db, post.ID, 2, 1, models.ContentStatusPublished)
if err := RefundBounty(post.ID, 99, true); err != nil {
t.Fatalf("管理员应可强制退回err=%v", err)

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"crypto/rand"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"errors"
@@ -7,7 +7,7 @@ import (
"gorm.io/gorm"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
type CommentService struct {
@@ -25,9 +25,9 @@ func (s *CommentService) HasUserReplied(postID, userID uint) bool {
return false
}
var count int64
err := model.DB.Model(&model.Comment{}).
err := models.DB.Model(&models.Comment{}).
Where("post_id = ? AND user_id = ? AND status IN ?", postID, userID,
[]string{model.ContentStatusPublished, model.ContentStatusPending}).
[]string{models.ContentStatusPublished, models.ContentStatusPending}).
Limit(1).
Count(&count).Error
return err == nil && count > 0
@@ -44,7 +44,7 @@ type CommentCreateInput struct {
IsPrivate bool
}
func (s *CommentService) canViewPrivate(c model.Comment, viewerID uint, isAdmin bool, postAuthorID uint, guestSet map[uint]struct{}) bool {
func (s *CommentService) canViewPrivate(c models.Comment, viewerID uint, isAdmin bool, postAuthorID uint, guestSet map[uint]struct{}) bool {
if !c.IsPrivate {
return true
}
@@ -63,8 +63,8 @@ func (s *CommentService) canViewPrivate(c model.Comment, viewerID uint, isAdmin
return false
}
func (s *CommentService) fillReplyTargets(comments []model.Comment, loadMissing bool) {
idMap := make(map[uint]model.Comment, len(comments))
func (s *CommentService) fillReplyTargets(comments []models.Comment, loadMissing bool) {
idMap := make(map[uint]models.Comment, len(comments))
for _, c := range comments {
idMap[c.ID] = c
}
@@ -78,27 +78,27 @@ func (s *CommentService) fillReplyTargets(comments []model.Comment, loadMissing
continue
}
if loadMissing {
var target model.Comment
if model.DB.Preload("User").First(&target, *comments[i].ReplyTo).Error == nil {
var target models.Comment
if models.DB.Preload("User").First(&target, *comments[i].ReplyTo).Error == nil {
comments[i].ReplyTarget = &target
}
}
}
}
func canViewComment(c model.Comment, viewerID uint, isAdmin bool) bool {
if isAdmin || c.Status == model.ContentStatusPublished || c.Status == "" {
func canViewComment(c models.Comment, viewerID uint, isAdmin bool) bool {
if isAdmin || c.Status == models.ContentStatusPublished || c.Status == "" {
return true
}
if c.Status == model.ContentStatusPending || c.Status == model.ContentStatusRejected {
if c.Status == models.ContentStatusPending || c.Status == models.ContentStatusRejected {
return viewerID > 0 && c.UserID == viewerID
}
return false
}
func (s *CommentService) ListByPost(postID, viewerID uint, isAdmin bool, postAuthorID uint, visibleGuestIDs []uint) ([]model.Comment, error) {
var comments []model.Comment
err := model.DB.Preload("User").Where("post_id = ?", postID).Order("floor asc").Find(&comments).Error
func (s *CommentService) ListByPost(postID, viewerID uint, isAdmin bool, postAuthorID uint, visibleGuestIDs []uint) ([]models.Comment, error) {
var comments []models.Comment
err := models.DB.Preload("User").Where("post_id = ?", postID).Order("floor asc").Find(&comments).Error
if err != nil {
return nil, err
}
@@ -108,12 +108,12 @@ func (s *CommentService) ListByPost(postID, viewerID uint, isAdmin bool, postAut
guestSet[id] = struct{}{}
}
allByID := make(map[uint]model.Comment, len(comments))
allByID := make(map[uint]models.Comment, len(comments))
for _, c := range comments {
allByID[c.ID] = c
}
visible := make([]model.Comment, 0, len(comments))
visible := make([]models.Comment, 0, len(comments))
visibleIDs := make(map[uint]struct{}, len(comments))
for i := range comments {
if !canViewComment(comments[i], viewerID, isAdmin) {
@@ -145,7 +145,7 @@ func (s *CommentService) ListByPost(postID, viewerID uint, isAdmin bool, postAut
}
// resolveThreadParent 计算嵌套展示父节点:优先直接父评论,否则沿 reply_to 向上找到最近可见祖先
func resolveThreadParent(replyTo *uint, visibleIDs map[uint]struct{}, allByID map[uint]model.Comment) *uint {
func resolveThreadParent(replyTo *uint, visibleIDs map[uint]struct{}, allByID map[uint]models.Comment) *uint {
if replyTo == nil {
return nil
}
@@ -169,7 +169,7 @@ func resolveThreadParent(replyTo *uint, visibleIDs map[uint]struct{}, allByID ma
return nil
}
func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) {
func (s *CommentService) Create(in CommentCreateInput) (*models.Comment, error) {
content := SanitizePostHTML(strings.TrimSpace(in.Content))
content = s.filter.Filter(content)
if content == "" {
@@ -179,16 +179,16 @@ func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) {
return nil, err
}
var post model.Post
if err := model.DB.First(&post, in.PostID).Error; err != nil {
var post models.Post
if err := models.DB.First(&post, in.PostID).Error; err != nil {
return nil, ErrPostNotFound
}
if in.UserID == 0 {
return nil, errors.New("请登录后评论")
}
var user model.User
if err := model.DB.First(&user, in.UserID).Error; err != nil {
var user models.User
if err := models.DB.First(&user, in.UserID).Error; err != nil {
return nil, errors.New("用户不存在")
}
if user.Banned {
@@ -201,31 +201,31 @@ func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) {
}
// 未公开帖仅作者/管理员可评论
if post.Status != model.ContentStatusPublished && post.Status != "" {
if user.Role != model.RoleAdmin && post.UserID != in.UserID {
if post.Status != models.ContentStatusPublished && post.Status != "" {
if user.Role != models.RoleAdmin && post.UserID != in.UserID {
return nil, errors.New("帖子审核中,暂不可评论")
}
}
var maxFloor int
model.DB.Model(&model.Comment{}).Where("post_id = ?", in.PostID).Select("COALESCE(MAX(floor), 0)").Scan(&maxFloor)
models.DB.Model(&models.Comment{}).Where("post_id = ?", in.PostID).Select("COALESCE(MAX(floor), 0)").Scan(&maxFloor)
if in.ReplyTo != nil {
var target model.Comment
if err := model.DB.Where("id = ? AND post_id = ?", *in.ReplyTo, in.PostID).First(&target).Error; err != nil {
var target models.Comment
if err := models.DB.Where("id = ? AND post_id = ?", *in.ReplyTo, in.PostID).First(&target).Error; err != nil {
return nil, ErrCommentNotFound
}
if !canViewComment(target, in.UserID, user.Role == model.RoleAdmin) {
if !canViewComment(target, in.UserID, user.Role == models.RoleAdmin) {
return nil, ErrCommentNotFound
}
}
status := model.ContentStatusPending
status := models.ContentStatusPending
if user.SkipsModeration() {
status = model.ContentStatusPublished
status = models.ContentStatusPublished
}
comment := &model.Comment{
comment := &models.Comment{
PostID: in.PostID,
UserID: in.UserID,
Floor: maxFloor + 1,
@@ -237,10 +237,10 @@ func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) {
IsPrivate: in.IsPrivate,
Status: status,
}
if err := model.DB.Create(comment).Error; err != nil {
if err := models.DB.Create(comment).Error; err != nil {
return nil, err
}
if status == model.ContentStatusPublished {
if status == models.ContentStatusPublished {
AddExp(in.UserID, 2)
}
return comment, nil
@@ -249,39 +249,39 @@ func (s *CommentService) Create(in CommentCreateInput) (*model.Comment, error) {
// SetStatus 设置评论审核状态
func (s *CommentService) SetStatus(commentID uint, status string) error {
switch status {
case model.ContentStatusPending, model.ContentStatusPublished, model.ContentStatusRejected:
case models.ContentStatusPending, models.ContentStatusPublished, models.ContentStatusRejected:
default:
return errors.New("无效的审核状态")
}
var comment model.Comment
if err := model.DB.Select("id", "user_id", "status").First(&comment, commentID).Error; err != nil {
var comment models.Comment
if err := models.DB.Select("id", "user_id", "status").First(&comment, commentID).Error; err != nil {
return ErrCommentNotFound
}
prev := comment.Status
res := model.DB.Model(&model.Comment{}).Where("id = ?", commentID).Update("status", status)
res := models.DB.Model(&models.Comment{}).Where("id = ?", commentID).Update("status", status)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return ErrCommentNotFound
}
if status == model.ContentStatusPublished && prev != model.ContentStatusPublished && comment.UserID > 0 {
if status == models.ContentStatusPublished && prev != models.ContentStatusPublished && comment.UserID > 0 {
AddExp(comment.UserID, 2)
}
return nil
}
// GetByID 获取评论
func (s *CommentService) GetByID(id uint) (*model.Comment, error) {
var c model.Comment
if err := model.DB.Preload("User").Preload("Post").First(&c, id).Error; err != nil {
func (s *CommentService) GetByID(id uint) (*models.Comment, error) {
var c models.Comment
if err := models.DB.Preload("User").Preload("Post").First(&c, id).Error; err != nil {
return nil, ErrCommentNotFound
}
return &c, nil
}
// fillLiked 批量标记当前用户是否已点赞
func (s *CommentService) fillLiked(comments []model.Comment, viewerID uint) {
func (s *CommentService) fillLiked(comments []models.Comment, viewerID uint) {
if viewerID == 0 || len(comments) == 0 {
return
}
@@ -289,8 +289,8 @@ func (s *CommentService) fillLiked(comments []model.Comment, viewerID uint) {
for _, c := range comments {
ids = append(ids, c.ID)
}
var likes []model.CommentLike
model.DB.Where("user_id = ? AND comment_id IN ?", viewerID, ids).Find(&likes)
var likes []models.CommentLike
models.DB.Where("user_id = ? AND comment_id IN ?", viewerID, ids).Find(&likes)
likedSet := make(map[uint]struct{}, len(likes))
for _, l := range likes {
likedSet[l.CommentID] = struct{}{}
@@ -302,29 +302,29 @@ func (s *CommentService) fillLiked(comments []model.Comment, viewerID uint) {
// ToggleLike 切换评论点赞
func (s *CommentService) ToggleLike(userID, commentID uint) (liked bool, likeCount int, err error) {
var comment model.Comment
if err := model.DB.Select("id", "like_count").First(&comment, commentID).Error; err != nil {
var comment models.Comment
if err := models.DB.Select("id", "like_count").First(&comment, commentID).Error; err != nil {
return false, 0, ErrCommentNotFound
}
var like model.CommentLike
result := model.DB.Where("comment_id = ? AND user_id = ?", commentID, userID).Limit(1).Find(&like)
var like models.CommentLike
result := models.DB.Where("comment_id = ? AND user_id = ?", commentID, userID).Limit(1).Find(&like)
if result.Error != nil {
return false, 0, result.Error
}
if result.RowsAffected > 0 {
if err := model.DB.Delete(&like).Error; err != nil {
if err := models.DB.Delete(&like).Error; err != nil {
return false, 0, err
}
model.DB.Model(&model.Comment{}).Where("id = ?", commentID).UpdateColumn("like_count", gorm.Expr("CASE WHEN like_count > 0 THEN like_count - 1 ELSE 0 END"))
_ = model.DB.Select("like_count").First(&comment, commentID)
models.DB.Model(&models.Comment{}).Where("id = ?", commentID).UpdateColumn("like_count", gorm.Expr("CASE WHEN like_count > 0 THEN like_count - 1 ELSE 0 END"))
_ = models.DB.Select("like_count").First(&comment, commentID)
return false, comment.LikeCount, nil
}
like = model.CommentLike{CommentID: commentID, UserID: userID}
if err := model.DB.Create(&like).Error; err != nil {
like = models.CommentLike{CommentID: commentID, UserID: userID}
if err := models.DB.Create(&like).Error; err != nil {
return false, 0, err
}
model.DB.Model(&model.Comment{}).Where("id = ?", commentID).UpdateColumn("like_count", gorm.Expr("like_count + 1"))
_ = model.DB.Select("like_count").First(&comment, commentID)
models.DB.Model(&models.Comment{}).Where("id = ?", commentID).UpdateColumn("like_count", gorm.Expr("like_count + 1"))
_ = models.DB.Select("like_count").First(&comment, commentID)
return true, comment.LikeCount, nil
}
@@ -334,14 +334,14 @@ func (s *CommentService) IsLiked(userID, commentID uint) bool {
return false
}
var count int64
model.DB.Model(&model.CommentLike{}).Where("comment_id = ? AND user_id = ?", commentID, userID).Count(&count)
models.DB.Model(&models.CommentLike{}).Where("comment_id = ? AND user_id = ?", commentID, userID).Count(&count)
return count > 0
}
// PendingCommentCount 待审评论数
func (s *CommentService) PendingCommentCount() (int64, error) {
var n int64
err := model.DB.Model(&model.Comment{}).Where("status = ?", model.ContentStatusPending).Count(&n).Error
err := models.DB.Model(&models.Comment{}).Where("status = ?", models.ContentStatusPending).Count(&n).Error
return n, err
}
@@ -353,8 +353,8 @@ func (s *CommentService) Delete(userID, commentID uint, isAdmin bool) error {
}
func (s *CommentService) Update(userID, commentID uint, isAdmin, skipModeration bool, content string) (string, bool, error) {
var comment model.Comment
if err := model.DB.First(&comment, commentID).Error; err != nil {
var comment models.Comment
if err := models.DB.First(&comment, commentID).Error; err != nil {
return "", false, ErrCommentNotFound
}
if !isAdmin && (comment.UserID == 0 || comment.UserID != userID) {
@@ -380,8 +380,8 @@ func (s *CommentService) Update(userID, commentID uint, isAdmin, skipModeration
}
enteredPending := false
err := model.DB.Transaction(func(tx *gorm.DB) error {
rev := model.CommentRevision{
err := models.DB.Transaction(func(tx *gorm.DB) error {
rev := models.CommentRevision{
CommentID: commentID,
EditorID: userID,
Content: comment.Content,
@@ -391,7 +391,7 @@ func (s *CommentService) Update(userID, commentID uint, isAdmin, skipModeration
}
updates := map[string]interface{}{"content": content}
if !skipModeration {
updates["status"] = model.ContentStatusPending
updates["status"] = models.ContentStatusPending
enteredPending = true
}
return tx.Model(&comment).Updates(updates).Error
@@ -413,11 +413,11 @@ func collectReplySubtreeIDs(db *gorm.DB, rootID uint, softDeletedOnly bool) ([]u
seen := map[uint]struct{}{rootID: {}}
frontier := []uint{rootID}
for len(frontier) > 0 {
childQ := q.Model(&model.Comment{}).Select("id").Where("reply_to IN ?", frontier)
childQ := q.Model(&models.Comment{}).Select("id").Where("reply_to IN ?", frontier)
if softDeletedOnly {
childQ = childQ.Where("deleted_at IS NOT NULL")
}
var children []model.Comment
var children []models.Comment
if err := childQ.Find(&children).Error; err != nil {
return nil, err
}
@@ -436,20 +436,20 @@ func collectReplySubtreeIDs(db *gorm.DB, rootID uint, softDeletedOnly bool) ([]u
// AdminDelete 软删除评论及其回复树(进入回收站);修订与点赞保留以便恢复
func (s *CommentService) AdminDelete(commentID uint) error {
var root model.Comment
if err := model.DB.First(&root, commentID).Error; err != nil {
var root models.Comment
if err := models.DB.First(&root, commentID).Error; err != nil {
return ErrCommentNotFound
}
ids, err := collectReplySubtreeIDs(model.DB, commentID, false)
ids, err := collectReplySubtreeIDs(models.DB, commentID, false)
if err != nil {
return err
}
return model.DB.Where("id IN ?", ids).Delete(&model.Comment{}).Error
return models.DB.Where("id IN ?", ids).Delete(&models.Comment{}).Error
}
// TrashCommentItem 评论回收站列表项
type TrashCommentItem struct {
model.Comment
models.Comment
DeletedAt time.Time `json:"deleted_at"`
}
@@ -459,7 +459,7 @@ func (s *CommentService) ListTrash(page, size int, keyword string) ([]TrashComme
page = 1
}
size = s.settings.NormalizePageSize(size)
db := model.DB.Unscoped().Model(&model.Comment{}).
db := models.DB.Unscoped().Model(&models.Comment{}).
Where("comments.deleted_at IS NOT NULL").
Joins("JOIN posts ON posts.id = comments.post_id AND posts.deleted_at IS NULL").
Preload("User").Preload("Post")
@@ -475,7 +475,7 @@ func (s *CommentService) ListTrash(page, size int, keyword string) ([]TrashComme
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var comments []model.Comment
var comments []models.Comment
if err := db.Order("comments.deleted_at DESC").Offset((page - 1) * size).Limit(size).Find(&comments).Error; err != nil {
return nil, 0, err
}
@@ -491,65 +491,65 @@ func (s *CommentService) ListTrash(page, size int, keyword string) ([]TrashComme
// Restore 从回收站恢复评论及其已软删的回复树
func (s *CommentService) Restore(commentID uint) error {
var comment model.Comment
if err := model.DB.Unscoped().First(&comment, commentID).Error; err != nil {
var comment models.Comment
if err := models.DB.Unscoped().First(&comment, commentID).Error; err != nil {
return ErrCommentNotFound
}
if !comment.DeletedAt.Valid {
return errors.New("评论未被删除")
}
// 所属帖子必须仍存在且未删除
var post model.Post
if err := model.DB.First(&post, comment.PostID).Error; err != nil {
var post models.Post
if err := models.DB.First(&post, comment.PostID).Error; err != nil {
return errors.New("所属帖子不存在或已在回收站,请先恢复帖子")
}
ids, err := collectReplySubtreeIDs(model.DB, commentID, true)
ids, err := collectReplySubtreeIDs(models.DB, commentID, true)
if err != nil {
return err
}
return model.DB.Unscoped().Model(&model.Comment{}).
return models.DB.Unscoped().Model(&models.Comment{}).
Where("id IN ?", ids).
Update("deleted_at", nil).Error
}
// Purge 永久删除回收站中的评论及其已软删回复(含修订、点赞)
func (s *CommentService) Purge(commentID uint) error {
var comment model.Comment
if err := model.DB.Unscoped().First(&comment, commentID).Error; err != nil {
var comment models.Comment
if err := models.DB.Unscoped().First(&comment, commentID).Error; err != nil {
return ErrCommentNotFound
}
if !comment.DeletedAt.Valid {
return errors.New("仅可彻底删除回收站中的评论,请先删除评论")
}
ids, err := collectReplySubtreeIDs(model.DB, commentID, true)
ids, err := collectReplySubtreeIDs(models.DB, commentID, true)
if err != nil {
return err
}
return model.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("comment_id IN ?", ids).Delete(&model.CommentRevision{}).Error; err != nil {
return models.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("comment_id IN ?", ids).Delete(&models.CommentRevision{}).Error; err != nil {
return err
}
if err := tx.Where("comment_id IN ?", ids).Delete(&model.CommentLike{}).Error; err != nil {
if err := tx.Where("comment_id IN ?", ids).Delete(&models.CommentLike{}).Error; err != nil {
return err
}
return tx.Unscoped().Where("id IN ?", ids).Delete(&model.Comment{}).Error
return tx.Unscoped().Where("id IN ?", ids).Delete(&models.Comment{}).Error
})
}
// ListRevisions 评论编辑历史(管理员查看)
func (s *CommentService) ListRevisions(commentID uint) ([]model.CommentRevision, error) {
func (s *CommentService) ListRevisions(commentID uint) ([]models.CommentRevision, error) {
if _, err := s.GetByID(commentID); err != nil {
return nil, err
}
var revs []model.CommentRevision
err := model.DB.Preload("Editor").
var revs []models.CommentRevision
err := models.DB.Preload("Editor").
Where("comment_id = ?", commentID).
Order("id desc").Find(&revs).Error
if err != nil {
return nil, err
}
if revs == nil {
revs = []model.CommentRevision{}
revs = []models.CommentRevision{}
}
return revs, nil
}
@@ -572,9 +572,9 @@ func (s *CommentService) ListRecentPublic(limit int) ([]RecentCommentItem, error
if limit < 1 {
limit = 8
}
var comments []model.Comment
err := model.DB.Preload("User").Preload("Post").
Where("is_private = ? AND status = ?", false, model.ContentStatusPublished).
var comments []models.Comment
err := models.DB.Preload("User").Preload("Post").
Where("is_private = ? AND status = ?", false, models.ContentStatusPublished).
Order("id desc").Limit(limit * 2). // 多取一些以跳过已删帖
Find(&comments).Error
if err != nil {
@@ -630,21 +630,21 @@ func truncateRunes(s string, n int) string {
}
// ListRecent 管理员查看最近评论
func (s *CommentService) ListRecent(page, size int, status string) ([]model.Comment, int64, error) {
func (s *CommentService) ListRecent(page, size int, status string) ([]models.Comment, int64, error) {
if page < 1 {
page = 1
}
if size < 1 {
size = 20
}
db := model.DB.Model(&model.Comment{})
db := models.DB.Model(&models.Comment{})
switch status {
case model.ContentStatusPending, model.ContentStatusPublished, model.ContentStatusRejected:
case models.ContentStatusPending, models.ContentStatusPublished, models.ContentStatusRejected:
db = db.Where("status = ?", status)
}
var total int64
db.Count(&total)
var comments []model.Comment
var comments []models.Comment
err := db.Preload("User").Preload("Post").
Order("CASE WHEN status = 'pending' THEN 0 ELSE 1 END, id DESC").
Offset((page - 1) * size).Limit(size).Find(&comments).Error

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"errors"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"regexp"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"strings"

View File

@@ -1,4 +1,4 @@
package service
package services
import "strings"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"crypto/rand"
@@ -8,7 +8,7 @@ import (
"sync"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
const (
@@ -65,8 +65,8 @@ func (s *EmailCodeService) sendCode(purpose, email string) error {
return err
}
var exist model.User
found := model.DB.Where("email = ?", email).First(&exist).Error == nil
var exist models.User
found := models.DB.Where("email = ?", email).First(&exist).Error == nil
switch purpose {
case EmailCodePurposeRegister:
if found {

View File

@@ -1,4 +1,4 @@
package service
package services
import "os"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"os"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"errors"
@@ -8,7 +8,7 @@ import (
"time"
"unicode/utf8"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
@@ -41,7 +41,7 @@ type FriendLinkApplyInput struct {
}
type FriendLinkApplyCreateResult struct {
Apply *model.FriendLinkApply
Apply *models.FriendLinkApply
}
type FriendLinkApplyService struct {
@@ -106,41 +106,41 @@ func (s *FriendLinkApplyService) Create(in FriendLinkApplyInput) (*FriendLinkApp
return nil, ErrFriendLinkApplyPending
}
apply := &model.FriendLinkApply{
apply := &models.FriendLinkApply{
UserID: in.UserID,
Name: name,
URL: href,
Logo: logo,
ReciprocalPageURL: reciprocal,
LinkOnHomepage: in.LinkOnHomepage,
Status: model.FriendLinkApplyStatusPending,
Status: models.FriendLinkApplyStatusPending,
}
if err := model.DB.Create(apply).Error; err != nil {
if err := models.DB.Create(apply).Error; err != nil {
return nil, err
}
s.startReciprocalCheck(apply.ID, reciprocal, in.OurSiteURL)
_ = model.DB.Preload("User").First(apply, apply.ID).Error
_ = models.DB.Preload("User").First(apply, apply.ID).Error
return &FriendLinkApplyCreateResult{Apply: apply}, nil
}
// PendingCount 待审数量
func (s *FriendLinkApplyService) PendingCount() (int64, error) {
var n int64
err := model.DB.Model(&model.FriendLinkApply{}).
Where("status = ?", model.FriendLinkApplyStatusPending).
err := models.DB.Model(&models.FriendLinkApply{}).
Where("status = ?", models.FriendLinkApplyStatusPending).
Count(&n).Error
return n, err
}
// ListAdmin 管理员列表
func (s *FriendLinkApplyService) ListAdmin(q FriendLinkApplyListQuery) ([]model.FriendLinkApply, int64, error) {
func (s *FriendLinkApplyService) ListAdmin(q FriendLinkApplyListQuery) ([]models.FriendLinkApply, int64, error) {
if q.Page < 1 {
q.Page = 1
}
if q.Size < 1 || q.Size > 50 {
q.Size = 20
}
db := model.DB.Model(&model.FriendLinkApply{})
db := models.DB.Model(&models.FriendLinkApply{})
status := strings.TrimSpace(q.Status)
if status != "" && status != "all" {
db = db.Where("status = ?", status)
@@ -149,7 +149,7 @@ func (s *FriendLinkApplyService) ListAdmin(q FriendLinkApplyListQuery) ([]model.
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var list []model.FriendLinkApply
var list []models.FriendLinkApply
err := db.Preload("User").
Order("CASE WHEN status = 'pending' THEN 0 ELSE 1 END, id DESC").
Offset((q.Page - 1) * q.Size).
@@ -161,22 +161,22 @@ func (s *FriendLinkApplyService) ListAdmin(q FriendLinkApplyListQuery) ([]model.
return list, total, nil
}
func (s *FriendLinkApplyService) getPending(id uint) (*model.FriendLinkApply, error) {
var apply model.FriendLinkApply
if err := model.DB.Preload("User").First(&apply, id).Error; err != nil {
func (s *FriendLinkApplyService) getPending(id uint) (*models.FriendLinkApply, error) {
var apply models.FriendLinkApply
if err := models.DB.Preload("User").First(&apply, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrFriendLinkApplyNotFound
}
return nil, err
}
if apply.Status != model.FriendLinkApplyStatusPending {
if apply.Status != models.FriendLinkApplyStatusPending {
return nil, ErrFriendLinkApplyHandled
}
return &apply, nil
}
// Approve 通过申请并写入友链
func (s *FriendLinkApplyService) Approve(id uint) (*model.FriendLinkApply, error) {
func (s *FriendLinkApplyService) Approve(id uint) (*models.FriendLinkApply, error) {
apply, err := s.getPending(id)
if err != nil {
return nil, err
@@ -211,13 +211,13 @@ func (s *FriendLinkApplyService) Approve(id uint) (*model.FriendLinkApply, error
}
now := time.Now()
if err := model.DB.Model(apply).Updates(map[string]interface{}{
"status": model.FriendLinkApplyStatusApproved,
if err := models.DB.Model(apply).Updates(map[string]interface{}{
"status": models.FriendLinkApplyStatusApproved,
"reviewed_at": now,
}).Error; err != nil {
return nil, err
}
apply.Status = model.FriendLinkApplyStatusApproved
apply.Status = models.FriendLinkApplyStatusApproved
apply.ReviewedAt = &now
if s.messages != nil && apply.UserID > 0 {
@@ -226,27 +226,27 @@ func (s *FriendLinkApplyService) Approve(id uint) (*model.FriendLinkApply, error
"你申请的友情链接「%s」%s已通过审核现已展示在友情链接页面。\n\n如有疑问可回复本私信联系管理员。",
apply.Name, apply.URL,
)
_, _ = s.messages.SendSystem(apply.UserID, subject, content, model.MessageKindSystem, nil, nil)
_, _ = s.messages.SendSystem(apply.UserID, subject, content, models.MessageKindSystem, nil, nil)
}
return apply, nil
}
// Reject 拒绝申请
func (s *FriendLinkApplyService) Reject(id uint, note string) (*model.FriendLinkApply, error) {
func (s *FriendLinkApplyService) Reject(id uint, note string) (*models.FriendLinkApply, error) {
apply, err := s.getPending(id)
if err != nil {
return nil, err
}
note = strings.TrimSpace(note)
now := time.Now()
if err := model.DB.Model(apply).Updates(map[string]interface{}{
"status": model.FriendLinkApplyStatusRejected,
if err := models.DB.Model(apply).Updates(map[string]interface{}{
"status": models.FriendLinkApplyStatusRejected,
"review_note": note,
"reviewed_at": now,
}).Error; err != nil {
return nil, err
}
apply.Status = model.FriendLinkApplyStatusRejected
apply.Status = models.FriendLinkApplyStatusRejected
apply.ReviewNote = note
apply.ReviewedAt = &now
@@ -260,7 +260,7 @@ func (s *FriendLinkApplyService) Reject(id uint, note string) (*model.FriendLink
"你申请的友情链接「%s」%s未通过审核。\n\n原因\n%s\n\n如有疑问可回复本私信联系管理员。",
apply.Name, apply.URL, reason,
)
_, _ = s.messages.SendSystem(apply.UserID, subject, content, model.MessageKindReject, nil, nil)
_, _ = s.messages.SendSystem(apply.UserID, subject, content, models.MessageKindReject, nil, nil)
}
return apply, nil
}
@@ -302,8 +302,8 @@ func (s *FriendLinkApplyService) prepareApplyFields(in FriendLinkApplyInput, all
}
func (s *FriendLinkApplyService) hasPendingApplyForURL(userID, excludeID uint, href string) (bool, error) {
db := model.DB.Model(&model.FriendLinkApply{}).
Where("user_id = ? AND status = ? AND url = ?", userID, model.FriendLinkApplyStatusPending, href)
db := models.DB.Model(&models.FriendLinkApply{}).
Where("user_id = ? AND status = ? AND url = ?", userID, models.FriendLinkApplyStatusPending, href)
if excludeID > 0 {
db = db.Where("id <> ?", excludeID)
}
@@ -346,8 +346,8 @@ func (s *FriendLinkApplyService) removePublishedFriendLink(href string) error {
// Update 修改并重新提交友链申请(待审 / 已拒绝 / 已通过)
func (s *FriendLinkApplyService) Update(userID, id uint, in FriendLinkApplyInput) (*FriendLinkApplyCreateResult, error) {
var apply model.FriendLinkApply
if err := model.DB.First(&apply, id).Error; err != nil {
var apply models.FriendLinkApply
if err := models.DB.First(&apply, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrFriendLinkApplyNotFound
}
@@ -356,13 +356,13 @@ func (s *FriendLinkApplyService) Update(userID, id uint, in FriendLinkApplyInput
if apply.UserID != userID {
return nil, errors.New("无权操作该申请")
}
if apply.Status != model.FriendLinkApplyStatusPending &&
apply.Status != model.FriendLinkApplyStatusRejected &&
apply.Status != model.FriendLinkApplyStatusApproved {
if apply.Status != models.FriendLinkApplyStatusPending &&
apply.Status != models.FriendLinkApplyStatusRejected &&
apply.Status != models.FriendLinkApplyStatusApproved {
return nil, errors.New("该申请不可修改")
}
wasApproved := apply.Status == model.FriendLinkApplyStatusApproved
wasApproved := apply.Status == models.FriendLinkApplyStatusApproved
allowPublishedURL := ""
if wasApproved {
allowPublishedURL = apply.URL
@@ -395,25 +395,25 @@ func (s *FriendLinkApplyService) Update(userID, id uint, in FriendLinkApplyInput
"reciprocal_verified": false,
"reciprocal_check_note": "",
"reciprocal_checked_at": nil,
"status": model.FriendLinkApplyStatusPending,
"status": models.FriendLinkApplyStatusPending,
"review_note": "",
"reviewed_at": nil,
}
if err := model.DB.Model(&apply).Updates(updates).Error; err != nil {
if err := models.DB.Model(&apply).Updates(updates).Error; err != nil {
return nil, err
}
s.startReciprocalCheck(apply.ID, reciprocal, in.OurSiteURL)
_ = model.DB.Preload("User").First(&apply, apply.ID).Error
_ = models.DB.Preload("User").First(&apply, apply.ID).Error
return &FriendLinkApplyCreateResult{Apply: &apply}, nil
}
// RecheckReciprocal 管理员触发重新检测回链
func (s *FriendLinkApplyService) RecheckReciprocal(id uint, ourSiteURL string) (*model.FriendLinkApply, error) {
func (s *FriendLinkApplyService) RecheckReciprocal(id uint, ourSiteURL string) (*models.FriendLinkApply, error) {
if !s.settings.FriendLinkReciprocalCheckEnabled() {
return nil, errors.New("回链检测已关闭")
}
var apply model.FriendLinkApply
if err := model.DB.Preload("User").First(&apply, id).Error; err != nil {
var apply models.FriendLinkApply
if err := models.DB.Preload("User").First(&apply, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrFriendLinkApplyNotFound
}
@@ -437,7 +437,7 @@ func (s *FriendLinkApplyService) startReciprocalCheck(applyID uint, pageURL, our
return
}
now := time.Now()
_ = model.DB.Model(&model.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{
_ = models.DB.Model(&models.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{
"reciprocal_verified": false,
"reciprocal_check_note": "",
"reciprocal_checked_at": now,
@@ -445,9 +445,9 @@ func (s *FriendLinkApplyService) startReciprocalCheck(applyID uint, pageURL, our
}
// ListMine 当前用户的友链申请
func (s *FriendLinkApplyService) ListMine(userID uint) ([]model.FriendLinkApply, error) {
var list []model.FriendLinkApply
err := model.DB.Where("user_id = ?", userID).
func (s *FriendLinkApplyService) ListMine(userID uint) ([]models.FriendLinkApply, error) {
var list []models.FriendLinkApply
err := models.DB.Where("user_id = ?", userID).
Order("id DESC").
Limit(50).
Find(&list).Error
@@ -456,8 +456,8 @@ func (s *FriendLinkApplyService) ListMine(userID uint) ([]model.FriendLinkApply,
// Cancel 撤销待审申请
func (s *FriendLinkApplyService) Cancel(userID, id uint) error {
var apply model.FriendLinkApply
if err := model.DB.First(&apply, id).Error; err != nil {
var apply models.FriendLinkApply
if err := models.DB.First(&apply, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrFriendLinkApplyNotFound
}
@@ -466,8 +466,8 @@ func (s *FriendLinkApplyService) Cancel(userID, id uint) error {
if apply.UserID != userID {
return errors.New("无权操作该申请")
}
if apply.Status != model.FriendLinkApplyStatusPending {
if apply.Status != models.FriendLinkApplyStatusPending {
return ErrFriendLinkApplyHandled
}
return model.DB.Delete(&apply).Error
return models.DB.Delete(&apply).Error
}

View File

@@ -1,10 +1,10 @@
package service
package services
import (
"encoding/json"
"strings"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
// EnrichFriendLinksLogos 为缺少 LOGO 的已发布友链,从已通过申请中按 URL 回填
@@ -27,9 +27,9 @@ func EnrichFriendLinksLogos(links []FriendLink) []FriendLink {
return links
}
var applies []model.FriendLinkApply
_ = model.DB.
Where("status = ? AND logo <> ''", model.FriendLinkApplyStatusApproved).
var applies []models.FriendLinkApply
_ = models.DB.
Where("status = ? AND logo <> ''", models.FriendLinkApplyStatusApproved).
Order("id DESC").
Find(&applies).Error

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"context"

View File

@@ -1,10 +1,10 @@
package service
package services
import (
"sync"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
const reciprocalCheckConcurrency = 3
@@ -45,7 +45,7 @@ func runReciprocalCheck(applyID uint, gen uint64, pageURL, ourSiteURL string) {
}
reciprocalCheckMu.Unlock()
_ = model.DB.Model(&model.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{
_ = models.DB.Model(&models.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{
"reciprocal_verified": verified,
"reciprocal_check_note": note,
"reciprocal_checked_at": now,
@@ -54,7 +54,7 @@ func runReciprocalCheck(applyID uint, gen uint64, pageURL, ourSiteURL string) {
// ResetReciprocalCheckState 重置为检测中,供重新检测使用
func ResetReciprocalCheckState(applyID uint) {
_ = model.DB.Model(&model.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{
_ = models.DB.Model(&models.FriendLinkApply{}).Where("id = ?", applyID).Updates(map[string]interface{}{
"reciprocal_verified": false,
"reciprocal_check_note": "",
"reciprocal_checked_at": nil,

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"net/url"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"encoding/json"
@@ -13,7 +13,7 @@ import (
"sync"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
var (
@@ -26,11 +26,11 @@ type GiteaOwnerView struct {
ID uint `json:"id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Role model.Role `json:"role"`
Role models.Role `json:"role"`
Verified bool `json:"verified"`
Exp int `json:"exp"`
Level int `json:"level"`
Badges []model.UserBadgeView `json:"badges,omitempty"`
Badges []models.UserBadgeView `json:"badges,omitempty"`
}
// GiteaRepoView 前台展示
@@ -123,7 +123,7 @@ func (g *GiteaService) ListPublic(page, size int, q string) ([]GiteaRepoView, in
BackfillForumUserIDs()
q = strings.TrimSpace(q)
db := model.DB.Model(&model.GiteaRepo{}).Where("private = ? AND forum_user_id IS NOT NULL AND forum_user_id > 0", false)
db := models.DB.Model(&models.GiteaRepo{}).Where("private = ? AND forum_user_id IS NOT NULL AND forum_user_id > 0", false)
if q != "" {
like := "%" + escapeLikePattern(q) + "%"
db = db.Where(
@@ -138,7 +138,7 @@ func (g *GiteaService) ListPublic(page, size int, q string) ([]GiteaRepoView, in
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var rows []model.GiteaRepo
var rows []models.GiteaRepo
err := db.Order("updated_at_remote desc, id desc").
Offset((page - 1) * size).
Limit(size).
@@ -155,13 +155,13 @@ func (g *GiteaService) ListPublic(page, size int, q string) ([]GiteaRepoView, in
// BackfillForumUserIDs 将缺失 forum_user_id 的公开仓按 owner_login忽略大小写匹配论坛 username
func BackfillForumUserIDs() int {
var rows []model.GiteaRepo
if err := model.DB.Where("private = ? AND (forum_user_id IS NULL OR forum_user_id = 0)", false).
var rows []models.GiteaRepo
if err := models.DB.Where("private = ? AND (forum_user_id IS NULL OR forum_user_id = 0)", false).
Find(&rows).Error; err != nil || len(rows) == 0 {
return 0
}
var users []model.User
if err := model.DB.Select("id", "username").Where("banned = ?", false).Find(&users).Error; err != nil || len(users) == 0 {
var users []models.User
if err := models.DB.Select("id", "username").Where("banned = ?", false).Find(&users).Error; err != nil || len(users) == 0 {
return 0
}
byLogin := make(map[string]uint, len(users))
@@ -179,7 +179,7 @@ func BackfillForumUserIDs() int {
if !ok || uid == 0 {
continue
}
if err := model.DB.Model(&rows[i]).Update("forum_user_id", uid).Error; err != nil {
if err := models.DB.Model(&rows[i]).Update("forum_user_id", uid).Error; err != nil {
log.Printf("[gitea] 回填 forum_user_id 失败 repo=%s: %v", rows[i].FullName, err)
continue
}
@@ -222,13 +222,13 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie
logins = append(logins, key)
}
byID := make(map[uint]*model.User)
byLogin := make(map[string]*model.User)
ptrs := make([]*model.User, 0, len(ids)+len(logins))
byID := make(map[uint]*models.User)
byLogin := make(map[string]*models.User)
ptrs := make([]*models.User, 0, len(ids)+len(logins))
if len(ids) > 0 {
var users []model.User
if err := model.DB.Where("id IN ? AND banned = ?", ids, false).Find(&users).Error; err == nil {
var users []models.User
if err := models.DB.Where("id IN ? AND banned = ?", ids, false).Find(&users).Error; err == nil {
for i := range users {
u := &users[i]
byID[u.ID] = u
@@ -241,8 +241,8 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie
}
}
if len(logins) > 0 {
var users []model.User
if err := model.DB.Where("banned = ? AND LOWER(username) IN ?", false, logins).Find(&users).Error; err == nil {
var users []models.User
if err := models.DB.Where("banned = ? AND LOWER(username) IN ?", false, logins).Find(&users).Error; err == nil {
for i := range users {
u := &users[i]
key := strings.ToLower(strings.TrimSpace(u.Username))
@@ -264,7 +264,7 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie
}
// 去重 ptrs
seenPtr := make(map[uint]struct{}, len(ptrs))
uniq := make([]*model.User, 0, len(ptrs))
uniq := make([]*models.User, 0, len(ptrs))
for _, u := range ptrs {
if u == nil || u.ID == 0 {
continue
@@ -279,14 +279,14 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie
badge.AttachBadgeSummaries(uniq, 3)
} else {
for _, u := range uniq {
u.Level = model.LevelFromExp(u.Exp)
u.Level = models.LevelFromExp(u.Exp)
}
}
out := make([]GiteaRepoView, 0, len(list))
for i := range list {
item := list[i]
var u *model.User
var u *models.User
if item.ForumUserID != nil && *item.ForumUserID > 0 {
u = byID[*item.ForumUserID]
}
@@ -297,7 +297,7 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie
uid := u.ID
item.ForumUserID = &uid
// 回写缺失关联,便于下次列表过滤命中
_ = model.DB.Model(&model.GiteaRepo{}).Where("id = ?", item.ID).
_ = models.DB.Model(&models.GiteaRepo{}).Where("id = ?", item.ID).
Update("forum_user_id", uid).Error
}
}
@@ -315,7 +315,7 @@ func AttachGiteaOwners(list []GiteaRepoView, badge *BadgeService) []GiteaRepoVie
Role: u.Role,
Verified: u.Verified,
Exp: u.Exp,
Level: model.LevelFromExp(u.Exp),
Level: models.LevelFromExp(u.Exp),
Badges: u.Badges,
}
out = append(out, item)
@@ -346,8 +346,8 @@ func (g *GiteaService) SyncRepos() (int, error) {
// 同步前先回填历史缺失关联
BackfillForumUserIDs()
var users []model.User
if err := model.DB.Where("banned = ?", false).Select("id", "username").Find(&users).Error; err != nil {
var users []models.User
if err := models.DB.Where("banned = ?", false).Select("id", "username").Find(&users).Error; err != nil {
return 0, err
}
@@ -378,7 +378,7 @@ func (g *GiteaService) SyncRepos() (int, error) {
if owner == "" {
owner = username
}
row := model.GiteaRepo{
row := models.GiteaRepo{
GiteaID: gr.ID,
OwnerLogin: owner,
Name: gr.Name,
@@ -393,16 +393,16 @@ func (g *GiteaService) SyncRepos() (int, error) {
ForumUserID: &uid,
SyncedAt: now,
}
var existing model.GiteaRepo
err := model.DB.Where("gitea_id = ?", gr.ID).First(&existing).Error
var existing models.GiteaRepo
err := models.DB.Where("gitea_id = ?", gr.ID).First(&existing).Error
if err != nil {
if err := model.DB.Create(&row).Error; err != nil {
if err := models.DB.Create(&row).Error; err != nil {
log.Printf("[gitea] 创建仓库失败 %s: %v", gr.FullName, err)
continue
}
} else {
row.ID = existing.ID
if err := model.DB.Model(&existing).Updates(map[string]any{
if err := models.DB.Model(&existing).Updates(map[string]any{
"owner_login": row.OwnerLogin,
"name": row.Name,
"full_name": row.FullName,
@@ -426,14 +426,14 @@ func (g *GiteaService) SyncRepos() (int, error) {
// 仅清理本次成功同步到的 owner 下、却未再出现的旧记录
if len(syncedOwners) > 0 {
var all []model.GiteaRepo
if err := model.DB.Where("private = ?", false).Find(&all).Error; err == nil {
var all []models.GiteaRepo
if err := models.DB.Where("private = ?", false).Find(&all).Error; err == nil {
for _, r := range all {
if _, ok := syncedOwners[strings.ToLower(r.OwnerLogin)]; !ok {
continue
}
if _, ok := seen[r.GiteaID]; !ok {
_ = model.DB.Delete(&r).Error
_ = models.DB.Delete(&r).Error
}
}
}
@@ -515,7 +515,7 @@ func (g *GiteaService) fetchUserPublicRepos(baseURL, token, username string) ([]
return all, nil
}
func toGiteaRepoView(r model.GiteaRepo) GiteaRepoView {
func toGiteaRepoView(r models.GiteaRepo) GiteaRepoView {
return GiteaRepoView{
ID: r.ID,
GiteaID: r.GiteaID,

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"bytes"

View File

@@ -1,11 +1,11 @@
package service
package services
import (
"crypto/rand"
"errors"
"math/big"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
@@ -34,15 +34,15 @@ func InitPostLottery(postID uint, winnerCount int) error {
if winnerCount < 1 || winnerCount > 20 {
return errors.New("开奖人数需 1-20")
}
return model.DB.Model(&model.Post{}).Where("id = ?", postID).Updates(map[string]interface{}{
return models.DB.Model(&models.Post{}).Where("id = ?", postID).Updates(map[string]interface{}{
"lottery_winner_count": winnerCount,
"lottery_status": model.PostLotteryStatusOpen,
"lottery_status": models.PostLotteryStatusOpen,
}).Error
}
// GetPostLotteryView 获取抽奖视图
func GetPostLotteryView(post *model.Post) (*PostLotteryView, error) {
if post == nil || post.PostType != model.PostTypeLottery {
func GetPostLotteryView(post *models.Post) (*PostLotteryView, error) {
if post == nil || post.PostType != models.PostTypeLottery {
return nil, nil
}
participants, err := lotteryParticipants(post.ID, post.UserID)
@@ -54,9 +54,9 @@ func GetPostLotteryView(post *model.Post) (*PostLotteryView, error) {
Status: post.LotteryStatus,
ParticipantCount: len(participants),
}
if post.LotteryStatus == model.PostLotteryStatusDrawn {
var winners []model.PostLotteryWinner
model.DB.Preload("User").Where("post_id = ?", post.ID).Find(&winners)
if post.LotteryStatus == models.PostLotteryStatusDrawn {
var winners []models.PostLotteryWinner
models.DB.Preload("User").Where("post_id = ?", post.ID).Find(&winners)
for _, w := range winners {
view.Winners = append(view.Winners, PostLotteryWinnerView{
UserID: w.UserID, Username: w.User.Username, Nickname: w.User.Nickname,
@@ -67,15 +67,15 @@ func GetPostLotteryView(post *model.Post) (*PostLotteryView, error) {
return view, nil
}
func lotteryParticipants(postID, authorID uint) ([]model.Comment, error) {
var comments []model.Comment
err := model.DB.Where("post_id = ? AND status = ? AND user_id <> ?", postID, model.ContentStatusPublished, authorID).
func lotteryParticipants(postID, authorID uint) ([]models.Comment, error) {
var comments []models.Comment
err := models.DB.Where("post_id = ? AND status = ? AND user_id <> ?", postID, models.ContentStatusPublished, authorID).
Order("id ASC").Find(&comments).Error
if err != nil {
return nil, err
}
seen := map[uint]bool{}
var unique []model.Comment
var unique []models.Comment
for _, c := range comments {
if seen[c.UserID] {
continue
@@ -88,17 +88,17 @@ func lotteryParticipants(postID, authorID uint) ([]model.Comment, error) {
// DrawPostLottery 开奖
func DrawPostLottery(postID, operatorID uint, isAdmin bool) (*PostLotteryView, error) {
var post model.Post
if err := model.DB.First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.First(&post, postID).Error; err != nil {
return nil, ErrPostNotFound
}
if post.PostType != model.PostTypeLottery {
if post.PostType != models.PostTypeLottery {
return nil, errors.New("非抽奖帖")
}
if !isAdmin && post.UserID != operatorID {
return nil, ErrPermissionDenied
}
if post.LotteryStatus == model.PostLotteryStatusDrawn {
if post.LotteryStatus == models.PostLotteryStatusDrawn {
return nil, ErrLotteryAlreadyDrawn
}
participants, err := lotteryParticipants(postID, post.UserID)
@@ -113,25 +113,25 @@ func DrawPostLottery(postID, operatorID uint, isAdmin bool) (*PostLotteryView, e
return nil, ErrLotteryNotEnough
}
picked := randomPickComments(participants, need)
err = model.DB.Transaction(func(tx *gorm.DB) error {
err = models.DB.Transaction(func(tx *gorm.DB) error {
for _, c := range picked {
w := model.PostLotteryWinner{PostID: postID, UserID: c.UserID, CommentID: c.ID}
w := models.PostLotteryWinner{PostID: postID, UserID: c.UserID, CommentID: c.ID}
if err := tx.Create(&w).Error; err != nil {
return err
}
}
return tx.Model(&post).Update("lottery_status", model.PostLotteryStatusDrawn).Error
return tx.Model(&post).Update("lottery_status", models.PostLotteryStatusDrawn).Error
})
if err != nil {
return nil, err
}
post.LotteryStatus = model.PostLotteryStatusDrawn
post.LotteryStatus = models.PostLotteryStatusDrawn
return GetPostLotteryView(&post)
}
func randomPickComments(comments []model.Comment, n int) []model.Comment {
pool := append([]model.Comment{}, comments...)
out := make([]model.Comment, 0, n)
func randomPickComments(comments []models.Comment, n int) []models.Comment {
pool := append([]models.Comment{}, comments...)
out := make([]models.Comment, 0, n)
for i := 0; i < n && len(pool) > 0; i++ {
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(pool))))
if err != nil {
@@ -146,5 +146,5 @@ func randomPickComments(comments []model.Comment, n int) []model.Comment {
// DeleteLotteryData 删帖清理
func DeleteLotteryData(tx *gorm.DB, postID uint) {
tx.Where("post_id = ?", postID).Delete(&model.PostLotteryWinner{})
tx.Where("post_id = ?", postID).Delete(&models.PostLotteryWinner{})
}

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"crypto/tls"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"context"
@@ -12,7 +12,7 @@ import (
"github.com/minio/minio-go/v7"
"git.iioio.com/freefire/jiang13-forum/config"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
// MediaItem 管理端媒体资源条目
@@ -47,7 +47,7 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL
if s == nil {
return nil, errors.New("上传存储未初始化")
}
if model.DB == nil {
if models.DB == nil {
return nil, errors.New("数据库未初始化")
}
if page < 1 {
@@ -69,7 +69,7 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL
// 索引为空时先同步一次,避免升级后首次打开空白
var indexed int64
_ = model.DB.Model(&model.Media{}).Count(&indexed).Error
_ = models.DB.Model(&models.Media{}).Count(&indexed).Error
if indexed == 0 {
_, _ = s.SyncMediaIndex()
}
@@ -84,7 +84,7 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL
Cnt int
}
var rows []catCount
if err := model.DB.Model(&model.Media{}).
if err := models.DB.Model(&models.Media{}).
Select("category, count(*) as cnt").
Group("category").
Scan(&rows).Error; err != nil {
@@ -94,7 +94,7 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL
counts[r.Category] = r.Cnt
}
dbq := model.DB.Model(&model.Media{})
dbq := models.DB.Model(&models.Media{})
if category != "all" {
dbq = dbq.Where("category = ?", category)
}
@@ -115,7 +115,7 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL
page = totalPages
}
var records []model.Media
var records []models.Media
offset := (page - 1) * size
if err := dbq.Order("created_at desc, id desc").Offset(offset).Limit(size).Find(&records).Error; err != nil {
return nil, err
@@ -178,7 +178,7 @@ func (s *UploadStore) DeleteMedia(urls []string) (int, error) {
// SyncMediaIndex 扫描当前存储后端,回填/校正媒体索引;返回写入或更新条数
func (s *UploadStore) SyncMediaIndex() (int, error) {
if s == nil || model.DB == nil {
if s == nil || models.DB == nil {
return 0, errors.New("存储或数据库未初始化")
}
mode, _, _, _ := s.snapshot()
@@ -206,19 +206,19 @@ func (s *UploadStore) SyncMediaIndex() (int, error) {
}
// 清理当前后端下已不存在的索引(其它后端记录保留)
var stale []model.Media
_ = model.DB.Where("storage_type = ?", storageType).Find(&stale).Error
var stale []models.Media
_ = models.DB.Where("storage_type = ?", storageType).Find(&stale).Error
for _, row := range stale {
if _, ok := seen[row.URL]; ok {
continue
}
_ = model.DB.Delete(&model.Media{}, row.ID).Error
_ = models.DB.Delete(&models.Media{}, row.ID).Error
}
return n, nil
}
func (s *UploadStore) upsertMediaRecord(category, name, url string, size int64, contentType, storageType string, userID *uint) error {
if model.DB == nil || strings.TrimSpace(url) == "" {
if models.DB == nil || strings.TrimSpace(url) == "" {
return nil
}
category = strings.TrimSpace(category)
@@ -231,8 +231,8 @@ func (s *UploadStore) upsertMediaRecord(category, name, url string, size int64,
contentType = imageContentType(strings.ToLower(filepath.Ext(name)))
}
var existing model.Media
err := model.DB.Where("url = ?", url).First(&existing).Error
var existing models.Media
err := models.DB.Where("url = ?", url).First(&existing).Error
if err == nil {
updates := map[string]interface{}{
"category": category,
@@ -244,10 +244,10 @@ func (s *UploadStore) upsertMediaRecord(category, name, url string, size int64,
if userID != nil {
updates["user_id"] = *userID
}
return model.DB.Model(&existing).Updates(updates).Error
return models.DB.Model(&existing).Updates(updates).Error
}
rec := model.Media{
rec := models.Media{
Category: category,
Name: name,
URL: url,
@@ -256,11 +256,11 @@ func (s *UploadStore) upsertMediaRecord(category, name, url string, size int64,
StorageType: storageType,
UserID: userID,
}
return model.DB.Create(&rec).Error
return models.DB.Create(&rec).Error
}
func (s *UploadStore) deleteMediaRecords(urls []string) {
if model.DB == nil || len(urls) == 0 {
if models.DB == nil || len(urls) == 0 {
return
}
clean := make([]string, 0, len(urls))
@@ -276,7 +276,7 @@ func (s *UploadStore) deleteMediaRecords(urls []string) {
if len(clean) == 0 {
return
}
_ = model.DB.Where("url IN ?", clean).Delete(&model.Media{}).Error
_ = models.DB.Where("url IN ?", clean).Delete(&models.Media{}).Error
}
func (s *UploadStore) resolveSiblingPublicURLs(rawURL string) []string {

View File

@@ -1,10 +1,10 @@
package service
package services
import (
"regexp"
"strings"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
const maxMentionsPerContent = 10
@@ -47,10 +47,10 @@ func ResolveMentionUserIDs(names []string, excludeUserID uint) []uint {
ids := make([]uint, 0, len(names))
seen := make(map[uint]struct{}, len(names))
for _, name := range names {
var u model.User
err := model.DB.Select("id").Where("username = ?", name).First(&u).Error
var u models.User
err := models.DB.Select("id").Where("username = ?", name).First(&u).Error
if err != nil {
err = model.DB.Select("id").Where("nickname = ?", name).First(&u).Error
err = models.DB.Select("id").Where("nickname = ?", name).First(&u).Error
}
if err != nil || u.ID == 0 || u.ID == excludeUserID {
continue

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"reflect"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"errors"
@@ -7,7 +7,7 @@ import (
"time"
"unicode/utf8"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
var (
@@ -34,7 +34,7 @@ type MessageSendInput struct {
}
// Send 发送私信(用户互发或系统通知)
func (s *MessageService) Send(in MessageSendInput) (*model.PrivateMessage, error) {
func (s *MessageService) Send(in MessageSendInput) (*models.PrivateMessage, error) {
if in.ToUserID == 0 {
return nil, errors.New("收件人不存在")
}
@@ -42,8 +42,8 @@ func (s *MessageService) Send(in MessageSendInput) (*model.PrivateMessage, error
return nil, ErrCannotMessageSelf
}
if in.FromUserID > 0 {
var to model.User
if err := model.DB.Select("id", "banned").First(&to, in.ToUserID).Error; err != nil {
var to models.User
if err := models.DB.Select("id", "banned").First(&to, in.ToUserID).Error; err != nil {
return nil, errors.New("收件人不存在")
}
if to.Banned {
@@ -75,13 +75,13 @@ func (s *MessageService) Send(in MessageSendInput) (*model.PrivateMessage, error
kind := in.Kind
if kind == "" {
if in.FromUserID == 0 {
kind = model.MessageKindSystem
kind = models.MessageKindSystem
} else {
kind = model.MessageKindUser
kind = models.MessageKindUser
}
}
msg := &model.PrivateMessage{
msg := &models.PrivateMessage{
FromUserID: in.FromUserID,
ToUserID: in.ToUserID,
Subject: subject,
@@ -91,17 +91,17 @@ func (s *MessageService) Send(in MessageSendInput) (*model.PrivateMessage, error
RelatedReportID: in.RelatedReportID,
IsRead: false,
}
if err := model.DB.Create(msg).Error; err != nil {
if err := models.DB.Create(msg).Error; err != nil {
return nil, err
}
_ = model.DB.Preload("FromUser").Preload("ToUser").First(msg, msg.ID).Error
_ = models.DB.Preload("FromUser").Preload("ToUser").First(msg, msg.ID).Error
return msg, nil
}
// SendSystem 系统私信(管理员/系统 → 用户)
func (s *MessageService) SendSystem(toUserID uint, subject, content, kind string, relatedPostID, relatedReportID *uint) (*model.PrivateMessage, error) {
func (s *MessageService) SendSystem(toUserID uint, subject, content, kind string, relatedPostID, relatedReportID *uint) (*models.PrivateMessage, error) {
if kind == "" {
kind = model.MessageKindSystem
kind = models.MessageKindSystem
}
return s.Send(MessageSendInput{
FromUserID: 0,
@@ -116,7 +116,7 @@ func (s *MessageService) SendSystem(toUserID uint, subject, content, kind string
// MarkAllRead 全部标为已读
func (s *MessageService) MarkAllRead(userID uint) error {
return model.DB.Model(&model.PrivateMessage{}).
return models.DB.Model(&models.PrivateMessage{}).
Where("to_user_id = ? AND is_read = ?", userID, false).
Update("is_read", true).Error
}
@@ -124,7 +124,7 @@ func (s *MessageService) MarkAllRead(userID uint) error {
// UnreadCount 未读数
func (s *MessageService) UnreadCount(userID uint) (int64, error) {
var n int64
err := model.DB.Model(&model.PrivateMessage{}).
err := models.DB.Model(&models.PrivateMessage{}).
Where("to_user_id = ? AND is_read = ?", userID, false).
Count(&n).Error
return n, err
@@ -132,13 +132,13 @@ func (s *MessageService) UnreadCount(userID uint) (int64, error) {
// UnreadCounts 未读总数,以及私信 / 系统通知分项
func (s *MessageService) UnreadCounts(userID uint) (total, dm, notify int64, err error) {
err = model.DB.Model(&model.PrivateMessage{}).
err = models.DB.Model(&models.PrivateMessage{}).
Where("to_user_id = ? AND is_read = ?", userID, false).
Count(&total).Error
if err != nil {
return 0, 0, 0, err
}
err = model.DB.Model(&model.PrivateMessage{}).
err = models.DB.Model(&models.PrivateMessage{}).
Where("to_user_id = ? AND is_read = ? AND from_user_id = 0", userID, false).
Count(&notify).Error
if err != nil {
@@ -152,12 +152,12 @@ func (s *MessageService) UnreadCounts(userID uint) (total, dm, notify int64, err
}
// ListNotifications 系统通知列表(按时间倒序,非聊天气泡)
func (s *MessageService) ListNotifications(userID uint, page, size int, kind string) ([]model.PrivateMessage, int64, error) {
func (s *MessageService) ListNotifications(userID uint, page, size int, kind string) ([]models.PrivateMessage, int64, error) {
if page < 1 {
page = 1
}
size = s.settings.NormalizePageSize(size)
db := model.DB.Model(&model.PrivateMessage{}).
db := models.DB.Model(&models.PrivateMessage{}).
Where("from_user_id = 0 AND to_user_id = ?", userID)
kind = strings.TrimSpace(kind)
if kind != "" && kind != "all" {
@@ -167,13 +167,13 @@ func (s *MessageService) ListNotifications(userID uint, page, size int, kind str
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var list []model.PrivateMessage
var list []models.PrivateMessage
err := db.Order("id desc").Offset((page - 1) * size).Limit(size).Find(&list).Error
if err != nil {
return nil, 0, err
}
if list == nil {
list = []model.PrivateMessage{}
list = []models.PrivateMessage{}
}
return list, total, nil
}
@@ -186,9 +186,9 @@ func (s *MessageService) MarkNotificationsRead(userID uint) error {
// MessageConversation 按对方聚合的会话摘要
type MessageConversation struct {
PeerUserID uint `json:"peer_user_id"` // 0 = 系统通知
PeerUser *model.User `json:"peer_user,omitempty"`
PeerUser *models.User `json:"peer_user,omitempty"`
IsSystem bool `json:"is_system"`
LastMessage *model.PrivateMessage `json:"last_message,omitempty"`
LastMessage *models.PrivateMessage `json:"last_message,omitempty"`
UnreadCount int64 `json:"unread_count"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -220,7 +220,7 @@ func (s *MessageService) ListConversations(q ConversationListQuery) ([]MessageCo
}
var rows []peerRow
// peer_id系统通知为 0否则为对话另一方
err := model.DB.Raw(`
err := models.DB.Raw(`
SELECT
CASE
WHEN from_user_id = 0 THEN 0
@@ -239,7 +239,7 @@ func (s *MessageService) ListConversations(q ConversationListQuery) ([]MessageCo
}
var total int64
err = model.DB.Raw(`
err = models.DB.Raw(`
SELECT COUNT(*) FROM (
SELECT
CASE
@@ -268,20 +268,20 @@ func (s *MessageService) ListConversations(q ConversationListQuery) ([]MessageCo
}
}
var lastMsgs []model.PrivateMessage
if err := model.DB.Preload("FromUser").Preload("ToUser").
var lastMsgs []models.PrivateMessage
if err := models.DB.Preload("FromUser").Preload("ToUser").
Where("id IN ?", lastIDs).Find(&lastMsgs).Error; err != nil {
return nil, 0, err
}
msgByID := make(map[uint]model.PrivateMessage, len(lastMsgs))
msgByID := make(map[uint]models.PrivateMessage, len(lastMsgs))
for i := range lastMsgs {
msgByID[lastMsgs[i].ID] = lastMsgs[i]
}
usersByID := make(map[uint]model.User)
usersByID := make(map[uint]models.User)
if len(peerIDs) > 0 {
var users []model.User
if err := model.DB.Where("id IN ?", peerIDs).Find(&users).Error; err != nil {
var users []models.User
if err := models.DB.Where("id IN ?", peerIDs).Find(&users).Error; err != nil {
return nil, 0, err
}
for i := range users {
@@ -294,7 +294,7 @@ func (s *MessageService) ListConversations(q ConversationListQuery) ([]MessageCo
Cnt int64
}
var unreadRows []unreadRow
_ = model.DB.Raw(`
_ = models.DB.Raw(`
SELECT
CASE WHEN from_user_id = 0 THEN 0 ELSE from_user_id END AS peer_id,
COUNT(*) AS cnt
@@ -332,13 +332,13 @@ func (s *MessageService) ListConversations(q ConversationListQuery) ([]MessageCo
}
// ListConversationMessages 某会话内消息(时间正序,支持 Before 向上翻页)
func (s *MessageService) ListConversationMessages(q ConversationMessagesQuery) ([]model.PrivateMessage, int64, error) {
func (s *MessageService) ListConversationMessages(q ConversationMessagesQuery) ([]models.PrivateMessage, int64, error) {
if q.Page < 1 {
q.Page = 1
}
q.Size = s.settings.NormalizePageSize(q.Size)
countDB := model.DB.Model(&model.PrivateMessage{})
countDB := models.DB.Model(&models.PrivateMessage{})
if q.PeerID == 0 {
countDB = countDB.Where("from_user_id = 0 AND to_user_id = ?", q.UserID)
} else {
@@ -353,7 +353,7 @@ func (s *MessageService) ListConversationMessages(q ConversationMessagesQuery) (
return nil, 0, err
}
qdb := model.DB.Preload("FromUser").Preload("ToUser")
qdb := models.DB.Preload("FromUser").Preload("ToUser")
if q.PeerID == 0 {
qdb = qdb.Where("from_user_id = 0 AND to_user_id = ?", q.UserID)
} else {
@@ -366,7 +366,7 @@ func (s *MessageService) ListConversationMessages(q ConversationMessagesQuery) (
qdb = qdb.Where("id < ?", q.Before)
}
var list []model.PrivateMessage
var list []models.PrivateMessage
// 先按 id desc 取一页,再反转为正序(聊天从旧到新)
err := qdb.Order("id desc").Limit(q.Size).Find(&list).Error
if err != nil {
@@ -380,7 +380,7 @@ func (s *MessageService) ListConversationMessages(q ConversationMessagesQuery) (
// MarkConversationRead 将会话内未读标为已读
func (s *MessageService) MarkConversationRead(userID, peerID uint) error {
db := model.DB.Model(&model.PrivateMessage{}).
db := models.DB.Model(&models.PrivateMessage{}).
Where("to_user_id = ? AND is_read = ?", userID, false)
if peerID == 0 {
db = db.Where("from_user_id = 0")

View File

@@ -1,11 +1,11 @@
package service
package services
import (
"fmt"
"strings"
"unicode/utf8"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
// NotifyService 站内消息 + 邮件提醒编排
@@ -35,7 +35,7 @@ func (s *NotifyService) goNotify(fn func()) {
}
// AsyncNotifyCommentPublished 异步:评论公开后通知被回复者或楼主
func (s *NotifyService) AsyncNotifyCommentPublished(comment *model.Comment) {
func (s *NotifyService) AsyncNotifyCommentPublished(comment *models.Comment) {
if s == nil || comment == nil {
return
}
@@ -44,7 +44,7 @@ func (s *NotifyService) AsyncNotifyCommentPublished(comment *model.Comment) {
}
// AsyncNotifyCommentMentions 异步:评论公开后通知被 @ 的用户
func (s *NotifyService) AsyncNotifyCommentMentions(comment *model.Comment) {
func (s *NotifyService) AsyncNotifyCommentMentions(comment *models.Comment) {
if s == nil || comment == nil {
return
}
@@ -53,7 +53,7 @@ func (s *NotifyService) AsyncNotifyCommentMentions(comment *model.Comment) {
}
// AsyncNotifyPendingPost 异步:待审帖通知管理员
func (s *NotifyService) AsyncNotifyPendingPost(post *model.Post) {
func (s *NotifyService) AsyncNotifyPendingPost(post *models.Post) {
if s == nil || post == nil {
return
}
@@ -62,7 +62,7 @@ func (s *NotifyService) AsyncNotifyPendingPost(post *model.Post) {
}
// AsyncNotifyPendingComment 异步:待审评论通知管理员
func (s *NotifyService) AsyncNotifyPendingComment(comment *model.Comment) {
func (s *NotifyService) AsyncNotifyPendingComment(comment *models.Comment) {
if s == nil || comment == nil {
return
}
@@ -71,8 +71,8 @@ func (s *NotifyService) AsyncNotifyPendingComment(comment *model.Comment) {
}
// NotifyCommentPublished 评论公开后通知被回复者或楼主
func (s *NotifyService) NotifyCommentPublished(comment *model.Comment) {
if s == nil || comment == nil || comment.Status != model.ContentStatusPublished {
func (s *NotifyService) NotifyCommentPublished(comment *models.Comment) {
if s == nil || comment == nil || comment.Status != models.ContentStatusPublished {
return
}
@@ -96,14 +96,14 @@ func (s *NotifyService) NotifyCommentPublished(comment *model.Comment) {
subject := "收到新回复"
content := FormatReplyContent(authorName, title, displayFloor, isNested)
pid := comment.PostID
_, _ = s.messages.SendSystem(toUserID, subject, content, model.MessageKindReply, &pid, nil)
_, _ = s.messages.SendSystem(toUserID, subject, content, models.MessageKindReply, &pid, nil)
s.sendReplyMail(toUserID, authorName, title, comment.PostID, displayFloor, isNested, comment.Content)
}
// NotifyCommentMentions 评论公开后通知被 @提及的用户(跳过已收到回复通知的人)
func (s *NotifyService) NotifyCommentMentions(comment *model.Comment) {
if s == nil || comment == nil || comment.Status != model.ContentStatusPublished {
func (s *NotifyService) NotifyCommentMentions(comment *models.Comment) {
if s == nil || comment == nil || comment.Status != models.ContentStatusPublished {
return
}
names := ExtractMentionNames(comment.Content)
@@ -132,13 +132,13 @@ func (s *NotifyService) NotifyCommentMentions(comment *model.Comment) {
if uid == 0 || uid == comment.UserID || uid == replyTo {
continue
}
_, _ = s.messages.SendSystem(uid, subject, content, model.MessageKindMention, &pid, nil)
_, _ = s.messages.SendSystem(uid, subject, content, models.MessageKindMention, &pid, nil)
}
}
// NotifyPendingPost 新帖进入待审时通知全部管理员
func (s *NotifyService) NotifyPendingPost(post *model.Post) {
if s == nil || post == nil || post.Status != model.ContentStatusPending {
func (s *NotifyService) NotifyPendingPost(post *models.Post) {
if s == nil || post == nil || post.Status != models.ContentStatusPending {
return
}
title := strings.TrimSpace(post.Title)
@@ -149,14 +149,14 @@ func (s *NotifyService) NotifyPendingPost(post *model.Post) {
subject := "新的待审核帖子"
content := FormatPendingPostContent(authorName, title, post.ID)
pid := post.ID
s.notifyAdmins(subject, content, model.MessageKindModeration, &pid, func(siteName, baseURL string) (string, string, string) {
s.notifyAdmins(subject, content, models.MessageKindModeration, &pid, func(siteName, baseURL string) (string, string, string) {
return BuildModerationMail(siteName, "帖子", authorName, title, post.ID, 0, false, AbsoluteURL(baseURL, "/admin/posts"))
})
}
// NotifyPendingComment 新评论进入待审时通知全部管理员
func (s *NotifyService) NotifyPendingComment(comment *model.Comment) {
if s == nil || comment == nil || comment.Status != model.ContentStatusPending {
func (s *NotifyService) NotifyPendingComment(comment *models.Comment) {
if s == nil || comment == nil || comment.Status != models.ContentStatusPending {
return
}
post, err := s.loadPost(comment.PostID)
@@ -173,7 +173,7 @@ func (s *NotifyService) NotifyPendingComment(comment *model.Comment) {
isNested := comment.ReplyTo != nil && *comment.ReplyTo > 0
content := FormatPendingCommentContent(authorName, title, displayFloor, isNested)
pid := comment.PostID
s.notifyAdmins(subject, content, model.MessageKindModeration, &pid, func(siteName, baseURL string) (string, string, string) {
s.notifyAdmins(subject, content, models.MessageKindModeration, &pid, func(siteName, baseURL string) (string, string, string) {
return BuildModerationMail(siteName, "评论", authorName, title, comment.PostID, displayFloor, isNested, AbsoluteURL(baseURL, "/admin/comments"))
})
}
@@ -215,8 +215,8 @@ func (s *NotifyService) sendReplyMail(toUserID uint, authorName, postTitle strin
if s.mail == nil || !s.settings.MailReady() {
return
}
var user model.User
if err := model.DB.Select("id", "email", "nickname", "username").First(&user, toUserID).Error; err != nil {
var user models.User
if err := models.DB.Select("id", "email", "nickname", "username").First(&user, toUserID).Error; err != nil {
return
}
email := strings.TrimSpace(user.Email)
@@ -232,10 +232,10 @@ func (s *NotifyService) sendReplyMail(toUserID uint, authorName, postTitle strin
_ = s.mail.SendHTML(email, subj, text, html)
}
func (s *NotifyService) resolveReplyRecipient(comment *model.Comment, post *model.Post) (uint, error) {
func (s *NotifyService) resolveReplyRecipient(comment *models.Comment, post *models.Post) (uint, error) {
if comment.ReplyTo != nil && *comment.ReplyTo > 0 {
var target model.Comment
if err := model.DB.Select("id", "user_id", "post_id").
var target models.Comment
if err := models.DB.Select("id", "user_id", "post_id").
Where("id = ? AND post_id = ?", *comment.ReplyTo, comment.PostID).
First(&target).Error; err != nil {
return 0, err
@@ -249,7 +249,7 @@ func (s *NotifyService) resolveReplyRecipient(comment *model.Comment, post *mode
}
// resolveDisplayFloor 解析页面可见的顶层楼号(子回复沿 reply_to 上溯)
func (s *NotifyService) resolveDisplayFloor(comment *model.Comment) int {
func (s *NotifyService) resolveDisplayFloor(comment *models.Comment) int {
if comment == nil {
return 0
}
@@ -264,8 +264,8 @@ func (s *NotifyService) resolveDisplayFloor(comment *model.Comment) int {
break
}
seen[curID] = struct{}{}
var ancestor model.Comment
if err := model.DB.Select("id", "floor", "reply_to").
var ancestor models.Comment
if err := models.DB.Select("id", "floor", "reply_to").
Where("id = ? AND post_id = ?", curID, comment.PostID).
First(&ancestor).Error; err != nil {
return comment.Floor
@@ -278,18 +278,18 @@ func (s *NotifyService) resolveDisplayFloor(comment *model.Comment) int {
return comment.Floor
}
func (s *NotifyService) loadPost(postID uint) (*model.Post, error) {
var post model.Post
if err := model.DB.Select("id", "user_id", "title", "status").First(&post, postID).Error; err != nil {
func (s *NotifyService) loadPost(postID uint) (*models.Post, error) {
var post models.Post
if err := models.DB.Select("id", "user_id", "title", "status").First(&post, postID).Error; err != nil {
return nil, err
}
return &post, nil
}
func (s *NotifyService) listAdmins() ([]model.User, error) {
var admins []model.User
err := model.DB.Select("id", "email", "nickname", "username").
Where("role = ? AND banned = ?", model.RoleAdmin, false).
func (s *NotifyService) listAdmins() ([]models.User, error) {
var admins []models.User
err := models.DB.Select("id", "email", "nickname", "username").
Where("role = ? AND banned = ?", models.RoleAdmin, false).
Find(&admins).Error
return admins, err
}
@@ -302,7 +302,7 @@ func (s *NotifyService) siteName() string {
return name
}
func (s *NotifyService) commentAuthorName(comment *model.Comment) string {
func (s *NotifyService) commentAuthorName(comment *models.Comment) string {
if comment.UserID > 0 {
if comment.User.ID == comment.UserID {
if n := DisplayName(&comment.User); n != "" {
@@ -321,8 +321,8 @@ func (s *NotifyService) userDisplayName(userID uint) string {
if userID == 0 {
return "用户"
}
var u model.User
if err := model.DB.Select("id", "nickname", "username").First(&u, userID).Error; err != nil {
var u models.User
if err := models.DB.Select("id", "nickname", "username").First(&u, userID).Error; err != nil {
return fmt.Sprintf("用户 #%d", userID)
}
if n := DisplayName(&u); n != "" {

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"crypto/rand"
@@ -7,7 +7,7 @@ import (
"strings"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
var (
@@ -44,8 +44,8 @@ type OAuthClientInput struct {
// ListOAuthClients 列出全部 OAuth 应用
func (s *ForumSettingsService) ListOAuthClients() ([]OAuthClientView, error) {
var rows []model.OAuthClient
if err := model.DB.Order("id asc").Find(&rows).Error; err != nil {
var rows []models.OAuthClient
if err := models.DB.Order("id asc").Find(&rows).Error; err != nil {
return nil, err
}
out := make([]OAuthClientView, 0, len(rows))
@@ -64,7 +64,7 @@ func (s *ForumSettingsService) CreateOAuthClient(in OAuthClientInput) (*OAuthCli
return nil, ErrOAuthClientInvalid
}
var n int64
model.DB.Model(&model.OAuthClient{}).Where("client_id = ?", clientID).Count(&n)
models.DB.Model(&models.OAuthClient{}).Where("client_id = ?", clientID).Count(&n)
if n > 0 {
return nil, ErrOAuthClientExists
}
@@ -85,14 +85,14 @@ func (s *ForumSettingsService) CreateOAuthClient(in OAuthClientInput) (*OAuthCli
if in.Enabled != nil {
enabled = *in.Enabled
}
row := model.OAuthClient{
row := models.OAuthClient{
ClientID: clientID,
ClientSecretHash: hash,
Name: name,
RedirectURIs: uris,
Enabled: enabled,
}
if err := model.DB.Create(&row).Error; err != nil {
if err := models.DB.Create(&row).Error; err != nil {
return nil, err
}
v := toOAuthClientView(row, plain)
@@ -101,8 +101,8 @@ func (s *ForumSettingsService) CreateOAuthClient(in OAuthClientInput) (*OAuthCli
// UpdateOAuthClient 更新应用
func (s *ForumSettingsService) UpdateOAuthClient(id uint, in OAuthClientInput) (*OAuthClientView, error) {
var row model.OAuthClient
if err := model.DB.First(&row, id).Error; err != nil {
var row models.OAuthClient
if err := models.DB.First(&row, id).Error; err != nil {
return nil, ErrOAuthClientNotFound
}
name := strings.TrimSpace(in.Name)
@@ -137,7 +137,7 @@ func (s *ForumSettingsService) UpdateOAuthClient(id uint, in OAuthClientInput) (
row.ClientSecretHash = hash
}
if err := model.DB.Save(&row).Error; err != nil {
if err := models.DB.Save(&row).Error; err != nil {
return nil, err
}
v := toOAuthClientView(row, plain)
@@ -146,7 +146,7 @@ func (s *ForumSettingsService) UpdateOAuthClient(id uint, in OAuthClientInput) (
// DeleteOAuthClient 删除应用
func (s *ForumSettingsService) DeleteOAuthClient(id uint) error {
res := model.DB.Delete(&model.OAuthClient{}, id)
res := models.DB.Delete(&models.OAuthClient{}, id)
if res.Error != nil {
return res.Error
}
@@ -157,23 +157,23 @@ func (s *ForumSettingsService) DeleteOAuthClient(id uint) error {
}
// FindEnabledOAuthClient 按 client_id 查找已启用应用
func FindEnabledOAuthClient(clientID string) (*model.OAuthClient, error) {
var row model.OAuthClient
if err := model.DB.Where("client_id = ? AND enabled = ?", clientID, true).First(&row).Error; err != nil {
func FindEnabledOAuthClient(clientID string) (*models.OAuthClient, error) {
var row models.OAuthClient
if err := models.DB.Where("client_id = ? AND enabled = ?", clientID, true).First(&row).Error; err != nil {
return nil, ErrOIDCInvalidClient
}
return &row, nil
}
// VerifyOAuthClientSecret 校验客户端密钥bcrypt 哈希)
func VerifyOAuthClientSecret(row *model.OAuthClient, secret string) bool {
func VerifyOAuthClientSecret(row *models.OAuthClient, secret string) bool {
if row == nil || secret == "" || row.ClientSecretHash == "" {
return false
}
return CheckPassword(row.ClientSecretHash, secret)
}
func toOAuthClientView(row model.OAuthClient, plainSecret string) OAuthClientView {
func toOAuthClientView(row models.OAuthClient, plainSecret string) OAuthClientView {
return OAuthClientView{
ID: row.ID,
ClientID: row.ClientID,
@@ -198,6 +198,6 @@ func generateClientSecret() (string, error) {
// CountEnabledOAuthClients 已启用客户端数量
func CountEnabledOAuthClients() int64 {
var n int64
model.DB.Model(&model.OAuthClient{}).Where("enabled = ?", true).Count(&n)
models.DB.Model(&models.OAuthClient{}).Where("enabled = ?", true).Count(&n)
return n
}

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"crypto/rand"
@@ -20,7 +20,7 @@ import (
"github.com/golang-jwt/jwt/v5"
"git.iioio.com/freefire/jiang13-forum/config"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
const (
@@ -242,8 +242,8 @@ func (s *OIDCService) IssueAuthCode(userID uint, req AuthorizeRequest) (string,
if err := s.ValidateAuthorize(req); err != nil {
return "", err
}
var user model.User
if err := model.DB.First(&user, userID).Error; err != nil {
var user models.User
if err := models.DB.First(&user, userID).Error; err != nil {
return "", ErrOIDCInvalidRequest
}
if user.Banned {
@@ -258,7 +258,7 @@ func (s *OIDCService) IssueAuthCode(userID uint, req AuthorizeRequest) (string,
if req.CodeChallenge != "" && method == "" {
method = "PLAIN"
}
rec := &model.OAuthAuthCode{
rec := &models.OAuthAuthCode{
Code: code,
ClientID: req.ClientID,
UserID: user.ID,
@@ -269,7 +269,7 @@ func (s *OIDCService) IssueAuthCode(userID uint, req AuthorizeRequest) (string,
CodeChallengeMethod: method,
ExpiresAt: time.Now().Add(oidcAuthCodeTTL),
}
if err := model.DB.Create(rec).Error; err != nil {
if err := models.DB.Create(rec).Error; err != nil {
return "", err
}
@@ -320,14 +320,14 @@ func (s *OIDCService) ExchangeCode(req TokenRequest) (*TokenResponse, error) {
return nil, ErrOIDCInvalidClient
}
var rec model.OAuthAuthCode
if err := model.DB.Where("code = ?", req.Code).First(&rec).Error; err != nil {
var rec models.OAuthAuthCode
if err := models.DB.Where("code = ?", req.Code).First(&rec).Error; err != nil {
return nil, ErrOIDCInvalidGrant
}
if rec.Used || time.Now().After(rec.ExpiresAt) {
// 重放:作废同用户同客户端未过期码
if rec.Used {
_ = model.DB.Model(&model.OAuthAuthCode{}).
_ = models.DB.Model(&models.OAuthAuthCode{}).
Where("client_id = ? AND user_id = ? AND used = ? AND expires_at > ?",
rec.ClientID, rec.UserID, false, time.Now()).
Update("used", true).Error
@@ -342,10 +342,10 @@ func (s *OIDCService) ExchangeCode(req TokenRequest) (*TokenResponse, error) {
}
rec.Used = true
_ = model.DB.Save(&rec).Error
_ = models.DB.Save(&rec).Error
var user model.User
if err := model.DB.First(&user, rec.UserID).Error; err != nil || user.Banned {
var user models.User
if err := models.DB.First(&user, rec.UserID).Error; err != nil || user.Banned {
return nil, ErrOIDCInvalidGrant
}
@@ -408,7 +408,7 @@ type oidcIDClaims struct {
jwt.RegisteredClaims
}
func (s *OIDCService) signAccessToken(user *model.User, scope, clientID string) (string, error) {
func (s *OIDCService) signAccessToken(user *models.User, scope, clientID string) (string, error) {
now := time.Now()
issuer := s.Issuer()
claims := oidcAccessClaims{
@@ -429,7 +429,7 @@ func (s *OIDCService) signAccessToken(user *model.User, scope, clientID string)
return t.SignedString(s.privateKey)
}
func (s *OIDCService) signIDToken(user *model.User, scope, clientID, nonce string) (string, error) {
func (s *OIDCService) signIDToken(user *models.User, scope, clientID, nonce string) (string, error) {
now := time.Now()
issuer := s.Issuer()
claims := oidcIDClaims{
@@ -462,13 +462,13 @@ func (s *OIDCService) signIDToken(user *model.User, scope, clientID, nonce strin
return t.SignedString(s.privateKey)
}
func (s *OIDCService) userGroups(user *model.User) []string {
func (s *OIDCService) userGroups(user *models.User) []string {
rt := s.runtime()
groups := make([]string, 0, 2)
if rt.UserGroup != "" {
groups = append(groups, rt.UserGroup)
}
if user.Role == model.RoleAdmin && rt.AdminGroup != "" {
if user.Role == models.RoleAdmin && rt.AdminGroup != "" {
groups = append(groups, rt.AdminGroup)
}
return groups
@@ -484,8 +484,8 @@ func (s *OIDCService) UserInfo(accessToken string) (map[string]any, error) {
if err != nil {
return nil, ErrOIDCInvalidToken
}
var user model.User
if err := model.DB.First(&user, uint(uid)).Error; err != nil || user.Banned {
var user models.User
if err := models.DB.First(&user, uint(uid)).Error; err != nil || user.Banned {
return nil, ErrOIDCInvalidToken
}
rt := s.runtime()
@@ -536,8 +536,8 @@ func (s *OIDCService) ResolveLogoutRedirect(postLogoutRedirectURI, state string)
if uri == "" {
return "/", nil
}
var clients []model.OAuthClient
if err := model.DB.Where("enabled = ?", true).Find(&clients).Error; err != nil {
var clients []models.OAuthClient
if err := models.DB.Where("enabled = ?", true).Find(&clients).Error; err != nil {
return "", err
}
allowed := false

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"crypto/rand"
@@ -7,7 +7,7 @@ import (
"math/big"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@@ -31,13 +31,13 @@ func todayLocal() string {
// AdjustPointsTx 在已有事务内调整积分并写流水;返回变动后余额
func AdjustPointsTx(tx *gorm.DB, userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
if delta == 0 {
var u model.User
var u models.User
if err := tx.Select("points").First(&u, userID).Error; err != nil {
return 0, err
}
return u.Points, nil
}
var user model.User
var user models.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
return 0, err
}
@@ -48,7 +48,7 @@ func AdjustPointsTx(tx *gorm.DB, userID uint, delta int, reason, refType string,
if err := tx.Model(&user).Update("points", newBal).Error; err != nil {
return 0, err
}
led := model.PointLedger{
led := models.PointLedger{
UserID: userID,
Delta: delta,
Balance: newBal,
@@ -66,7 +66,7 @@ func AdjustPointsTx(tx *gorm.DB, userID uint, delta int, reason, refType string,
// AdjustPoints 独立事务调整积分
func (s *PointsService) AdjustPoints(userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
var bal int
err := model.DB.Transaction(func(tx *gorm.DB) error {
err := models.DB.Transaction(func(tx *gorm.DB) error {
var e error
bal, e = AdjustPointsTx(tx, userID, delta, reason, refType, refID, note)
return e
@@ -79,7 +79,7 @@ func (s *PointsService) AdminAdjust(userID uint, delta int, note string) (int, e
if delta == 0 {
return 0, ErrInvalidPointsDelta
}
return s.AdjustPoints(userID, delta, model.PointReasonAdminAdjust, "admin", 0, note)
return s.AdjustPoints(userID, delta, models.PointReasonAdminAdjust, "admin", 0, note)
}
// CheckInStatus 今日签到状态
@@ -93,8 +93,8 @@ type CheckInStatus struct {
func (s *PointsService) GetCheckInStatus(userID uint) (CheckInStatus, error) {
day := todayLocal()
st := CheckInStatus{Day: day}
var row model.CheckIn
err := model.DB.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&row).Error
var row models.CheckIn
err := models.DB.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&row).Error
if err != nil {
return st, err
}
@@ -112,8 +112,8 @@ func (s *PointsService) GetCheckInStatus(userID uint) (CheckInStatus, error) {
func (s *PointsService) computeNextStreak(userID uint, today string) int {
yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
var prev model.CheckIn
model.DB.Where("user_id = ? AND day = ?", userID, yesterday).Limit(1).Find(&prev)
var prev models.CheckIn
models.DB.Where("user_id = ? AND day = ?", userID, yesterday).Limit(1).Find(&prev)
if prev.ID > 0 {
return prev.Streak + 1
}
@@ -136,8 +136,8 @@ func checkInReward(streak int) int {
func (s *PointsService) CheckIn(userID uint) (CheckInStatus, error) {
day := todayLocal()
var out CheckInStatus
err := model.DB.Transaction(func(tx *gorm.DB) error {
var existing model.CheckIn
err := models.DB.Transaction(func(tx *gorm.DB) error {
var existing models.CheckIn
if err := tx.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&existing).Error; err != nil {
return err
}
@@ -145,18 +145,18 @@ func (s *PointsService) CheckIn(userID uint) (CheckInStatus, error) {
return ErrAlreadyCheckedIn
}
yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
var prev model.CheckIn
var prev models.CheckIn
_ = tx.Where("user_id = ? AND day = ?", userID, yesterday).Limit(1).Find(&prev).Error
streak := 1
if prev.ID > 0 {
streak = prev.Streak + 1
}
pts := checkInReward(streak)
row := model.CheckIn{UserID: userID, Day: day, Points: pts, Streak: streak}
row := models.CheckIn{UserID: userID, Day: day, Points: pts, Streak: streak}
if err := tx.Create(&row).Error; err != nil {
return err
}
if _, err := AdjustPointsTx(tx, userID, pts, model.PointReasonCheckIn, "check_in", row.ID, fmt.Sprintf("连续签到 %d 天", streak)); err != nil {
if _, err := AdjustPointsTx(tx, userID, pts, models.PointReasonCheckIn, "check_in", row.ID, fmt.Sprintf("连续签到 %d 天", streak)); err != nil {
return err
}
out = CheckInStatus{CheckedIn: true, Streak: streak, TodayPoints: pts, Day: day}
@@ -191,8 +191,8 @@ type LotteryStatus struct {
func (s *PointsService) GetLotteryStatus(userID uint) (LotteryStatus, error) {
day := todayLocal()
st := LotteryStatus{Day: day, Pool: defaultLotteryPool, Cost: 0}
var row model.LotteryDraw
if err := model.DB.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&row).Error; err != nil {
var row models.LotteryDraw
if err := models.DB.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&row).Error; err != nil {
return st, err
}
if row.ID > 0 {
@@ -228,8 +228,8 @@ func pickLottery(pool []LotteryPrize) (int, error) {
func (s *PointsService) DrawLottery(userID uint) (LotteryStatus, error) {
day := todayLocal()
var out LotteryStatus
err := model.DB.Transaction(func(tx *gorm.DB) error {
var existing model.LotteryDraw
err := models.DB.Transaction(func(tx *gorm.DB) error {
var existing models.LotteryDraw
if err := tx.Where("user_id = ? AND day = ?", userID, day).Limit(1).Find(&existing).Error; err != nil {
return err
}
@@ -240,12 +240,12 @@ func (s *PointsService) DrawLottery(userID uint) (LotteryStatus, error) {
if err != nil {
return err
}
row := model.LotteryDraw{UserID: userID, Day: day, Points: pts}
row := models.LotteryDraw{UserID: userID, Day: day, Points: pts}
if err := tx.Create(&row).Error; err != nil {
return err
}
if pts > 0 {
if _, err := AdjustPointsTx(tx, userID, pts, model.PointReasonLottery, "lottery", row.ID, "每日抽奖"); err != nil {
if _, err := AdjustPointsTx(tx, userID, pts, models.PointReasonLottery, "lottery", row.ID, "每日抽奖"); err != nil {
return err
}
}
@@ -256,7 +256,7 @@ func (s *PointsService) DrawLottery(userID uint) (LotteryStatus, error) {
}
// ListLedger 积分流水
func (s *PointsService) ListLedger(userID uint, page, size int) ([]model.PointLedger, int64, error) {
func (s *PointsService) ListLedger(userID uint, page, size int) ([]models.PointLedger, int64, error) {
if page < 1 {
page = 1
}
@@ -264,9 +264,9 @@ func (s *PointsService) ListLedger(userID uint, page, size int) ([]model.PointLe
size = 20
}
var total int64
model.DB.Model(&model.PointLedger{}).Where("user_id = ?", userID).Count(&total)
var rows []model.PointLedger
err := model.DB.Where("user_id = ?", userID).Order("id desc").
models.DB.Model(&models.PointLedger{}).Where("user_id = ?", userID).Count(&total)
var rows []models.PointLedger
err := models.DB.Where("user_id = ?", userID).Order("id desc").
Offset((page - 1) * size).Limit(size).Find(&rows).Error
return rows, total, err
}

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"encoding/json"
@@ -6,7 +6,7 @@ import (
"strings"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
@@ -54,7 +54,7 @@ func CreatePollForPost(tx *gorm.DB, postID uint, multi bool, maxChoices int, end
} else if maxChoices < 1 || maxChoices > len(options) {
maxChoices = len(options)
}
poll := model.Poll{PostID: postID, Multi: multi, MaxChoices: maxChoices, EndsAt: endsAt}
poll := models.Poll{PostID: postID, Multi: multi, MaxChoices: maxChoices, EndsAt: endsAt}
if err := tx.Create(&poll).Error; err != nil {
return err
}
@@ -66,7 +66,7 @@ func CreatePollForPost(tx *gorm.DB, postID uint, multi bool, maxChoices int, end
if len([]rune(text)) > 64 {
return errors.New("投票选项最多 64 字")
}
row := model.PollOption{PostID: postID, Text: text, SortOrder: i}
row := models.PollOption{PostID: postID, Text: text, SortOrder: i}
if err := tx.Create(&row).Error; err != nil {
return err
}
@@ -131,8 +131,8 @@ func parsePollEndsAt(raw string) (*time.Time, error) {
// closePollIfExpired 若已过截止时间则自动关闭投票
func closePollIfExpired(postID uint) error {
var poll model.Poll
if err := model.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil {
var poll models.Poll
if err := models.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil {
return err
}
if poll.Closed || poll.EndsAt == nil {
@@ -141,7 +141,7 @@ func closePollIfExpired(postID uint) error {
if time.Now().Before(*poll.EndsAt) {
return nil
}
res := model.DB.Model(&poll).Where("post_id = ? AND closed = ?", postID, false).Update("closed", true)
res := models.DB.Model(&poll).Where("post_id = ? AND closed = ?", postID, false).Update("closed", true)
return res.Error
}
@@ -150,12 +150,12 @@ func GetPollView(postID, viewerID uint) (*PollView, error) {
if err := closePollIfExpired(postID); err != nil {
return nil, err
}
var poll model.Poll
if err := model.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil {
var poll models.Poll
if err := models.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil {
return nil, err
}
var opts []model.PollOption
if err := model.DB.Where("post_id = ?", postID).Order("sort_order ASC, id ASC").Find(&opts).Error; err != nil {
var opts []models.PollOption
if err := models.DB.Where("post_id = ?", postID).Order("sort_order ASC, id ASC").Find(&opts).Error; err != nil {
return nil, err
}
total := 0
@@ -165,8 +165,8 @@ func GetPollView(postID, viewerID uint) (*PollView, error) {
showResults := poll.Closed
var myIDs []uint
if viewerID > 0 {
var votes []model.PollVote
model.DB.Where("post_id = ? AND user_id = ?", postID, viewerID).Find(&votes)
var votes []models.PollVote
models.DB.Where("post_id = ? AND user_id = ?", postID, viewerID).Find(&votes)
for _, v := range votes {
myIDs = append(myIDs, v.OptionID)
}
@@ -196,15 +196,15 @@ func VotePoll(postID, userID uint, optionIDs []uint) error {
if err := closePollIfExpired(postID); err != nil {
return err
}
var poll model.Poll
if err := model.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil {
var poll models.Poll
if err := models.DB.Where("post_id = ?", postID).First(&poll).Error; err != nil {
return err
}
if poll.Closed {
return ErrPollClosed
}
var existing int64
model.DB.Model(&model.PollVote{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&existing)
models.DB.Model(&models.PollVote{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&existing)
if existing > 0 {
return ErrPollAlreadyVoted
}
@@ -223,18 +223,18 @@ func VotePoll(postID, userID uint, optionIDs []uint) error {
return ErrPollInvalidVote
}
seen[oid] = true
var opt model.PollOption
if err := model.DB.Where("id = ? AND post_id = ?", oid, postID).First(&opt).Error; err != nil {
var opt models.PollOption
if err := models.DB.Where("id = ? AND post_id = ?", oid, postID).First(&opt).Error; err != nil {
return ErrPollInvalidVote
}
}
return model.DB.Transaction(func(tx *gorm.DB) error {
return models.DB.Transaction(func(tx *gorm.DB) error {
for _, oid := range optionIDs {
v := model.PollVote{PostID: postID, OptionID: oid, UserID: userID}
v := models.PollVote{PostID: postID, OptionID: oid, UserID: userID}
if err := tx.Create(&v).Error; err != nil {
return err
}
if err := tx.Model(&model.PollOption{}).Where("id = ?", oid).
if err := tx.Model(&models.PollOption{}).Where("id = ?", oid).
UpdateColumn("vote_count", gorm.Expr("vote_count + 1")).Error; err != nil {
return err
}
@@ -248,7 +248,7 @@ func ClosePoll(postID, userID uint, isAdmin bool, postAuthorID uint) error {
if !isAdmin && userID != postAuthorID {
return ErrPermissionDenied
}
res := model.DB.Model(&model.Poll{}).Where("post_id = ?", postID).Update("closed", true)
res := models.DB.Model(&models.Poll{}).Where("post_id = ?", postID).Update("closed", true)
if res.Error != nil {
return res.Error
}
@@ -261,20 +261,20 @@ func ClosePoll(postID, userID uint, isAdmin bool, postAuthorID uint) error {
// LockPollOptions 编辑时锁定选项(已发布帖不允许改选项文案)
func LockPollOptions(postID uint) bool {
var n int64
model.DB.Model(&model.PollVote{}).Where("post_id = ?", postID).Count(&n)
models.DB.Model(&models.PollVote{}).Where("post_id = ?", postID).Count(&n)
return n > 0
}
// EnsurePollExists 检查投票帖是否有 poll 记录
func EnsurePollExists(postID uint) bool {
var n int64
model.DB.Model(&model.Poll{}).Where("post_id = ?", postID).Count(&n)
models.DB.Model(&models.Poll{}).Where("post_id = ?", postID).Count(&n)
return n > 0
}
// DeletePollData 删帖时清理投票数据
func DeletePollData(tx *gorm.DB, postID uint) {
tx.Where("post_id = ?", postID).Delete(&model.PollVote{})
tx.Where("post_id = ?", postID).Delete(&model.PollOption{})
tx.Where("post_id = ?", postID).Delete(&model.Poll{})
tx.Where("post_id = ?", postID).Delete(&models.PollVote{})
tx.Where("post_id = ?", postID).Delete(&models.PollOption{})
tx.Where("post_id = ?", postID).Delete(&models.Poll{})
}

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"errors"
@@ -6,7 +6,7 @@ import (
"strings"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
@@ -21,21 +21,21 @@ func NewPostService(filter *SensitiveFilter, settings *ForumSettingsService) *Po
func normalizePostType(raw string) string {
switch strings.TrimSpace(raw) {
case model.PostTypeQuestion:
return model.PostTypeQuestion
case model.PostTypePoll:
return model.PostTypePoll
case model.PostTypeBounty:
return model.PostTypeBounty
case model.PostTypeLottery:
return model.PostTypeLottery
case models.PostTypeQuestion:
return models.PostTypeQuestion
case models.PostTypePoll:
return models.PostTypePoll
case models.PostTypeBounty:
return models.PostTypeBounty
case models.PostTypeLottery:
return models.PostTypeLottery
default:
return model.PostTypeNormal
return models.PostTypeNormal
}
}
func isSpecialPostType(t string) bool {
return t == model.PostTypePoll || t == model.PostTypeBounty || t == model.PostTypeLottery
return t == models.PostTypePoll || t == models.PostTypeBounty || t == models.PostTypeLottery
}
type PostListQuery struct {
@@ -55,16 +55,16 @@ type PostListQuery struct {
// PostListItem 帖子列表项(含评论数等扩展字段)
type PostListItem struct {
model.Post
models.Post
CommentCount int `json:"comment_count"`
LastReplyAt *time.Time `json:"last_reply_at,omitempty"`
LastReplyUser *model.User `json:"last_reply_user,omitempty"`
LastReplyUser *models.User `json:"last_reply_user,omitempty"`
LastReplyGuestNick string `json:"last_reply_guest_nick,omitempty"`
}
type lastReplyInfo struct {
At *time.Time
User *model.User
User *models.User
GuestNick string
}
@@ -102,8 +102,8 @@ func (s *PostService) commentCountMap(postIDs []uint) map[uint]int {
Count int
}
var rows []row
model.DB.Model(&model.Comment{}).Select("post_id, count(*) as count").
Where("post_id IN ? AND status = ?", postIDs, model.ContentStatusPublished).
models.DB.Model(&models.Comment{}).Select("post_id, count(*) as count").
Where("post_id IN ? AND status = ?", postIDs, models.ContentStatusPublished).
Group("post_id").Scan(&rows)
m := make(map[uint]int)
for _, r := range rows {
@@ -122,9 +122,9 @@ func (s *PostService) lastReplyInfoMap(postIDs []uint) map[uint]lastReplyInfo {
MaxID uint
}
var idRows []idRow
model.DB.Model(&model.Comment{}).
models.DB.Model(&models.Comment{}).
Select("post_id, MAX(id) as max_id").
Where("post_id IN ? AND status = ?", postIDs, model.ContentStatusPublished).
Where("post_id IN ? AND status = ?", postIDs, models.ContentStatusPublished).
Group("post_id").
Scan(&idRows)
if len(idRows) == 0 {
@@ -134,8 +134,8 @@ func (s *PostService) lastReplyInfoMap(postIDs []uint) map[uint]lastReplyInfo {
for i, r := range idRows {
commentIDs[i] = r.MaxID
}
var comments []model.Comment
if err := model.DB.Preload("User").Where("id IN ?", commentIDs).Find(&comments).Error; err != nil {
var comments []models.Comment
if err := models.DB.Preload("User").Where("id IN ?", commentIDs).Find(&comments).Error; err != nil {
return m
}
for i := range comments {
@@ -162,16 +162,16 @@ func (s *PostService) HotPosts(limit int) ([]PostListItem, error) {
limit = 10
}
since := time.Now().Add(-7 * 24 * time.Hour)
var posts []model.Post
err := model.DB.Preload("User").Preload("Board").
Where("status = ?", model.ContentStatusPublished).
var posts []models.Post
err := models.DB.Preload("User").Preload("Board").
Where("status = ?", models.ContentStatusPublished).
Where(`EXISTS (
SELECT 1 FROM comments
WHERE comments.post_id = posts.id
AND comments.deleted_at IS NULL
AND comments.status = ?
AND comments.created_at >= ?
)`, model.ContentStatusPublished, since).
)`, models.ContentStatusPublished, since).
Order(`(
SELECT MAX(created_at) FROM comments
WHERE comments.post_id = posts.id
@@ -214,9 +214,9 @@ func (s *PostService) PopularTags(limit int) ([]TagCount, error) {
limit = 40
}
var rows []struct{ Tags string }
if err := model.DB.Model(&model.Post{}).
if err := models.DB.Model(&models.Post{}).
Select("tags").
Where("status = ? AND tags <> '' AND tags IS NOT NULL", model.ContentStatusPublished).
Where("status = ? AND tags <> '' AND tags IS NOT NULL", models.ContentStatusPublished).
Find(&rows).Error; err != nil {
return nil, err
}
@@ -258,21 +258,21 @@ func (s *PostService) PopularTags(limit int) ([]TagCount, error) {
func (s *PostService) CommentCount(postID uint) int {
var count int64
model.DB.Model(&model.Comment{}).
Where("post_id = ? AND status = ?", postID, model.ContentStatusPublished).
models.DB.Model(&models.Comment{}).
Where("post_id = ? AND status = ?", postID, models.ContentStatusPublished).
Count(&count)
return int(count)
}
// CanViewPost 是否可查看该帖pending/rejected 仅作者与管理员)
func CanViewPost(post *model.Post, viewerID uint, isAdmin bool) bool {
func CanViewPost(post *models.Post, viewerID uint, isAdmin bool) bool {
if post == nil {
return false
}
if isAdmin || post.Status == model.ContentStatusPublished || post.Status == "" {
if isAdmin || post.Status == models.ContentStatusPublished || post.Status == "" {
return true
}
if post.Status == model.ContentStatusPending || post.Status == model.ContentStatusRejected {
if post.Status == models.ContentStatusPending || post.Status == models.ContentStatusRejected {
return viewerID > 0 && post.UserID == viewerID
}
return false
@@ -281,7 +281,7 @@ func CanViewPost(post *model.Post, viewerID uint, isAdmin bool) bool {
func applyPostVisibility(db *gorm.DB, q PostListQuery) *gorm.DB {
if q.ViewerIsAdmin {
switch q.Status {
case model.ContentStatusPending, model.ContentStatusPublished, model.ContentStatusRejected:
case models.ContentStatusPending, models.ContentStatusPublished, models.ContentStatusRejected:
return db.Where("status = ?", q.Status)
case "all", "":
return db
@@ -292,15 +292,15 @@ func applyPostVisibility(db *gorm.DB, q PostListQuery) *gorm.DB {
if q.ViewerID > 0 {
return db.Where(
"status = ? OR (status IN ? AND user_id = ?)",
model.ContentStatusPublished,
[]string{model.ContentStatusPending, model.ContentStatusRejected},
models.ContentStatusPublished,
[]string{models.ContentStatusPending, models.ContentStatusRejected},
q.ViewerID,
)
}
return db.Where("status = ?", model.ContentStatusPublished)
return db.Where("status = ?", models.ContentStatusPublished)
}
func (s *PostService) List(q PostListQuery) ([]model.Post, int64, error) {
func (s *PostService) List(q PostListQuery) ([]models.Post, int64, error) {
if q.Page < 1 {
q.Page = 1
}
@@ -317,11 +317,11 @@ func (s *PostService) List(q PostListQuery) ([]model.Post, int64, error) {
if uid, ok := resolveAuthorUserID(author); ok {
q.UserID = uid
} else {
return []model.Post{}, 0, nil
return []models.Post{}, 0, nil
}
}
}
db := model.DB.Model(&model.Post{}).Preload("User").Preload("Board")
db := models.DB.Model(&models.Post{}).Preload("User").Preload("Board")
db = applyPostVisibility(db, q)
if q.BoardID > 0 {
db = db.Where("board_id = ?", q.BoardID)
@@ -345,7 +345,7 @@ func (s *PostService) List(q PostListQuery) ([]model.Post, int64, error) {
}
var total int64
db.Count(&total)
var posts []model.Post
var posts []models.Post
db = db.Order("pinned desc")
if q.BoardID > 0 {
db = db.Order("board_pinned desc")
@@ -396,19 +396,19 @@ func resolveAuthorUserID(author string) (uint, bool) {
if author == "" {
return 0, false
}
var u model.User
if err := model.DB.Select("id").Where("username = ?", author).First(&u).Error; err == nil {
var u models.User
if err := models.DB.Select("id").Where("username = ?", author).First(&u).Error; err == nil {
return u.ID, true
}
if err := model.DB.Select("id").Where("nickname = ?", author).First(&u).Error; err == nil {
if err := models.DB.Select("id").Where("nickname = ?", author).First(&u).Error; err == nil {
return u.ID, true
}
return 0, false
}
func (s *PostService) FindByID(id uint) (*model.Post, error) {
var post model.Post
err := model.DB.Preload("User").Preload("Board").First(&post, id).Error
func (s *PostService) FindByID(id uint) (*models.Post, error) {
var post models.Post
err := models.DB.Preload("User").Preload("Board").First(&post, id).Error
if err != nil {
return nil, ErrPostNotFound
}
@@ -416,11 +416,11 @@ func (s *PostService) FindByID(id uint) (*model.Post, error) {
}
func (s *PostService) RecordView(id uint) {
model.DB.Model(&model.Post{}).Where("id = ?", id).
models.DB.Model(&models.Post{}).Where("id = ?", id).
UpdateColumn("view_count", gorm.Expr("view_count + 1"))
}
func (s *PostService) GetByID(id uint) (*model.Post, error) {
func (s *PostService) GetByID(id uint) (*models.Post, error) {
post, err := s.FindByID(id)
if err != nil {
return nil, err
@@ -429,7 +429,7 @@ func (s *PostService) GetByID(id uint) (*model.Post, error) {
return post, nil
}
func (s *PostService) Create(userID, boardID uint, title, content, tags, postType string, skipModeration bool) (*model.Post, error) {
func (s *PostService) Create(userID, boardID uint, title, content, tags, postType string, skipModeration bool) (*models.Post, error) {
title = s.filter.Filter(strings.TrimSpace(title))
content = s.filter.Filter(SanitizePostHTML(content))
tags = s.filter.Filter(strings.TrimSpace(tags))
@@ -449,11 +449,11 @@ func (s *PostService) Create(userID, boardID uint, title, content, tags, postTyp
if _, err := NewBoardService().GetByID(boardID); err != nil {
return nil, err
}
status := model.ContentStatusPending
status := models.ContentStatusPending
if skipModeration {
status = model.ContentStatusPublished
status = models.ContentStatusPublished
}
post := &model.Post{
post := &models.Post{
BoardID: boardID,
UserID: userID,
Title: title,
@@ -464,10 +464,10 @@ func (s *PostService) Create(userID, boardID uint, title, content, tags, postTyp
QuestionResolved: false,
Status: status,
}
if err := model.DB.Create(post).Error; err != nil {
if err := models.DB.Create(post).Error; err != nil {
return nil, err
}
if status == model.ContentStatusPublished {
if status == models.ContentStatusPublished {
AddExp(userID, 10)
}
return post, nil
@@ -476,8 +476,8 @@ func (s *PostService) Create(userID, boardID uint, title, content, tags, postTyp
// Update 更新帖子。boardID>0 时可改板块;为 0 时保持原板块。
// postType 为空时保持原类型;改为非 question 时清除已解决标记。
func (s *PostService) Update(userID, postID uint, isAdmin, skipModeration bool, title, content, tags, postType string, boardID uint) error {
var post model.Post
if err := model.DB.First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.First(&post, postID).Error; err != nil {
return ErrPostNotFound
}
if !isAdmin && post.UserID != userID {
@@ -517,11 +517,11 @@ func (s *PostService) Update(userID, postID uint, isAdmin, skipModeration bool,
return errors.New("不能改为特殊帖子类型")
}
nextResolved := post.QuestionResolved
if nextType != model.PostTypeQuestion {
if nextType != models.PostTypeQuestion {
nextResolved = false
}
return model.DB.Transaction(func(tx *gorm.DB) error {
rev := model.PostRevision{
return models.DB.Transaction(func(tx *gorm.DB) error {
rev := models.PostRevision{
PostID: postID, EditorID: userID,
Title: post.Title, Content: post.Content, Tags: post.Tags,
}
@@ -539,7 +539,7 @@ func (s *PostService) Update(userID, postID uint, isAdmin, skipModeration bool,
}
// 非免审用户修改后重新进入审核
if !skipModeration {
updates["status"] = model.ContentStatusPending
updates["status"] = models.ContentStatusPending
}
return tx.Model(&post).Updates(updates).Error
})
@@ -548,16 +548,16 @@ func (s *PostService) Update(userID, postID uint, isAdmin, skipModeration bool,
// SetStatus 设置帖子审核状态
func (s *PostService) SetStatus(postID uint, status string) error {
switch status {
case model.ContentStatusPending, model.ContentStatusPublished, model.ContentStatusRejected:
case models.ContentStatusPending, models.ContentStatusPublished, models.ContentStatusRejected:
default:
return errors.New("无效的审核状态")
}
var post model.Post
if err := model.DB.Select("id", "user_id", "status").First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.Select("id", "user_id", "status").First(&post, postID).Error; err != nil {
return ErrPostNotFound
}
prev := post.Status
res := model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("status", status)
res := models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("status", status)
if res.Error != nil {
return res.Error
}
@@ -565,7 +565,7 @@ func (s *PostService) SetStatus(postID uint, status string) error {
return ErrPostNotFound
}
// 首次变为已发布时加经验
if status == model.ContentStatusPublished && prev != model.ContentStatusPublished {
if status == models.ContentStatusPublished && prev != models.ContentStatusPublished {
AddExp(post.UserID, 10)
}
return nil
@@ -574,24 +574,24 @@ func (s *PostService) SetStatus(postID uint, status string) error {
// PendingPostCount 待审帖数量
func (s *PostService) PendingPostCount() (int64, error) {
var n int64
err := model.DB.Model(&model.Post{}).Where("status = ?", model.ContentStatusPending).Count(&n).Error
err := models.DB.Model(&models.Post{}).Where("status = ?", models.ContentStatusPending).Count(&n).Error
return n, err
}
// CanEdit 判断当前用户是否可编辑帖子
func (s *PostService) CanEdit(post *model.Post, isAdmin bool) bool {
func (s *PostService) CanEdit(post *models.Post, isAdmin bool) bool {
return s.checkEditable(post, isAdmin) == nil
}
// EditBlockReason 返回不可编辑的原因(可编辑时返回空字符串)
func (s *PostService) EditBlockReason(post *model.Post, isAdmin bool) string {
func (s *PostService) EditBlockReason(post *models.Post, isAdmin bool) string {
if err := s.checkEditable(post, isAdmin); err != nil {
return err.Error()
}
return ""
}
func (s *PostService) checkEditable(post *model.Post, isAdmin bool) error {
func (s *PostService) checkEditable(post *models.Post, isAdmin bool) error {
if isAdmin {
return nil
}
@@ -606,7 +606,7 @@ func (s *PostService) checkEditable(post *model.Post, isAdmin bool) error {
}
// CanUserEdit 判断指定用户是否可编辑帖子
func (s *PostService) CanUserEdit(post *model.Post, userID uint, isAdmin bool) bool {
func (s *PostService) CanUserEdit(post *models.Post, userID uint, isAdmin bool) bool {
if userID == 0 {
return false
}
@@ -617,7 +617,7 @@ func (s *PostService) CanUserEdit(post *model.Post, userID uint, isAdmin bool) b
}
// UserEditBlockReason 返回用户不可编辑的原因
func (s *PostService) UserEditBlockReason(post *model.Post, userID uint, isAdmin bool) string {
func (s *PostService) UserEditBlockReason(post *models.Post, userID uint, isAdmin bool) string {
if userID == 0 {
return "请先登录"
}
@@ -628,7 +628,7 @@ func (s *PostService) UserEditBlockReason(post *model.Post, userID uint, isAdmin
}
func (s *PostService) SetEditLocked(postID uint, locked bool) error {
res := model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("edit_locked", locked)
res := models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("edit_locked", locked)
if res.Error != nil {
return res.Error
}
@@ -640,7 +640,7 @@ func (s *PostService) SetEditLocked(postID uint, locked bool) error {
// SetCommentsLocked 锁定/解锁讨论(禁止新评论)
func (s *PostService) SetCommentsLocked(postID uint, locked bool) error {
res := model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("comments_locked", locked)
res := models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("comments_locked", locked)
if res.Error != nil {
return res.Error
}
@@ -650,22 +650,22 @@ func (s *PostService) SetCommentsLocked(postID uint, locked bool) error {
return nil
}
func (s *PostService) ListRevisions(postID uint) ([]model.PostRevision, error) {
var revs []model.PostRevision
err := model.DB.Preload("Editor").Where("post_id = ?", postID).
func (s *PostService) ListRevisions(postID uint) ([]models.PostRevision, error) {
var revs []models.PostRevision
err := models.DB.Preload("Editor").Where("post_id = ?", postID).
Order("id desc").Find(&revs).Error
if err != nil {
return nil, err
}
if revs == nil {
revs = []model.PostRevision{}
revs = []models.PostRevision{}
}
return revs, nil
}
func (s *PostService) GetRevision(postID, revID uint) (*model.PostRevision, error) {
var rev model.PostRevision
err := model.DB.Preload("Editor").
func (s *PostService) GetRevision(postID, revID uint) (*models.PostRevision, error) {
var rev models.PostRevision
err := models.DB.Preload("Editor").
Where("id = ? AND post_id = ?", revID, postID).First(&rev).Error
if err != nil {
return nil, ErrRevisionNotFound
@@ -678,17 +678,17 @@ func (s *PostService) Delete(userID, postID uint, isAdmin bool) error {
if !isAdmin {
return ErrPermissionDenied
}
var post model.Post
if err := model.DB.First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.First(&post, postID).Error; err != nil {
return ErrPostNotFound
}
return model.DB.Transaction(func(tx *gorm.DB) error {
return models.DB.Transaction(func(tx *gorm.DB) error {
if err := RefundBountyIfOpen(tx, &post); err != nil {
return err
}
DeletePollData(tx, postID)
DeleteLotteryData(tx, postID)
if err := tx.Where("post_id = ?", postID).Delete(&model.Comment{}).Error; err != nil {
if err := tx.Where("post_id = ?", postID).Delete(&models.Comment{}).Error; err != nil {
return err
}
return tx.Delete(&post).Error
@@ -707,7 +707,7 @@ func (s *PostService) ListTrash(page, size int, keyword string) ([]TrashPostItem
page = 1
}
size = s.settings.NormalizePageSize(size)
db := model.DB.Unscoped().Model(&model.Post{}).
db := models.DB.Unscoped().Model(&models.Post{}).
Where("deleted_at IS NOT NULL").
Preload("User").Preload("Board")
if keyword != "" {
@@ -722,7 +722,7 @@ func (s *PostService) ListTrash(page, size int, keyword string) ([]TrashPostItem
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var posts []model.Post
var posts []models.Post
if err := db.Order("deleted_at DESC").Offset((page - 1) * size).Limit(size).Find(&posts).Error; err != nil {
return nil, 0, err
}
@@ -739,7 +739,7 @@ func (s *PostService) ListTrash(page, size int, keyword string) ([]TrashPostItem
Cnt int
}
var rows []row
_ = model.DB.Unscoped().Model(&model.Comment{}).
_ = models.DB.Unscoped().Model(&models.Comment{}).
Select("post_id, COUNT(*) as cnt").
Where("post_id IN ?", ids).
Group("post_id").Scan(&rows)
@@ -760,15 +760,15 @@ func (s *PostService) ListTrash(page, size int, keyword string) ([]TrashPostItem
// Restore 从回收站恢复帖子及评论
func (s *PostService) Restore(postID uint) error {
var post model.Post
if err := model.DB.Unscoped().First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.Unscoped().First(&post, postID).Error; err != nil {
return ErrPostNotFound
}
if !post.DeletedAt.Valid {
return errors.New("帖子未被删除")
}
return model.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Model(&model.Comment{}).
return models.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Unscoped().Model(&models.Comment{}).
Where("post_id = ? AND deleted_at IS NOT NULL", postID).
Update("deleted_at", nil).Error; err != nil {
return err
@@ -779,33 +779,33 @@ func (s *PostService) Restore(postID uint) error {
// Purge 永久删除回收站中的帖子(含评论、点赞、收藏、修订)
func (s *PostService) Purge(postID uint) error {
var post model.Post
if err := model.DB.Unscoped().First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.Unscoped().First(&post, postID).Error; err != nil {
return ErrPostNotFound
}
if !post.DeletedAt.Valid {
return errors.New("仅可彻底删除回收站中的帖子,请先删除帖子")
}
return model.DB.Transaction(func(tx *gorm.DB) error {
return models.DB.Transaction(func(tx *gorm.DB) error {
var commentIDs []uint
if err := tx.Unscoped().Model(&model.Comment{}).Where("post_id = ?", postID).Pluck("id", &commentIDs).Error; err != nil {
if err := tx.Unscoped().Model(&models.Comment{}).Where("post_id = ?", postID).Pluck("id", &commentIDs).Error; err != nil {
return err
}
if len(commentIDs) > 0 {
if err := tx.Where("comment_id IN ?", commentIDs).Delete(&model.CommentRevision{}).Error; err != nil {
if err := tx.Where("comment_id IN ?", commentIDs).Delete(&models.CommentRevision{}).Error; err != nil {
return err
}
}
if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&model.Comment{}).Error; err != nil {
if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&models.Comment{}).Error; err != nil {
return err
}
if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&model.PostLike{}).Error; err != nil {
if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&models.PostLike{}).Error; err != nil {
return err
}
if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&model.PostFavorite{}).Error; err != nil {
if err := tx.Unscoped().Where("post_id = ?", postID).Delete(&models.PostFavorite{}).Error; err != nil {
return err
}
if err := tx.Where("post_id = ?", postID).Delete(&model.PostRevision{}).Error; err != nil {
if err := tx.Where("post_id = ?", postID).Delete(&models.PostRevision{}).Error; err != nil {
return err
}
return tx.Unscoped().Delete(&post).Error
@@ -813,52 +813,52 @@ func (s *PostService) Purge(postID uint) error {
}
func (s *PostService) SetPinned(postID uint, pinned bool) error {
return model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("pinned", pinned).Error
return models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("pinned", pinned).Error
}
func (s *PostService) SetBoardPinned(postID uint, boardPinned bool) error {
return model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("board_pinned", boardPinned).Error
return models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("board_pinned", boardPinned).Error
}
func (s *PostService) SetFeatured(postID uint, featured bool) error {
return model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("featured", featured).Error
return models.DB.Model(&models.Post{}).Where("id = ?", postID).Update("featured", featured).Error
}
// SetQuestionResolved 标记问答帖已解决 / 未解决(作者或管理员)
func (s *PostService) SetQuestionResolved(userID, postID uint, isAdmin bool, resolved bool) error {
var post model.Post
if err := model.DB.First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.First(&post, postID).Error; err != nil {
return ErrPostNotFound
}
if !isAdmin && post.UserID != userID {
return ErrPermissionDenied
}
if post.PostType != model.PostTypeQuestion {
if post.PostType != models.PostTypeQuestion {
return errors.New("仅问答帖可标记解决状态")
}
return model.DB.Model(&post).Update("question_resolved", resolved).Error
return models.DB.Model(&post).Update("question_resolved", resolved).Error
}
func (s *PostService) ToggleLike(userID, postID uint) (liked bool, err error) {
var post model.Post
if err := model.DB.Select("id", "user_id").First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.Select("id", "user_id").First(&post, postID).Error; err != nil {
return false, ErrPostNotFound
}
var like model.PostLike
result := model.DB.Where("post_id = ? AND user_id = ?", postID, userID).Limit(1).Find(&like)
var like models.PostLike
result := models.DB.Where("post_id = ? AND user_id = ?", postID, userID).Limit(1).Find(&like)
if result.Error != nil {
return false, result.Error
}
if result.RowsAffected > 0 {
model.DB.Delete(&like)
model.DB.Model(&model.Post{}).Where("id = ?", postID).UpdateColumn("like_count", gorm.Expr("like_count - 1"))
models.DB.Delete(&like)
models.DB.Model(&models.Post{}).Where("id = ?", postID).UpdateColumn("like_count", gorm.Expr("like_count - 1"))
return false, nil
}
like = model.PostLike{PostID: postID, UserID: userID}
if err := model.DB.Create(&like).Error; err != nil {
like = models.PostLike{PostID: postID, UserID: userID}
if err := models.DB.Create(&like).Error; err != nil {
return false, err
}
model.DB.Model(&model.Post{}).Where("id = ?", postID).UpdateColumn("like_count", gorm.Expr("like_count + 1"))
models.DB.Model(&models.Post{}).Where("id = ?", postID).UpdateColumn("like_count", gorm.Expr("like_count + 1"))
// 他人点赞给作者加经验;自赞不计
if userID != post.UserID {
AddExp(post.UserID, 1)
@@ -871,24 +871,24 @@ func (s *PostService) ToggleLike(userID, postID uint) (liked bool, err error) {
func (s *PostService) IsLiked(userID, postID uint) bool {
var count int64
model.DB.Model(&model.PostLike{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count)
models.DB.Model(&models.PostLike{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count)
return count > 0
}
func (s *PostService) ToggleFavorite(userID, postID uint) (faved bool, err error) {
var fav model.PostFavorite
result := model.DB.Where("post_id = ? AND user_id = ?", postID, userID).Limit(1).Find(&fav)
var fav models.PostFavorite
result := models.DB.Where("post_id = ? AND user_id = ?", postID, userID).Limit(1).Find(&fav)
if result.Error != nil {
return false, result.Error
}
if result.RowsAffected > 0 {
if err := model.DB.Delete(&fav).Error; err != nil {
if err := models.DB.Delete(&fav).Error; err != nil {
return false, err
}
return false, nil
}
fav = model.PostFavorite{PostID: postID, UserID: userID}
if err := model.DB.Create(&fav).Error; err != nil {
fav = models.PostFavorite{PostID: postID, UserID: userID}
if err := models.DB.Create(&fav).Error; err != nil {
return false, err
}
return true, nil
@@ -896,11 +896,11 @@ func (s *PostService) ToggleFavorite(userID, postID uint) (faved bool, err error
func (s *PostService) IsFavorited(userID, postID uint) bool {
var count int64
model.DB.Model(&model.PostFavorite{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count)
models.DB.Model(&models.PostFavorite{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count)
return count > 0
}
func (s *PostService) ListFavorites(userID uint, page, size int) ([]model.PostFavorite, int64, error) {
func (s *PostService) ListFavorites(userID uint, page, size int) ([]models.PostFavorite, int64, error) {
if page < 1 {
page = 1
}
@@ -908,17 +908,17 @@ func (s *PostService) ListFavorites(userID uint, page, size int) ([]model.PostFa
size = 20
}
// 仅统计可查看的收藏(已公开,或本人未公开帖)
base := model.DB.Model(&model.PostFavorite{}).
base := models.DB.Model(&models.PostFavorite{}).
Joins("JOIN posts ON posts.id = post_favorites.post_id AND posts.deleted_at IS NULL").
Where("post_favorites.user_id = ?", userID).
Where("posts.status = ? OR posts.user_id = ?", model.ContentStatusPublished, userID)
Where("posts.status = ? OR posts.user_id = ?", models.ContentStatusPublished, userID)
var total int64
base.Count(&total)
var favs []model.PostFavorite
err := model.DB.Preload("Post.User").Preload("Post.Board").
var favs []models.PostFavorite
err := models.DB.Preload("Post.User").Preload("Post.Board").
Joins("JOIN posts ON posts.id = post_favorites.post_id AND posts.deleted_at IS NULL").
Where("post_favorites.user_id = ?", userID).
Where("posts.status = ? OR posts.user_id = ?", model.ContentStatusPublished, userID).
Where("posts.status = ? OR posts.user_id = ?", models.ContentStatusPublished, userID).
Order("post_favorites.id desc").
Offset((page - 1) * size).Limit(size).Find(&favs).Error
return favs, total, err
@@ -937,9 +937,9 @@ func (s *PostService) ListSitemap(limit int) ([]SitemapPost, error) {
limit = 5000
}
var rows []SitemapPost
err := model.DB.Model(&model.Post{}).
err := models.DB.Model(&models.Post{}).
Select("id, created_at, updated_at").
Where("status = ?", model.ContentStatusPublished).
Where("status = ?", models.ContentStatusPublished).
Order("updated_at desc, id desc").
Limit(limit).
Find(&rows).Error

View File

@@ -1,10 +1,10 @@
package service
package services
import (
"errors"
"strconv"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
@@ -16,30 +16,30 @@ type PostCreateExtras struct {
}
// FinalizeSpecialPostCreate 创建帖后初始化投票/悬赏/抽奖
func FinalizeSpecialPostCreate(post *model.Post, userID uint, extras PostCreateExtras) error {
func FinalizeSpecialPostCreate(post *models.Post, userID uint, extras PostCreateExtras) error {
if post == nil {
return errors.New("帖子不存在")
}
return model.DB.Transaction(func(tx *gorm.DB) error {
return models.DB.Transaction(func(tx *gorm.DB) error {
switch post.PostType {
case model.PostTypePoll:
case models.PostTypePoll:
opts, multi, maxChoices, endsAt, err := ParsePollOptionsJSON(extras.PollOptionsJSON)
if err != nil {
return err
}
return CreatePollForPost(tx, post.ID, multi, maxChoices, endsAt, opts)
case model.PostTypeBounty:
case models.PostTypeBounty:
if extras.BountyPoints < 1 {
return ErrBountyInvalidPoint
}
if err := tx.Model(post).Updates(map[string]interface{}{
"bounty_points": extras.BountyPoints,
"bounty_status": model.BountyStatusOpen,
"bounty_status": models.BountyStatusOpen,
}).Error; err != nil {
return err
}
return EscrowBounty(tx, userID, post.ID, extras.BountyPoints)
case model.PostTypeLottery:
case models.PostTypeLottery:
count := extras.LotteryWinnerCount
if count < 1 {
count = 1
@@ -49,7 +49,7 @@ func FinalizeSpecialPostCreate(post *model.Post, userID uint, extras PostCreateE
}
return tx.Model(post).Updates(map[string]interface{}{
"lottery_winner_count": count,
"lottery_status": model.PostLotteryStatusOpen,
"lottery_status": models.PostLotteryStatusOpen,
}).Error
default:
return nil

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"sync"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"errors"
@@ -7,7 +7,7 @@ import (
"time"
"unicode/utf8"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
@@ -38,11 +38,11 @@ func NewReportService(
func normalizeReportReason(reason string) (string, error) {
switch strings.TrimSpace(reason) {
case model.ReportReasonSpam,
model.ReportReasonAbuse,
model.ReportReasonIllegal,
model.ReportReasonIrrelevant,
model.ReportReasonOther:
case models.ReportReasonSpam,
models.ReportReasonAbuse,
models.ReportReasonIllegal,
models.ReportReasonIrrelevant,
models.ReportReasonOther:
return reason, nil
default:
return "", errors.New("请选择有效的举报原因")
@@ -51,15 +51,15 @@ func normalizeReportReason(reason string) (string, error) {
func ReportReasonLabel(reason string) string {
switch reason {
case model.ReportReasonSpam:
case models.ReportReasonSpam:
return "垃圾广告"
case model.ReportReasonAbuse:
case models.ReportReasonAbuse:
return "人身攻击 / 辱骂"
case model.ReportReasonIllegal:
case models.ReportReasonIllegal:
return "违法违规"
case model.ReportReasonIrrelevant:
case models.ReportReasonIrrelevant:
return "内容无关 / 灌水"
case model.ReportReasonOther:
case models.ReportReasonOther:
return "其他"
default:
return reason
@@ -67,7 +67,7 @@ func ReportReasonLabel(reason string) string {
}
// Create 用户举报帖子
func (s *ReportService) Create(reporterID, postID uint, reason, detail string) (*model.PostReport, error) {
func (s *ReportService) Create(reporterID, postID uint, reason, detail string) (*models.PostReport, error) {
reason, err := normalizeReportReason(reason)
if err != nil {
return nil, err
@@ -80,8 +80,8 @@ func (s *ReportService) Create(reporterID, postID uint, reason, detail string) (
detail = s.filter.Filter(detail)
}
var post model.Post
if err := model.DB.Select("id", "user_id", "title").First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.Select("id", "user_id", "title").First(&post, postID).Error; err != nil {
return nil, ErrPostNotFound
}
if post.UserID == reporterID {
@@ -89,29 +89,29 @@ func (s *ReportService) Create(reporterID, postID uint, reason, detail string) (
}
var existing int64
model.DB.Model(&model.PostReport{}).
Where("post_id = ? AND reporter_id = ? AND status = ?", postID, reporterID, model.ReportStatusPending).
models.DB.Model(&models.PostReport{}).
Where("post_id = ? AND reporter_id = ? AND status = ?", postID, reporterID, models.ReportStatusPending).
Count(&existing)
if existing > 0 {
return nil, ErrReportAlreadyExists
}
rep := &model.PostReport{
rep := &models.PostReport{
PostID: postID,
ReporterID: reporterID,
Reason: reason,
Detail: detail,
Status: model.ReportStatusPending,
Status: models.ReportStatusPending,
}
if err := model.DB.Create(rep).Error; err != nil {
if err := models.DB.Create(rep).Error; err != nil {
return nil, err
}
_ = model.DB.Preload("Post").Preload("Reporter").First(rep, rep.ID).Error
_ = models.DB.Preload("Post").Preload("Reporter").First(rep, rep.ID).Error
return rep, nil
}
// CreateCommentReport 用户举报评论
func (s *ReportService) CreateCommentReport(reporterID, commentID uint, reason, detail string) (*model.PostReport, error) {
func (s *ReportService) CreateCommentReport(reporterID, commentID uint, reason, detail string) (*models.PostReport, error) {
reason, err := normalizeReportReason(reason)
if err != nil {
return nil, err
@@ -133,26 +133,26 @@ func (s *ReportService) CreateCommentReport(reporterID, commentID uint, reason,
}
var existing int64
model.DB.Model(&model.PostReport{}).
Where("comment_id = ? AND reporter_id = ? AND status = ?", commentID, reporterID, model.ReportStatusPending).
models.DB.Model(&models.PostReport{}).
Where("comment_id = ? AND reporter_id = ? AND status = ?", commentID, reporterID, models.ReportStatusPending).
Count(&existing)
if existing > 0 {
return nil, ErrReportAlreadyExists
}
cid := commentID
rep := &model.PostReport{
rep := &models.PostReport{
PostID: comment.PostID,
CommentID: &cid,
ReporterID: reporterID,
Reason: reason,
Detail: detail,
Status: model.ReportStatusPending,
Status: models.ReportStatusPending,
}
if err := model.DB.Create(rep).Error; err != nil {
if err := models.DB.Create(rep).Error; err != nil {
return nil, err
}
_ = model.DB.Preload("Post").Preload("Comment").Preload("Reporter").First(rep, rep.ID).Error
_ = models.DB.Preload("Post").Preload("Comment").Preload("Reporter").First(rep, rep.ID).Error
return rep, nil
}
@@ -163,13 +163,13 @@ type ReportListQuery struct {
}
// ListAdmin 管理员举报列表
func (s *ReportService) ListAdmin(q ReportListQuery) ([]model.PostReport, int64, error) {
func (s *ReportService) ListAdmin(q ReportListQuery) ([]models.PostReport, int64, error) {
if q.Page < 1 {
q.Page = 1
}
q.Size = s.settings.NormalizePageSize(q.Size)
db := model.DB.Model(&model.PostReport{})
db := models.DB.Model(&models.PostReport{})
if q.Status != "" && q.Status != "all" {
db = db.Where("status = ?", q.Status)
}
@@ -179,7 +179,7 @@ func (s *ReportService) ListAdmin(q ReportListQuery) ([]model.PostReport, int64,
return nil, 0, err
}
var list []model.PostReport
var list []models.PostReport
err := db.Preload("Post", func(tx *gorm.DB) *gorm.DB {
return tx.Unscoped()
}).Preload("Post.User").Preload("Comment", func(tx *gorm.DB) *gorm.DB {
@@ -195,8 +195,8 @@ func (s *ReportService) ListAdmin(q ReportListQuery) ([]model.PostReport, int64,
// PendingCount 待处理举报数
func (s *ReportService) PendingCount() (int64, error) {
var n int64
err := model.DB.Model(&model.PostReport{}).
Where("status = ?", model.ReportStatusPending).
err := models.DB.Model(&models.PostReport{}).
Where("status = ?", models.ReportStatusPending).
Count(&n).Error
return n, err
}
@@ -210,16 +210,16 @@ type HandleReportInput struct {
}
// Handle 处理举报
func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error) {
var rep model.PostReport
if err := model.DB.Preload("Post", func(tx *gorm.DB) *gorm.DB {
func (s *ReportService) Handle(in HandleReportInput) (*models.PostReport, error) {
var rep models.PostReport
if err := models.DB.Preload("Post", func(tx *gorm.DB) *gorm.DB {
return tx.Unscoped()
}).Preload("Comment", func(tx *gorm.DB) *gorm.DB {
return tx.Unscoped()
}).First(&rep, in.ReportID).Error; err != nil {
return nil, ErrReportNotFound
}
if rep.Status != model.ReportStatusPending {
if rep.Status != models.ReportStatusPending {
return nil, errors.New("该举报已处理")
}
@@ -251,9 +251,9 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
switch in.Action {
case "dismiss":
rep.Status = model.ReportStatusDismissed
rep.Status = models.ReportStatusDismissed
case "resolve":
rep.Status = model.ReportStatusResolved
rep.Status = models.ReportStatusResolved
case "reject_post":
if isCommentReport {
return nil, errors.New("评论举报请使用「拒绝该评论」")
@@ -265,10 +265,10 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
if utf8.RuneCountInString(reason) > 1000 {
return nil, errors.New("拒绝原因过长")
}
if err := s.posts.SetStatus(postID, model.ContentStatusRejected); err != nil {
if err := s.posts.SetStatus(postID, models.ContentStatusRejected); err != nil {
return nil, err
}
rep.Status = model.ReportStatusResolved
rep.Status = models.ReportStatusResolved
if note == "" {
rep.HandleNote = "已拒绝该帖并通知作者"
}
@@ -279,7 +279,7 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
authorID,
fmt.Sprintf("帖子《%s》未通过审核", postTitle),
FormatRejectContent(postTitle, postID, reason),
model.MessageKindReject,
models.MessageKindReject,
&pid,
&rid,
)
@@ -295,10 +295,10 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
if utf8.RuneCountInString(reason) > 1000 {
return nil, errors.New("拒绝原因过长")
}
if err := s.comments.SetStatus(*rep.CommentID, model.ContentStatusRejected); err != nil {
if err := s.comments.SetStatus(*rep.CommentID, models.ContentStatusRejected); err != nil {
return nil, err
}
rep.Status = model.ReportStatusResolved
rep.Status = models.ReportStatusResolved
if note == "" {
rep.HandleNote = "已拒绝该评论并通知作者"
}
@@ -310,7 +310,7 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
commentAuthorID,
fmt.Sprintf("评论未通过审核 · 《%s》", postTitle),
body,
model.MessageKindReject,
models.MessageKindReject,
&pid,
&rid,
)
@@ -319,13 +319,13 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
return nil, errors.New("无效的处理操作")
}
if err := model.DB.Save(&rep).Error; err != nil {
if err := models.DB.Save(&rep).Error; err != nil {
return nil, err
}
// 通知举报人处理结果
resultText := "已忽略"
if rep.Status == model.ReportStatusResolved {
if rep.Status == models.ReportStatusResolved {
switch in.Action {
case "reject_post":
resultText = "已核实并下架该帖"
@@ -349,12 +349,12 @@ func (s *ReportService) Handle(in HandleReportInput) (*model.PostReport, error)
rep.ReporterID,
"举报处理结果通知",
content,
model.MessageKindReportResult,
models.MessageKindReportResult,
&pid,
&rid,
)
_ = model.DB.Preload("Post", func(tx *gorm.DB) *gorm.DB {
_ = models.DB.Preload("Post", func(tx *gorm.DB) *gorm.DB {
return tx.Unscoped()
}).Preload("Comment", func(tx *gorm.DB) *gorm.DB {
return tx.Unscoped()

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"sync"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"strings"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"net/url"
@@ -7,7 +7,7 @@ import (
"time"
"unicode/utf8"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
var (
@@ -89,7 +89,7 @@ func FirstImageURL(htmlContent string) string {
}
// DisplayName 用户展示名
func DisplayName(u *model.User) string {
func DisplayName(u *models.User) string {
if u == nil {
return ""
}

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"encoding/json"
@@ -8,7 +8,7 @@ import (
"strings"
"sync"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
// 论坛设置键名
@@ -403,72 +403,72 @@ func NewForumSettingsService() *ForumSettingsService {
func (s *ForumSettingsService) ensureDefaults() {
for _, def := range forumSettingDefs {
var count int64
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", def.key).Count(&count)
models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", def.key).Count(&count)
if count == 0 {
model.DB.Create(&model.ForumSetting{Key: def.key, Value: def.defaultVal})
models.DB.Create(&models.ForumSetting{Key: def.key, Value: def.defaultVal})
}
}
for key, val := range feedSettingDefaults {
var count int64
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count)
if count == 0 {
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
models.DB.Create(&models.ForumSetting{Key: key, Value: val})
}
}
for key, val := range asideSettingDefaults {
var count int64
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count)
if count == 0 {
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
models.DB.Create(&models.ForumSetting{Key: key, Value: val})
}
}
for key, val := range mailSettingDefaults {
var count int64
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count)
if count == 0 {
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
models.DB.Create(&models.ForumSetting{Key: key, Value: val})
}
}
for key, val := range oidcSettingDefaults {
var count int64
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count)
if count == 0 {
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
models.DB.Create(&models.ForumSetting{Key: key, Value: val})
}
}
for key, val := range giteaSettingDefaults {
var count int64
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count)
if count == 0 {
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
models.DB.Create(&models.ForumSetting{Key: key, Value: val})
}
}
for key, val := range storageSettingDefaults {
var count int64
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count)
if count == 0 {
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
models.DB.Create(&models.ForumSetting{Key: key, Value: val})
}
}
for key, val := range siteBrandingDefaults {
var count int64
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count)
if count == 0 {
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
models.DB.Create(&models.ForumSetting{Key: key, Value: val})
}
}
for key, val := range friendLinkSettingDefaults {
var count int64
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
models.DB.Model(&models.ForumSetting{}).Where("`key` = ?", key).Count(&count)
if count == 0 {
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
models.DB.Create(&models.ForumSetting{Key: key, Value: val})
}
}
}
func (s *ForumSettingsService) getString(key, fallback string) string {
var setting model.ForumSetting
if err := model.DB.First(&setting, "`key` = ?", key).Error; err != nil {
var setting models.ForumSetting
if err := models.DB.First(&setting, "`key` = ?", key).Error; err != nil {
return fallback
}
return setting.Value
@@ -477,12 +477,12 @@ func (s *ForumSettingsService) getString(key, fallback string) string {
func (s *ForumSettingsService) setString(key, value string) error {
s.mu.Lock()
defer s.mu.Unlock()
return model.DB.Save(&model.ForumSetting{Key: key, Value: value}).Error
return models.DB.Save(&models.ForumSetting{Key: key, Value: value}).Error
}
func (s *ForumSettingsService) getInt(key string, fallback int) int {
var setting model.ForumSetting
if err := model.DB.First(&setting, "`key` = ?", key).Error; err != nil {
var setting models.ForumSetting
if err := models.DB.First(&setting, "`key` = ?", key).Error; err != nil {
return fallback
}
v, err := strconv.Atoi(setting.Value)
@@ -505,7 +505,7 @@ func (s *ForumSettingsService) setInt(key string, value int) error {
}
s.mu.Lock()
defer s.mu.Unlock()
return model.DB.Save(&model.ForumSetting{Key: key, Value: strconv.Itoa(value)}).Error
return models.DB.Save(&models.ForumSetting{Key: key, Value: strconv.Itoa(value)}).Error
}
return ErrInvalidSetting
}
@@ -1042,7 +1042,7 @@ func (s *ForumSettingsService) GiteaSyncConfig() GiteaSyncConfig {
}
cfg.Ready = cfg.Enabled && base != "" && cfg.HasToken
var n int64
model.DB.Model(&model.GiteaRepo{}).Where("private = ?", false).Count(&n)
models.DB.Model(&models.GiteaRepo{}).Where("private = ?", false).Count(&n)
cfg.RepoCount = n
return cfg
}

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"strings"

View File

@@ -1,10 +1,10 @@
package service
package services
import (
"errors"
"strings"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
var (
@@ -32,8 +32,8 @@ type SitePageSummary struct {
}
func (s *SitePageService) ListPublished() ([]SitePageSummary, error) {
var rows []model.SitePage
err := model.DB.Where("published = ?", true).
var rows []models.SitePage
err := models.DB.Where("published = ?", true).
Order("sort_order ASC, id ASC").
Find(&rows).Error
if err != nil {
@@ -49,22 +49,22 @@ func (s *SitePageService) ListPublished() ([]SitePageSummary, error) {
return out, nil
}
func (s *SitePageService) ListAll() ([]model.SitePage, error) {
var rows []model.SitePage
err := model.DB.Order("sort_order ASC, id ASC").Find(&rows).Error
func (s *SitePageService) ListAll() ([]models.SitePage, error) {
var rows []models.SitePage
err := models.DB.Order("sort_order ASC, id ASC").Find(&rows).Error
if err != nil {
return nil, err
}
return rows, nil
}
func (s *SitePageService) GetBySlug(slug string, allowUnpublished bool) (*model.SitePage, error) {
func (s *SitePageService) GetBySlug(slug string, allowUnpublished bool) (*models.SitePage, error) {
slug, ok := NormalizePageSlug(slug)
if !ok {
return nil, ErrSitePageNotFound
}
var page model.SitePage
q := model.DB.Where("slug = ?", slug)
var page models.SitePage
q := models.DB.Where("slug = ?", slug)
if !allowUnpublished {
q = q.Where("published = ?", true)
}
@@ -75,9 +75,9 @@ func (s *SitePageService) GetBySlug(slug string, allowUnpublished bool) (*model.
return &page, nil
}
func (s *SitePageService) GetByID(id uint) (*model.SitePage, error) {
var page model.SitePage
if err := model.DB.First(&page, id).Error; err != nil {
func (s *SitePageService) GetByID(id uint) (*models.SitePage, error) {
var page models.SitePage
if err := models.DB.First(&page, id).Error; err != nil {
return nil, ErrSitePageNotFound
}
page.Content = SanitizePostHTML(UnwrapContentGateTags(page.Content))
@@ -94,17 +94,17 @@ type SitePageInput struct {
ShowInNav bool `json:"show_in_nav"`
}
func (s *SitePageService) Create(in SitePageInput) (*model.SitePage, error) {
func (s *SitePageService) Create(in SitePageInput) (*models.SitePage, error) {
page, err := s.normalizeInput(in)
if err != nil {
return nil, err
}
var exists int64
model.DB.Model(&model.SitePage{}).Where("slug = ?", page.Slug).Count(&exists)
models.DB.Model(&models.SitePage{}).Where("slug = ?", page.Slug).Count(&exists)
if exists > 0 {
return nil, ErrSitePageSlugUsed
}
if err := model.DB.Create(page).Error; err != nil {
if err := models.DB.Create(page).Error; err != nil {
return nil, err
}
return page, nil
@@ -120,11 +120,11 @@ func (s *SitePageService) Update(id uint, in SitePageInput) error {
return err
}
var exists int64
model.DB.Model(&model.SitePage{}).Where("slug = ? AND id <> ?", next.Slug, id).Count(&exists)
models.DB.Model(&models.SitePage{}).Where("slug = ? AND id <> ?", next.Slug, id).Count(&exists)
if exists > 0 {
return ErrSitePageSlugUsed
}
return model.DB.Model(page).Updates(map[string]interface{}{
return models.DB.Model(page).Updates(map[string]interface{}{
"title": next.Title,
"slug": next.Slug,
"content": next.Content,
@@ -136,7 +136,7 @@ func (s *SitePageService) Update(id uint, in SitePageInput) error {
}
func (s *SitePageService) Delete(id uint) error {
res := model.DB.Delete(&model.SitePage{}, id)
res := models.DB.Delete(&models.SitePage{}, id)
if res.Error != nil {
return res.Error
}
@@ -152,20 +152,20 @@ func (s *SitePageService) SetPublished(id uint, published bool) error {
if err != nil {
return err
}
return model.DB.Model(page).Update("published", published).Error
return models.DB.Model(page).Update("published", published).Error
}
func (s *SitePageService) ListSitemap(limit int) ([]model.SitePage, error) {
func (s *SitePageService) ListSitemap(limit int) ([]models.SitePage, error) {
if limit <= 0 {
limit = 500
}
var rows []model.SitePage
err := model.DB.Where("published = ?", true).
var rows []models.SitePage
err := models.DB.Where("published = ?", true).
Order("updated_at DESC").Limit(limit).Find(&rows).Error
return rows, err
}
func (s *SitePageService) normalizeInput(in SitePageInput) (*model.SitePage, error) {
func (s *SitePageService) normalizeInput(in SitePageInput) (*models.SitePage, error) {
title := s.filter.Filter(strings.TrimSpace(in.Title))
slug, ok := NormalizePageSlug(in.Slug)
if !ok {
@@ -179,7 +179,7 @@ func (s *SitePageService) normalizeInput(in SitePageInput) (*model.SitePage, err
if content == "" {
return nil, errors.New("正文不能为空")
}
return &model.SitePage{
return &models.SitePage{
Title: title, Slug: slug, Content: content,
Published: in.Published, SortOrder: in.SortOrder,
ShowInFooter: in.ShowInFooter, ShowInNav: in.ShowInNav,

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"bytes"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"errors"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"crypto/sha256"
@@ -10,7 +10,7 @@ import (
"strings"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
@@ -110,8 +110,8 @@ func ListUnlockedKeys(userID, postID uint) (map[string]bool, error) {
if userID == 0 || postID == 0 {
return out, nil
}
var rows []model.PostContentUnlock
if err := model.DB.Select("block_key").Where("user_id = ? AND post_id = ?", userID, postID).Find(&rows).Error; err != nil {
var rows []models.PostContentUnlock
if err := models.DB.Select("block_key").Where("user_id = ? AND post_id = ?", userID, postID).Find(&rows).Error; err != nil {
return out, err
}
for _, r := range rows {
@@ -131,8 +131,8 @@ type UnlockResult struct {
// UnlockPointsBlock 积分解锁付费块
func UnlockPointsBlock(readerID, postID uint, blockKey string) (*UnlockResult, error) {
var post model.Post
if err := model.DB.First(&post, postID).Error; err != nil {
var post models.Post
if err := models.DB.First(&post, postID).Error; err != nil {
return nil, errors.New("帖子不存在")
}
block, ok := FindPointsBlock(post.Content, blockKey)
@@ -143,26 +143,26 @@ func UnlockPointsBlock(readerID, postID uint, blockKey string) (*UnlockResult, e
// 作者自己免费解锁记录(无分成)
if readerID == post.UserID {
var n int64
model.DB.Model(&model.PostContentUnlock{}).Where("user_id = ? AND post_id = ? AND block_key = ?", readerID, postID, blockKey).Count(&n)
models.DB.Model(&models.PostContentUnlock{}).Where("user_id = ? AND post_id = ? AND block_key = ?", readerID, postID, blockKey).Count(&n)
if n == 0 {
_ = model.DB.Create(&model.PostContentUnlock{
_ = models.DB.Create(&models.PostContentUnlock{
UserID: readerID, PostID: postID, BlockKey: blockKey, Cost: 0,
}).Error
}
return &UnlockResult{BlockKey: blockKey, Cost: 0, AuthorShare: 0, InnerHTML: block.Inner}, nil
}
var existing model.PostContentUnlock
model.DB.Where("user_id = ? AND post_id = ? AND block_key = ?", readerID, postID, blockKey).Limit(1).Find(&existing)
var existing models.PostContentUnlock
models.DB.Where("user_id = ? AND post_id = ? AND block_key = ?", readerID, postID, blockKey).Limit(1).Find(&existing)
if existing.ID > 0 {
return nil, ErrAlreadyUnlocked
}
var reader, author model.User
if err := model.DB.First(&reader, readerID).Error; err != nil {
var reader, author models.User
if err := models.DB.First(&reader, readerID).Error; err != nil {
return nil, err
}
if err := model.DB.First(&author, post.UserID).Error; err != nil {
if err := models.DB.First(&author, post.UserID).Error; err != nil {
return nil, err
}
@@ -174,8 +174,8 @@ func UnlockPointsBlock(readerID, postID uint, blockKey string) (*UnlockResult, e
cost := block.Cost
authorShare := cost * CreatorSharePercent / 100
var bal int
err := model.DB.Transaction(func(tx *gorm.DB) error {
var again model.PostContentUnlock
err := models.DB.Transaction(func(tx *gorm.DB) error {
var again models.PostContentUnlock
if err := tx.Where("user_id = ? AND post_id = ? AND block_key = ?", readerID, postID, blockKey).Limit(1).Find(&again).Error; err != nil {
return err
}
@@ -183,20 +183,20 @@ func UnlockPointsBlock(readerID, postID uint, blockKey string) (*UnlockResult, e
return ErrAlreadyUnlocked
}
var e error
bal, e = AdjustPointsTx(tx, readerID, -cost, model.PointReasonUnlockSpend, "post_unlock", postID, "解锁付费内容")
bal, e = AdjustPointsTx(tx, readerID, -cost, models.PointReasonUnlockSpend, "post_unlock", postID, "解锁付费内容")
if e != nil {
return e
}
if authorShare > 0 {
if _, e = AdjustPointsTx(tx, author.ID, authorShare, model.PointReasonCreatorIncome, "post_unlock", postID, "创作分成"); e != nil {
if _, e = AdjustPointsTx(tx, author.ID, authorShare, models.PointReasonCreatorIncome, "post_unlock", postID, "创作分成"); e != nil {
return e
}
if e = tx.Model(&model.User{}).Where("id = ?", author.ID).
if e = tx.Model(&models.User{}).Where("id = ?", author.ID).
UpdateColumn("creator_income_total", gorm.Expr("creator_income_total + ?", authorShare)).Error; e != nil {
return e
}
}
return tx.Create(&model.PostContentUnlock{
return tx.Create(&models.PostContentUnlock{
UserID: readerID, PostID: postID, BlockKey: blockKey, Cost: cost,
}).Error
})
@@ -213,7 +213,7 @@ func UnlockPointsBlock(readerID, postID uint, blockKey string) (*UnlockResult, e
}, nil
}
func suspiciousUnlockPair(reader, author *model.User) bool {
func suspiciousUnlockPair(reader, author *models.User) bool {
if reader == nil || author == nil {
return false
}

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"mime/multipart"

View File

@@ -1,4 +1,4 @@
package service
package services
import (
"errors"
@@ -8,7 +8,7 @@ import (
"strings"
"time"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/models"
)
type UserService struct {
@@ -21,9 +21,9 @@ func NewUserService(filter *SensitiveFilter, settings *ForumSettingsService) *Us
}
// GetByID 获取用户信息
func (s *UserService) GetByID(id uint) (*model.User, error) {
var user model.User
if err := model.DB.First(&user, id).Error; err != nil {
func (s *UserService) GetByID(id uint) (*models.User, error) {
var user models.User
if err := models.DB.First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
@@ -43,17 +43,17 @@ func (s *UserService) ActivityStats(userID uint) (UserActivityStats, error) {
if userID == 0 {
return st, errors.New("无效用户")
}
if err := model.DB.Model(&model.Post{}).Where("user_id = ?", userID).Count(&st.PostCount).Error; err != nil {
if err := models.DB.Model(&models.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 {
if err := models.DB.Model(&models.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 {
if err := models.DB.Model(&models.PostFavorite{}).Where("user_id = ?", userID).Count(&st.FavoriteCount).Error; err != nil {
return st, err
}
var likeSum int64
if err := model.DB.Model(&model.Post{}).
if err := models.DB.Model(&models.Post{}).
Select("COALESCE(SUM(like_count), 0)").
Where("user_id = ?", userID).
Scan(&likeSum).Error; err != nil {
@@ -64,19 +64,19 @@ func (s *UserService) ActivityStats(userID uint) (UserActivityStats, error) {
}
// 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 {
func (s *UserService) GetByUsername(username string) (*models.User, error) {
var user models.User
if err := models.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) {
func (s *UserService) GetByEmail(email string) (*models.User, error) {
email = NormalizeEmail(email)
var user model.User
if err := model.DB.Where("email = ?", email).First(&user).Error; err != nil {
var user models.User
if err := models.DB.Where("email = ?", email).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
@@ -95,21 +95,21 @@ func (s *UserService) ResetPasswordByEmail(email, newPass string) error {
if err != nil {
return err
}
return model.DB.Model(&model.User{}).Where("id = ?", user.ID).Update("password", hash).Error
return models.DB.Model(&models.User{}).Where("id = ?", user.ID).Update("password", hash).Error
}
// SearchUsersBrief 公开用户搜索(@补全):匹配用户名/昵称,不含邮箱
func (s *UserService) SearchUsersBrief(keyword string, limit int) ([]model.User, error) {
func (s *UserService) SearchUsersBrief(keyword string, limit int) ([]models.User, error) {
keyword = strings.TrimSpace(keyword)
if keyword == "" {
return []model.User{}, nil
return []models.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").
var users []models.User
err := models.DB.Select("id", "username", "nickname", "avatar", "role", "verified").
Where("username LIKE ? OR nickname LIKE ?", like, like).
Order("username ASC").
Limit(limit).
@@ -118,7 +118,7 @@ func (s *UserService) SearchUsersBrief(keyword string, limit int) ([]model.User,
return nil, err
}
if users == nil {
users = []model.User{}
users = []models.User{}
}
return users, nil
}
@@ -136,8 +136,8 @@ 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").
var users []models.User
err := models.DB.Select("id", "username", "nickname", "avatar", "created_at").
Where("banned = ?", false).
Order("created_at DESC, id DESC").
Limit(limit).
@@ -168,7 +168,7 @@ func (s *UserService) UpdateNickname(userID uint, nickname string) error {
return errors.New("昵称不能为空")
}
nickname = s.filter.Filter(nickname)
return model.DB.Model(&model.User{}).Where("id = ?", userID).Update("nickname", nickname).Error
return models.DB.Model(&models.User{}).Where("id = ?", userID).Update("nickname", nickname).Error
}
// UpdateSignature 修改个人签名
@@ -184,7 +184,7 @@ func (s *UserService) UpdateSignature(userID uint, signature string) error {
if signature != "" {
signature = s.filter.Filter(signature)
}
return model.DB.Model(&model.User{}).Where("id = ?", userID).Update("signature", signature).Error
return models.DB.Model(&models.User{}).Where("id = ?", userID).Update("signature", signature).Error
}
// UpdatePassword 修改密码
@@ -192,8 +192,8 @@ 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 {
var user models.User
if err := models.DB.First(&user, userID).Error; err != nil {
return err
}
if !CheckPassword(user.Password, oldPass) {
@@ -203,20 +203,20 @@ func (s *UserService) UpdatePassword(userID uint, oldPass, newPass string) error
if err != nil {
return err
}
return model.DB.Model(&user).Update("password", hash).Error
return models.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 {
var user models.User
if err := models.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 {
if err := models.DB.Model(&models.User{}).Where("id = ?", userID).Update("avatar", url).Error; err != nil {
return "", err
}
if old := strings.TrimSpace(user.Avatar); old != "" && old != url {
@@ -234,7 +234,7 @@ type UserListQuery struct {
Filter string // all | verified | banned | admin
}
func (s *UserService) ListUsers(q UserListQuery) ([]model.User, int64, error) {
func (s *UserService) ListUsers(q UserListQuery) ([]models.User, int64, error) {
if q.Page < 1 {
q.Page = 1
}
@@ -245,7 +245,7 @@ func (s *UserService) ListUsers(q UserListQuery) ([]model.User, int64, error) {
q.Size = 100
}
db := model.DB.Model(&model.User{})
db := models.DB.Model(&models.User{})
kw := strings.TrimSpace(q.Keyword)
if kw != "" {
like := "%" + kw + "%"
@@ -257,18 +257,18 @@ func (s *UserService) ListUsers(q UserListQuery) ([]model.User, int64, error) {
}
switch strings.TrimSpace(q.Filter) {
case "verified":
db = db.Where("verified = ? AND role <> ?", true, model.RoleAdmin)
db = db.Where("verified = ? AND role <> ?", true, models.RoleAdmin)
case "banned":
db = db.Where("banned = ?", true)
case "admin":
db = db.Where("role = ?", model.RoleAdmin)
db = db.Where("role = ?", models.RoleAdmin)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var users []model.User
var users []models.User
offset := (q.Page - 1) * q.Size
err := db.Order("id desc").Offset(offset).Limit(q.Size).Find(&users).Error
return users, total, err
@@ -276,11 +276,11 @@ func (s *UserService) ListUsers(q UserListQuery) ([]model.User, int64, error) {
// BanUser 禁言用户
func (s *UserService) BanUser(userID uint, banned bool) error {
var user model.User
if err := model.DB.First(&user, userID).Error; err != nil {
var user models.User
if err := models.DB.First(&user, userID).Error; err != nil {
return errors.New("用户不存在")
}
if user.Role == model.RoleAdmin {
if user.Role == models.RoleAdmin {
return errors.New("不能禁言管理员账号")
}
now := time.Now()
@@ -288,7 +288,7 @@ func (s *UserService) BanUser(userID uint, banned bool) error {
if banned {
updates["banned_at"] = &now
}
return model.DB.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error
return models.DB.Model(&models.User{}).Where("id = ?", userID).Updates(updates).Error
}
// SitemapUser 站点地图用的轻量用户字段
@@ -303,7 +303,7 @@ func (s *UserService) ListSitemap(limit int) ([]SitemapUser, error) {
limit = 5000
}
var rows []SitemapUser
err := model.DB.Model(&model.User{}).
err := models.DB.Model(&models.User{}).
Select("id, updated_at").
Where("banned = ?", false).
Order("updated_at desc, id desc").