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

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
2026-10-01 03:06:05 +08:00

308 lines
9.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"encoding/json"
"sort"
"strings"
"github.com/freefire/jiang13-bbs/model"
)
// 权限码:后台功能点。路由通过 RequirePerm 声明,
// 板块管理员的板块范围由 Actor.BoardIDs 在业务层二次校验。
const (
PermUsers = "users" // 用户与权限管理(超管/站长)
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(站长授予);站长/超管不必依赖此字段
ExtraPerms []string // 账号级授予的渠道权限(角色固有之外的增量)
}
// IsStaff 是否管理团队成员
func (a *Actor) IsStaff() bool {
return a != nil && model.IsStaff(a.Role)
}
// HasPerm 是否拥有某后台功能点:角色固有权限 ∪ 账号级授予
func (a *Actor) HasPerm(p string) bool {
if a == nil {
return false
}
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
}
// HasSiteMessagePerm 站点级消息管理:站长/超管固有,或被授予 CanManageMessages
func (a *Actor) HasSiteMessagePerm() bool {
if a == nil {
return false
}
if a.Role == model.RoleOwner || a.Role == model.RoleSuperAdmin {
return true
}
return a.CanManageMessages
}
// CanPurgeContent 彻底删除帖/评:仅站长
func (a *Actor) CanPurgeContent() bool {
return a != nil && a.Role == model.RoleOwner
}
// CanModerateBoard 是否可管理指定板块的内容:
// 管理员及以上不限板块;板块管理员仅限被授权板块
func (a *Actor) CanModerateBoard(boardID uint) bool {
if a == nil {
return false
}
if model.RoleLevel(a.Role) >= model.RoleLevel(model.RoleAdmin) {
return true
}
if a.Role != model.RoleBoardAdmin {
return false
}
for _, id := range a.BoardIDs {
if id == boardID {
return true
}
}
return false
}
// CanModerateAuthor 板管不可软删/恢复管理员及以上作者的内容;其他角色不受此限
func (a *Actor) CanModerateAuthor(authorRole model.Role) bool {
if a == nil {
return false
}
if a.Role == model.RoleBoardAdmin {
return model.RoleLevel(authorRole) < model.RoleLevel(model.RoleAdmin)
}
return true
}
// CanAssignRole 操作者能否把他人设置为目标角色(站长不可被授予,仅站长可授予超管)
func (a *Actor) CanAssignRole(target model.Role) bool {
if a == nil {
return false
}
if target == model.RoleOwner {
return false // 站长身份不通过授权产生
}
if target == model.RoleSuperAdmin {
return a.Role == model.RoleOwner
}
return a.HasPerm(PermUsers)
}
// LoadActor 读取用户实时角色、板块授权与账号级渠道权限
func (s *AuthService) LoadActor(id uint) (*Actor, error) {
var u model.User
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
if err := s.db.Model(&model.UserBoard{}).
Where("user_id = ?", id).
Order("board_id ASC").
Pluck("board_id", &ids).Error; err != nil {
return nil, err
}
actor.BoardIDs = ids
}
return actor, nil
}
// GetUserRole 读取用户角色(内容作者保护用);用户不存在返回 user
func (s *AuthService) GetUserRole(id uint) (model.Role, error) {
var u model.User
if err := s.db.Select("role").First(&u, id).Error; err != nil {
return model.RoleUser, err
}
return u.Role, nil
}
// GetUserBoardIDs 读取用户被授权的板块 ID(供 /me 等场景)
func (s *AuthService) GetUserBoardIDs(id uint) ([]uint, error) {
var ids []uint
err := s.db.Model(&model.UserBoard{}).
Where("user_id = ?", id).Order("board_id ASC").Pluck("board_id", &ids).Error
return ids, err
}
// GetUserIDByUsername 按用户名查 ID(登录失败审计用),用户不存在返回 0
func (s *AuthService) GetUserIDByUsername(username string) uint {
username = strings.TrimSpace(username)
if username == "" {
return 0
}
var u model.User
if err := s.db.Select("id").Where("username = ?", username).First(&u).Error; err != nil {
return 0
}
return u.ID
}
// ListBoardModeratorIDs 可审核指定板块的用户 ID:
// 站长/超管/全站管理员 + 被授权该板的板块管理员(未封禁、未删除)
func (s *AuthService) ListBoardModeratorIDs(boardID uint) ([]uint, error) {
var ids []uint
// 全站可审:admin / super_admin / owner
if err := s.db.Model(&model.User{}).
Where("deleted_at IS NULL AND banned = ? AND role IN ?", false,
[]string{string(model.RoleAdmin), string(model.RoleSuperAdmin), string(model.RoleOwner)}).
Pluck("id", &ids).Error; err != nil {
return nil, err
}
// 板块管理员(授权该板)
var boardAdminIDs []uint
if err := s.db.Model(&model.UserBoard{}).
Select("user_boards.user_id").
Joins("JOIN users ON users.id = user_boards.user_id").
Where("user_boards.board_id = ? AND users.deleted_at IS NULL AND users.banned = ? AND users.role = ?",
boardID, false, string(model.RoleBoardAdmin)).
Pluck("user_boards.user_id", &boardAdminIDs).Error; err != nil {
return nil, err
}
seen := make(map[uint]struct{}, len(ids)+len(boardAdminIDs))
out := make([]uint, 0, len(ids)+len(boardAdminIDs))
for _, id := range ids {
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
for _, id := range boardAdminIDs {
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out, nil
}