1280 lines
51 KiB
JavaScript
1280 lines
51 KiB
JavaScript
const express = require('express')
|
|
const cors = require('cors')
|
|
const sqlite3 = require('sqlite3').verbose()
|
|
const bcrypt = require('bcryptjs')
|
|
const jwt = require('jsonwebtoken')
|
|
const path = require('path')
|
|
|
|
const app = express()
|
|
app.use(cors())
|
|
app.use(express.json())
|
|
|
|
// 配置(支持环境变量自定义)
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this-in-production'
|
|
const JWT_EXPIRES_IN = '7d'
|
|
const PORT = process.env.PORT || 3002
|
|
// 🔧 支持自定义路由前缀,方便与其他服务共存
|
|
const API_PREFIX = process.env.API_PREFIX || '/api' // API 前缀,如 /api1
|
|
const ADMIN_PATH = process.env.ADMIN_PATH || '/admin' // 管理后台路径,如 /admin1
|
|
|
|
// 静态文件服务(管理后台)
|
|
app.use(ADMIN_PATH, express.static(path.join(__dirname, 'public'), {
|
|
setHeaders: (res) => {
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
|
|
res.setHeader('Pragma', 'no-cache')
|
|
res.setHeader('Expires', '0')
|
|
}
|
|
}))
|
|
|
|
// SQLite 数据库初始化
|
|
const db = new sqlite3.Database(path.join(__dirname, 'users.db'), (err) => {
|
|
if (err) console.error('❌ 数据库连接失败:', err)
|
|
else console.log('✅ SQLite 数据库已连接')
|
|
})
|
|
|
|
// 创建表
|
|
db.serialize(() => {
|
|
// 用户表
|
|
db.run(`CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
email TEXT UNIQUE NOT NULL,
|
|
password TEXT NOT NULL,
|
|
avatar TEXT,
|
|
role TEXT DEFAULT 'user',
|
|
status TEXT DEFAULT 'active',
|
|
device_fingerprint TEXT,
|
|
max_devices INTEGER DEFAULT 1,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_login_at DATETIME,
|
|
last_login_device TEXT
|
|
)`)
|
|
|
|
// 用户订阅表
|
|
db.run(`CREATE TABLE IF NOT EXISTS user_subscriptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
subscription_type TEXT DEFAULT 'trial',
|
|
expires_at DATETIME NOT NULL,
|
|
trial_started_at DATETIME,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
)`)
|
|
|
|
// 设备绑定表
|
|
db.run(`CREATE TABLE IF NOT EXISTS user_devices (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
device_fingerprint TEXT NOT NULL,
|
|
device_name TEXT,
|
|
is_active BOOLEAN DEFAULT 1,
|
|
first_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
UNIQUE(user_id, device_fingerprint)
|
|
)`)
|
|
|
|
// 操作日志表
|
|
db.run(`CREATE TABLE IF NOT EXISTS user_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER,
|
|
action TEXT NOT NULL,
|
|
message TEXT,
|
|
device_fingerprint TEXT,
|
|
ip_address TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`)
|
|
|
|
// 时长管理日志表
|
|
db.run(`CREATE TABLE IF NOT EXISTS time_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
operator_id INTEGER,
|
|
action TEXT NOT NULL,
|
|
days_added INTEGER,
|
|
new_expires_at DATETIME,
|
|
reason TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`)
|
|
|
|
// 为用户表添加积分字段(如果不存在)
|
|
db.run(`ALTER TABLE users ADD COLUMN points INTEGER DEFAULT 0`, (err) => {
|
|
if (err && !err.message.includes('duplicate column')) {
|
|
console.log('积分字段已存在或添加失败')
|
|
}
|
|
})
|
|
|
|
// 积分变动日志表
|
|
db.run(`CREATE TABLE IF NOT EXISTS points_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
operator_id INTEGER,
|
|
action TEXT NOT NULL,
|
|
points_change INTEGER NOT NULL,
|
|
points_after INTEGER NOT NULL,
|
|
reason TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
)`)
|
|
|
|
// 为用户表添加当前有效token字段(用于单点登录)
|
|
db.run(`ALTER TABLE users ADD COLUMN current_token TEXT`, (err) => {
|
|
if (err && !err.message.includes('duplicate column')) {
|
|
console.log('current_token字段已存在')
|
|
}
|
|
})
|
|
|
|
// 为用户表添加云端数字人禁用字段
|
|
db.run(`ALTER TABLE users ADD COLUMN cloud_digital_human_disabled INTEGER DEFAULT 0`, (err) => {
|
|
if (err && !err.message.includes('duplicate column')) {
|
|
console.log('cloud_digital_human_disabled字段已存在')
|
|
}
|
|
})
|
|
|
|
db.run(`ALTER TABLE users ADD COLUMN is_super_admin INTEGER DEFAULT 0`, (err) => {
|
|
if (err && !err.message.includes('duplicate column')) {
|
|
console.log('is_super_admin字段已存在或添加失败')
|
|
}
|
|
})
|
|
|
|
db.run(`ALTER TABLE users ADD COLUMN owner_admin_id INTEGER`, (err) => {
|
|
if (err && !err.message.includes('duplicate column')) {
|
|
console.log('owner_admin_id字段已存在或添加失败')
|
|
}
|
|
})
|
|
|
|
db.run(`ALTER TABLE users ADD COLUMN invite_code TEXT`, (err) => {
|
|
if (err && !err.message.includes('duplicate column')) {
|
|
console.log('invite_code字段已存在或添加失败')
|
|
}
|
|
})
|
|
|
|
db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_users_invite_code ON users(invite_code) WHERE invite_code IS NOT NULL AND invite_code != ''`)
|
|
|
|
db.run(`UPDATE users SET is_super_admin = 1 WHERE id = (SELECT MIN(id) FROM users WHERE role = 'admin') AND NOT EXISTS (SELECT 1 FROM users WHERE is_super_admin = 1)`)
|
|
|
|
// 系统配置表(API Key等)
|
|
db.run(`CREATE TABLE IF NOT EXISTS system_config (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
config_key TEXT UNIQUE NOT NULL,
|
|
config_value TEXT NOT NULL,
|
|
description TEXT,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_by TEXT
|
|
)`)
|
|
|
|
db.run(`CREATE TABLE IF NOT EXISTS generation_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
type TEXT NOT NULL,
|
|
detail TEXT,
|
|
device_fingerprint TEXT,
|
|
ip_address TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`)
|
|
|
|
const defaultConfigs = [
|
|
['aliyun_bailian_api_key', '', '阿里云百炼 DashScope API Key'],
|
|
['aliyun_bailian_base_url', 'https://dashscope.aliyuncs.com/compatible-mode/v1', '阿里云百炼 API Base URL'],
|
|
['aliyun_oss_access_key_id', '', '阿里云 OSS AccessKey ID'],
|
|
['aliyun_oss_access_key_secret', '', '阿里云 OSS AccessKey Secret'],
|
|
['aliyun_oss_bucket', '', '阿里云 OSS Bucket'],
|
|
['aliyun_oss_region', 'oss-cn-beijing', '阿里云 OSS Region'],
|
|
['volcengine_api_key', '', '火山引擎 API Key'],
|
|
['volcengine_model', 'doubao-seed-2-0-mini-260215', '火山引擎默认模型'],
|
|
['runninghub_api_key', '', 'RunningHub API Key'],
|
|
['runninghub_workflow_id_1', '2013514129943826433', 'RunningHub 云端模式1 工作流ID'],
|
|
['runninghub_workflow_id_2', '2013514129943826433', 'RunningHub 云端模式plus 工作流ID'],
|
|
['runninghub_tts_webapp_id_v2', '1965614643077070850', 'RunningHub TTS V2 应用ID'],
|
|
['runninghub_tts_webapp_id_v3', '2028779949334728706', 'RunningHub TTS V3 应用ID'],
|
|
['runninghub_tts_v2_audio_node', '13', 'RunningHub TTS V2 音频节点ID'],
|
|
['runninghub_tts_v2_text_node', '14', 'RunningHub TTS V2 文本节点ID'],
|
|
['runninghub_tts_v2_emotion_node', '15', 'RunningHub TTS V2 情感音频节点ID'],
|
|
['cloud_beauty_base_url', '', '极速模型服务 Base URL'],
|
|
['cloud_beauty_api_key', '', '极速模型服务 API Key'],
|
|
['cloud_beauty_default_mode', '256m', '极速模型默认模式'],
|
|
['cloud_beauty_enabled', 'false', '是否开启自定义模型通道'],
|
|
['trial_duration_hours', '24', '默认试用时长(小时)']
|
|
]
|
|
|
|
defaultConfigs.forEach(([key, value, desc]) => {
|
|
db.run(
|
|
`INSERT OR IGNORE INTO system_config (config_key, config_value, description) VALUES (?, ?, ?)`,
|
|
[key, value, desc]
|
|
)
|
|
})
|
|
})
|
|
|
|
// 辅助函数:记录日志
|
|
function logAction(userId, action, message, deviceFingerprint = null, ipAddress = null) {
|
|
db.run(
|
|
'INSERT INTO user_logs (user_id, action, message, device_fingerprint, ip_address) VALUES (?, ?, ?, ?, ?)',
|
|
[userId, action, message, deviceFingerprint, ipAddress]
|
|
)
|
|
}
|
|
|
|
function isSuperAdmin(user) {
|
|
return Number(user?.is_super_admin || 0) === 1
|
|
}
|
|
|
|
function normalizeInviteCode(code) {
|
|
return String(code || '').trim().toUpperCase()
|
|
}
|
|
|
|
function requireSuperAdmin(req, res, next) {
|
|
if (!isSuperAdmin(req.adminUser)) {
|
|
return res.status(403).json({ success: false, message: '仅总管理员可操作' })
|
|
}
|
|
next()
|
|
}
|
|
|
|
function canManageUser(adminUser, targetUserId, callback) {
|
|
if (isSuperAdmin(adminUser)) {
|
|
return callback(null, true)
|
|
}
|
|
|
|
db.get('SELECT id FROM users WHERE id = ? AND role = ? AND owner_admin_id = ?', [targetUserId, 'user', adminUser.id], (err, row) => {
|
|
if (err) return callback(err)
|
|
callback(null, !!row)
|
|
})
|
|
}
|
|
|
|
function denyIfNoUserAccess(req, res, userId, next) {
|
|
canManageUser(req.adminUser, userId, (err, allowed) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
if (!allowed) return res.status(403).json({ success: false, message: '无权管理该用户' })
|
|
next()
|
|
})
|
|
}
|
|
|
|
// ==================== 管理员认证中间件 ====================
|
|
function adminAuth(req, res, next) {
|
|
const authHeader = req.headers.authorization
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
return res.status(401).json({ success: false, message: '未提供认证Token' })
|
|
}
|
|
|
|
const token = authHeader.substring(7)
|
|
try {
|
|
const payload = jwt.verify(token, JWT_SECRET)
|
|
|
|
// 验证用户是否为管理员
|
|
db.get('SELECT * FROM users WHERE id = ? AND role = ?', [payload.userId, 'admin'], (err, user) => {
|
|
if (!user) {
|
|
return res.status(403).json({ success: false, message: '无管理员权限' })
|
|
}
|
|
if (user.status !== 'active') {
|
|
return res.status(403).json({ success: false, message: '账户已被禁用' })
|
|
}
|
|
req.adminUser = user
|
|
next()
|
|
})
|
|
} catch (error) {
|
|
return res.status(401).json({ success: false, message: 'Token无效或已过期' })
|
|
}
|
|
}
|
|
|
|
// ==================== User registration ====================
|
|
app.post(`${API_PREFIX}/register`, async (req, res) => {
|
|
const { username, email, password, device_fingerprint } = req.body
|
|
const inviteCode = normalizeInviteCode(req.body.invite_code || req.body.invitation_code)
|
|
const clientIp = req.ip || req.connection.remoteAddress
|
|
|
|
if (!username || !email || !password) {
|
|
return res.json({ success: false, message: 'username, phone and password are required' })
|
|
}
|
|
|
|
db.get('SELECT id FROM users WHERE username = ? OR email = ?', [username, email], async (err, row) => {
|
|
if (err) return res.json({ success: false, message: 'register failed: ' + err.message })
|
|
if (row) return res.json({ success: false, message: 'username or phone already exists' })
|
|
|
|
try {
|
|
db.get('SELECT COUNT(*) as count FROM users', async (err, countRow) => {
|
|
if (err) return res.json({ success: false, message: 'register failed: ' + err.message })
|
|
|
|
const isFirstUser = countRow?.count === 0
|
|
const role = isFirstUser ? 'admin' : 'user'
|
|
const salt = await bcrypt.genSalt(10)
|
|
const hashedPassword = await bcrypt.hash(password, salt)
|
|
|
|
const createUser = (ownerAdminId) => {
|
|
db.run(
|
|
`INSERT INTO users (username, email, password, role, device_fingerprint, is_super_admin, owner_admin_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
[username, email, hashedPassword, role, device_fingerprint, isFirstUser ? 1 : 0, ownerAdminId || null],
|
|
function (err) {
|
|
if (err) return res.json({ success: false, message: 'register failed: ' + err.message })
|
|
|
|
const userId = this.lastID
|
|
|
|
db.get('SELECT config_value FROM system_config WHERE config_key = ?', ['trial_duration_hours'], (err, row) => {
|
|
const parsedTrialHours = row ? parseFloat(row.config_value) : NaN
|
|
const trialHours = Number.isFinite(parsedTrialHours) && parsedTrialHours >= 0 ? parsedTrialHours : 24
|
|
const trialExpiresAt = new Date()
|
|
trialExpiresAt.setHours(trialExpiresAt.getHours() + trialHours)
|
|
|
|
db.run(
|
|
`INSERT INTO user_subscriptions (user_id, subscription_type, expires_at, trial_started_at)
|
|
VALUES (?, 'trial', ?, CURRENT_TIMESTAMP)`,
|
|
[userId, trialExpiresAt.toISOString()]
|
|
)
|
|
|
|
if (device_fingerprint) {
|
|
db.run(
|
|
`INSERT INTO user_devices (user_id, device_fingerprint, device_name) VALUES (?, ?, ?)`,
|
|
[userId, device_fingerprint, 'first register device']
|
|
)
|
|
}
|
|
|
|
logAction(userId, 'register', ownerAdminId ? `registered under admin ID ${ownerAdminId}` : 'registered under super admin', device_fingerprint, clientIp)
|
|
|
|
const token = jwt.sign(
|
|
{ userId, username, email, role },
|
|
JWT_SECRET,
|
|
{ expiresIn: JWT_EXPIRES_IN }
|
|
)
|
|
|
|
res.json({
|
|
success: true,
|
|
token,
|
|
user: { id: userId, username, email, role },
|
|
subscription: {
|
|
type: 'trial',
|
|
expires_at: trialExpiresAt.toISOString()
|
|
},
|
|
message: isFirstUser ? 'registered as the first super admin' : `registered successfully, trial ${trialHours} hours`
|
|
})
|
|
})
|
|
}
|
|
)
|
|
}
|
|
|
|
if (!isFirstUser && inviteCode) {
|
|
db.get(
|
|
`SELECT id FROM users WHERE role = 'admin' AND status = 'active' AND invite_code = ?`,
|
|
[inviteCode],
|
|
(inviteErr, inviter) => {
|
|
if (inviteErr) return res.json({ success: false, message: 'invite code check failed: ' + inviteErr.message })
|
|
if (!inviter) return res.json({ success: false, message: 'invalid invite code' })
|
|
createUser(inviter.id)
|
|
}
|
|
)
|
|
} else {
|
|
createUser(null)
|
|
}
|
|
})
|
|
} catch (error) {
|
|
res.json({ success: false, message: 'register failed: ' + error.message })
|
|
}
|
|
})
|
|
})
|
|
app.post(`${API_PREFIX}/login`, async (req, res) => {
|
|
const { email, password, device_fingerprint } = req.body
|
|
const clientIp = req.ip || req.connection.remoteAddress
|
|
|
|
if (!email || !password) {
|
|
return res.json({ success: false, message: '手机号和密码不能为空' })
|
|
}
|
|
|
|
db.get('SELECT * FROM users WHERE email = ?', [email], async (err, user) => {
|
|
if (!user) {
|
|
logAction(null, 'login_failed', `登录失败: 手机号 ${email} 不存在`, null, clientIp)
|
|
return res.json({ success: false, message: '手机号或密码错误' })
|
|
}
|
|
|
|
// 验证密码
|
|
const isPasswordValid = await bcrypt.compare(password, user.password)
|
|
if (!isPasswordValid) {
|
|
logAction(user.id, 'login_failed', '登录失败: 密码错误', null, clientIp)
|
|
return res.json({ success: false, message: '手机号或密码错误' })
|
|
}
|
|
|
|
// 检查账户状态
|
|
if (user.status !== 'active') {
|
|
return res.json({ success: false, message: '账户已被禁用,请联系管理员' })
|
|
}
|
|
|
|
// 单点登录:直接完成登录,新登录会使旧token失效
|
|
completeLogin(user, null, clientIp, res)
|
|
})
|
|
})
|
|
|
|
function completeLogin(user, device_fingerprint, clientIp, res) {
|
|
// 更新最后登录时间
|
|
const now = new Date().toISOString()
|
|
console.log(`[Login] 用户 ${user.username} 登录成功,更新 last_login_at: ${now}`)
|
|
|
|
// 获取订阅信息
|
|
db.get('SELECT * FROM user_subscriptions WHERE user_id = ?', [user.id], (err, subscription) => {
|
|
// 生成 JWT Token
|
|
const token = jwt.sign(
|
|
{ userId: user.id, username: user.username, email: user.email, role: user.role },
|
|
JWT_SECRET,
|
|
{ expiresIn: JWT_EXPIRES_IN }
|
|
)
|
|
|
|
// 保存当前token到数据库(单点登录:新登录会使旧token失效)
|
|
db.run(
|
|
'UPDATE users SET last_login_at = ?, current_token = ?, updated_at = ? WHERE id = ?',
|
|
[now, token, now, user.id]
|
|
)
|
|
|
|
logAction(user.id, 'login_success', '登录成功', null, clientIp)
|
|
|
|
res.json({
|
|
success: true,
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
is_super_admin: user.is_super_admin || 0,
|
|
invite_code: user.invite_code || '',
|
|
owner_admin_id: user.owner_admin_id || null,
|
|
status: user.status,
|
|
points: user.points || 0,
|
|
cloud_digital_human_disabled: user.cloud_digital_human_disabled || 0
|
|
},
|
|
subscription: subscription || null
|
|
})
|
|
})
|
|
}
|
|
|
|
// ==================== 验证Token ====================
|
|
app.post(`${API_PREFIX}/verify`, (req, res) => {
|
|
const { token } = req.body
|
|
|
|
if (!token) {
|
|
return res.json({ valid: false, message: 'Token不存在' })
|
|
}
|
|
|
|
try {
|
|
const payload = jwt.verify(token, JWT_SECRET)
|
|
|
|
// 获取用户最新信息
|
|
db.get('SELECT * FROM users WHERE id = ?', [payload.userId], (err, user) => {
|
|
if (!user) {
|
|
return res.json({ valid: false, message: '用户不存在' })
|
|
}
|
|
|
|
if (user.status !== 'active') {
|
|
return res.json({ valid: false, message: '账户已被禁用' })
|
|
}
|
|
|
|
// 单点登录检查:验证是否为当前有效token
|
|
if (user.current_token && user.current_token !== token) {
|
|
return res.json({
|
|
valid: false,
|
|
message: '您的账号已在其他设备登录,当前会话已失效',
|
|
kicked: true
|
|
})
|
|
}
|
|
|
|
// 获取订阅信息
|
|
db.get('SELECT * FROM user_subscriptions WHERE user_id = ?', [user.id], (err, subscription) => {
|
|
const now = new Date()
|
|
const expiresAt = subscription ? new Date(subscription.expires_at) : null
|
|
const isExpired = expiresAt ? now > expiresAt : true
|
|
const remainingMs = expiresAt ? expiresAt - now : 0
|
|
const remainingDays = Math.ceil(remainingMs / (1000 * 60 * 60 * 24))
|
|
|
|
res.json({
|
|
valid: true,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
is_super_admin: user.is_super_admin || 0,
|
|
invite_code: user.invite_code || '',
|
|
owner_admin_id: user.owner_admin_id || null,
|
|
points: user.points || 0,
|
|
cloud_digital_human_disabled: user.cloud_digital_human_disabled || 0
|
|
},
|
|
subscription: subscription,
|
|
isExpired: isExpired,
|
|
remainingDays: remainingDays > 0 ? remainingDays : 0
|
|
})
|
|
})
|
|
})
|
|
} catch (error) {
|
|
res.json({ valid: false, message: 'Token无效或已过期' })
|
|
}
|
|
})
|
|
|
|
// ==================== 检查订阅状态 ====================
|
|
app.post(`${API_PREFIX}/subscription/check`, (req, res) => {
|
|
const { user_id } = req.body
|
|
|
|
db.get('SELECT * FROM user_subscriptions WHERE user_id = ?', [user_id], (err, subscription) => {
|
|
if (!subscription) {
|
|
return res.json({ hasAccess: false, isExpired: true })
|
|
}
|
|
|
|
const now = new Date()
|
|
const expiresAt = new Date(subscription.expires_at)
|
|
const isExpired = now > expiresAt
|
|
const remainingMs = expiresAt - now
|
|
const remainingDays = Math.ceil(remainingMs / (1000 * 60 * 60 * 24))
|
|
|
|
res.json({
|
|
hasAccess: !isExpired,
|
|
isExpired: isExpired,
|
|
expiresAt: subscription.expires_at,
|
|
remainingDays: remainingDays > 0 ? remainingDays : 0,
|
|
subscription: subscription
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:用户列表(支持搜索) ====================
|
|
app.get(`${API_PREFIX}/admin/users`, adminAuth, (req, res) => {
|
|
const { search, status, subscription } = req.query
|
|
let sql = `
|
|
SELECT u.*, s.subscription_type, s.expires_at, owner.username as owner_admin_name,
|
|
(SELECT COUNT(*) FROM user_devices WHERE user_id = u.id) as device_count
|
|
FROM users u
|
|
LEFT JOIN user_subscriptions s ON u.id = s.user_id
|
|
LEFT JOIN users owner ON u.owner_admin_id = owner.id
|
|
WHERE 1=1`
|
|
const params = []
|
|
|
|
if (!isSuperAdmin(req.adminUser)) {
|
|
sql += ` AND u.role = 'user' AND u.owner_admin_id = ?`
|
|
params.push(req.adminUser.id)
|
|
}
|
|
|
|
if (search && search.trim()) {
|
|
const keyword = `%${search.trim()}%`
|
|
sql += ` AND (u.username LIKE ? OR u.email LIKE ? OR CAST(u.id AS TEXT) LIKE ?)`
|
|
params.push(keyword, keyword, `%${search.trim()}%`)
|
|
}
|
|
|
|
if (status) {
|
|
sql += ` AND u.status = ?`
|
|
params.push(status)
|
|
}
|
|
|
|
if (subscription === 'expired') {
|
|
sql += ` AND (s.expires_at IS NULL OR s.expires_at < datetime('now'))`
|
|
} else if (subscription === 'valid') {
|
|
sql += ` AND s.expires_at IS NOT NULL AND s.expires_at >= datetime('now')`
|
|
}
|
|
|
|
sql += ` ORDER BY u.created_at DESC`
|
|
|
|
db.all(sql, params, (err, rows) => {
|
|
if (err) {
|
|
return res.json({ success: false, error: err.message })
|
|
}
|
|
const users = rows.map(u => {
|
|
const { password, ...userWithoutPassword } = u
|
|
return userWithoutPassword
|
|
})
|
|
res.json({ success: true, users })
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:添加时长 ====================
|
|
app.post(`${API_PREFIX}/admin/add-time`, adminAuth, (req, res) => {
|
|
const { user_id, days, hours, reason } = req.body
|
|
const addHours = hours != null ? parseInt(hours) : (parseInt(days) || 0) * 24
|
|
|
|
denyIfNoUserAccess(req, res, user_id, () => {
|
|
db.get('SELECT * FROM user_subscriptions WHERE user_id = ?', [user_id], (err, subscription) => {
|
|
let newExpiresAt
|
|
if (subscription) {
|
|
const currentExpires = new Date(subscription.expires_at)
|
|
const now = new Date()
|
|
const baseDate = currentExpires > now ? currentExpires : now
|
|
newExpiresAt = new Date(baseDate)
|
|
newExpiresAt.setHours(newExpiresAt.getHours() + addHours)
|
|
|
|
db.run(
|
|
'UPDATE user_subscriptions SET expires_at = ?, subscription_type = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?',
|
|
[newExpiresAt.toISOString(), 'pro', user_id]
|
|
)
|
|
} else {
|
|
newExpiresAt = new Date()
|
|
newExpiresAt.setHours(newExpiresAt.getHours() + addHours)
|
|
|
|
db.run(
|
|
'INSERT INTO user_subscriptions (user_id, subscription_type, expires_at) VALUES (?, ?, ?)',
|
|
[user_id, 'pro', newExpiresAt.toISOString()]
|
|
)
|
|
}
|
|
|
|
db.run(
|
|
'INSERT INTO time_logs (user_id, action, days_added, new_expires_at, reason) VALUES (?, ?, ?, ?, ?)',
|
|
[user_id, 'add_time', addHours, newExpiresAt.toISOString(), reason || '管理员添加']
|
|
)
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `成功添加 ${addHours} 小时`,
|
|
newExpiresAt: newExpiresAt.toISOString()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:禁用/启用用户 ====================
|
|
app.post(`${API_PREFIX}/admin/user/status`, adminAuth, (req, res) => {
|
|
const { user_id, status } = req.body
|
|
|
|
denyIfNoUserAccess(req, res, user_id, () => {
|
|
db.run('UPDATE users SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [status, user_id], (err) => {
|
|
if (err) {
|
|
return res.json({ success: false, error: err.message })
|
|
}
|
|
logAction(user_id, 'status_change', `账户状态变更为: ${status}`)
|
|
res.json({ success: true, message: `用户状态已更新为: ${status}` })
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:重置密码 ====================
|
|
app.post(`${API_PREFIX}/admin/user/reset-password`, adminAuth, async (req, res) => {
|
|
const { user_id, new_password } = req.body
|
|
|
|
if (!user_id || !new_password) {
|
|
return res.json({ success: false, message: '用户ID和新密码不能为空' })
|
|
}
|
|
|
|
if (new_password.length < 6) {
|
|
return res.json({ success: false, message: '密码长度至少6位' })
|
|
}
|
|
|
|
try {
|
|
canManageUser(req.adminUser, user_id, async (accessErr, allowed) => {
|
|
if (accessErr) return res.json({ success: false, error: accessErr.message })
|
|
if (!allowed) return res.status(403).json({ success: false, message: '无权管理该用户' })
|
|
|
|
// 加密新密码
|
|
const salt = await bcrypt.genSalt(10)
|
|
const hashedPassword = await bcrypt.hash(new_password, salt)
|
|
|
|
db.run(
|
|
'UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[hashedPassword, user_id],
|
|
(err) => {
|
|
if (err) {
|
|
return res.json({ success: false, error: err.message })
|
|
}
|
|
logAction(user_id, 'password_reset', '管理员重置了密码')
|
|
console.log(`[Admin] 用户ID ${user_id} 密码已重置`)
|
|
res.json({ success: true, message: '密码重置成功' })
|
|
}
|
|
)
|
|
})
|
|
} catch (error) {
|
|
res.json({ success: false, message: '密码重置失败: ' + error.message })
|
|
}
|
|
})
|
|
|
|
// ==================== 管理员:解绑设备 ====================
|
|
app.post(`${API_PREFIX}/admin/device/unbind`, adminAuth, (req, res) => {
|
|
const { user_id, device_fingerprint } = req.body
|
|
|
|
denyIfNoUserAccess(req, res, user_id, () => {
|
|
if (device_fingerprint) {
|
|
db.run('DELETE FROM user_devices WHERE user_id = ? AND device_fingerprint = ?', [user_id, device_fingerprint], (err) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
logAction(user_id, 'device_unbind', `设备已解绑: ${device_fingerprint}`)
|
|
res.json({ success: true, message: '设备已解绑' })
|
|
})
|
|
} else {
|
|
// 解绑所有设备
|
|
db.run('DELETE FROM user_devices WHERE user_id = ?', [user_id], (err) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
logAction(user_id, 'device_unbind_all', '所有设备已解绑')
|
|
res.json({ success: true, message: '所有设备已解绑' })
|
|
})
|
|
}
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:获取用户设备列表 ====================
|
|
app.get(`${API_PREFIX}/admin/user/:userId/devices`, adminAuth, (req, res) => {
|
|
const userId = req.params.userId
|
|
denyIfNoUserAccess(req, res, userId, () => {
|
|
db.all('SELECT * FROM user_devices WHERE user_id = ? ORDER BY last_seen_at DESC', [userId], (err, devices) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
res.json({ success: true, devices })
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== 客户端:上报生成记录 ====================
|
|
app.post(`${API_PREFIX}/user/generation-log`, (req, res) => {
|
|
const { userId, type, detail } = req.body
|
|
const validTypes = ['script', 'voice', 'video', 'subtitle']
|
|
if (!type || !validTypes.includes(type)) {
|
|
return res.json({ success: false, message: '无效的生成类型' })
|
|
}
|
|
if (!userId) {
|
|
return res.json({ success: false, message: '缺少userId' })
|
|
}
|
|
const ip = req.ip || req.connection.remoteAddress
|
|
const fingerprint = req.headers['x-device-fingerprint'] || null
|
|
db.run(
|
|
'INSERT INTO generation_logs (user_id, type, detail, device_fingerprint, ip_address) VALUES (?, ?, ?, ?, ?)',
|
|
[userId, type, detail || '', fingerprint, ip],
|
|
function(err) {
|
|
if (err) return res.json({ success: false, message: err.message })
|
|
res.json({ success: true })
|
|
}
|
|
)
|
|
})
|
|
|
|
// ==================== 管理员:生成记录统计 ====================
|
|
app.get(`${API_PREFIX}/admin/generation-stats`, adminAuth, (req, res) => {
|
|
const userId = req.query.userId
|
|
if (userId) {
|
|
return denyIfNoUserAccess(req, res, userId, () => {
|
|
const finish = () => {
|
|
let sql, params
|
|
if (userId) {
|
|
sql = `SELECT type, COUNT(*) as count FROM generation_logs WHERE user_id = ? GROUP BY type`
|
|
params = [userId]
|
|
} else {
|
|
sql = `SELECT type, COUNT(*) as count FROM generation_logs GROUP BY type`
|
|
}
|
|
db.all(sql, params, (err, rows) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
const result = { script: 0, voice: 0, video: 0, subtitle: 0, total: 0 }
|
|
rows.forEach(r => {
|
|
result[r.type] = r.count
|
|
result.total += r.count
|
|
})
|
|
res.json({ success: true, stats: result })
|
|
})
|
|
}
|
|
finish()
|
|
})
|
|
}
|
|
let sql, params
|
|
if (userId) {
|
|
sql = `SELECT type, COUNT(*) as count FROM generation_logs WHERE user_id = ? GROUP BY type`
|
|
params = [userId]
|
|
} else if (!isSuperAdmin(req.adminUser)) {
|
|
sql = `SELECT gl.type, COUNT(*) as count
|
|
FROM generation_logs gl
|
|
JOIN users u ON gl.user_id = u.id
|
|
WHERE u.owner_admin_id = ? AND u.role = 'user'
|
|
GROUP BY gl.type`
|
|
params = [req.adminUser.id]
|
|
} else {
|
|
sql = `SELECT type, COUNT(*) as count FROM generation_logs GROUP BY type`
|
|
}
|
|
db.all(sql, params, (err, rows) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
const result = { script: 0, voice: 0, video: 0, subtitle: 0, total: 0 }
|
|
rows.forEach(r => {
|
|
result[r.type] = r.count
|
|
result.total += r.count
|
|
})
|
|
res.json({ success: true, stats: result })
|
|
})
|
|
})
|
|
|
|
app.get(`${API_PREFIX}/admin/generation-logs`, adminAuth, (req, res) => {
|
|
const { userId, type, page = 1, limit = 50 } = req.query
|
|
const offset = (page - 1) * limit
|
|
let where = '1=1'
|
|
const params = []
|
|
if (userId) { where += ' AND gl.user_id = ?'; params.push(userId) }
|
|
if (type) { where += ' AND gl.type = ?'; params.push(type) }
|
|
if (!isSuperAdmin(req.adminUser)) {
|
|
where += ` AND u.owner_admin_id = ? AND u.role = 'user'`
|
|
params.push(req.adminUser.id)
|
|
}
|
|
db.all(
|
|
`SELECT gl.*, u.username FROM generation_logs gl LEFT JOIN users u ON gl.user_id = u.id WHERE ${where} ORDER BY gl.created_at DESC LIMIT ? OFFSET ?`,
|
|
[...params, limit, offset],
|
|
(err, logs) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
db.get(`SELECT COUNT(*) as total FROM generation_logs gl LEFT JOIN users u ON gl.user_id = u.id WHERE ${where}`, params, (err2, countRow) => {
|
|
res.json({ success: true, logs, total: countRow?.total || 0 })
|
|
})
|
|
}
|
|
)
|
|
})
|
|
|
|
// ==================== 管理员:统计信息 ====================
|
|
app.get(`${API_PREFIX}/admin/stats`, adminAuth, (req, res) => {
|
|
if (!isSuperAdmin(req.adminUser)) {
|
|
return db.get(`
|
|
SELECT
|
|
(SELECT COUNT(*) FROM users WHERE role = 'user' AND owner_admin_id = ?) as total_users,
|
|
(SELECT COUNT(*) FROM users WHERE role = 'user' AND owner_admin_id = ? AND status = 'active') as active_users,
|
|
0 as admin_count,
|
|
(SELECT COUNT(*) FROM user_subscriptions s JOIN users u ON s.user_id = u.id WHERE u.role = 'user' AND u.owner_admin_id = ? AND s.expires_at > datetime('now')) as valid_subscriptions,
|
|
(SELECT COUNT(*) FROM user_subscriptions s JOIN users u ON s.user_id = u.id WHERE u.role = 'user' AND u.owner_admin_id = ? AND s.expires_at <= datetime('now')) as expired_subscriptions,
|
|
(SELECT COUNT(*) FROM user_devices d JOIN users u ON d.user_id = u.id WHERE u.role = 'user' AND u.owner_admin_id = ?) as total_devices
|
|
`, [req.adminUser.id, req.adminUser.id, req.adminUser.id, req.adminUser.id, req.adminUser.id], (err, stats) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
res.json({ success: true, stats })
|
|
})
|
|
}
|
|
|
|
db.get(`
|
|
SELECT
|
|
(SELECT COUNT(*) FROM users) as total_users,
|
|
(SELECT COUNT(*) FROM users WHERE status = 'active') as active_users,
|
|
(SELECT COUNT(*) FROM users WHERE role = 'admin') as admin_count,
|
|
(SELECT COUNT(*) FROM user_subscriptions WHERE expires_at > datetime('now')) as valid_subscriptions,
|
|
(SELECT COUNT(*) FROM user_subscriptions WHERE expires_at <= datetime('now')) as expired_subscriptions,
|
|
(SELECT COUNT(*) FROM user_devices) as total_devices
|
|
`, [], (err, stats) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
res.json({ success: true, stats })
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:创建子管理员 ====================
|
|
app.post(`${API_PREFIX}/admin/create-admin`, adminAuth, requireSuperAdmin, async (req, res) => {
|
|
const { username, email, password } = req.body
|
|
const inviteCode = normalizeInviteCode(req.body.invite_code)
|
|
const clientIp = req.ip || req.connection.remoteAddress
|
|
|
|
if (!username || !email || !password) {
|
|
return res.json({ success: false, message: '用户名、手机号和密码不能为空' })
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
return res.json({ success: false, message: '密码长度至少6位' })
|
|
}
|
|
|
|
// 检查用户名或邮箱是否已存在
|
|
db.get('SELECT id FROM users WHERE username = ? OR email = ?', [username, email], async (err, row) => {
|
|
if (row) {
|
|
return res.json({ success: false, message: '用户名或手机号已存在' })
|
|
}
|
|
|
|
try {
|
|
// 加密密码
|
|
const salt = await bcrypt.genSalt(10)
|
|
const hashedPassword = await bcrypt.hash(password, salt)
|
|
|
|
// 创建管理员用户
|
|
db.run(
|
|
`INSERT INTO users (username, email, password, role, status, is_super_admin, invite_code) VALUES (?, ?, ?, 'admin', 'active', 0, ?)`,
|
|
[username, email, hashedPassword, inviteCode || null],
|
|
function (err) {
|
|
if (err) {
|
|
return res.json({ success: false, message: '创建失败: ' + err.message })
|
|
}
|
|
|
|
const userId = this.lastID
|
|
|
|
// 给管理员一个永久订阅(100年)
|
|
const adminExpiresAt = new Date()
|
|
adminExpiresAt.setFullYear(adminExpiresAt.getFullYear() + 100)
|
|
|
|
db.run(
|
|
`INSERT INTO user_subscriptions (user_id, subscription_type, expires_at) VALUES (?, 'admin', ?)`,
|
|
[userId, adminExpiresAt.toISOString()]
|
|
)
|
|
|
|
logAction(req.adminUser.id, 'create_admin', `创建了子管理员: ${username}`, null, clientIp)
|
|
console.log(`[Admin] ${req.adminUser.username} 创建了子管理员: ${username}`)
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `子管理员 ${username} 创建成功`,
|
|
user: { id: userId, username, email, role: 'admin' }
|
|
})
|
|
}
|
|
)
|
|
} catch (error) {
|
|
res.json({ success: false, message: '创建失败: ' + error.message })
|
|
}
|
|
})
|
|
})
|
|
|
|
app.get(`${API_PREFIX}/admin/admins`, adminAuth, requireSuperAdmin, (req, res) => {
|
|
db.all(
|
|
`SELECT id, username, email, invite_code, is_super_admin, status,
|
|
(SELECT COUNT(*) FROM users owned WHERE owned.owner_admin_id = users.id AND owned.role = 'user') as owned_user_count
|
|
FROM users
|
|
WHERE role = 'admin'
|
|
ORDER BY is_super_admin DESC, id ASC`,
|
|
[],
|
|
(err, admins) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
res.json({ success: true, admins })
|
|
}
|
|
)
|
|
})
|
|
|
|
app.put(`${API_PREFIX}/admin/admin/invite-code`, adminAuth, requireSuperAdmin, (req, res) => {
|
|
const { admin_id } = req.body
|
|
const inviteCode = normalizeInviteCode(req.body.invite_code)
|
|
|
|
if (!admin_id) return res.json({ success: false, message: '管理员ID不能为空' })
|
|
|
|
db.run(
|
|
`UPDATE users SET invite_code = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND role = 'admin'`,
|
|
[inviteCode || null, admin_id],
|
|
function (err) {
|
|
if (err) return res.json({ success: false, message: err.message })
|
|
if (this.changes === 0) return res.json({ success: false, message: '管理员不存在' })
|
|
logAction(admin_id, 'invite_code_update', `邀请码更新为: ${inviteCode || '空'}`)
|
|
res.json({ success: true, message: '邀请码已更新', invite_code: inviteCode })
|
|
}
|
|
)
|
|
})
|
|
|
|
app.post(`${API_PREFIX}/admin/user/assign-owner`, adminAuth, requireSuperAdmin, (req, res) => {
|
|
const { user_id } = req.body
|
|
const ownerAdminId = req.body.owner_admin_id || null
|
|
|
|
if (!user_id) return res.json({ success: false, message: '用户ID不能为空' })
|
|
|
|
const updateOwner = () => {
|
|
db.run(
|
|
`UPDATE users SET owner_admin_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND role = 'user'`,
|
|
[ownerAdminId, user_id],
|
|
function (err) {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
if (this.changes === 0) return res.json({ success: false, message: '用户不存在或不是普通用户' })
|
|
logAction(user_id, 'owner_assign', ownerAdminId ? `用户分配给管理员ID ${ownerAdminId}` : '用户分配回总后台')
|
|
res.json({ success: true, message: '用户归属已更新' })
|
|
}
|
|
)
|
|
}
|
|
|
|
if (!ownerAdminId) return updateOwner()
|
|
|
|
db.get(`SELECT id FROM users WHERE id = ? AND role = 'admin' AND status = 'active'`, [ownerAdminId], (err, admin) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
if (!admin) return res.json({ success: false, message: '目标子管理员不存在或不可用' })
|
|
updateOwner()
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:修改用户角色 ====================
|
|
app.post(`${API_PREFIX}/admin/user/role`, adminAuth, requireSuperAdmin, (req, res) => {
|
|
const { user_id, role } = req.body
|
|
const inviteCode = normalizeInviteCode(req.body.invite_code)
|
|
|
|
if (!['user', 'admin'].includes(role)) {
|
|
return res.json({ success: false, message: '无效的角色' })
|
|
}
|
|
|
|
// 不能修改自己的角色
|
|
if (Number(user_id) === Number(req.adminUser.id)) {
|
|
return res.json({ success: false, message: '不能修改自己的角色' })
|
|
}
|
|
|
|
const updateFields = [`role = ?`, `updated_at = CURRENT_TIMESTAMP`]
|
|
const params = [role]
|
|
|
|
if (role === 'admin') {
|
|
updateFields.push(`is_super_admin = 0`)
|
|
updateFields.push(`invite_code = ?`)
|
|
updateFields.push(`owner_admin_id = NULL`)
|
|
params.push(inviteCode || null)
|
|
} else {
|
|
updateFields.push(`is_super_admin = 0`)
|
|
updateFields.push(`invite_code = NULL`)
|
|
updateFields.push(`owner_admin_id = NULL`)
|
|
}
|
|
|
|
params.push(user_id)
|
|
|
|
db.run(`UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`, params, (err) => {
|
|
if (err) {
|
|
return res.json({ success: false, error: err.message })
|
|
}
|
|
logAction(user_id, 'role_change', role === 'admin' ? `升级为子管理员,邀请码: ${inviteCode || '空'}` : `角色变更为: ${role}`)
|
|
console.log(`[Admin] ${req.adminUser.username} 将用户ID ${user_id} 的角色改为: ${role}`)
|
|
res.json({ success: true, message: `用户角色已更新为: ${role}` })
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:修改用户积分 ====================
|
|
app.post(`${API_PREFIX}/admin/points/modify`, adminAuth, (req, res) => {
|
|
const { user_id, points_change, reason } = req.body
|
|
|
|
if (!user_id || points_change === undefined || points_change === 0) {
|
|
return res.json({ success: false, message: '用户ID和积分变动不能为空' })
|
|
}
|
|
|
|
const pointsChange = parseInt(points_change)
|
|
|
|
denyIfNoUserAccess(req, res, user_id, () => {
|
|
// 获取用户当前积分
|
|
db.get('SELECT id, username, points FROM users WHERE id = ?', [user_id], (err, user) => {
|
|
if (!user) {
|
|
return res.json({ success: false, message: '用户不存在' })
|
|
}
|
|
|
|
const currentPoints = user.points || 0
|
|
const newPoints = currentPoints + pointsChange
|
|
|
|
if (newPoints < 0) {
|
|
return res.json({ success: false, message: '积分不足,无法扣减' })
|
|
}
|
|
|
|
// 更新积分
|
|
db.run('UPDATE users SET points = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [newPoints, user_id], (err) => {
|
|
if (err) {
|
|
return res.json({ success: false, error: err.message })
|
|
}
|
|
|
|
// 记录积分日志
|
|
db.run(
|
|
'INSERT INTO points_logs (user_id, operator_id, action, points_change, points_after, reason) VALUES (?, ?, ?, ?, ?, ?)',
|
|
[user_id, req.adminUser.id, pointsChange > 0 ? 'add' : 'deduct', pointsChange, newPoints, reason || (pointsChange > 0 ? '管理员添加' : '管理员扣减')]
|
|
)
|
|
|
|
logAction(user_id, 'points_change', `积分变动: ${pointsChange > 0 ? '+' : ''}${pointsChange},当前: ${newPoints}`)
|
|
console.log(`[Admin] ${req.adminUser.username} 将用户 ${user.username} 的积分变动 ${pointsChange > 0 ? '+' : ''}${pointsChange},当前: ${newPoints}`)
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `积分${pointsChange > 0 ? '增加' : '扣减'}成功`,
|
|
pointsChange: pointsChange,
|
|
newPoints: newPoints
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:查询用户积分日志 ====================
|
|
app.get(`${API_PREFIX}/admin/points/logs/:userId`, adminAuth, (req, res) => {
|
|
const userId = req.params.userId
|
|
|
|
denyIfNoUserAccess(req, res, userId, () => {
|
|
db.all(
|
|
`SELECT pl.*, u.username as operator_name
|
|
FROM points_logs pl
|
|
LEFT JOIN users u ON pl.operator_id = u.id
|
|
WHERE pl.user_id = ?
|
|
ORDER BY pl.created_at DESC
|
|
LIMIT 50`,
|
|
[userId],
|
|
(err, logs) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
res.json({ success: true, logs })
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:查询用户登录日志 ====================
|
|
app.get(`${API_PREFIX}/admin/logs/user/:userId`, adminAuth, (req, res) => {
|
|
const userId = req.params.userId
|
|
|
|
denyIfNoUserAccess(req, res, userId, () => {
|
|
db.all(
|
|
`SELECT * FROM user_logs
|
|
WHERE user_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT 100`,
|
|
[userId],
|
|
(err, logs) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
res.json({ success: true, logs })
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:查询所有管理员操作日志 ====================
|
|
app.get(`${API_PREFIX}/admin/logs/admin-actions`, adminAuth, (req, res) => {
|
|
db.all(
|
|
`SELECT ul.*, u.username as target_username, admin.username as operator_username
|
|
FROM user_logs ul
|
|
LEFT JOIN users u ON ul.user_id = u.id
|
|
LEFT JOIN users admin ON ul.ip_address = CAST(admin.id AS TEXT)
|
|
WHERE ul.action IN ('create_admin', 'status_change', 'password_reset', 'role_change', 'points_change', 'device_unbind', 'device_unbind_all')
|
|
ORDER BY ul.created_at DESC
|
|
LIMIT 200`,
|
|
[],
|
|
(err, logs) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
res.json({ success: true, logs })
|
|
}
|
|
)
|
|
})
|
|
|
|
// ==================== 管理员:查询所有日志(综合) ====================
|
|
app.get(`${API_PREFIX}/admin/logs/all`, adminAuth, (req, res) => {
|
|
const page = parseInt(req.query.page) || 1
|
|
const limit = parseInt(req.query.limit) || 50
|
|
const offset = (page - 1) * limit
|
|
const actionFilter = req.query.action || ''
|
|
|
|
let whereClause = ''
|
|
let params = []
|
|
|
|
if (actionFilter) {
|
|
whereClause = 'WHERE ul.action = ?'
|
|
params.push(actionFilter)
|
|
}
|
|
|
|
if (!isSuperAdmin(req.adminUser)) {
|
|
whereClause += whereClause ? ' AND ' : 'WHERE '
|
|
whereClause += `u.owner_admin_id = ? AND u.role = 'user'`
|
|
params.push(req.adminUser.id)
|
|
}
|
|
|
|
db.all(
|
|
`SELECT ul.*, u.username
|
|
FROM user_logs ul
|
|
LEFT JOIN users u ON ul.user_id = u.id
|
|
${whereClause}
|
|
ORDER BY ul.created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[...params, limit, offset],
|
|
(err, logs) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
res.json({ success: true, logs, page, limit })
|
|
}
|
|
)
|
|
})
|
|
|
|
// ==================== 设备签到 ====================
|
|
app.post(`${API_PREFIX}/device/checkin`, (req, res) => {
|
|
const { user_id, device_fingerprint } = req.body
|
|
|
|
db.run(
|
|
'UPDATE user_devices SET last_seen_at = CURRENT_TIMESTAMP WHERE user_id = ? AND device_fingerprint = ?',
|
|
[user_id, device_fingerprint],
|
|
(err) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
res.json({ success: true, message: '签到成功' })
|
|
}
|
|
)
|
|
})
|
|
|
|
// ==================== 客户端:获取系统公开配置(API Key等) ====================
|
|
app.get(`${API_PREFIX}/system/config/public`, (req, res) => {
|
|
const publicKeys = [
|
|
'aliyun_bailian_api_key',
|
|
'aliyun_bailian_base_url',
|
|
'aliyun_oss_access_key_id',
|
|
'aliyun_oss_access_key_secret',
|
|
'aliyun_oss_bucket',
|
|
'aliyun_oss_region',
|
|
'volcengine_api_key',
|
|
'volcengine_model',
|
|
'runninghub_api_key',
|
|
'runninghub_workflow_id_1',
|
|
'runninghub_workflow_id_2',
|
|
'runninghub_tts_webapp_id_v2',
|
|
'runninghub_tts_webapp_id_v3',
|
|
'runninghub_tts_v2_audio_node',
|
|
'runninghub_tts_v2_text_node',
|
|
'runninghub_tts_v2_emotion_node',
|
|
'cloud_beauty_base_url',
|
|
'cloud_beauty_api_key',
|
|
'cloud_beauty_default_mode',
|
|
'cloud_beauty_enabled'
|
|
]
|
|
|
|
const placeholders = publicKeys.map(() => '?').join(',')
|
|
db.all(
|
|
`SELECT config_key, config_value FROM system_config WHERE config_key IN (${placeholders})`,
|
|
publicKeys,
|
|
(err, rows) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
|
|
const config = {}
|
|
rows.forEach(row => {
|
|
config[row.config_key] = row.config_value
|
|
})
|
|
|
|
res.json({ success: true, config })
|
|
}
|
|
)
|
|
})
|
|
|
|
// ==================== 管理员:获取所有系统配置 ====================
|
|
app.get(`${API_PREFIX}/admin/system/config`, adminAuth, requireSuperAdmin, (req, res) => {
|
|
db.all('SELECT * FROM system_config ORDER BY id', [], (err, rows) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
res.json({ success: true, configs: rows })
|
|
})
|
|
})
|
|
|
|
// ==================== 管理员:批量更新系统配置 ====================
|
|
app.put(`${API_PREFIX}/admin/system/config`, adminAuth, requireSuperAdmin, (req, res) => {
|
|
const { configs } = req.body
|
|
|
|
if (!configs || typeof configs !== 'object') {
|
|
return res.json({ success: false, message: '配置数据格式错误' })
|
|
}
|
|
|
|
const entries = Object.entries(configs)
|
|
let updated = 0
|
|
|
|
entries.forEach(([key, value]) => {
|
|
db.run(
|
|
'INSERT OR IGNORE INTO system_config (config_key, config_value) VALUES (?, ?)',
|
|
[key, ''],
|
|
() => {
|
|
db.run(
|
|
'UPDATE system_config SET config_value = ?, updated_at = CURRENT_TIMESTAMP, updated_by = ? WHERE config_key = ?',
|
|
[String(value), req.adminUser.username, key],
|
|
(err) => {
|
|
if (!err) updated++
|
|
}
|
|
)
|
|
}
|
|
)
|
|
})
|
|
|
|
logAction(req.adminUser.id, 'system_config_update', `更新了 ${entries.length} 项系统配置`)
|
|
|
|
setTimeout(() => {
|
|
res.json({ success: true, message: `已更新 ${entries.length} 项配置`, updated })
|
|
}, 100)
|
|
})
|
|
|
|
// ==================== 管理员:切换用户云端数字人权限 ====================
|
|
app.put(`${API_PREFIX}/admin/user/cloud-digital-human`, adminAuth, (req, res) => {
|
|
const { user_id, disabled } = req.body
|
|
|
|
if (!user_id) {
|
|
return res.json({ success: false, message: '用户ID不能为空' })
|
|
}
|
|
|
|
denyIfNoUserAccess(req, res, user_id, () => {
|
|
db.run(
|
|
'UPDATE users SET cloud_digital_human_disabled = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[disabled ? 1 : 0, user_id],
|
|
(err) => {
|
|
if (err) return res.json({ success: false, error: err.message })
|
|
logAction(user_id, 'cloud_digital_human_toggle', `云端数字人功能已${disabled ? '关闭' : '开启'}`)
|
|
res.json({
|
|
success: true,
|
|
message: `云端数字人功能已${disabled ? '关闭' : '开启'}`,
|
|
cloud_digital_human_disabled: disabled ? 1 : 0
|
|
})
|
|
}
|
|
)
|
|
})
|
|
})
|
|
|
|
// Web 管理后台路由
|
|
app.get(`${ADMIN_PATH}`, (req, res) => {
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
|
|
res.setHeader('Pragma', 'no-cache')
|
|
res.setHeader('Expires', '0')
|
|
res.sendFile(path.join(__dirname, 'public', 'admin.html'))
|
|
})
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`\n✅ 用户管理服务器已启动`)
|
|
console.log(`📋 管理后台: http://localhost:${PORT}${ADMIN_PATH}`)
|
|
console.log(`📡 API 地址: http://localhost:${PORT}${API_PREFIX}\n`)
|
|
})
|