refactor: restructure entire project for human maintainability
Server: - Add Express framework, replace custom router/request parser - Create DAO layer (12 files) centralizing all 73 SQL queries - Rewrite 7 route files as thin Express controllers calling DAOs - Add SCF-to-Express adapter (lib/serverless.js) - Add auth middleware (middleware/auth.js) - Remove dead code from lib/auth.js Admin console: - Extract DataTable component (table + pagination) - Extract ConfirmModal component (modal + form styles) - Create listMixin for paginated list pages - Move form styles to common.css for slot compatibility - Refactor device + subscription pages as examples Miniprogram: - Split 734-line BLE monolith into 4 focused modules (protocol, connection, commands, barrel index) - Create API module (utils/api.js) with named functions - Create page utilities (utils/page.js) - Refactor index + profile pages to use API module
这个提交包含在:
+207
-336
@@ -1,364 +1,235 @@
|
||||
const { one, query, limitClause } = require('../lib/db')
|
||||
const router = require('express').Router()
|
||||
const { ok, fail } = require('../lib/response')
|
||||
const { hashPassword, hashPasswordLegacy, verifyPassword, signAdmin, requireAdmin } = require('../lib/auth')
|
||||
const { writeLog } = require('../lib/log')
|
||||
const { hashPassword, hashPasswordLegacy, verifyPassword, signAdmin } = require('../lib/auth')
|
||||
const { requireAdmin } = require('../middleware/auth')
|
||||
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')
|
||||
|
||||
function pageParams(ctx) {
|
||||
const page = Math.max(1, parseInt(ctx.query.page, 10) || 1)
|
||||
const pageSize = Math.min(Math.max(1, parseInt(ctx.query.page_size, 10) || 20), 100)
|
||||
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 }
|
||||
}
|
||||
|
||||
function register(router) {
|
||||
router.post('/api/v1/admin/login', async ctx => {
|
||||
const username = ctx.body.username || ''
|
||||
const password = ctx.body.password || ''
|
||||
const admin = await one('SELECT * FROM admin_accounts WHERE username = :username AND status = 1', { username })
|
||||
if (!admin) return fail(1001, '用户名或密码错误')
|
||||
let matched = verifyPassword(password, admin.password_hash)
|
||||
if (!matched) {
|
||||
// Try legacy SHA-256 verification for migration
|
||||
if (admin.password_salt && hashPasswordLegacy(password, admin.password_salt) === admin.password_hash) {
|
||||
// Auto-migrate to bcrypt
|
||||
const newHash = hashPassword(password)
|
||||
await query('UPDATE admin_accounts SET password_hash = :password_hash, password_salt = :password_salt WHERE admin_id = :admin_id', { password_hash: newHash, password_salt: '', admin_id: admin.admin_id })
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if (!matched) return fail(1001, '用户名或密码错误')
|
||||
const token = signAdmin(admin)
|
||||
await writeLog({ admin_id: admin.admin_id, action: 'admin_login', detail: '管理员登录: ' + username, ip: ctx.ip })
|
||||
return ok({ token, admin_id: String(admin.admin_id), username: admin.username, real_name: admin.real_name, role: admin.role })
|
||||
})
|
||||
// --- Auth ---
|
||||
|
||||
router.post('/api/v1/admin/password', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const oldPassword = ctx.body.old_password || ''
|
||||
const newPassword = ctx.body.new_password || ''
|
||||
if (newPassword.length < 6) return fail(2001, 'password too short')
|
||||
const current = await one('SELECT * FROM admin_accounts WHERE admin_id = :admin_id AND status = 1', { admin_id: admin.admin_id })
|
||||
if (!current) return fail(1002, '未授权,请重新登录')
|
||||
let matched = verifyPassword(oldPassword, current.password_hash)
|
||||
if (!matched && current.password_salt && hashPasswordLegacy(oldPassword, current.password_salt) === current.password_hash) {
|
||||
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 fail(1001, '原密码错误')
|
||||
const newHash = hashPassword(newPassword)
|
||||
await query('UPDATE admin_accounts SET password_hash = :password_hash, password_salt = :password_salt WHERE admin_id = :admin_id', { password_hash: newHash, password_salt: '', admin_id: admin.admin_id })
|
||||
await writeLog({ admin_id: admin.admin_id, action: 'admin_change_password', detail: '管理员修改密码', ip: ctx.ip })
|
||||
return ok({ message: 'success' })
|
||||
})
|
||||
}
|
||||
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.get('/api/v1/admin/dashboard', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const rows = await Promise.all([
|
||||
query('SELECT COUNT(*) AS total FROM devices', {}),
|
||||
query('SELECT COUNT(*) AS total FROM users', {}),
|
||||
query('SELECT COUNT(*) AS total FROM treatment_records', {}),
|
||||
query('SELECT COUNT(*) AS total FROM subscriptions WHERE status = 1 AND expire_time > NOW()', {})
|
||||
])
|
||||
const subStats = await query('SELECT ' +
|
||||
'SUM(CASE WHEN plan = \'monthly\' AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS monthly_count, ' +
|
||||
'SUM(CASE WHEN plan = \'yearly\' AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS yearly_count, ' +
|
||||
'SUM(CASE WHEN plan = \'trial\' AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS trial_count, ' +
|
||||
'COALESCE(SUM(CASE WHEN MONTH(start_time) = MONTH(NOW()) AND YEAR(start_time) = YEAR(NOW()) THEN amount ELSE 0 END), 0) AS monthly_revenue ' +
|
||||
'FROM subscriptions', {})
|
||||
return ok({
|
||||
device_count: rows[0][0].total,
|
||||
user_count: rows[1][0].total,
|
||||
treatment_count: rows[2][0].total,
|
||||
subscription_count: rows[3][0].total,
|
||||
sub_stats: subStats[0] || {}
|
||||
})
|
||||
})
|
||||
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' }))
|
||||
}))
|
||||
|
||||
router.get('/api/v1/admin/devices', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const p = pageParams(ctx)
|
||||
const keyword = (ctx.query.keyword || '').trim()
|
||||
let where = ''
|
||||
const params = {}
|
||||
if (keyword) {
|
||||
where = ' WHERE d.device_id LIKE :kw OR d.device_name LIKE :kw'
|
||||
params.kw = '%' + keyword + '%'
|
||||
}
|
||||
const total = await query('SELECT COUNT(*) AS total FROM devices d' + where, params)
|
||||
const records = await query('SELECT d.*, b.user_id AS bound_user, b.bind_time AS activated_at FROM devices d LEFT JOIN bindings b ON b.device_id = d.device_id AND b.bind_status = 1' + where + ' ORDER BY d.created_at DESC' + limitClause(p.pageSize, p.offset), params)
|
||||
return ok({ records, total: total[0].total })
|
||||
})
|
||||
// --- Dashboard ---
|
||||
|
||||
router.post('/api/v1/admin/devices', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const deviceId = String(ctx.body.device_id || '').trim()
|
||||
if (!deviceId) return fail(2001, 'device_id required')
|
||||
await query(
|
||||
'INSERT INTO devices (device_id, product_id, device_secret, device_name, firmware_version, status) VALUES (:device_id, :product_id, :device_secret, :device_name, :firmware_version, 1) ON DUPLICATE KEY UPDATE product_id = VALUES(product_id), device_secret = VALUES(device_secret), device_name = VALUES(device_name), firmware_version = VALUES(firmware_version), status = 1',
|
||||
{
|
||||
device_id: deviceId,
|
||||
product_id: ctx.body.product_id || 'HOX_LIGHT_MASK',
|
||||
device_secret: ctx.body.device_secret || '',
|
||||
device_name: ctx.body.device_name || '光子美容仪',
|
||||
firmware_version: ctx.body.firmware_version || '1.0.0'
|
||||
}
|
||||
)
|
||||
await writeLog({ admin_id: admin.admin_id, action: 'admin_device_create', detail: '预生成产品码: ' + deviceId, ip: ctx.ip })
|
||||
return ok({ device_id: deviceId })
|
||||
})
|
||||
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
|
||||
}))
|
||||
}))
|
||||
|
||||
router.post('/api/v1/admin/devices/batch', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const deviceIds = ctx.body.device_ids
|
||||
if (!Array.isArray(deviceIds) || deviceIds.length === 0 || deviceIds.length > 500) return fail(2001, 'device_ids must be an array with 1-500 items')
|
||||
let successCount = 0
|
||||
const failedIds = []
|
||||
for (const id of deviceIds) {
|
||||
const deviceId = String(id || '').trim()
|
||||
if (!deviceId) { failedIds.push(id); continue }
|
||||
try {
|
||||
await query(
|
||||
'INSERT INTO devices (device_id, product_id, device_secret, device_name, firmware_version, status) VALUES (:device_id, :product_id, :device_secret, :device_name, :firmware_version, 1) ON DUPLICATE KEY UPDATE product_id = VALUES(product_id), device_secret = VALUES(device_secret), device_name = VALUES(device_name), firmware_version = VALUES(firmware_version), status = 1',
|
||||
{
|
||||
device_id: deviceId,
|
||||
product_id: 'HOX_LIGHT_MASK',
|
||||
device_secret: '',
|
||||
device_name: '光子美容仪',
|
||||
firmware_version: '1.0.0'
|
||||
}
|
||||
)
|
||||
successCount++
|
||||
} catch (err) {
|
||||
failedIds.push(deviceId)
|
||||
}
|
||||
}
|
||||
await writeLog({ admin_id: admin.admin_id, action: 'admin_device_batch_create', detail: '批量预生成产品码: ' + successCount + '/' + deviceIds.length, ip: ctx.ip })
|
||||
return ok({ created: successCount, failed: failedIds })
|
||||
})
|
||||
// --- Devices ---
|
||||
|
||||
router.get('/api/v1/admin/devices/:device_id', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const device = await one('SELECT d.*, b.user_id AS bound_user, b.bind_time AS activated_at FROM devices d LEFT JOIN bindings b ON b.device_id = d.device_id AND b.bind_status = 1 WHERE d.device_id = :device_id', { device_id: ctx.params.device_id })
|
||||
if (!device) return fail(1005, 'DEVICE_NOT_FOUND')
|
||||
const bindingHistory = await query('SELECT b.*, u.nickname FROM bindings b LEFT JOIN users u ON u.user_id = b.user_id WHERE b.device_id = :device_id ORDER BY b.bind_time DESC', { device_id: ctx.params.device_id })
|
||||
const recentTreatments = await query('SELECT r.*, u.nickname FROM treatment_records r LEFT JOIN users u ON u.user_id = r.user_id WHERE r.device_id = :device_id ORDER BY r.created_at DESC LIMIT 5', { device_id: ctx.params.device_id })
|
||||
return ok(Object.assign({}, device, { binding_history: bindingHistory, recent_treatments: recentTreatments }))
|
||||
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('/api/v1/admin/devices/:device_id/unbind', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
await query('UPDATE bindings SET bind_status = 2, unbind_time = NOW() WHERE device_id = :device_id AND bind_status = 1', { device_id: ctx.params.device_id })
|
||||
await writeLog({ admin_id: admin.admin_id, action: 'admin_device_unbind', detail: '后台解绑设备: ' + ctx.params.device_id, ip: ctx.ip })
|
||||
return ok({ message: 'success' })
|
||||
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 || '光子美容仪',
|
||||
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('/api/v1/admin/devices/:device_id/command', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const opcode = parseInt(ctx.body.opcode, 10)
|
||||
if (!opcode) return fail(2001, 'opcode required')
|
||||
await query(
|
||||
'INSERT INTO device_commands (device_id, admin_id, opcode, payload_json, status) VALUES (:device_id, :admin_id, :opcode, :payload_json, 1)',
|
||||
{ device_id: ctx.params.device_id, admin_id: admin.admin_id, opcode, payload_json: JSON.stringify(ctx.body) }
|
||||
)
|
||||
await writeLog({ admin_id: admin.admin_id, action: 'admin_device_command', detail: '记录远程指令: ' + ctx.params.device_id, ip: ctx.ip })
|
||||
return ok({ message: 'queued', command: ctx.body })
|
||||
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) return res.json(fail(1005, 'DEVICE_NOT_FOUND'))
|
||||
res.json(ok(result))
|
||||
}))
|
||||
|
||||
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('/api/v1/admin/devices/:device_id/commands', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const p = pageParams(ctx)
|
||||
const total = await query('SELECT COUNT(*) AS total FROM device_commands WHERE device_id = :device_id', { device_id: ctx.params.device_id })
|
||||
const records = await query(
|
||||
'SELECT command_id, device_id, admin_id, opcode, payload_json, status, created_at, pulled_at, finished_at, result_json FROM device_commands WHERE device_id = :device_id ORDER BY created_at DESC' + limitClause(p.pageSize, p.offset),
|
||||
{ device_id: ctx.params.device_id }
|
||||
)
|
||||
return ok({ records, total: total[0].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))
|
||||
}))
|
||||
|
||||
// --- 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.get('/api/v1/admin/users', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const p = pageParams(ctx)
|
||||
const keyword = (ctx.query.keyword || '').trim()
|
||||
let where = ''
|
||||
const params = {}
|
||||
if (keyword) {
|
||||
where = ' WHERE u.nickname LIKE :kw OR u.phone LIKE :kw OR u.user_id = :keyword'
|
||||
params.kw = '%' + keyword + '%'
|
||||
params.keyword = keyword
|
||||
}
|
||||
const total = await query('SELECT COUNT(*) AS total FROM users u' + where, params)
|
||||
const records = await query(
|
||||
'SELECT u.*,' +
|
||||
' (SELECT COUNT(*) FROM bindings WHERE user_id = u.user_id AND bind_status = 1) AS device_count,' +
|
||||
' (SELECT COUNT(*) FROM treatment_records WHERE user_id = u.user_id) AS treatment_count,' +
|
||||
' COALESCE((SELECT status FROM subscriptions WHERE user_id = u.user_id AND status = 1 AND expire_time > NOW() ORDER BY expire_time DESC LIMIT 1), 0) AS subscription_status' +
|
||||
' FROM users u' + where + ' ORDER BY u.created_at DESC' + limitClause(p.pageSize, p.offset),
|
||||
params
|
||||
)
|
||||
return ok({ records, total: total[0].total })
|
||||
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,
|
||||
dateFrom: req.query.date_from,
|
||||
dateTo: req.query.date_to,
|
||||
pageSize,
|
||||
offset
|
||||
})
|
||||
res.json(ok({ records, total }))
|
||||
}))
|
||||
|
||||
router.get('/api/v1/admin/users/:user_id', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const user = await one('SELECT * FROM users WHERE user_id = :user_id', { user_id: ctx.params.user_id })
|
||||
if (!user) return fail(1004, 'USER_NOT_FOUND')
|
||||
const devices = await query('SELECT d.device_id, d.device_name FROM bindings b JOIN devices d ON d.device_id = b.device_id WHERE b.user_id = :user_id AND b.bind_status = 1', { user_id: user.user_id })
|
||||
const treatments = await query('SELECT * FROM treatment_records WHERE user_id = :user_id ORDER BY created_at DESC LIMIT 5', { user_id: user.user_id })
|
||||
const subscription = await one('SELECT plan, status, start_time, expire_time FROM subscriptions WHERE user_id = :user_id AND status = 1 AND expire_time > NOW() ORDER BY expire_time DESC LIMIT 1', { user_id: user.user_id })
|
||||
const stats = await one('SELECT COUNT(*) AS treatment_count, COALESCE(SUM(total_duration_ms), 0) AS total_duration FROM treatment_records WHERE user_id = :user_id', { user_id: user.user_id })
|
||||
return ok(Object.assign({}, user, {
|
||||
devices,
|
||||
recent_treatments: treatments,
|
||||
subscription_status: subscription ? subscription.status : 0,
|
||||
subscription_type: subscription ? subscription.plan : null,
|
||||
subscription_expire: subscription ? subscription.expire_time : null,
|
||||
treatment_count: stats ? stats.treatment_count : 0,
|
||||
total_duration: stats ? stats.total_duration : 0
|
||||
}))
|
||||
// --- 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 }))
|
||||
}))
|
||||
|
||||
router.get('/api/v1/admin/subscriptions', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const p = pageParams(ctx)
|
||||
const tab = (ctx.query.tab || '').trim()
|
||||
let where = ''
|
||||
const params = {}
|
||||
if (tab && tab !== 'all') {
|
||||
if (tab === 'expired') {
|
||||
where = ' WHERE s.status = 2'
|
||||
} else {
|
||||
where = ' WHERE s.plan = :plan'
|
||||
params.plan = tab
|
||||
}
|
||||
}
|
||||
const total = await query('SELECT COUNT(*) AS total FROM subscriptions s' + where, params)
|
||||
const records = await query(
|
||||
'SELECT s.*, u.nickname FROM subscriptions s LEFT JOIN users u ON u.user_id = s.user_id' + where + ' ORDER BY s.created_at DESC' + limitClause(p.pageSize, p.offset),
|
||||
params
|
||||
)
|
||||
const statsRow = await query('SELECT ' +
|
||||
'SUM(CASE WHEN plan = \'monthly\' AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS monthly_count, ' +
|
||||
'SUM(CASE WHEN plan = \'yearly\' AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS yearly_count, ' +
|
||||
'SUM(CASE WHEN plan = \'trial\' AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS trial_count, ' +
|
||||
'COALESCE(SUM(CASE WHEN MONTH(start_time) = MONTH(NOW()) AND YEAR(start_time) = YEAR(NOW()) THEN amount ELSE 0 END), 0) AS monthly_revenue ' +
|
||||
'FROM subscriptions', {})
|
||||
return ok({ records, total: total[0].total, stats: statsRow[0] || {} })
|
||||
})
|
||||
// --- Settings ---
|
||||
|
||||
router.post('/api/v1/admin/subscriptions', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const targetUser = await one('SELECT user_id FROM users WHERE user_id = :user_id', { user_id: ctx.body.user_id })
|
||||
if (!targetUser) return fail(1004, 'user_not_found')
|
||||
await query('UPDATE subscriptions SET status = 2 WHERE user_id = :user_id AND status = 1', { user_id: ctx.body.user_id })
|
||||
await query('INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (:user_id, :plan, 1, :amount, :order_id, NOW(), DATE_ADD(NOW(), INTERVAL :days DAY))', {
|
||||
user_id: ctx.body.user_id,
|
||||
plan: ctx.body.plan || 'monthly',
|
||||
amount: ctx.body.amount || 0,
|
||||
order_id: ctx.body.order_id || 'ADMIN' + Date.now(),
|
||||
days: ctx.body.days || 30
|
||||
})
|
||||
return ok({ message: 'success' })
|
||||
})
|
||||
router.get('/settings', requireAdmin, wrap(async (req, res) => {
|
||||
const settings = await settingsDao.getAll()
|
||||
res.json(ok(settings))
|
||||
}))
|
||||
|
||||
router.post('/api/v1/admin/subscriptions/cancel', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const subscriptionId = ctx.body.subscription_id
|
||||
if (!subscriptionId) return fail(2001, 'subscription_id required')
|
||||
const result = await query('UPDATE subscriptions SET status = 3 WHERE subscription_id = :subscription_id AND status = 1', { subscription_id: subscriptionId })
|
||||
if (result.affectedRows === 0) return fail(2001, '未找到有效订阅')
|
||||
await writeLog({ admin_id: admin.admin_id, action: 'subscription_cancel', detail: '取消订阅 #' + subscriptionId, ip: ctx.ip })
|
||||
return ok({ message: 'success' })
|
||||
})
|
||||
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])
|
||||
}
|
||||
res.json(ok({ message: 'success' }))
|
||||
}))
|
||||
|
||||
router.get('/api/v1/admin/records', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const p = pageParams(ctx)
|
||||
const keyword = (ctx.query.keyword || '').trim()
|
||||
const dateFrom = (ctx.query.date_from || '').trim()
|
||||
const dateTo = (ctx.query.date_to || '').trim()
|
||||
const conditions = []
|
||||
const params = {}
|
||||
if (keyword) {
|
||||
conditions.push('u.nickname LIKE :kw')
|
||||
params.kw = '%' + keyword + '%'
|
||||
}
|
||||
if (dateFrom) {
|
||||
conditions.push('r.created_at >= :date_from')
|
||||
params.date_from = dateFrom
|
||||
}
|
||||
if (dateTo) {
|
||||
conditions.push('r.created_at <= :date_to')
|
||||
params.date_to = dateTo
|
||||
}
|
||||
const where = conditions.length ? ' WHERE ' + conditions.join(' AND ') : ''
|
||||
const total = await query('SELECT COUNT(*) AS total FROM treatment_records r LEFT JOIN users u ON u.user_id = r.user_id' + where, params)
|
||||
const records = await query(
|
||||
'SELECT r.*, u.nickname FROM treatment_records r LEFT JOIN users u ON u.user_id = r.user_id' + where + ' ORDER BY r.created_at DESC' + limitClause(p.pageSize, p.offset),
|
||||
params
|
||||
)
|
||||
return ok({ records, total: total[0].total })
|
||||
})
|
||||
|
||||
router.get('/api/v1/admin/logs', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const p = pageParams(ctx)
|
||||
const type = (ctx.query.type || '').trim()
|
||||
const deviceId = (ctx.query.device_id || '').trim()
|
||||
const conditions = []
|
||||
const params = {}
|
||||
if (type) {
|
||||
conditions.push('action LIKE :type')
|
||||
params.type = '%' + type + '%'
|
||||
}
|
||||
if (deviceId) {
|
||||
conditions.push('detail LIKE :device_id')
|
||||
params.device_id = '%' + deviceId + '%'
|
||||
}
|
||||
const where = conditions.length ? ' WHERE ' + conditions.join(' AND ') : ''
|
||||
const total = await query('SELECT COUNT(*) AS total FROM operation_logs' + where, params)
|
||||
const records = await query('SELECT * FROM operation_logs' + where + ' ORDER BY created_at DESC' + limitClause(p.pageSize, p.offset), params)
|
||||
return ok({ records, total: total[0].total })
|
||||
})
|
||||
|
||||
router.get('/api/v1/admin/settings', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
const rows = await query('SELECT setting_key, setting_value FROM system_settings', {})
|
||||
const settings = {}
|
||||
rows.forEach(row => {
|
||||
if (typeof row.setting_value === 'string') {
|
||||
try { settings[row.setting_key] = JSON.parse(row.setting_value) } catch (_) { settings[row.setting_key] = row.setting_value }
|
||||
} else {
|
||||
settings[row.setting_key] = row.setting_value
|
||||
}
|
||||
})
|
||||
return ok(settings)
|
||||
})
|
||||
|
||||
router.post('/api/v1/admin/settings', async ctx => {
|
||||
const admin = await requireAdmin(ctx)
|
||||
if (!admin) return fail(1002, '未授权,请重新登录')
|
||||
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(ctx.body || {})) {
|
||||
if (!ALLOWED_KEYS.includes(key)) continue
|
||||
await query('REPLACE INTO system_settings (setting_key, setting_value) VALUES (:setting_key, :setting_value)', { setting_key: key, setting_value: JSON.stringify(ctx.body[key]) })
|
||||
}
|
||||
return ok({ message: 'success' })
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = register
|
||||
module.exports = router
|
||||
|
||||
在新工单中引用
屏蔽一个用户