feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等

新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
This commit is contained in:
2026-10-01 03:06:05 +08:00
parent 7fbd6ba699
commit ff2ab286fb
141 changed files with 7527 additions and 1407 deletions

View File

@@ -54,6 +54,7 @@ type AdminUserItem struct {
BoardIDs []uint `json:"board_ids"`
Banned bool `json:"banned"`
CanManageMessages bool `json:"can_manage_messages"`
PermOverrides []string `json:"perm_overrides"` // 账号级渠道权限(角色固有之外的增量授予)
PostCount int64 `json:"post_count"`
CommentCount int64 `json:"comment_count"`
Points int `json:"points"`
@@ -71,7 +72,7 @@ type AdminUserItem struct {
// - total / admins / banned:筛选 Tab 角标(不在顶部卡片展示,避免与仪表盘重复)
type AdminUserSummary struct {
Online int64 `json:"online"`
InCooldown int64 `json:"in_cooldown"`
InCooldown int64 `json:"in_cooldown"`
Active7d int64 `json:"active_7d"`
FailedLogins24h int64 `json:"failed_logins_24h"`
Total int64 `json:"total"`
@@ -237,6 +238,7 @@ func (s *AdminUserService) toItems(users []model.User) []AdminUserItem {
BoardIDs: []uint{},
Banned: u.Banned,
CanManageMessages: u.CanManageMessages,
PermOverrides: ParsePermOverrides(u.PermOverrides),
Points: u.Points,
TotalPoints: u.TotalPoints,
Level: u.Level,
@@ -407,6 +409,12 @@ func (s *AdminUserService) SetStaff(operator *Actor, targetID uint, role model.R
return err
}
}
// 升至管理员及以上后渠道权限变为角色自带,账号级授予清空以免误导
if roleChanged && model.RoleLevel(role) >= model.RoleLevel(model.RoleAdmin) && u.PermOverrides != "" && u.PermOverrides != "[]" {
if err := tx.Model(&u).Update("perm_overrides", "[]").Error; err != nil {
return err
}
}
return nil
})
if err != nil {
@@ -415,6 +423,53 @@ func (s *AdminUserService) SetStaff(operator *Actor, targetID uint, role model.R
return s.getItem(s.db, targetID)
}
// GetPermOverrides 读取目标账号的渠道权限(PermUsers 持有者可读,供授权弹窗回显)
func (s *AdminUserService) GetPermOverrides(operator *Actor, targetID uint) ([]string, error) {
if operator == nil || !operator.HasPerm(PermUsers) {
return nil, ErrCannotAssignRole
}
var u model.User
if err := s.db.Select("id", "perm_overrides").First(&u, targetID).Error; err != nil {
return nil, err
}
return ParsePermOverrides(u.PermOverrides), nil
}
// SetPermOverrides 授予/撤销账号级渠道权限(公告/单页/书库/广告)。
// 授予者需持有 PermUsers(超管/站长);目标为管理员及以上时渠道为角色自带,
// 授权无意义,强制清空。Actor 每次请求现查 DB,变更即时生效,无需强制下线。
func (s *AdminUserService) SetPermOverrides(operator *Actor, targetID uint, perms []string) (*AdminUserItem, error) {
if operator == nil || !operator.HasPerm(PermUsers) {
return nil, ErrCannotAssignRole
}
if operator.ID == targetID {
return nil, ErrAdminSelfAction
}
norm := NormalizePermOverrides(perms)
err := s.db.Transaction(func(tx *gorm.DB) error {
var u model.User
if err := tx.First(&u, targetID).Error; err != nil {
return err
}
if u.Role == model.RoleOwner {
return ErrProtectedOwner
}
// 管理员及以上角色自带全部渠道权限,账号级授予保持为空
if model.RoleLevel(u.Role) >= model.RoleLevel(model.RoleAdmin) {
norm = []string{}
}
raw := SerializePermOverrides(norm)
if u.PermOverrides == raw {
return nil
}
return tx.Model(&u).Update("perm_overrides", raw).Error
})
if err != nil {
return nil, err
}
return s.getItem(s.db, targetID)
}
// SetCanManageMessages 站长授予/撤销站点消息管理权限(不可操作站长账号与自己)
func (s *AdminUserService) SetCanManageMessages(operator *Actor, targetID uint, enabled bool) (*AdminUserItem, error) {
if operator == nil || operator.Role != model.RoleOwner {
@@ -564,12 +619,12 @@ func normalizeAuditPage(page, size int) (int, int) {
// AdminUserAuditProfile 站长查看用户档案摘要
type AdminUserAuditProfile struct {
User AdminUserItem `json:"user"`
PostsTotal int64 `json:"posts_total"` // 含各状态与软删
CommentsTotal int64 `json:"comments_total"` // 含各状态与软删
MessagesTotal int64 `json:"messages_total"` // 含撤回与软删
PublishedPosts int64 `json:"published_posts"`
PublishedComments int64 `json:"published_comments"`
User AdminUserItem `json:"user"`
PostsTotal int64 `json:"posts_total"` // 含各状态与软删
CommentsTotal int64 `json:"comments_total"` // 含各状态与软删
MessagesTotal int64 `json:"messages_total"` // 含撤回与软删
PublishedPosts int64 `json:"published_posts"`
PublishedComments int64 `json:"published_comments"`
}
// AdminAuditPostItem 审计帖子行
@@ -594,6 +649,8 @@ type AdminAuditCommentItem struct {
Deleted bool `json:"deleted"`
CreatedAt time.Time `json:"created_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
Floor uint `json:"floor"`
IsRoot bool `json:"is_root"`
}
// AdminAuditMessageItem 审计聊天消息行
@@ -686,6 +743,11 @@ func (s *AdminUserService) ListAuditComments(actor *Actor, userID uint, page, si
Offset((page - 1) * size).Limit(size).Find(&comments).Error; err != nil {
return nil, 0, page, err
}
commentIDs := make([]uint, 0, len(comments))
for _, c := range comments {
commentIDs = append(commentIDs, c.ID)
}
anchors := CommentAnchors(s.db, commentIDs)
items := make([]AdminAuditCommentItem, 0, len(comments))
for _, c := range comments {
it := AdminAuditCommentItem{
@@ -695,6 +757,10 @@ func (s *AdminUserService) ListAuditComments(actor *Actor, userID uint, page, si
Status: c.Status,
CreatedAt: c.CreatedAt,
}
if a, ok := anchors[c.ID]; ok {
it.Floor = a.Floor
it.IsRoot = a.IsRoot
}
if c.DeletedAt.Valid {
it.Deleted = true
t := c.DeletedAt.Time