refactor: migrate to Tencent Cloud backend
这个提交包含在:
@@ -0,0 +1,150 @@
|
||||
const { one, query, transaction } = require('../lib/db')
|
||||
const { ok, fail } = require('../lib/response')
|
||||
const { requireUser, randomHex } = require('../lib/auth')
|
||||
const { writeLog } = require('../lib/log')
|
||||
|
||||
function formatDate(date) {
|
||||
return date.toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
|
||||
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
|
||||
const expire = formatDate(new Date(Date.now() + 7 * 24 * 3600 * 1000))
|
||||
await conn.execute('INSERT INTO subscriptions (user_id, plan, status, amount, start_time, expire_time) VALUES (?, ?, 1, 0, NOW(), ?)', [userId, 'trial', expire])
|
||||
}
|
||||
|
||||
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 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 }
|
||||
|
||||
const bindToken = randomHex(8)
|
||||
const bindExpires = formatDate(new Date(Date.now() + 10 * 60 * 1000))
|
||||
await conn.execute(
|
||||
'INSERT INTO bindings (user_id, device_id, bind_token, bind_expires, bind_status, bind_time) VALUES (?, ?, ?, ?, 3, NOW())',
|
||||
[user.user_id, deviceId, bindToken, bindExpires]
|
||||
)
|
||||
return { device_id: deviceId, bind_token: bindToken, bind_expires: bindExpires }
|
||||
})
|
||||
|
||||
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 })
|
||||
return ok(Object.assign(result, { subscription: { plan: 'trial', remaining_days: 7 } }))
|
||||
})
|
||||
|
||||
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')
|
||||
|
||||
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 })
|
||||
return ok({ message: 'success', subscription: { plan: 'trial', remaining_days: 7 } })
|
||||
})
|
||||
|
||||
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')
|
||||
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')
|
||||
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
|
||||
在新工单中引用
屏蔽一个用户