Files
Henry_Du a0184fdeb0 feat(admin+server): 酷狗对接 - 扫码绑定管理员账号 / 按排期自动创建酷狗日歌单 + 同步歌曲
数据模型:SystemSettings 新增 kugouApiBaseUrl / kugouAdminCookie / kugouAdminToken / kugouAdminUser / kugouAdminBoundAt 5 字段;新建 KuGouPlaylistSync 同步历史表(12 字段 + playDate 倒序索引)。迁移脚本:app/drizzle/migrations/20260919143000_add_kugou_integration.sql

后端 Server:新增 server/services/kugouService.ts 封装 KuGouMusicApi sidecar 转发(自动拼 Cookie / Token header);新增 6 个 admin/kugou 路由:status / qr-create / qr-check / unbind / sync-schedule-playlists / sync-history。同步核心:逐日出非草稿排期 → kgHash 直取优先 → 失败用 cloudsearch/search 按标题+歌手三级召回 → /playlist/create + /playlist/tracks op=add 每 100 首分批;写同步历史(status、added/missing、歌单 URL)。

后台 UI:新建 Admin/KuGouImporter.client.vue 三栏布局——左栏 sidecar 健康卡+部署说明+扫码绑定/解绑弹窗;中栏多选排期日期+逐日出排期预览卡+按日同步;下栏最近 100 条同步历史(7 种状态 badge + 歌单链接跳转)。dashboard.vue L105 playlist-card 挂载点由 PlaylistCardPage 替换为 KuGouImporter;权限 permissions.js 5 个 ADMIN 族角色开放 playlist-card 页面访问(接口内部绑定/同步再强校验 ADMIN/SUPER_ADMIN)。

