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`) })