From 6201b97fcdd72c13789554fbea816a3090b770f4 Mon Sep 17 00:00:00 2001 From: Guoguo Date: Tue, 28 Apr 2026 18:46:03 -0700 Subject: [PATCH] fix: resolve critical audit issues across all modules Server: add filter/search/pagination to admin list endpoints, enrich user/device queries with JOINs and subqueries, prevent duplicate active subscriptions on creation. Admin console: fix record page TypeError on numeric record_id, correct mode comparison (integer vs string), fix device detail field names and command opcode, remove hardcoded login credentials, wire up dead buttons (unbind, view logs, export), fix user/subscription field mappings. Miniprogram: fix subscription status string/number mismatches across index/treatment-setup/profile pages, fix device name field reference, fix treatment-done null device_id by capturing at onLoad. --- admin-console/src/pages/dashboard/index.vue | 2 +- .../src/pages/device-detail/index.vue | 6 +- admin-console/src/pages/device/index.vue | 20 +++- admin-console/src/pages/log/index.vue | 2 +- admin-console/src/pages/login/index.vue | 2 +- admin-console/src/pages/record/index.vue | 8 +- .../src/pages/subscription/index.vue | 2 +- admin-console/src/pages/user-detail/index.vue | 5 +- admin-console/src/pages/user/index.vue | 13 +-- miniprogram/pages/index/index.js | 6 +- miniprogram/pages/profile/profile.js | 4 +- miniprogram/pages/treating/treating.js | 4 +- .../pages/treatment-done/treatment-done.js | 3 +- .../pages/treatment-setup/treatment-setup.js | 2 +- server/src/routes/admin.js | 96 +++++++++++++++++-- 15 files changed, 132 insertions(+), 43 deletions(-) diff --git a/admin-console/src/pages/dashboard/index.vue b/admin-console/src/pages/dashboard/index.vue index 1976679..aaafa12 100644 --- a/admin-console/src/pages/dashboard/index.vue +++ b/admin-console/src/pages/dashboard/index.vue @@ -27,7 +27,7 @@ 实时护理数据 - 查看全部 › + 查看全部 › diff --git a/admin-console/src/pages/device-detail/index.vue b/admin-console/src/pages/device-detail/index.vue index 70b3833..2a8f013 100644 --- a/admin-console/src/pages/device-detail/index.vue +++ b/admin-console/src/pages/device-detail/index.vue @@ -28,7 +28,7 @@ 固件版本 - {{ device.fw_version || '-' }} + {{ device.firmware_version || '-' }} @@ -42,7 +42,7 @@ 最后在线 - {{ device.last_online || '-' }} + {{ device.last_online_at || '-' }} @@ -94,7 +94,7 @@ - + diff --git a/admin-console/src/pages/device/index.vue b/admin-console/src/pages/device/index.vue index 261dbe7..d02c1ab 100644 --- a/admin-console/src/pages/device/index.vue +++ b/admin-console/src/pages/device/index.vue @@ -42,7 +42,7 @@ 详情 - 解绑 + 解绑 @@ -123,6 +123,24 @@ export default { } }) }, + onUnbind(item) { + const self = this + uni.showModal({ + title: '确认解绑', + content: '确定要解绑设备 ' + item.device_id + ' 吗?', + success: async function (res) { + if (res.confirm) { + try { + await post('/api/v1/admin/devices/' + item.device_id + '/unbind', {}) + uni.showToast({ title: '解绑成功', icon: 'success' }) + self.loadDevices() + } catch (e) { + uni.showToast({ title: '解绑失败', icon: 'none' }) + } + } + } + }) + }, statusBadge(status) { const map = { 2: 'badge badge-success', 3: 'badge badge-warning', 1: 'badge badge-blue', 4: 'badge badge-error' } return map[status] || 'badge badge-default' diff --git a/admin-console/src/pages/log/index.vue b/admin-console/src/pages/log/index.vue index 9c0f8c2..8fa6452 100644 --- a/admin-console/src/pages/log/index.vue +++ b/admin-console/src/pages/log/index.vue @@ -118,7 +118,7 @@ export default { exportCSV('logs_' + new Date().toISOString().slice(0, 10) + '.csv', ['时间', '操作类型', '操作详情', '操作者'], records.map(function (r) { - return [r.created_at ? r.created_at.slice(0, 19).replace('T', ' ') : '', r.action || '', r.detail || '', r.openid || ''] + return [r.created_at ? r.created_at.slice(0, 19).replace('T', ' ') : '', r.action || '', r.detail || '', r.admin_id ? '管理员#' + r.admin_id : r.user_id ? '用户#' + r.user_id : '系统'] }) ) uni.showToast({ title: '导出成功', icon: 'success' }) diff --git a/admin-console/src/pages/login/index.vue b/admin-console/src/pages/login/index.vue index 9a9c128..e618e89 100644 --- a/admin-console/src/pages/login/index.vue +++ b/admin-console/src/pages/login/index.vue @@ -41,7 +41,7 @@ import { useUserStore } from '../../store/user' export default { data() { return { - form: { username: 'admin', password: 'admin' }, + form: { username: '', password: '' }, loading: false } }, diff --git a/admin-console/src/pages/record/index.vue b/admin-console/src/pages/record/index.vue index 0dac666..3a15e12 100644 --- a/admin-console/src/pages/record/index.vue +++ b/admin-console/src/pages/record/index.vue @@ -33,12 +33,12 @@ - {{ item.record_id ? item.record_id.slice(0, 8) : '-' }} + {{ item.record_id ? String(item.record_id) : '-' }} {{ item.nickname || item.user_id || '-' }} {{ item.device_id || '-' }} - - {{ item.mode === 'smart' ? '✨ 智能模式' : '🔄 普通模式' }} + + {{ item.mode === 1 ? '✨ 智能模式' : '🔄 普通模式' }} {{ formatRegions(item.regions) }} @@ -140,7 +140,7 @@ export default { var regions = [] var bits = [1, 2, 4, 8, 16, 32, 64] for (var i = 0; i < bits.length; i++) { if (r.regions & bits[i]) regions.push(REGION_MAP[bits[i]]) } - return [r.record_id || '', r.nickname || r.user_id || '', r.device_id || '', r.mode === 'smart' ? '智能模式' : '普通模式', regions.join('+'), Math.floor((r.total_duration_ms || 0) / 60000), r.start_time ? r.start_time.slice(0, 10) : ''] + return [r.record_id || '', r.nickname || r.user_id || '', r.device_id || '', r.mode === 1 ? '智能模式' : '普通模式', regions.join('+'), Math.floor((r.total_duration_ms || 0) / 60000), r.start_time ? r.start_time.slice(0, 10) : ''] }) ) uni.showToast({ title: '导出成功', icon: 'success' }) diff --git a/admin-console/src/pages/subscription/index.vue b/admin-console/src/pages/subscription/index.vue index 01c6a4e..38a82ed 100644 --- a/admin-console/src/pages/subscription/index.vue +++ b/admin-console/src/pages/subscription/index.vue @@ -217,7 +217,7 @@ export default { const data = await get('/api/v1/admin/subscriptions', { page: 1, page_size: 9999, tab: this.activeTab }) const records = data.records || [] var planMap = { monthly: '月卡', quarterly: '季卡', yearly: '年卡', trial: '试用' } - var statusMap = { 1: '生效中', 2: '试用中', 3: '已过期' } + var statusMap = { 1: '生效中', 2: '已过期', 3: '已取消' } exportCSV('subscriptions_' + new Date().toISOString().slice(0, 10) + '.csv', ['用户', '订阅类型', '订单金额', '开始日期', '到期日期', '状态'], records.map(function (r) { diff --git a/admin-console/src/pages/user-detail/index.vue b/admin-console/src/pages/user-detail/index.vue index 9b4c249..63f4907 100644 --- a/admin-console/src/pages/user-detail/index.vue +++ b/admin-console/src/pages/user-detail/index.vue @@ -23,7 +23,7 @@ 护理次数 - {{ user.total_duration || '0h' }} + {{ user.total_duration ? Math.round(user.total_duration / 3600000) + 'h' : '0h' }} 累计时长 @@ -112,8 +112,7 @@ export default { uni.navigateTo({ url: '/pages/record/index?user_id=' + this.userId }) }, subStatusText(status) { - const map = { yearly: '年卡会员', monthly: '月卡会员', trial: '试用中', none: '未订阅' } - return map[status] || '未订阅' + return status === 1 ? '已订阅' : '未订阅' }, subTypeText(type) { const map = { yearly: '年卡', monthly: '月卡', trial: '试用' } diff --git a/admin-console/src/pages/user/index.vue b/admin-console/src/pages/user/index.vue index ca712f6..a728746 100644 --- a/admin-console/src/pages/user/index.vue +++ b/admin-console/src/pages/user/index.vue @@ -110,17 +110,10 @@ export default { uni.navigateTo({ url: '/pages/record/index?user_id=' + userId }) }, subStatusText(status) { - const map = { yearly: '年卡会员', monthly: '月卡会员', trial: '试用中', none: '未订阅' } - return map[status] || '未订阅' + return status === 1 ? '已订阅' : '未订阅' }, subBadge(status) { - const map = { - yearly: 'badge badge-success', - monthly: 'badge badge-warning', - trial: 'badge badge-blue', - none: 'badge badge-blue' - } - return map[status] || 'badge badge-default' + return status === 1 ? 'badge badge-success' : 'badge badge-default' }, formatDate: formatDateShort, async onExport() { @@ -130,7 +123,7 @@ export default { exportCSV('users_' + new Date().toISOString().slice(0, 10) + '.csv', ['用户ID', '昵称', '手机号', '绑定设备数', '护理次数', '订阅状态', '注册时间'], records.map(function (r) { - return [r._id, r.nickname || '', r.phone || '', r.device_count || 0, r.treatment_count || 0, r.subscription_status || '', r.created_at ? r.created_at.slice(0, 10) : ''] + return [r.user_id, r.nickname || '', r.phone || '', r.device_count || 0, r.treatment_count || 0, r.subscription_status === 1 ? '已订阅' : '未订阅', r.created_at ? r.created_at.slice(0, 10) : ''] }) ) uni.showToast({ title: '导出成功', icon: 'success' }) diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index 3ce5d97..31beb1c 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -52,7 +52,7 @@ Page({ if (devices.length > 0) { app.globalData.currentDevice = devices[0] self.setData({ - deviceName: devices[0].name || devices[0].device_id || '我的光面膜', + deviceName: devices[0].device_name || devices[0].device_id || '我的光面膜', deviceInfo: devices[0] }) } @@ -138,7 +138,7 @@ Page({ }, onStartTreatment: function () { - if (!this.data.subscription || this.data.subscription.status === 0) { + if (!this.data.subscription || this.data.subscription.status !== 'active') { wx.navigateTo({ url: '/pages/subscribe-prompt/subscribe-prompt' }) return } @@ -146,7 +146,7 @@ Page({ }, onViewSubscription: function () { - if (!this.data.subscription || this.data.subscription.status === 0) { + if (!this.data.subscription || this.data.subscription.status !== 'active') { wx.navigateTo({ url: '/pages/subscribe-plans/subscribe-plans' }) } } diff --git a/miniprogram/pages/profile/profile.js b/miniprogram/pages/profile/profile.js index b53c8d3..437d285 100644 --- a/miniprogram/pages/profile/profile.js +++ b/miniprogram/pages/profile/profile.js @@ -35,13 +35,13 @@ Page({ }, onViewSubscription: function () { - if (!this.data.subscription || this.data.subscription.status === 0) { + if (!this.data.subscription || this.data.subscription.status !== 'active') { wx.navigateTo({ url: '/pages/subscribe-plans/subscribe-plans' }) } else { var sub = this.data.subscription wx.showModal({ title: '订阅信息', - content: '套餐类型:' + (sub.plan_type || '未知') + '\n剩余天数:' + (sub.remaining_days || 0) + '天', + content: '套餐类型:' + (sub.plan || '未知') + '\n剩余天数:' + (sub.remaining_days || 0) + '天', showCancel: false }) } diff --git a/miniprogram/pages/treating/treating.js b/miniprogram/pages/treating/treating.js index b334f35..23e71e2 100644 --- a/miniprogram/pages/treating/treating.js +++ b/miniprogram/pages/treating/treating.js @@ -23,6 +23,7 @@ Page({ onLoad: function (options) { var app = getApp() this.setData({ statusBarHeight: app.globalData.statusBarHeight, devMode: config.__DEV__ || false }) + this._deviceId = ble.getDeviceId() || (app.globalData.currentDevice && app.globalData.currentDevice.device_id) || '' this.setData({ regions: parseInt(options.regions) || 0x7F, wavelength: parseInt(options.wavelength) || 2, @@ -115,7 +116,8 @@ Page({ '®ions=' + result.regions + '&duration=' + result.total_duration_ms + '&avg_pd=' + result.avg_pd + - '&mode=' + (self.data.mode || 0) + '&mode=' + (self.data.mode || 0) + + '&device_id=' + encodeURIComponent(self._deviceId || '') }) }, 1000) }, diff --git a/miniprogram/pages/treatment-done/treatment-done.js b/miniprogram/pages/treatment-done/treatment-done.js index 8342147..d3f8c73 100644 --- a/miniprogram/pages/treatment-done/treatment-done.js +++ b/miniprogram/pages/treatment-done/treatment-done.js @@ -22,6 +22,7 @@ Page({ onLoad: function (options) { this.setData({ statusBarHeight: app.globalData.statusBarHeight }) + this._deviceId = options.device_id || (app.globalData.currentDevice && app.globalData.currentDevice.device_id) || ble.getDeviceId() || '' var durationMs = parseInt(options.duration) || 0 var mins = Math.floor(durationMs / 60000) var secs = Math.floor((durationMs % 60000) / 1000) @@ -53,7 +54,7 @@ Page({ http.post('/api/v1/treatment/sync', { session_id: self.data.sessionId, - device_id: ble.getDeviceId(), + device_id: self._deviceId || '', start_time: new Date(Date.now() - self.data.duration).toISOString(), end_time: new Date().toISOString(), regions: self.data.regions, diff --git a/miniprogram/pages/treatment-setup/treatment-setup.js b/miniprogram/pages/treatment-setup/treatment-setup.js index 13ab5c8..e044ce8 100644 --- a/miniprogram/pages/treatment-setup/treatment-setup.js +++ b/miniprogram/pages/treatment-setup/treatment-setup.js @@ -39,7 +39,7 @@ Page({ var self = this http.get('/api/v1/subscription').then(function (sub) { self.setData({ - subExpired: sub.status !== 1, + subExpired: sub.status !== 'active', subDays: sub.remaining_days || 0 }) }).catch(function () {}) diff --git a/server/src/routes/admin.js b/server/src/routes/admin.js index 72cde46..eae0099 100644 --- a/server/src/routes/admin.js +++ b/server/src/routes/admin.js @@ -76,7 +76,9 @@ function register(router) { 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') - return ok(device) + 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.post('/api/v1/admin/devices/:device_id/unbind', async ctx => { @@ -116,8 +118,23 @@ function register(router) { const admin = await requireAdmin(ctx) if (!admin) return fail(1002, '未授权,请重新登录') const p = pageParams(ctx) - const total = await query('SELECT COUNT(*) AS total FROM users', {}) - const records = await query('SELECT * FROM users ORDER BY created_at DESC' + limitClause(p.pageSize, p.offset), {}) + 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 }) }) @@ -128,15 +145,39 @@ function register(router) { 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 }) - return ok(Object.assign({}, user, { devices, recent_treatments: treatments })) + 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 + })) }) router.get('/api/v1/admin/subscriptions', 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 subscriptions', {}) - const records = await query('SELECT * FROM subscriptions ORDER BY created_at DESC' + limitClause(p.pageSize, p.offset), {}) + 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 + ) return ok({ records, total: total[0].total }) }) @@ -145,6 +186,7 @@ function register(router) { 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', @@ -159,8 +201,29 @@ function register(router) { const admin = await requireAdmin(ctx) if (!admin) return fail(1002, '未授权,请重新登录') const p = pageParams(ctx) - const total = await query('SELECT COUNT(*) AS total FROM treatment_records', {}) - const records = await query('SELECT * FROM treatment_records ORDER BY created_at DESC' + limitClause(p.pageSize, p.offset), {}) + 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 }) }) @@ -168,8 +231,21 @@ function register(router) { const admin = await requireAdmin(ctx) if (!admin) return fail(1002, '未授权,请重新登录') const p = pageParams(ctx) - const total = await query('SELECT COUNT(*) AS total FROM operation_logs', {}) - const records = await query('SELECT * FROM operation_logs ORDER BY created_at DESC' + limitClause(p.pageSize, p.offset), {}) + 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 }) })