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
这个提交包含在:
+124
-165
@@ -1,176 +1,135 @@
|
||||
const { one, query, transaction } = require('../lib/db')
|
||||
const router = require('express').Router()
|
||||
const { ok, fail } = require('../lib/response')
|
||||
const { requireUser, randomHex } = require('../lib/auth')
|
||||
const { writeLog } = require('../lib/log')
|
||||
const { requireUser } = require('../middleware/auth')
|
||||
const { randomHex } = require('../lib/auth')
|
||||
const bindingDao = require('../dao/binding.dao')
|
||||
const deviceDao = require('../dao/device.dao')
|
||||
const commandDao = require('../dao/command.dao')
|
||||
const subscriptionDao = require('../dao/subscription.dao')
|
||||
const deviceEventDao = require('../dao/device-event.dao')
|
||||
const logDao = require('../dao/log.dao')
|
||||
|
||||
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
|
||||
|
||||
async function ensureTrial(conn, userId) {
|
||||
const [subs] = await conn.execute('SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() LIMIT 1', [userId])
|
||||
if (subs.length > 0) return
|
||||
await conn.execute('INSERT INTO subscriptions (user_id, plan, status, amount, start_time, expire_time) VALUES (?, ?, 1, 0, NOW(), DATE_ADD(NOW(), INTERVAL 7 DAY))', [userId, 'trial'])
|
||||
}
|
||||
router.post('/device/bind', requireUser, wrap(async (req, res) => {
|
||||
const deviceId = String(req.body.device_id || '').trim()
|
||||
if (!deviceId) return res.json(fail(2001, 'device_id required'))
|
||||
|
||||
function register(router) {
|
||||
router.post('/api/v1/device/bind', async ctx => {
|
||||
const user = await requireUser(ctx)
|
||||
if (!user) return fail(1001, 'invalid_token')
|
||||
const deviceId = String(ctx.body.device_id || '').trim()
|
||||
if (!deviceId) return fail(2001, 'device_id required')
|
||||
const active = await bindingDao.findActiveByUser(req.user.user_id)
|
||||
if (active) return res.json(fail(2001, '已绑定设备', { device_id: active.device_id }))
|
||||
|
||||
const result = await transaction(async conn => {
|
||||
const [active] = await conn.execute('SELECT device_id FROM bindings WHERE user_id = ? AND bind_status = 1 LIMIT 1', [user.user_id])
|
||||
if (active.length > 0) return { duplicated: true, device_id: active[0].device_id }
|
||||
const device = await bindingDao.findDeviceExists(deviceId)
|
||||
if (!device) return res.json(fail(1005, 'DEVICE_NOT_FOUND'))
|
||||
|
||||
const [devices] = await conn.execute('SELECT * FROM devices WHERE device_id = ? AND status <> 4 LIMIT 1', [deviceId])
|
||||
if (devices.length === 0) return { invalid: true }
|
||||
const bindToken = randomHex(8)
|
||||
await bindingDao.createPending(req.user.user_id, deviceId, bindToken)
|
||||
await logDao.write({ user_id: req.user.user_id, action: 'device_bind_request', detail: '申请绑定设备: ' + deviceId, ip: req.ip })
|
||||
|
||||
const bindToken = randomHex(8)
|
||||
await conn.execute(
|
||||
'INSERT INTO bindings (user_id, device_id, bind_token, bind_expires, bind_status, bind_time) VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE), 3, NOW())',
|
||||
[user.user_id, deviceId, bindToken]
|
||||
)
|
||||
return { device_id: deviceId, bind_token: bindToken }
|
||||
})
|
||||
const sub = await subscriptionDao.findActive(req.user.user_id)
|
||||
res.json(ok({
|
||||
device_id: deviceId,
|
||||
bind_token: bindToken,
|
||||
subscription: sub ? { plan: sub.plan, remaining_days: sub.remaining_days } : { plan: 'none', remaining_days: 0 }
|
||||
}))
|
||||
}))
|
||||
|
||||
if (result.invalid) return fail(1005, 'DEVICE_NOT_FOUND')
|
||||
if (result.duplicated) return fail(2001, '已绑定设备', { device_id: result.device_id })
|
||||
await writeLog({ user_id: user.user_id, action: 'device_bind_request', detail: '申请绑定设备: ' + deviceId, ip: ctx.ip })
|
||||
const sub = await one('SELECT plan, GREATEST(DATEDIFF(expire_time, NOW()), 0) AS remaining_days 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 })
|
||||
return ok(Object.assign(result, { subscription: sub ? { plan: sub.plan, remaining_days: sub.remaining_days } : { plan: 'none', remaining_days: 0 } }))
|
||||
router.post('/device/bind/confirm', requireUser, wrap(async (req, res) => {
|
||||
const deviceId = String(req.body.device_id || '').trim()
|
||||
const bindToken = String(req.body.bind_token || '').trim()
|
||||
if (!deviceId || !bindToken) return res.json(fail(2001, 'device_id and bind_token required'))
|
||||
|
||||
const confirmed = await bindingDao.confirmBind(req.user.user_id, deviceId, bindToken)
|
||||
if (!confirmed) return res.json(fail(2001, 'bind_token invalid or expired'))
|
||||
|
||||
await logDao.write({ user_id: req.user.user_id, action: 'device_bind_confirm', detail: '确认绑定设备: ' + deviceId, ip: req.ip })
|
||||
const sub = await subscriptionDao.findActive(req.user.user_id)
|
||||
res.json(ok({
|
||||
message: 'success',
|
||||
subscription: sub ? { plan: sub.plan, remaining_days: sub.remaining_days } : { plan: 'none', remaining_days: 0 }
|
||||
}))
|
||||
}))
|
||||
|
||||
router.post('/device/mock-bind', requireUser, wrap(async (req, res) => {
|
||||
const config = require('../config')
|
||||
if (config.nodeEnv === 'production') return res.json(fail(2001, 'not available in production'))
|
||||
const deviceId = String(req.body.device_id || '').trim()
|
||||
if (!deviceId) return res.json(fail(2001, 'device_id required'))
|
||||
|
||||
const active = await bindingDao.findActiveByUser(req.user.user_id)
|
||||
if (active) return res.json(fail(2001, '已绑定设备', { device_id: active.device_id }))
|
||||
|
||||
const device = await bindingDao.findDeviceExists(deviceId)
|
||||
if (!device) return res.json(fail(1005, 'DEVICE_NOT_FOUND'))
|
||||
|
||||
await bindingDao.mockBind(req.user.user_id, deviceId)
|
||||
await logDao.write({ user_id: req.user.user_id, action: 'device_bind_confirm', detail: '模拟绑定设备: ' + deviceId, ip: req.ip })
|
||||
res.json(ok({ message: 'success', device_id: deviceId }))
|
||||
}))
|
||||
|
||||
router.post('/device/unbind', requireUser, wrap(async (req, res) => {
|
||||
const deviceId = req.body.device_id || null
|
||||
await bindingDao.unbindByUser(req.user.user_id, deviceId)
|
||||
await logDao.write({ user_id: req.user.user_id, action: 'device_unbind', detail: '解绑设备: ' + (deviceId || 'current'), ip: req.ip })
|
||||
res.json(ok({ message: 'success' }))
|
||||
}))
|
||||
|
||||
router.get('/device/list', requireUser, wrap(async (req, res) => {
|
||||
const devices = await deviceDao.listByUser(req.user.user_id)
|
||||
res.json(ok({ devices, total: devices.length }))
|
||||
}))
|
||||
|
||||
router.get('/device/command/pending', requireUser, wrap(async (req, res) => {
|
||||
const deviceId = String(req.query.device_id || '').trim()
|
||||
if (!deviceId) return res.json(fail(2001, 'device_id required'))
|
||||
|
||||
const active = await bindingDao.findActiveByUser(req.user.user_id)
|
||||
if (!active || active.device_id !== deviceId) return res.json(fail(1006, 'DEVICE_NOT_BOUND'))
|
||||
|
||||
const commands = await commandDao.getPending(deviceId)
|
||||
if (commands.length > 0) {
|
||||
await commandDao.markPulled(commands.map(c => c.command_id))
|
||||
}
|
||||
res.json(ok({
|
||||
commands: commands.map(c => ({
|
||||
seq: c.command_id,
|
||||
opcode: c.opcode,
|
||||
payload: typeof c.payload_json === 'string' ? JSON.parse(c.payload_json) : c.payload_json || {}
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
|
||||
router.post('/device/command/result', requireUser, wrap(async (req, res) => {
|
||||
const commandId = parseInt(req.body.command_id || req.body.seq, 10)
|
||||
const success = req.body.success !== false
|
||||
if (!commandId) return res.json(fail(2001, 'command_id required'))
|
||||
// commandDao.finish verifies device ownership via user binding
|
||||
await commandDao.finish(commandId, success, JSON.stringify(req.body), req.user.user_id)
|
||||
res.json(ok({ message: 'success' }))
|
||||
}))
|
||||
|
||||
router.post('/device/event', requireUser, wrap(async (req, res) => {
|
||||
const deviceId = String(req.body.device_id || '').trim()
|
||||
if (!deviceId) return res.json(fail(2001, 'device_id required'))
|
||||
|
||||
const active = await bindingDao.findActiveByUser(req.user.user_id)
|
||||
if (!active || active.device_id !== deviceId) return res.json(fail(1006, 'device_not_bound'))
|
||||
|
||||
await deviceEventDao.create({
|
||||
device_id: deviceId,
|
||||
user_id: req.user.user_id,
|
||||
event_type: req.body.event_type || 'device_error',
|
||||
error_code: req.body.error_code || null,
|
||||
temperature: req.body.temperature || null,
|
||||
payload: req.body
|
||||
})
|
||||
await logDao.write({ user_id: req.user.user_id, action: 'device_event', detail: '设备事件: ' + deviceId, ip: req.ip })
|
||||
res.json(ok({ message: 'ok' }))
|
||||
}))
|
||||
|
||||
router.post('/api/v1/device/bind/confirm', async ctx => {
|
||||
const user = await requireUser(ctx)
|
||||
if (!user) return fail(1001, 'invalid_token')
|
||||
const deviceId = String(ctx.body.device_id || '').trim()
|
||||
const bindToken = String(ctx.body.bind_token || '').trim()
|
||||
if (!deviceId || !bindToken) return fail(2001, 'device_id and bind_token required')
|
||||
router.get('/device/:device_id', requireUser, wrap(async (req, res) => {
|
||||
const device = await deviceDao.findBoundDevice(req.user.user_id, req.params.device_id)
|
||||
if (!device) return res.json(fail(1006, 'DEVICE_NOT_BOUND'))
|
||||
res.json(ok(device))
|
||||
}))
|
||||
|
||||
const updated = await transaction(async conn => {
|
||||
const [rows] = await conn.execute(
|
||||
'SELECT binding_id FROM bindings WHERE user_id = ? AND device_id = ? AND bind_token = ? AND bind_status = 3 AND bind_expires > NOW() LIMIT 1',
|
||||
[user.user_id, deviceId, bindToken]
|
||||
)
|
||||
if (rows.length === 0) return false
|
||||
await conn.execute('UPDATE bindings SET bind_status = 1, bind_time = NOW() WHERE binding_id = ?', [rows[0].binding_id])
|
||||
await ensureTrial(conn, user.user_id)
|
||||
return true
|
||||
})
|
||||
if (!updated) return fail(2001, 'bind_token invalid or expired')
|
||||
await writeLog({ user_id: user.user_id, action: 'device_bind_confirm', detail: '确认绑定设备: ' + deviceId, ip: ctx.ip })
|
||||
const sub = await one('SELECT plan, GREATEST(DATEDIFF(expire_time, NOW()), 0) AS remaining_days 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 })
|
||||
return ok({ message: 'success', subscription: sub ? { plan: sub.plan, remaining_days: sub.remaining_days } : { plan: 'none', remaining_days: 0 } })
|
||||
})
|
||||
|
||||
router.post('/api/v1/device/mock-bind', async ctx => {
|
||||
const config = require('../config')
|
||||
if (config.nodeEnv === 'production') return fail(2001, 'not available in production')
|
||||
const user = await requireUser(ctx)
|
||||
if (!user) return fail(1001, 'invalid_token')
|
||||
const deviceId = String(ctx.body.device_id || '').trim()
|
||||
if (!deviceId) return fail(2001, 'device_id required')
|
||||
|
||||
const result = await transaction(async conn => {
|
||||
const [active] = await conn.execute('SELECT device_id FROM bindings WHERE user_id = ? AND bind_status = 1 LIMIT 1', [user.user_id])
|
||||
if (active.length > 0) return { duplicated: true, device_id: active[0].device_id }
|
||||
const [devices] = await conn.execute('SELECT * FROM devices WHERE device_id = ? AND status <> 4 LIMIT 1', [deviceId])
|
||||
if (devices.length === 0) return { invalid: true }
|
||||
await conn.execute('UPDATE bindings SET bind_status = 2 WHERE user_id = ? AND bind_status = 3', [user.user_id])
|
||||
await conn.execute('INSERT INTO bindings (user_id, device_id, bind_token, bind_expires, bind_status, bind_time) VALUES (?, ?, ?, NOW(), 1, NOW())', [user.user_id, deviceId, 'mock'])
|
||||
await ensureTrial(conn, user.user_id)
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
if (result.invalid) return fail(1005, 'DEVICE_NOT_FOUND')
|
||||
if (result.duplicated) return fail(2001, '已绑定设备', { device_id: result.device_id })
|
||||
await writeLog({ user_id: user.user_id, action: 'device_bind_confirm', detail: '模拟绑定设备: ' + deviceId, ip: ctx.ip })
|
||||
return ok({ message: 'success', device_id: deviceId })
|
||||
})
|
||||
|
||||
router.post('/api/v1/device/unbind', async ctx => {
|
||||
const user = await requireUser(ctx)
|
||||
if (!user) return fail(1001, 'invalid_token')
|
||||
const deviceId = ctx.body.device_id || null
|
||||
await query(
|
||||
'UPDATE bindings SET bind_status = 2, unbind_time = NOW() WHERE user_id = :user_id AND bind_status = 1 AND (:device_id IS NULL OR device_id = :device_id)',
|
||||
{ user_id: user.user_id, device_id: deviceId }
|
||||
)
|
||||
await writeLog({ user_id: user.user_id, action: 'device_unbind', detail: '解绑设备: ' + (deviceId || 'current'), ip: ctx.ip })
|
||||
return ok({ message: 'success' })
|
||||
})
|
||||
|
||||
router.get('/api/v1/device/list', async ctx => {
|
||||
const user = await requireUser(ctx)
|
||||
if (!user) return fail(1001, 'invalid_token')
|
||||
const devices = await query(
|
||||
'SELECT d.device_id, d.device_name, d.status, d.battery, d.firmware_version, d.last_online_at, b.bind_time FROM bindings b JOIN devices d ON d.device_id = b.device_id WHERE b.user_id = :user_id AND b.bind_status = 1 ORDER BY b.bind_time DESC',
|
||||
{ user_id: user.user_id }
|
||||
)
|
||||
return ok({ devices, total: devices.length })
|
||||
})
|
||||
|
||||
router.get('/api/v1/device/command/pending', async ctx => {
|
||||
const user = await requireUser(ctx)
|
||||
if (!user) return fail(1001, 'invalid_token')
|
||||
const deviceId = String(ctx.query.device_id || '').trim()
|
||||
if (!deviceId) return fail(2001, 'device_id required')
|
||||
const bound = await one('SELECT binding_id FROM bindings WHERE user_id = :user_id AND device_id = :device_id AND bind_status = 1', { user_id: user.user_id, device_id: deviceId })
|
||||
if (!bound) return fail(1006, 'DEVICE_NOT_BOUND')
|
||||
const commands = await query('SELECT command_id, opcode, payload_json FROM device_commands WHERE device_id = :device_id AND status = 1 ORDER BY created_at ASC LIMIT 10', { device_id: deviceId })
|
||||
if (commands.length > 0) {
|
||||
await query('UPDATE device_commands SET status = 2, pulled_at = NOW() WHERE command_id IN (' + commands.map(c => Number(c.command_id)).join(',') + ')', {})
|
||||
}
|
||||
return ok({ commands: commands.map(c => ({ seq: c.command_id, opcode: c.opcode, payload: typeof c.payload_json === 'string' ? JSON.parse(c.payload_json) : c.payload_json || {} })) })
|
||||
})
|
||||
|
||||
router.post('/api/v1/device/command/result', async ctx => {
|
||||
const user = await requireUser(ctx)
|
||||
if (!user) return fail(1001, 'invalid_token')
|
||||
const commandId = parseInt(ctx.body.command_id || ctx.body.seq, 10)
|
||||
const success = ctx.body.success !== false
|
||||
if (!commandId) return fail(2001, 'command_id required')
|
||||
const cmd = await one('SELECT dc.command_id FROM device_commands dc JOIN bindings b ON b.device_id = dc.device_id AND b.user_id = :user_id AND b.bind_status = 1 WHERE dc.command_id = :command_id', { user_id: user.user_id, command_id: commandId })
|
||||
if (!cmd) return fail(1006, 'device_not_bound')
|
||||
await query('UPDATE device_commands SET status = :status, finished_at = NOW(), result_json = :result_json WHERE command_id = :command_id', {
|
||||
command_id: commandId,
|
||||
status: success ? 3 : 4,
|
||||
result_json: JSON.stringify(ctx.body)
|
||||
})
|
||||
return ok({ message: 'success' })
|
||||
})
|
||||
|
||||
router.get('/api/v1/device/:device_id', async ctx => {
|
||||
const user = await requireUser(ctx)
|
||||
if (!user) return fail(1001, 'invalid_token')
|
||||
const device = await one(
|
||||
'SELECT d.device_id, d.device_name, d.status, d.battery, d.temperature, d.firmware_version, d.last_online_at, b.bind_time FROM bindings b JOIN devices d ON d.device_id = b.device_id WHERE b.user_id = :user_id AND b.bind_status = 1 AND b.device_id = :device_id',
|
||||
{ user_id: user.user_id, device_id: ctx.params.device_id }
|
||||
)
|
||||
if (!device) return fail(1006, 'DEVICE_NOT_BOUND')
|
||||
return ok(device)
|
||||
})
|
||||
|
||||
router.post('/api/v1/device/event', async ctx => {
|
||||
const user = await requireUser(ctx)
|
||||
if (!user) return fail(1001, 'invalid_token')
|
||||
const deviceId = String(ctx.body.device_id || '').trim()
|
||||
if (!deviceId) return fail(2001, 'device_id required')
|
||||
const binding = await one('SELECT binding_id FROM bindings WHERE user_id = :user_id AND device_id = :device_id AND bind_status = 1', { user_id: user.user_id, device_id: deviceId })
|
||||
if (!binding) return fail(1006, 'device_not_bound')
|
||||
await query(
|
||||
'INSERT INTO device_events (device_id, user_id, event_type, error_code, temperature, payload_json) VALUES (:device_id, :user_id, :event_type, :error_code, :temperature, :payload_json)',
|
||||
{
|
||||
device_id: deviceId,
|
||||
user_id: user.user_id,
|
||||
event_type: ctx.body.event_type || 'device_error',
|
||||
error_code: ctx.body.error_code || null,
|
||||
temperature: ctx.body.temperature || null,
|
||||
payload_json: JSON.stringify(ctx.body)
|
||||
}
|
||||
)
|
||||
await writeLog({ user_id: user.user_id, action: 'device_event', detail: '设备事件: ' + deviceId, ip: ctx.ip })
|
||||
return ok({ message: 'ok' })
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = register
|
||||
module.exports = router
|
||||
|
||||
在新工单中引用
屏蔽一个用户