fix: 完成 Gitea 目录改组收尾(import、构建与 LICENSE)
同步包路径与路由,去掉 SPA 构建步骤,对齐 Gitea 式 LICENSE,并更新规格/规则与占位 SSR。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
|
||||
@@ -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 token;clientIP 写入上次登录记录
|
||||
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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@@ -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("等级须在 1–10")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import "strings"
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import "strings"
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import "os"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
@@ -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(¬ify).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")
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
262
services/post.go
262
services/post.go
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
|
||||
@@ -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").
|
||||
|
||||
Reference in New Issue
Block a user