feat: 管理端板块管理与旧版数据导入,补充部署运营文档

- 新增管理端板块管理页面与后端接口(admin_board)
- 新增旧版数据导入:legacyimport 服务、导入面板、importusers 命令行工具
- 聊天用户卡片、板块图标等 UI 组件与界面优化
- 补充 about/公告/1Panel 部署等文档
- gitignore 排除 dist/ 构建产物与 .agents/ 本地工具目录
This commit is contained in:
2026-09-24 03:37:44 +08:00
parent c1f6a6636b
commit 3e55b5d230
53 changed files with 5078 additions and 409 deletions

View File

@@ -23,6 +23,7 @@ var (
type PostService struct {
db *gorm.DB
setting *SettingService
board *BoardService
devMode bool
}
@@ -36,6 +37,12 @@ func (s *PostService) WithSetting(setting *SettingService) *PostService {
return s
}
// WithBoard 注入板块服务(发帖策略校验用);可链式调用
func (s *PostService) WithBoard(board *BoardService) *PostService {
s.board = board
return s
}
// WithDevMode 开发态:抽奖同 IP 去重对回环地址放宽
func (s *PostService) WithDevMode(dev bool) *PostService {
s.devMode = dev
@@ -744,35 +751,37 @@ func (s *PostService) Create(in CreatePostInput) (*PostDetail, error) {
typeMeta, _ = encodeMeta(qm)
}
// 悬赏:创建时托管积分 + 防刷(未结算上限 / 每日上限)
// 悬赏:创建时托管积分 + 防刷(未结算上限 / 每日上限);管理角色豁免防刷
var bountyPts int
if postType == model.PostTypeBounty {
bm, _ := parseBountyMeta(typeMeta)
bountyPts = bm.Points
var bountyPosts []model.Post
s.db.Select("type_meta").
Where("user_id = ? AND post_type = ? AND deleted_at IS NULL", in.UserID, model.PostTypeBounty).
Find(&bountyPosts)
open := 0
for _, bp := range bountyPosts {
m, err := parseBountyMeta(bp.TypeMeta)
if err != nil {
continue
if !s.isStaffUser(in.UserID) {
var bountyPosts []model.Post
s.db.Select("type_meta").
Where("user_id = ? AND post_type = ? AND deleted_at IS NULL", in.UserID, model.PostTypeBounty).
Find(&bountyPosts)
open := 0
for _, bp := range bountyPosts {
m, err := parseBountyMeta(bp.TypeMeta)
if err != nil {
continue
}
if m.Escrowed && m.AcceptedCommentID == 0 && !m.Refunded && !m.Expired {
open++
}
}
if m.Escrowed && m.AcceptedCommentID == 0 && !m.Refunded && !m.Expired {
open++
if open >= 3 {
return nil, errors.New("未结算悬赏最多同时 3 个,请先采纳或退回")
}
var todayN int64
s.db.Model(&model.Post{}).
Where("user_id = ? AND post_type = ? AND created_at >= CURRENT_DATE AND deleted_at IS NULL",
in.UserID, model.PostTypeBounty).
Count(&todayN)
if todayN >= 5 {
return nil, errors.New("今日悬赏发帖已达上限(5)")
}
}
if open >= 3 {
return nil, errors.New("未结算悬赏最多同时 3 个,请先采纳或退回")
}
var todayN int64
s.db.Model(&model.Post{}).
Where("user_id = ? AND post_type = ? AND created_at >= CURRENT_DATE AND deleted_at IS NULL",
in.UserID, model.PostTypeBounty).
Count(&todayN)
if todayN >= 5 {
return nil, errors.New("今日悬赏发帖已达上限(5)")
}
bm.Escrowed = true
bm.Refunded = false
@@ -1051,7 +1060,7 @@ func (s *PostService) EnsurePostVisible(postID, viewerID uint, loadActor func()
return nil
}
// checkNewUserCooldown 新用户发帖冷静期(小时数取自站点设置,0=关闭)
// checkNewUserCooldown 新用户发帖冷静期(小时数取自站点设置,0=关闭);管理角色豁免
func (s *PostService) checkNewUserCooldown(userID uint) error {
hours := DefaultCooldownHours
if s.setting != nil {
@@ -1066,12 +1075,25 @@ func (s *PostService) checkNewUserCooldown(userID uint) error {
if err := s.db.First(&user, userID).Error; err != nil {
return err
}
if model.IsStaff(user.Role) { // 管理角色不受发帖冷静期限制
return nil
}
if time.Since(user.CreatedAt) < time.Duration(hours)*time.Hour {
return fmt.Errorf("新用户注册 %d 小时后才能发帖", hours)
}
return nil
}
// isStaffUser 作者是否管理角色(悬赏防刷等限制豁免用)
func (s *PostService) isStaffUser(userID uint) bool {
var role model.Role
if err := s.db.Model(&model.User{}).Where("id = ?", userID).
Select("role").Scan(&role).Error; err != nil {
return false
}
return model.IsStaff(role)
}
// DeletePostOpts 删帖选项:staff 软删必填类型与理由;作者自删传零值
type DeletePostOpts struct {
DeleteType string