侧car要求:部署 KuGouMusicApi(参考文档 https://kugoumusicapi-docs.4everland.app),默认本机 http://127.0.0.1:3000,可通过 SystemSettings.kugouApiBaseUrl 或环境变量 KUGOU_API_BASE 覆盖。
2026-09-19 11:57:02 +08:00

198 lines
5.3 KiB
JavaScript

import fs from 'fs'
import path from 'path'
// 获取角色配置文件路径
const getRolesFilePath = () => {
return path.resolve(process.cwd(), 'server', 'config', 'roles.json')
}
// 获取当前角色配置
export let ROLES = {}
try {
const filePath = getRolesFilePath()
if (fs.existsSync(filePath)) {
const data = fs.readFileSync(filePath, 'utf-8')
ROLES = JSON.parse(data)
}
} catch (err) {
console.error('Failed to load roles.json:', err)
}
// 确保角色对象存在
if (!ROLES.USER) {
ROLES = {
USER: { displayName: '学生(普通用户)', pages: [], canAccessAdmin: false },
SONG_ADMIN: { displayName: '工作人员', pages: ['schedule', 'songs', 'print', 'playlist-card'], canAccessAdmin: true },
VICE_MINISTER: { displayName: '副部', pages: ['overview', 'schedule', 'songs', 'print', 'playlist-card', 'users'], canAccessAdmin: true },
ADMIN: { displayName: '部长', pages: ['overview', 'schedule', 'songs', 'print', 'playlist-card', 'users', 'data-analysis', 'notifications', 'playtimes', 'semesters'], canAccessAdmin: true },
RESIDENT: { displayName: '常驻', pages: ['schedule', 'songs', 'print', 'playlist-card'], canAccessAdmin: true },
SUPER_ADMIN: { displayName: '技术运维', pages: ['overview', 'workflow-tutorial', 'schedule', 'songs', 'print', 'playlist-card', 'users', 'roles', 'blacklist', 'api-keys', 'notifications', 'smtp-config', 'playtimes', 'request-times', 'semesters', 'settings', 'site-config', 'database', 'data-analysis'], canAccessAdmin: true }
}
}
// 保存角色配置
export function saveRolesConfig(newRoles) {
try {
ROLES = newRoles
const filePath = getRolesFilePath()
const dir = path.dirname(filePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(filePath, JSON.stringify(ROLES, null, 2), 'utf-8')
return true
} catch (err) {
console.error('Failed to save roles.json:', err)
return false
}
}
// 检查用户是否可以访问后台
function canAccessAdmin(user) {
if (!user || !user.role) return false
const roleConfig = ROLES[user.role]
if (!roleConfig) return false
return roleConfig.canAccessAdmin
}
// 检查用户是否可以访问指定页面
function canAccessPage(user, page) {
if (!user || !user.role) return false
// 超级管理员可以访问所有页面
if (user.role === 'SUPER_ADMIN') return true
const roleConfig = ROLES[user.role]
if (!roleConfig) return false
return roleConfig.pages.includes(page)
}
// 获取用户可访问的页面列表
function getUserPages(user) {
if (!user || !user.role) return []
const roleConfig = ROLES[user.role]
if (!roleConfig) return []
return roleConfig.pages
}
// 获取角色信息
function getRoleInfo(roleName) {
return ROLES[roleName] || null
}
const ROLE_LEVELS = {
SUPER_ADMIN: 100,
ADMIN: 80,
MINISTER: 80,
VICE_MINISTER: 70,
SONG_ADMIN: 60,
STAFF: 60,
RESIDENT: 50,
TEACHER: 40,
USER: 10
}
export function getRoleLevel(roleName) {
if (!roleName) return 0
const upperRole = String(roleName).toUpperCase()
return ROLE_LEVELS[upperRole] ?? 0
}
export function isRoleHigherOrEqual(currentRole, targetRole) {
return getRoleLevel(currentRole) >= getRoleLevel(targetRole)
}
export function isRoleHigher(currentRole, targetRole) {
return getRoleLevel(currentRole) > getRoleLevel(targetRole)
}
function canResetPasswordFor(currentUser, targetUser) {
if (!currentUser || !currentUser.role || !targetUser) return false
const currentLevel = getRoleLevel(currentUser.role)
const targetLevel = getRoleLevel(targetUser.role)
if (currentUser.role === 'SUPER_ADMIN') {
return true
}
if (currentUser.role === 'ADMIN' || currentUser.role === 'MINISTER') {
return targetLevel <= getRoleLevel('USER')
}
return false
}
function canChangeRoleOf(currentUser, targetUser, newRole) {
if (!currentUser || !currentUser.role || !targetUser) return false
const currentLevel = getRoleLevel(currentUser.role)
const targetLevel = getRoleLevel(targetUser.role)
const newRoleLevel = getRoleLevel(newRole)
if (String(currentUser.id) === String(targetUser.id)) {
return false
}
if (currentUser.role === 'SUPER_ADMIN') {
if (targetUser.id === 1) return false
return newRoleLevel <= currentLevel
}
if (currentLevel < targetLevel) {
return false
}
if (newRoleLevel >= currentLevel) {
return false
}
return true
}
function canDeleteUser(currentUser, targetUser) {
if (!currentUser || !currentUser.role || !targetUser) return false
if (String(currentUser.id) === String(targetUser.id)) {
return false
}
if (targetUser.id === 1) {
return false
}
const currentLevel = getRoleLevel(currentUser.role)
const targetLevel = getRoleLevel(targetUser.role)
return currentLevel > targetLevel
}
function canManageUserRole(currentUser, targetRole) {
if (!currentUser || !currentUser.role) return false
if (currentUser.role === 'SUPER_ADMIN') {
return Object.keys(ROLES).includes(targetRole) && targetRole !== 'SUPER_ADMIN'
}
const currentLevel = getRoleLevel(currentUser.role)
const targetLevel = getRoleLevel(targetRole)
return targetLevel < currentLevel
}
// 导出所有函数和常量
export {
canAccessAdmin,
canAccessPage,
getUserPages,
getRoleInfo,
canManageUserRole,
canResetPasswordFor,
canChangeRoleOf,
canDeleteUser
}