const router = require('express').Router() const { ok, fail } = require('../lib/response') const { hashPassword, hashPasswordLegacy, verifyPassword, signAdmin } = require('../lib/auth') const { requireAdmin } = require('../middleware/auth') const { invalidateCache } = require('../lib/settings-cache') const adminDao = require('../dao/admin.dao') const deviceDao = require('../dao/device.dao') const bindingDao = require('../dao/binding.dao') const commandDao = require('../dao/command.dao') const userDao = require('../dao/user.dao') const subscriptionDao = require('../dao/subscription.dao') const treatmentDao = require('../dao/treatment.dao') const logDao = require('../dao/log.dao') const settingsDao = require('../dao/settings.dao') const wrap = fn => (req, res, next) => fn(req, res, next).catch(next) function pageParams(query) { const page = Math.max(1, parseInt(query.page, 10) || 1) const pageSize = Math.min(Math.max(1, parseInt(query.page_size, 10) || 20), 100) return { page, pageSize, offset: (page - 1) * pageSize } } // --- Auth --- router.post('/login', wrap(async (req, res) => { const username = req.body.username || '' const password = req.body.password || '' const admin = await adminDao.findByUsername(username) if (!admin) return res.json(fail(1001, '用户名或密码错误')) let matched = verifyPassword(password, admin.password_hash) if (!matched) { if (admin.password_salt && hashPasswordLegacy(password, admin.password_salt) === admin.password_hash) { const newHash = hashPassword(password) await adminDao.updatePassword(admin.admin_id, newHash) matched = true } } if (!matched) return res.json(fail(1001, '用户名或密码错误')) const token = signAdmin(admin) await logDao.write({ admin_id: admin.admin_id, action: 'admin_login', detail: '管理员登录: ' + username, ip: req.ip }) res.json(ok({ token, admin_id: String(admin.admin_id), username: admin.username, real_name: admin.real_name, role: admin.role })) })) router.post('/password', requireAdmin, wrap(async (req, res) => { const oldPassword = req.body.old_password || '' const newPassword = req.body.new_password || '' if (newPassword.length < 6) return res.json(fail(2001, 'password too short')) const current = await adminDao.findById(req.admin.admin_id) if (!current) return res.json(fail(1002, '未授权,请重新登录')) let matched = verifyPassword(oldPassword, current.password_hash) if (!matched && current.password_salt && hashPasswordLegacy(oldPassword, current.password_salt) === current.password_hash) { matched = true } if (!matched) return res.json(fail(1001, '原密码错误')) const newHash = hashPassword(newPassword) await adminDao.updatePassword(req.admin.admin_id, newHash) await logDao.write({ admin_id: req.admin.admin_id, action: 'admin_change_password', detail: '管理员修改密码', ip: req.ip }) res.json(ok({ message: 'success' })) })) // --- Dashboard --- router.get('/dashboard', requireAdmin, wrap(async (req, res) => { const counts = await adminDao.getDashboardCounts() const subStats = await adminDao.getSubscriptionStats() res.json(ok({ device_count: counts.device_count, user_count: counts.user_count, treatment_count: counts.treatment_count, subscription_count: counts.subscription_count, sub_stats: subStats })) })) // --- Devices --- router.get('/devices', requireAdmin, wrap(async (req, res) => { const { page, pageSize, offset } = pageParams(req.query) const { records, total } = await deviceDao.list({ keyword: req.query.keyword, pageSize, offset }) res.json(ok({ records, total })) })) router.post('/devices', requireAdmin, wrap(async (req, res) => { const deviceId = String(req.body.device_id || '').trim() if (!deviceId) return res.json(fail(2001, 'device_id required')) await deviceDao.create({ device_id: deviceId, product_id: req.body.product_id || 'HOX_LIGHT_MASK', device_secret: req.body.device_secret || '', device_name: req.body.device_name || 'LumiFlow', firmware_version: req.body.firmware_version || '1.0.0' }) await logDao.write({ admin_id: req.admin.admin_id, action: 'admin_device_create', detail: '预生成产品码: ' + deviceId, ip: req.ip }) res.json(ok({ device_id: deviceId })) })) router.post('/devices/batch', requireAdmin, wrap(async (req, res) => { const deviceIds = req.body.device_ids if (!Array.isArray(deviceIds) || deviceIds.length === 0 || deviceIds.length > 500) { return res.json(fail(2001, 'device_ids must be an array with 1-500 items')) } const { created, failed } = await deviceDao.createBatch(deviceIds) await logDao.write({ admin_id: req.admin.admin_id, action: 'admin_device_batch_create', detail: '批量预生成产品码: ' + created + '/' + deviceIds.length, ip: req.ip }) res.json(ok({ created, failed })) })) router.get('/devices/:device_id', requireAdmin, wrap(async (req, res) => { const result = await deviceDao.findByIdWithHistory(req.params.device_id) if (!result || !result.device) return res.json(fail(1005, 'DEVICE_NOT_FOUND')) res.json(ok(Object.assign({}, result.device, { binding_history: result.binding_history, recent_treatments: result.recent_treatments }))) })) router.post('/devices/:device_id/unbind', requireAdmin, wrap(async (req, res) => { await deviceDao.unbind(req.params.device_id) await logDao.write({ admin_id: req.admin.admin_id, action: 'admin_device_unbind', detail: '后台解绑设备: ' + req.params.device_id, ip: req.ip }) res.json(ok({ message: 'success' })) })) router.post('/devices/:device_id/command', requireAdmin, wrap(async (req, res) => { const opcode = parseInt(req.body.opcode, 10) if (!opcode) return res.json(fail(2001, 'opcode required')) await commandDao.create(req.params.device_id, req.admin.admin_id, opcode, req.body) await logDao.write({ admin_id: req.admin.admin_id, action: 'admin_device_command', detail: '记录远程指令: ' + req.params.device_id, ip: req.ip }) res.json(ok({ message: 'queued', command: req.body })) })) router.get('/devices/:device_id/commands', requireAdmin, wrap(async (req, res) => { const { page, pageSize, offset } = pageParams(req.query) const { records, total } = await commandDao.listByDevice(req.params.device_id, { pageSize, offset }) res.json(ok({ records, total })) })) // --- Users --- router.get('/users', requireAdmin, wrap(async (req, res) => { const { page, pageSize, offset } = pageParams(req.query) const { records, total } = await userDao.listAdmin({ keyword: req.query.keyword, pageSize, offset }) res.json(ok({ records, total })) })) router.get('/users/:user_id', requireAdmin, wrap(async (req, res) => { const result = await userDao.findByIdAdmin(req.params.user_id) if (!result) return res.json(fail(1004, 'USER_NOT_FOUND')) res.json(ok(result)) })) router.post('/users/:user_id/unbind', requireAdmin, wrap(async (req, res) => { const userId = req.params.user_id const targetUser = await userDao.findById(userId) if (!targetUser) return res.json(fail(1004, 'USER_NOT_FOUND')) const deviceId = req.body.device_id ? String(req.body.device_id).trim() : null const result = await bindingDao.unbindByUser(userId, deviceId) await logDao.write({ admin_id: req.admin.admin_id, user_id: userId, action: 'admin_user_unbind', detail: '后台解绑用户设备: user=' + userId + ', device=' + (deviceId || 'all'), ip: req.ip }) res.json(ok({ message: 'success', affected_rows: result.affectedRows || 0 })) })) router.post('/users/:user_id/deactivate', requireAdmin, wrap(async (req, res) => { const userId = req.params.user_id const targetUser = await userDao.findById(userId) if (!targetUser) return res.json(fail(1004, 'USER_NOT_FOUND')) const result = await userDao.deactivate(userId) await logDao.write({ admin_id: req.admin.admin_id, user_id: userId, action: 'admin_user_deactivate', detail: '后台注销用户: user=' + userId + ', unbound=' + result.unboundRows, ip: req.ip }) res.json(ok({ message: 'success', unbound_rows: result.unboundRows })) })) // --- Subscriptions --- router.get('/subscriptions', requireAdmin, wrap(async (req, res) => { const { page, pageSize, offset } = pageParams(req.query) const { records, total } = await subscriptionDao.list({ tab: req.query.tab, pageSize, offset }) const stats = await subscriptionDao.getStats() res.json(ok({ records, total, stats })) })) router.post('/subscriptions', requireAdmin, wrap(async (req, res) => { const userId = req.body.user_id if (!userId) return res.json(fail(2001, 'user_id required')) const targetUser = await userDao.findById(userId) if (!targetUser) return res.json(fail(1004, 'user_not_found')) await subscriptionDao.adminCreate( userId, req.body.plan || 'monthly', req.body.amount || 0, req.body.order_id || 'ADMIN' + Date.now(), req.body.days || 30 ) res.json(ok({ message: 'success' })) })) router.post('/subscriptions/cancel', requireAdmin, wrap(async (req, res) => { const subscriptionId = req.body.subscription_id if (!subscriptionId) return res.json(fail(2001, 'subscription_id required')) const result = await subscriptionDao.cancel(subscriptionId) if (result.affectedRows === 0) return res.json(fail(2001, '未找到有效订阅')) await logDao.write({ admin_id: req.admin.admin_id, action: 'subscription_cancel', detail: '取消订阅 #' + subscriptionId, ip: req.ip }) res.json(ok({ message: 'success' })) })) // --- Treatment Records --- router.get('/records', requireAdmin, wrap(async (req, res) => { const { page, pageSize, offset } = pageParams(req.query) const { records, total } = await treatmentDao.listAdmin({ keyword: req.query.keyword, userId: req.query.user_id, dateFrom: req.query.date_from, dateTo: req.query.date_to, pageSize, offset }) res.json(ok({ records, total })) })) // --- Logs --- router.get('/logs', requireAdmin, wrap(async (req, res) => { const { page, pageSize, offset } = pageParams(req.query) const { records, total } = await logDao.list({ type: req.query.type, deviceId: req.query.device_id, pageSize, offset }) res.json(ok({ records, total })) })) // --- Settings --- router.get('/settings', requireAdmin, wrap(async (req, res) => { const settings = await settingsDao.getAll() res.json(ok(settings)) })) router.post('/settings', requireAdmin, wrap(async (req, res) => { const ALLOWED_KEYS = ['system_name', 'admin_email', 'timezone', 'monthly_price', 'yearly_price', 'trial_days', 'enable_register', 'enable_binding', 'enable_free_mode', 'enable_smart_mode', 'maintenance_mode'] for (const key of Object.keys(req.body || {})) { if (!ALLOWED_KEYS.includes(key)) continue await settingsDao.update(key, req.body[key]) } invalidateCache() res.json(ok({ message: 'success' })) })) module.exports = router