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
这个提交包含在:
Guoguo
2026-04-29 05:58:20 -07:00
父节点 9f0e629c82
当前提交 bb4b80f867
修改 46 个文件,包含 4306 行新增2020 行删除
+93
查看文件
@@ -0,0 +1,93 @@
const { query, one } = require('../lib/db')
/**
* Find admin account by username (active only)
* @param {string} username
* @returns {Promise<Object|null>} admin row or null
*/
async function findByUsername(username) {
return one(
'SELECT * FROM admin_accounts WHERE username = :username AND status = 1',
{ username }
)
}
/**
* Find admin account by ID (active only)
* @param {number} adminId
* @returns {Promise<Object|null>} admin row or null
*/
async function findById(adminId) {
return one(
'SELECT * FROM admin_accounts WHERE admin_id = :admin_id AND status = 1',
{ admin_id: adminId }
)
}
/**
* Update admin password hash and clear legacy salt
* @param {number} adminId
* @param {string} passwordHash - bcrypt hash
* @returns {Promise<Array>} query result
*/
async function updatePassword(adminId, passwordHash) {
return query(
'UPDATE admin_accounts SET password_hash = :password_hash, password_salt = :password_salt WHERE admin_id = :admin_id',
{ password_hash: passwordHash, password_salt: '', admin_id: adminId }
)
}
/**
* Auto-migrate password from legacy SHA-256 to bcrypt
* @param {number} adminId
* @param {string} newHash - bcrypt hash
* @returns {Promise<Array>} query result
*/
async function migratePassword(adminId, newHash) {
return updatePassword(adminId, newHash)
}
/**
* Get dashboard aggregate counts (devices, users, treatments, active subscriptions)
* @returns {Promise<{device_count: number, user_count: number, treatment_count: number, subscription_count: number}>}
*/
async function getDashboardCounts() {
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()', {})
])
return {
device_count: rows[0][0].total,
user_count: rows[1][0].total,
treatment_count: rows[2][0].total,
subscription_count: rows[3][0].total
}
}
/**
* Get subscription breakdown stats (monthly/yearly/trial counts, monthly revenue)
* @returns {Promise<Object>} { monthly_count, yearly_count, trial_count, monthly_revenue }
*/
async function getSubscriptionStats() {
const rows = 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 rows[0] || {}
}
module.exports = {
findByUsername,
findById,
updatePassword,
migratePassword,
getDashboardCounts,
getSubscriptionStats
}
+203
查看文件
@@ -0,0 +1,203 @@
const { query, one, transaction } = require('../lib/db')
/**
* Find active binding for a user (bind_status=1)
* Uses transaction connection when provided
* @param {number} userId
* @param {Object} [conn] - optional transaction connection
* @returns {Promise<Object|null>}
*/
async function findActiveByUser(userId, conn) {
if (conn) {
const [rows] = await conn.execute(
'SELECT device_id FROM bindings WHERE user_id = ? AND bind_status = 1 LIMIT 1',
[userId]
)
return rows[0] || null
}
return one(
'SELECT device_id FROM bindings WHERE user_id = :user_id AND bind_status = 1 LIMIT 1',
{ user_id: userId }
)
}
/**
* Check if a device exists and is not disabled (status <> 4)
* Uses transaction connection when provided
* @param {string} deviceId
* @param {Object} [conn] - optional transaction connection
* @returns {Promise<Object|null>}
*/
async function findDeviceExists(deviceId, conn) {
if (conn) {
const [rows] = await conn.execute(
'SELECT * FROM devices WHERE device_id = ? AND status <> 4 LIMIT 1',
[deviceId]
)
return rows[0] || null
}
return one(
'SELECT * FROM devices WHERE device_id = :device_id AND status <> 4 LIMIT 1',
{ device_id: deviceId }
)
}
/**
* Create a pending binding request with 10-minute expiry
* @param {number} userId
* @param {string} deviceId
* @param {string} bindToken
* @param {Object} [conn] - optional transaction connection
* @returns {Promise<void>}
*/
async function createPending(userId, deviceId, bindToken, conn) {
if (conn) {
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())',
[userId, deviceId, bindToken]
)
return
}
await query(
'INSERT INTO bindings (user_id, device_id, bind_token, bind_expires, bind_status, bind_time) VALUES (:user_id, :device_id, :bind_token, DATE_ADD(NOW(), INTERVAL 10 MINUTE), 3, NOW())',
{ user_id: userId, device_id: deviceId, bind_token: bindToken }
)
}
/**
* Confirm a pending binding by token, set bind_status=1.
* Also ensures trial subscription if none active.
* @param {number} userId
* @param {string} deviceId
* @param {string} bindToken
* @returns {Promise<boolean>} true if confirmed, false if token invalid/expired
*/
async function confirmBind(userId, deviceId, bindToken) {
return 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',
[userId, 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]
)
// Ensure trial subscription
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) {
await conn.execute(
"INSERT INTO subscriptions (user_id, plan, status, amount, start_time, expire_time) VALUES (?, 'trial', 1, 0, NOW(), DATE_ADD(NOW(), INTERVAL 7 DAY))",
[userId]
)
}
return true
})
}
/**
* Mock bind (dev/test only): directly bind with status=1, auto-trial
* @param {number} userId
* @param {string} deviceId
* @returns {Promise<{success?: boolean, duplicated?: boolean, device_id?: string, invalid?: boolean}>}
*/
async function mockBind(userId, deviceId) {
return transaction(async conn => {
const [active] = await conn.execute(
'SELECT device_id FROM bindings WHERE user_id = ? AND bind_status = 1 LIMIT 1',
[userId]
)
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',
[userId]
)
await conn.execute(
"INSERT INTO bindings (user_id, device_id, bind_token, bind_expires, bind_status, bind_time) VALUES (?, ?, 'mock', NOW(), 1, NOW())",
[userId, deviceId]
)
// Ensure trial
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) {
await conn.execute(
"INSERT INTO subscriptions (user_id, plan, status, amount, start_time, expire_time) VALUES (?, 'trial', 1, 0, NOW(), DATE_ADD(NOW(), INTERVAL 7 DAY))",
[userId]
)
}
return { success: true }
})
}
/**
* Unbind device(s) for a user
* @param {number} userId
* @param {string|null} deviceId - specific device or null for all active bindings
* @returns {Promise<Array>} query result
*/
async function unbindByUser(userId, deviceId) {
return 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: userId, device_id: deviceId }
)
}
/**
* Get binding history for a device with user nicknames
* @param {string} deviceId
* @returns {Promise<Array>}
*/
async function getHistoryByDevice(deviceId) {
return 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: deviceId }
)
}
/**
* Check if a user has an active binding to a specific device
* @param {number} userId
* @param {string} deviceId
* @returns {Promise<Object|null>}
*/
async function findUserDeviceBinding(userId, deviceId) {
return one(
'SELECT binding_id FROM bindings WHERE user_id = :user_id AND device_id = :device_id AND bind_status = 1',
{ user_id: userId, device_id: deviceId }
)
}
/**
* Count active bindings for a user
* @param {number} userId
* @returns {Promise<number>}
*/
async function countActiveByUser(userId) {
const rows = await query(
'SELECT COUNT(*) AS total FROM bindings WHERE user_id = :user_id AND bind_status = 1',
{ user_id: userId }
)
return rows[0].total
}
module.exports = {
findActiveByUser,
findDeviceExists,
createPending,
confirmBind,
mockBind,
unbindByUser,
getHistoryByDevice,
findUserDeviceBinding,
countActiveByUser
}
+120
查看文件
@@ -0,0 +1,120 @@
const { query, one, limitClause } = require('../lib/db')
/**
* Create a device command
* @param {string} deviceId
* @param {number} adminId
* @param {number} opcode
* @param {Object} payload - will be JSON-stringified
* @returns {Promise<Array>} query result
*/
async function create(deviceId, adminId, opcode, payload) {
return query(
'INSERT INTO device_commands (device_id, admin_id, opcode, payload_json, status) VALUES (:device_id, :admin_id, :opcode, :payload_json, 1)',
{
device_id: deviceId,
admin_id: adminId,
opcode,
payload_json: JSON.stringify(payload)
}
)
}
/**
* List commands for a device with pagination
* @param {string} deviceId
* @param {Object} opts
* @param {number} opts.pageSize
* @param {number} opts.offset
* @returns {Promise<{records: Array, total: number}>}
*/
async function listByDevice(deviceId, { pageSize, offset }) {
const total = await query(
'SELECT COUNT(*) AS total FROM device_commands WHERE device_id = :device_id',
{ device_id: deviceId }
)
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(pageSize, offset),
{ device_id: deviceId }
)
return { records, total: total[0].total }
}
/**
* Count commands for a device
* @param {string} deviceId
* @returns {Promise<number>}
*/
async function countByDevice(deviceId) {
const rows = await query(
'SELECT COUNT(*) AS total FROM device_commands WHERE device_id = :device_id',
{ device_id: deviceId }
)
return rows[0].total
}
/**
* Get pending commands for a device (status=1), ordered by creation time
* @param {string} deviceId
* @returns {Promise<Array>} commands with command_id, opcode, payload_json
*/
async function getPending(deviceId) {
return 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 }
)
}
/**
* Mark commands as pulled (status=2)
* @param {number[]} commandIds - array of command IDs
* @returns {Promise<Array>} query result
*/
async function markPulled(commandIds) {
if (!commandIds || commandIds.length === 0) return []
return query(
'UPDATE device_commands SET status = 2, pulled_at = NOW() WHERE command_id IN (' + commandIds.map(Number).join(',') + ')',
{}
)
}
/**
* Finish a command execution (set status=3 success or status=4 failure)
* @param {number} commandId
* @param {boolean} success
* @param {Object} result - result payload to store as JSON
* @returns {Promise<Array>} query result
*/
async function finish(commandId, success, result) {
return 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(result)
}
)
}
/**
* Find a command by ID that belongs to a device bound to a specific user
* @param {number} commandId
* @param {number} userId
* @returns {Promise<Object|null>}
*/
async function findByIdForUser(commandId, userId) {
return 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: userId, command_id: commandId }
)
}
module.exports = {
create,
listByDevice,
countByDevice,
getPending,
markPulled,
finish,
findByIdForUser
}
+30
查看文件
@@ -0,0 +1,30 @@
const { query } = require('../lib/db')
/**
* Insert a device event record
* @param {Object} event
* @param {string} event.device_id
* @param {number} event.user_id
* @param {string} [event.event_type] - defaults to 'device_error'
* @param {string|null} [event.error_code]
* @param {number|null} [event.temperature]
* @param {Object} event.payload - raw payload, will be JSON-stringified
* @returns {Promise<Array>} query result
*/
async function create(event) {
return 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: event.device_id,
user_id: event.user_id,
event_type: event.event_type || 'device_error',
error_code: event.error_code || null,
temperature: event.temperature || null,
payload_json: JSON.stringify(event.payload || {})
}
)
}
module.exports = {
create
}
+172
查看文件
@@ -0,0 +1,172 @@
const { query, one, limitClause } = require('../lib/db')
/**
* List devices with pagination and optional keyword search
* @param {Object} opts
* @param {string} [opts.keyword] - search device_id or device_name
* @param {number} opts.pageSize
* @param {number} opts.offset
* @returns {Promise<{records: Array, total: number}>}
*/
async function list({ keyword, pageSize, offset }) {
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(pageSize, offset),
params
)
return { records, total: total[0].total }
}
/**
* Count devices matching optional keyword
* @param {Object} opts
* @param {string} [opts.keyword]
* @returns {Promise<number>}
*/
async function count({ keyword }) {
let where = ''
const params = {}
if (keyword) {
where = ' WHERE d.device_id LIKE :kw OR d.device_name LIKE :kw'
params.kw = '%' + keyword + '%'
}
const rows = await query('SELECT COUNT(*) AS total FROM devices d' + where, params)
return rows[0].total
}
/**
* Find a single device by ID with bound user info
* @param {string} deviceId
* @returns {Promise<Object|null>}
*/
async function findById(deviceId) {
return 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: deviceId }
)
}
/**
* Find device by ID with binding history and recent treatments
* @param {string} deviceId
* @returns {Promise<{device: Object|null, binding_history: Array, recent_treatments: Array}>}
*/
async function findByIdWithHistory(deviceId) {
const device = await findById(deviceId)
if (!device) return { device: null, binding_history: [], recent_treatments: [] }
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: deviceId }
)
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: deviceId }
)
return { device, binding_history: bindingHistory, recent_treatments: recentTreatments }
}
/**
* Create or update a device (upsert)
* @param {Object} device
* @param {string} device.device_id
* @param {string} [device.product_id]
* @param {string} [device.device_secret]
* @param {string} [device.device_name]
* @param {string} [device.firmware_version]
* @returns {Promise<Array>} query result
*/
async function create(device) {
return 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: device.device_id,
product_id: device.product_id || 'HOX_LIGHT_MASK',
device_secret: device.device_secret || '',
device_name: device.device_name || '光子美容仪',
firmware_version: device.firmware_version || '1.0.0'
}
)
}
/**
* Batch-create devices by IDs, return success/failure counts
* @param {string[]} deviceIds
* @returns {Promise<{created: number, failed: string[]}>}
*/
async function createBatch(deviceIds) {
let created = 0
const failed = []
for (const id of deviceIds) {
const deviceId = String(id || '').trim()
if (!deviceId) { failed.push(id); continue }
try {
await create({
device_id: deviceId,
product_id: 'HOX_LIGHT_MASK',
device_secret: '',
device_name: '光子美容仪',
firmware_version: '1.0.0'
})
created++
} catch (err) {
failed.push(deviceId)
}
}
return { created, failed }
}
/**
* Admin-unbind a device (set bind_status=2)
* @param {string} deviceId
* @returns {Promise<Array>} query result
*/
async function unbind(deviceId) {
return query(
'UPDATE bindings SET bind_status = 2, unbind_time = NOW() WHERE device_id = :device_id AND bind_status = 1',
{ device_id: deviceId }
)
}
/**
* List user's bound devices with device details
* @param {number} userId
* @returns {Promise<Array>}
*/
async function listByUser(userId) {
return 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: userId }
)
}
/**
* Get a single bound device for a user
* @param {number} userId
* @param {string} deviceId
* @returns {Promise<Object|null>}
*/
async function findBoundDevice(userId, deviceId) {
return 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: userId, device_id: deviceId }
)
}
module.exports = {
list,
count,
findById,
findByIdWithHistory,
create,
createBatch,
unbind,
listByUser,
findBoundDevice
}
+68
查看文件
@@ -0,0 +1,68 @@
const { query, one } = require('../lib/db')
/**
* List all firmware files ordered by creation date (newest first)
* @returns {Promise<Array>}
*/
async function list() {
return query(
'SELECT firmware_id, version, device_type, cos_key, size_bytes, sha256, status, created_at FROM firmware_files ORDER BY created_at DESC',
{}
)
}
/**
* Create a firmware record
* @param {Object} firmware
* @param {string} firmware.version
* @param {string} [firmware.device_type]
* @param {string} firmware.cos_key
* @param {number} [firmware.size_bytes]
* @param {string} [firmware.sha256]
* @param {number} [firmware.status] - 0=disabled, 1=enabled (default 1)
* @returns {Promise<Object>} query result with insertId
*/
async function create(firmware) {
return query(
'INSERT INTO firmware_files (version, device_type, cos_key, size_bytes, sha256, status) VALUES (:version, :device_type, :cos_key, :size_bytes, :sha256, :status)',
{
version: firmware.version,
device_type: firmware.device_type || '',
cos_key: firmware.cos_key,
size_bytes: Number(firmware.size_bytes || 0),
sha256: firmware.sha256 || '',
status: firmware.status === 0 ? 0 : 1
}
)
}
/**
* Update firmware status (enable/disable)
* @param {number} firmwareId
* @param {number} status - 0 or 1
* @returns {Promise<Array>} query result
*/
async function updateStatus(firmwareId, status) {
return query(
'UPDATE firmware_files SET status = :status WHERE firmware_id = :firmware_id',
{ status, firmware_id: firmwareId }
)
}
/**
* Find the latest enabled firmware
* @returns {Promise<Object|null>}
*/
async function findLatest() {
return one(
'SELECT * FROM firmware_files WHERE status = 1 ORDER BY created_at DESC LIMIT 1',
{}
)
}
module.exports = {
list,
create,
updateStatus,
findLatest
}
+13
查看文件
@@ -0,0 +1,13 @@
module.exports = {
adminDao: require('./admin.dao'),
deviceDao: require('./device.dao'),
bindingDao: require('./binding.dao'),
subscriptionDao: require('./subscription.dao'),
treatmentDao: require('./treatment.dao'),
userDao: require('./user.dao'),
logDao: require('./log.dao'),
commandDao: require('./command.dao'),
settingsDao: require('./settings.dao'),
firmwareDao: require('./firmware.dao'),
deviceEventDao: require('./device-event.dao')
}
+82
查看文件
@@ -0,0 +1,82 @@
const { query, limitClause } = require('../lib/db')
/**
* Write an operation log entry
* @param {Object} options
* @param {number|null} [options.user_id]
* @param {number|null} [options.admin_id]
* @param {string} options.action
* @param {string} [options.detail]
* @param {string} [options.ip]
* @returns {Promise<Array>} query result
*/
async function write(options) {
return query(
'INSERT INTO operation_logs (user_id, admin_id, action, detail, ip) VALUES (:user_id, :admin_id, :action, :detail, :ip)',
{
user_id: options.user_id || null,
admin_id: options.admin_id || null,
action: options.action,
detail: options.detail || '',
ip: options.ip || ''
}
)
}
/**
* Admin paginated list of operation logs with optional filters
* @param {Object} opts
* @param {string} [opts.type] - filter by action (LIKE match)
* @param {string} [opts.deviceId] - filter by detail containing device ID
* @param {number} opts.pageSize
* @param {number} opts.offset
* @returns {Promise<{records: Array, total: number}>}
*/
async function list({ type, deviceId, pageSize, offset }) {
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(pageSize, offset),
params
)
return { records, total: total[0].total }
}
/**
* Count operation logs with optional filters
* @param {Object} opts
* @param {string} [opts.type]
* @param {string} [opts.deviceId]
* @returns {Promise<number>}
*/
async function count({ type, deviceId }) {
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 rows = await query('SELECT COUNT(*) AS total FROM operation_logs' + where, params)
return rows[0].total
}
module.exports = {
write,
list,
count
}
+40
查看文件
@@ -0,0 +1,40 @@
const { query } = require('../lib/db')
/**
* Get all system settings, parsing JSON values where possible
* @returns {Promise<Object>} key-value map of settings
*/
async function getAll() {
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 settings
}
/**
* Upsert a system setting (REPLACE INTO)
* @param {string} key - setting_key
* @param {*} value - will be JSON-stringified
* @returns {Promise<Array>} query result
*/
async function update(key, value) {
return query(
'REPLACE INTO system_settings (setting_key, setting_value) VALUES (:setting_key, :setting_value)',
{ setting_key: key, setting_value: JSON.stringify(value) }
)
}
module.exports = {
getAll,
update
}
+197
查看文件
@@ -0,0 +1,197 @@
const { query, one, transaction, limitClause } = require('../lib/db')
/**
* Find active subscription for a user with remaining days
* @param {number} userId
* @returns {Promise<Object|null>} subscription row with remaining_days, or null
*/
async function findActive(userId) {
return one(
'SELECT *, GREATEST(DATEDIFF(expire_time, NOW()), 0) AS remaining_days FROM subscriptions WHERE user_id = :user_id AND status = 1 ORDER BY expire_time DESC LIMIT 1',
{ user_id: userId }
)
}
/**
* Find active subscription summary (plan + remaining_days) for a user
* @param {number} userId
* @returns {Promise<Object|null>}
*/
async function findActiveSummary(userId) {
return 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: userId }
)
}
/**
* Check if a user has ever had a trial subscription
* @param {number} userId
* @returns {Promise<Object|null>} subscription row or null
*/
async function findTrial(userId) {
return one(
"SELECT subscription_id FROM subscriptions WHERE user_id = :user_id AND plan = 'trial' LIMIT 1",
{ user_id: userId }
)
}
/**
* Check if a user has any active subscription
* @param {number} userId
* @returns {Promise<Object|null>}
*/
async function findAnyActive(userId) {
return one(
'SELECT subscription_id FROM subscriptions WHERE user_id = :user_id AND status = 1 LIMIT 1',
{ user_id: userId }
)
}
/**
* Create a trial subscription (7 days, amount=0)
* @param {number} userId
* @param {string} [orderId] - optional order ID
* @returns {Promise<Array>} query result
*/
async function createTrial(userId, orderId) {
return query(
"INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (:user_id, 'trial', 1, 0, :order_id, NOW(), DATE_ADD(NOW(), INTERVAL 7 DAY))",
{ user_id: userId, order_id: orderId || 'TRIAL' + Date.now() }
)
}
/**
* Purchase / activate a subscription: expire old active subs, insert new one
* @param {number} userId
* @param {string} plan - 'monthly' | 'yearly' | 'trial'
* @param {number} amount
* @param {string} orderId
* @param {number} days
* @returns {Promise<void>}
*/
async function purchase(userId, plan, amount, orderId, days) {
return transaction(async conn => {
await conn.execute(
'UPDATE subscriptions SET status = 2 WHERE user_id = ? AND status = 1',
[userId]
)
await conn.execute(
'INSERT INTO subscriptions (user_id, plan, status, amount, order_id, start_time, expire_time) VALUES (?, ?, 1, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY))',
[userId, plan, amount, orderId, days]
)
})
}
/**
* Admin-create a subscription (expire old, insert new) without transaction
* Used by admin subscription creation endpoint
* @param {number} userId
* @param {string} plan
* @param {number} amount
* @param {string} orderId
* @param {number} days
* @returns {Promise<void>}
*/
async function adminCreate(userId, plan, amount, orderId, days) {
await query(
'UPDATE subscriptions SET status = 2 WHERE user_id = :user_id AND status = 1',
{ user_id: userId }
)
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: userId, plan, amount, order_id: orderId, days }
)
}
/**
* Cancel a subscription (set status=3)
* @param {number} subscriptionId
* @returns {Promise<Array>} query result (check affectedRows)
*/
async function cancel(subscriptionId) {
return query(
'UPDATE subscriptions SET status = 3 WHERE subscription_id = :subscription_id AND status = 1',
{ subscription_id: subscriptionId }
)
}
/**
* Admin paginated subscription list with user nickname and optional tab filter
* @param {Object} opts
* @param {string} [opts.tab] - 'all' | 'monthly' | 'yearly' | 'trial' | 'expired'
* @param {number} opts.pageSize
* @param {number} opts.offset
* @returns {Promise<{records: Array, total: number}>}
*/
async function list({ tab, pageSize, offset }) {
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(pageSize, offset),
params
)
return { records, total: total[0].total }
}
/**
* Count subscriptions with optional tab filter
* @param {Object} opts
* @param {string} [opts.tab]
* @returns {Promise<number>}
*/
async function count({ tab }) {
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 rows = await query('SELECT COUNT(*) AS total FROM subscriptions s' + where, params)
return rows[0].total
}
/**
* Get subscription stats: plan counts + monthly revenue
* @returns {Promise<Object>} { monthly_count, yearly_count, trial_count, monthly_revenue }
*/
async function getStats() {
const rows = 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 rows[0] || {}
}
module.exports = {
findActive,
findActiveSummary,
findTrial,
findAnyActive,
createTrial,
purchase,
adminCreate,
cancel,
list,
count,
getStats
}
+202
查看文件
@@ -0,0 +1,202 @@
const { query, one, limitClause } = require('../lib/db')
/**
* List treatment records for a user with pagination
* @param {number} userId
* @param {Object} opts
* @param {number} opts.pageSize
* @param {number} opts.offset
* @returns {Promise<{records: Array, total: number}>}
*/
async function listByUser(userId, { pageSize, offset }) {
const total = await query(
'SELECT COUNT(*) AS total FROM treatment_records WHERE user_id = :user_id',
{ user_id: userId }
)
const records = await query(
'SELECT * FROM treatment_records WHERE user_id = :user_id ORDER BY created_at DESC' + limitClause(pageSize, offset),
{ user_id: userId }
)
return { records, total: total[0].total }
}
/**
* Count treatment records for a user
* @param {number} userId
* @returns {Promise<number>}
*/
async function countByUser(userId) {
const rows = await query(
'SELECT COUNT(*) AS total FROM treatment_records WHERE user_id = :user_id',
{ user_id: userId }
)
return rows[0].total
}
/**
* Find a treatment record by session ID (optionally scoped to user)
* @param {string} sessionId
* @param {number} [userId] - if provided, restrict to this user
* @returns {Promise<Object|null>}
*/
async function findBySession(sessionId, userId) {
if (userId !== undefined) {
return one(
'SELECT * FROM treatment_records WHERE session_id = :session_id AND user_id = :user_id',
{ session_id: sessionId, user_id: userId }
)
}
return one(
'SELECT * FROM treatment_records WHERE session_id = :session_id',
{ session_id: sessionId }
)
}
/**
* Create or update a treatment record (upsert by session_id)
* @param {Object} record
* @param {string} record.session_id
* @param {string} record.device_id
* @param {number} record.user_id
* @param {string|null} record.start_time - MySQL datetime string
* @param {string|null} record.end_time
* @param {string} record.regions - comma-separated
* @param {number} record.total_duration_ms
* @param {number} record.mode
* @param {number} record.avg_pd
* @param {number|null} record.battery
* @param {number|null} record.temperature
* @param {number|null} record.wavelength
* @param {number|null} record.brightness
* @param {string} record.pd_json - JSON string
* @returns {Promise<Array>} query result
*/
async function create(record) {
return query(
`INSERT INTO treatment_records
(session_id, device_id, user_id, start_time, end_time, regions, total_duration_ms, mode, avg_pd, battery, temperature, wavelength, brightness, pd_json)
VALUES (:session_id, :device_id, :user_id, :start_time, :end_time, :regions, :total_duration_ms, :mode, :avg_pd, :battery, :temperature, :wavelength, :brightness, :pd_json)
ON DUPLICATE KEY UPDATE end_time = VALUES(end_time), total_duration_ms = VALUES(total_duration_ms), avg_pd = VALUES(avg_pd), battery = VALUES(battery), temperature = VALUES(temperature), pd_json = VALUES(pd_json)`,
record
)
}
/**
* Update device battery, temperature, and last_online_at
* @param {string} deviceId
* @param {number|null} battery
* @param {number|null} temperature
* @returns {Promise<Array>} query result
*/
async function updateDevice(deviceId, battery, temperature) {
return query(
'UPDATE devices SET battery = COALESCE(:battery, battery), temperature = COALESCE(:temperature, temperature), last_online_at = NOW() WHERE device_id = :device_id',
{ device_id: deviceId, battery, temperature }
)
}
/**
* Admin paginated list of treatment records with user nickname
* @param {Object} opts
* @param {string} [opts.keyword] - search by user nickname
* @param {string} [opts.dateFrom] - start date filter (inclusive)
* @param {string} [opts.dateTo] - end date filter (inclusive)
* @param {number} opts.pageSize
* @param {number} opts.offset
* @returns {Promise<{records: Array, total: number}>}
*/
async function listAdmin({ keyword, dateFrom, dateTo, pageSize, offset }) {
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(pageSize, offset),
params
)
return { records, total: total[0].total }
}
/**
* Count admin treatment records with filters
* @param {Object} opts
* @param {string} [opts.keyword]
* @param {string} [opts.dateFrom]
* @param {string} [opts.dateTo]
* @returns {Promise<number>}
*/
async function countAdmin({ keyword, dateFrom, dateTo }) {
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 rows = await query(
'SELECT COUNT(*) AS total FROM treatment_records r LEFT JOIN users u ON u.user_id = r.user_id' + where,
params
)
return rows[0].total
}
/**
* Get treatment stats for a user (count + total duration)
* @param {number} userId
* @returns {Promise<{treatment_count: number, total_duration: number}>}
*/
async function getStatsByUser(userId) {
const row = 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: userId }
)
return row || { treatment_count: 0, total_duration: 0 }
}
/**
* Get recent treatments for a user (limit 5)
* @param {number} userId
* @returns {Promise<Array>}
*/
async function recentByUser(userId) {
return query(
'SELECT * FROM treatment_records WHERE user_id = :user_id ORDER BY created_at DESC LIMIT 5',
{ user_id: userId }
)
}
module.exports = {
listByUser,
countByUser,
findBySession,
create,
updateDevice,
listAdmin,
countAdmin,
getStatsByUser,
recentByUser
}
+178
查看文件
@@ -0,0 +1,178 @@
const { query, one, limitClause } = require('../lib/db')
/**
* Find user by WeChat openid
* @param {string} openid
* @returns {Promise<Object|null>}
*/
async function findByOpenid(openid) {
return one(
'SELECT * FROM users WHERE openid = :openid',
{ openid }
)
}
/**
* Create a new user from WeChat login
* @param {string} openid
* @returns {Promise<Object>} query result with insertId
*/
async function create(openid) {
return query(
'INSERT INTO users (openid, nickname, avatar, status) VALUES (:openid, :nickname, :avatar, 1)',
{ openid, nickname: '', avatar: '' }
)
}
/**
* Find user by user_id
* @param {number} userId
* @returns {Promise<Object|null>}
*/
async function findById(userId) {
return one(
'SELECT * FROM users WHERE user_id = :user_id',
{ user_id: userId }
)
}
/**
* Find active user by user_id (status=1)
* @param {number} userId
* @returns {Promise<Object|null>}
*/
async function findActiveById(userId) {
return one(
'SELECT * FROM users WHERE user_id = :user_id AND status = 1',
{ user_id: userId }
)
}
/**
* Update user profile fields (nickname, avatar, gender)
* @param {number} userId
* @param {Object} fields
* @param {string|null} [fields.nickname]
* @param {string|null} [fields.avatar]
* @param {number|null} [fields.gender]
* @returns {Promise<Array>} query result
*/
async function updateProfile(userId, fields) {
return query(
'UPDATE users SET nickname = COALESCE(:nickname, nickname), avatar = COALESCE(:avatar, avatar), gender = COALESCE(:gender, gender) WHERE user_id = :user_id',
{
user_id: userId,
nickname: fields.nickname || null,
avatar: fields.avatar || null,
gender: fields.gender === undefined ? null : fields.gender
}
)
}
/**
* Update user phone number
* @param {number} userId
* @param {string} phone
* @returns {Promise<Array>} query result
*/
async function updatePhone(userId, phone) {
return query(
'UPDATE users SET phone = :phone WHERE user_id = :user_id',
{ user_id: userId, phone }
)
}
/**
* Admin paginated user list with device/treatment/subscription subquery stats
* @param {Object} opts
* @param {string} [opts.keyword] - search nickname, phone, or exact user_id
* @param {number} opts.pageSize
* @param {number} opts.offset
* @returns {Promise<{records: Array, total: number}>}
*/
async function listAdmin({ keyword, pageSize, offset }) {
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(pageSize, offset),
params
)
return { records, total: total[0].total }
}
/**
* Count admin users with optional keyword
* @param {Object} opts
* @param {string} [opts.keyword]
* @returns {Promise<number>}
*/
async function countAdmin({ keyword }) {
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 rows = await query('SELECT COUNT(*) AS total FROM users u' + where, params)
return rows[0].total
}
/**
* Admin detail view: user + bound devices, recent treatments, subscription, stats
* @param {number} userId
* @returns {Promise<Object|null>} enriched user object or null
*/
async function findByIdAdmin(userId) {
const user = await one('SELECT * FROM users WHERE user_id = :user_id', { user_id: userId })
if (!user) return null
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: userId }
)
const treatments = await query(
'SELECT * FROM treatment_records WHERE user_id = :user_id ORDER BY created_at DESC LIMIT 5',
{ user_id: userId }
)
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: userId }
)
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: userId }
)
return 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
})
}
module.exports = {
findByOpenid,
create,
findById,
findActiveById,
updateProfile,
updatePhone,
listAdmin,
countAdmin,
findByIdAdmin
}