Files
WYF-koubo/user-server/server.js
T
2026-06-19 18:45:55 +08:00

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