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

@@ -1,6 +1,8 @@
package service
import (
"encoding/json"
"sort"
"strings"
"github.com/freefire/jiang13-bbs/model"
@@ -10,21 +12,119 @@ import (
// 板块管理员的板块范围由 Actor.BoardIDs 在业务层二次校验。
const (
PermUsers = "users" // 用户与权限管理(超管/站长)
PermAnnouncements = "announcements" // 公告管理(管理员及以上)
PermAnnouncements = "announcements" // 公告管理(管理员及以上;可账号级授予)
PermPages = "pages" // 单页管理(管理员及以上;可账号级授予)
PermLibrary = "library" // 书库管理(管理员及以上;可账号级授予)
PermAds = "ads" // 广告与赞助管理(管理员及以上;可账号级授予)
PermSettings = "settings" // 站点外观设置(超管/站长)
PermModeration = "moderation" // 内容审核(任意管理角色,板块范围受限)
PermMessages = "messages" // 后台消息管理(站长/超管/站点消息 flag;群管另有业务层放行)
PermBoards = "boards" // 板块管理(仅站长)
)
// GrantablePerms 可账号级授予的渠道权限码(授予者需持有 PermUsers)。
// 管理员及以上角色对这些渠道为角色自带,账号级授予只对板块管理员/普通用户生效。
var GrantablePerms = []string{PermAnnouncements, PermPages, PermLibrary, PermAds}
// ValidPermOverride 权限码是否可被账号级授予
func ValidPermOverride(p string) bool {
for _, g := range GrantablePerms {
if p == g {
return true
}
}
return false
}
// NormalizePermOverrides 白名单过滤 + 去重 + 排序,得到可落库的权限码集
func NormalizePermOverrides(perms []string) []string {
seen := map[string]bool{}
out := make([]string, 0, len(perms))
for _, p := range perms {
p = strings.TrimSpace(p)
if p == "" || seen[p] || !ValidPermOverride(p) {
continue
}
seen[p] = true
out = append(out, p)
}
sort.Strings(out)
return out
}
// ParsePermOverrides 解析 users.perm_overrides JSON 列(容错:非法内容视为空)
func ParsePermOverrides(raw string) []string {
if raw == "" {
return []string{}
}
var arr []string
if err := json.Unmarshal([]byte(raw), &arr); err != nil {
return []string{}
}
return NormalizePermOverrides(arr)
}
// SerializePermOverrides 序列化为落库 JSON(空集统一 "[]",避免 NULL/空串分歧)
func SerializePermOverrides(perms []string) string {
norm := NormalizePermOverrides(perms)
b, err := json.Marshal(norm)
if err != nil {
return "[]"
}
return string(b)
}
// roleDefaultPerm 角色对功能点的固有权限(不含账号级授予)
func roleDefaultPerm(role model.Role, p string) bool {
switch p {
case PermBoards:
return role == model.RoleOwner
case PermUsers, PermSettings:
return role == model.RoleSuperAdmin || role == model.RoleOwner
case PermAnnouncements, PermPages, PermLibrary, PermAds:
return model.RoleLevel(role) >= model.RoleLevel(model.RoleAdmin)
case PermModeration:
return model.IsStaff(role)
}
return false
}
// EffectivePerms 生效权限码列表(角色默认 ∪ 账号授予 + 消息管理),
// 供 /me 下发,前端据此渲染后台入口/按钮;最终权限仍以 HasPerm 校验为准。
func EffectivePerms(role model.Role, permOverridesRaw string, canManageMessages bool) []string {
set := map[string]bool{}
all := append([]string{
PermUsers, PermAnnouncements, PermPages, PermLibrary, PermAds,
PermSettings, PermModeration, PermBoards,
}, GrantablePerms...)
for _, p := range all {
if roleDefaultPerm(role, p) {
set[p] = true
}
}
for _, p := range ParsePermOverrides(permOverridesRaw) {
set[p] = true
}
if role == model.RoleOwner || role == model.RoleSuperAdmin || canManageMessages {
set[PermMessages] = true
}
out := make([]string, 0, len(set))
for p := range set {
out = append(out, p)
}
sort.Strings(out)
return out
}
// Actor 当前请求操作者的实时权限快照(每次后台请求从 DB 现取,
// 不依赖 JWT 内的 role claim,角色/授权变更立即生效)
type Actor struct {
ID uint
Username string
Role model.Role
BoardIDs []uint // 板块管理员被授权的板块;其他角色为空
CanManageMessages bool // 站点级消息管理 flag(站长授予);站长/超管不必依赖此字段
BoardIDs []uint // 板块管理员被授权的板块;其他角色为空
CanManageMessages bool // 站点级消息管理 flag(站长授予);站长/超管不必依赖此字段
ExtraPerms []string // 账号级授予的渠道权限(角色固有之外的增量)
}
// IsStaff 是否管理团队成员
@@ -32,23 +132,22 @@ func (a *Actor) IsStaff() bool {
return a != nil && model.IsStaff(a.Role)
}
// HasPerm 是否拥有某后台功能点
// HasPerm 是否拥有某后台功能点:角色固有权限 ∪ 账号级授予
func (a *Actor) HasPerm(p string) bool {
if a == nil {
return false
}
switch p {
case PermBoards:
return a.Role == model.RoleOwner
case PermUsers, PermSettings:
return a.Role == model.RoleSuperAdmin || a.Role == model.RoleOwner
case PermAnnouncements:
return model.RoleLevel(a.Role) >= model.RoleLevel(model.RoleAdmin)
case PermModeration:
return a.IsStaff()
case PermMessages:
if roleDefaultPerm(a.Role, p) {
return true
}
if p == PermMessages {
return a.HasSiteMessagePerm()
}
for _, e := range a.ExtraPerms {
if e == p {
return true
}
}
return false
}
@@ -113,15 +212,16 @@ func (a *Actor) CanAssignRole(target model.Role) bool {
return a.HasPerm(PermUsers)
}
// LoadActor 读取用户实时角色与板块授权
// LoadActor 读取用户实时角色、板块授权与账号级渠道权限
func (s *AuthService) LoadActor(id uint) (*Actor, error) {
var u model.User
if err := s.db.Select("id", "username", "role", "can_manage_messages").First(&u, id).Error; err != nil {
if err := s.db.Select("id", "username", "role", "can_manage_messages", "perm_overrides").First(&u, id).Error; err != nil {
return nil, err
}
actor := &Actor{
ID: u.ID, Username: u.Username, Role: u.Role, BoardIDs: []uint{},
CanManageMessages: u.CanManageMessages,
ExtraPerms: ParsePermOverrides(u.PermOverrides),
}
if u.Role == model.RoleBoardAdmin {
var ids []uint