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.
这个提交包含在:
Guoguo
2026-04-28 18:46:03 -07:00
父节点 c5f6033ccf
当前提交 6201b97fcd
修改 15 个文件,包含 132 行新增43 行删除
+1 -1
查看文件
@@ -27,7 +27,7 @@
<view class="card table-card"> <view class="card table-card">
<view class="card-header"> <view class="card-header">
<text class="card-title">实时护理数据</text> <text class="card-title">实时护理数据</text>
<text class="card-extra">查看全部 </text> <text class="card-extra" @click="onNav('/pages/record/index')">查看全部 </text>
</view> </view>
<view class="data-table"> <view class="data-table">
<view class="t-header"> <view class="t-header">
@@ -28,7 +28,7 @@
<view class="info-item"> <view class="info-item">
<text class="info-label">固件版本</text> <text class="info-label">固件版本</text>
<view class="info-value-row"> <view class="info-value-row">
<text class="info-value">{{ device.fw_version || '-' }}</text> <text class="info-value">{{ device.firmware_version || '-' }}</text>
<button class="btn-default btn-xs" @click="onCheckUpdate">检查更新</button> <button class="btn-default btn-xs" @click="onCheckUpdate">检查更新</button>
</view> </view>
</view> </view>
@@ -42,7 +42,7 @@
</view> </view>
<view class="info-item"> <view class="info-item">
<text class="info-label">最后在线</text> <text class="info-label">最后在线</text>
<text class="info-value">{{ device.last_online || '-' }}</text> <text class="info-value">{{ device.last_online_at || '-' }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -94,7 +94,7 @@
</view> </view>
<view class="action-row"> <view class="action-row">
<button class="btn-primary" @click="onCommand('query')">发送指令</button> <button class="btn-primary" @click="onCommand(4)">查询状态</button>
<button class="btn-default" @click="onViewLogs">查看完整日志</button> <button class="btn-default" @click="onViewLogs">查看完整日志</button>
</view> </view>
</AdminLayout> </AdminLayout>
+19 -1
查看文件
@@ -42,7 +42,7 @@
</text> </text>
<text class="t-td flex1"> <text class="t-td flex1">
<text class="action-link" @click="onDetail(item.device_id)">详情</text> <text class="action-link" @click="onDetail(item.device_id)">详情</text>
<text class="action-link" v-if="item.bound_user">解绑</text> <text class="action-link" v-if="item.bound_user" @click="onUnbind(item)">解绑</text>
</text> </text>
</view> </view>
</view> </view>
@@ -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) { statusBadge(status) {
const map = { 2: 'badge badge-success', 3: 'badge badge-warning', 1: 'badge badge-blue', 4: 'badge badge-error' } const map = { 2: 'badge badge-success', 3: 'badge badge-warning', 1: 'badge badge-blue', 4: 'badge badge-error' }
return map[status] || 'badge badge-default' return map[status] || 'badge badge-default'
+1 -1
查看文件
@@ -118,7 +118,7 @@ export default {
exportCSV('logs_' + new Date().toISOString().slice(0, 10) + '.csv', exportCSV('logs_' + new Date().toISOString().slice(0, 10) + '.csv',
['时间', '操作类型', '操作详情', '操作者'], ['时间', '操作类型', '操作详情', '操作者'],
records.map(function (r) { 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' }) uni.showToast({ title: '导出成功', icon: 'success' })
+1 -1
查看文件
@@ -41,7 +41,7 @@ import { useUserStore } from '../../store/user'
export default { export default {
data() { data() {
return { return {
form: { username: 'admin', password: 'admin' }, form: { username: '', password: '' },
loading: false loading: false
} }
}, },
+4 -4
查看文件
@@ -33,12 +33,12 @@
</view> </view>
<view class="t-body"> <view class="t-body">
<view class="t-row" v-for="item in records" :key="item.record_id"> <view class="t-row" v-for="item in records" :key="item.record_id">
<text class="t-td flex1">{{ item.record_id ? item.record_id.slice(0, 8) : '-' }}</text> <text class="t-td flex1">{{ item.record_id ? String(item.record_id) : '-' }}</text>
<text class="t-td flex1">{{ item.nickname || item.user_id || '-' }}</text> <text class="t-td flex1">{{ item.nickname || item.user_id || '-' }}</text>
<text class="t-td flex2">{{ item.device_id || '-' }}</text> <text class="t-td flex2">{{ item.device_id || '-' }}</text>
<text class="t-td flex1"> <text class="t-td flex1">
<text :class="item.mode === 'smart' ? 'badge badge-blue' : 'badge badge-warning'"> <text :class="item.mode === 1 ? 'badge badge-blue' : 'badge badge-warning'">
{{ item.mode === 'smart' ? '✨ 智能模式' : '🔄 普通模式' }} {{ item.mode === 1 ? '✨ 智能模式' : '🔄 普通模式' }}
</text> </text>
</text> </text>
<text class="t-td flex1">{{ formatRegions(item.regions) }}</text> <text class="t-td flex1">{{ formatRegions(item.regions) }}</text>
@@ -140,7 +140,7 @@ export default {
var regions = [] var regions = []
var bits = [1, 2, 4, 8, 16, 32, 64] 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]]) } 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' }) uni.showToast({ title: '导出成功', icon: 'success' })
@@ -217,7 +217,7 @@ export default {
const data = await get('/api/v1/admin/subscriptions', { page: 1, page_size: 9999, tab: this.activeTab }) const data = await get('/api/v1/admin/subscriptions', { page: 1, page_size: 9999, tab: this.activeTab })
const records = data.records || [] const records = data.records || []
var planMap = { monthly: '月卡', quarterly: '季卡', yearly: '年卡', trial: '试用' } 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', exportCSV('subscriptions_' + new Date().toISOString().slice(0, 10) + '.csv',
['用户', '订阅类型', '订单金额', '开始日期', '到期日期', '状态'], ['用户', '订阅类型', '订单金额', '开始日期', '到期日期', '状态'],
records.map(function (r) { records.map(function (r) {
@@ -23,7 +23,7 @@
<text class="stat-label">护理次数</text> <text class="stat-label">护理次数</text>
</view> </view>
<view class="stat-item"> <view class="stat-item">
<text class="stat-value">{{ user.total_duration || '0h' }}</text> <text class="stat-value">{{ user.total_duration ? Math.round(user.total_duration / 3600000) + 'h' : '0h' }}</text>
<text class="stat-label">累计时长</text> <text class="stat-label">累计时长</text>
</view> </view>
<view class="stat-item"> <view class="stat-item">
@@ -112,8 +112,7 @@ export default {
uni.navigateTo({ url: '/pages/record/index?user_id=' + this.userId }) uni.navigateTo({ url: '/pages/record/index?user_id=' + this.userId })
}, },
subStatusText(status) { subStatusText(status) {
const map = { yearly: '年卡会员', monthly: '月卡会员', trial: '试用中', none: '未订阅' } return status === 1 ? '已订阅' : '未订阅'
return map[status] || '未订阅'
}, },
subTypeText(type) { subTypeText(type) {
const map = { yearly: '年卡', monthly: '月卡', trial: '试用' } const map = { yearly: '年卡', monthly: '月卡', trial: '试用' }
+3 -10
查看文件
@@ -110,17 +110,10 @@ export default {
uni.navigateTo({ url: '/pages/record/index?user_id=' + userId }) uni.navigateTo({ url: '/pages/record/index?user_id=' + userId })
}, },
subStatusText(status) { subStatusText(status) {
const map = { yearly: '年卡会员', monthly: '月卡会员', trial: '试用中', none: '未订阅' } return status === 1 ? '已订阅' : '未订阅'
return map[status] || '未订阅'
}, },
subBadge(status) { subBadge(status) {
const map = { return status === 1 ? 'badge badge-success' : 'badge badge-default'
yearly: 'badge badge-success',
monthly: 'badge badge-warning',
trial: 'badge badge-blue',
none: 'badge badge-blue'
}
return map[status] || 'badge badge-default'
}, },
formatDate: formatDateShort, formatDate: formatDateShort,
async onExport() { async onExport() {
@@ -130,7 +123,7 @@ export default {
exportCSV('users_' + new Date().toISOString().slice(0, 10) + '.csv', exportCSV('users_' + new Date().toISOString().slice(0, 10) + '.csv',
['用户ID', '昵称', '手机号', '绑定设备数', '护理次数', '订阅状态', '注册时间'], ['用户ID', '昵称', '手机号', '绑定设备数', '护理次数', '订阅状态', '注册时间'],
records.map(function (r) { 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' }) uni.showToast({ title: '导出成功', icon: 'success' })
+3 -3
查看文件
@@ -52,7 +52,7 @@ Page({
if (devices.length > 0) { if (devices.length > 0) {
app.globalData.currentDevice = devices[0] app.globalData.currentDevice = devices[0]
self.setData({ self.setData({
deviceName: devices[0].name || devices[0].device_id || '我的光面膜', deviceName: devices[0].device_name || devices[0].device_id || '我的光面膜',
deviceInfo: devices[0] deviceInfo: devices[0]
}) })
} }
@@ -138,7 +138,7 @@ Page({
}, },
onStartTreatment: function () { 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' }) wx.navigateTo({ url: '/pages/subscribe-prompt/subscribe-prompt' })
return return
} }
@@ -146,7 +146,7 @@ Page({
}, },
onViewSubscription: function () { 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' }) wx.navigateTo({ url: '/pages/subscribe-plans/subscribe-plans' })
} }
} }
+2 -2
查看文件
@@ -35,13 +35,13 @@ Page({
}, },
onViewSubscription: function () { 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' }) wx.navigateTo({ url: '/pages/subscribe-plans/subscribe-plans' })
} else { } else {
var sub = this.data.subscription var sub = this.data.subscription
wx.showModal({ wx.showModal({
title: '订阅信息', title: '订阅信息',
content: '套餐类型:' + (sub.plan_type || '未知') + '\n剩余天数:' + (sub.remaining_days || 0) + '天', content: '套餐类型:' + (sub.plan || '未知') + '\n剩余天数:' + (sub.remaining_days || 0) + '天',
showCancel: false showCancel: false
}) })
} }
+3 -1
查看文件
@@ -23,6 +23,7 @@ Page({
onLoad: function (options) { onLoad: function (options) {
var app = getApp() var app = getApp()
this.setData({ statusBarHeight: app.globalData.statusBarHeight, devMode: config.__DEV__ || false }) this.setData({ statusBarHeight: app.globalData.statusBarHeight, devMode: config.__DEV__ || false })
this._deviceId = ble.getDeviceId() || (app.globalData.currentDevice && app.globalData.currentDevice.device_id) || ''
this.setData({ this.setData({
regions: parseInt(options.regions) || 0x7F, regions: parseInt(options.regions) || 0x7F,
wavelength: parseInt(options.wavelength) || 2, wavelength: parseInt(options.wavelength) || 2,
@@ -115,7 +116,8 @@ Page({
'&regions=' + result.regions + '&regions=' + result.regions +
'&duration=' + result.total_duration_ms + '&duration=' + result.total_duration_ms +
'&avg_pd=' + result.avg_pd + '&avg_pd=' + result.avg_pd +
'&mode=' + (self.data.mode || 0) '&mode=' + (self.data.mode || 0) +
'&device_id=' + encodeURIComponent(self._deviceId || '')
}) })
}, 1000) }, 1000)
}, },
@@ -22,6 +22,7 @@ Page({
onLoad: function (options) { onLoad: function (options) {
this.setData({ statusBarHeight: app.globalData.statusBarHeight }) 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 durationMs = parseInt(options.duration) || 0
var mins = Math.floor(durationMs / 60000) var mins = Math.floor(durationMs / 60000)
var secs = Math.floor((durationMs % 60000) / 1000) var secs = Math.floor((durationMs % 60000) / 1000)
@@ -53,7 +54,7 @@ Page({
http.post('/api/v1/treatment/sync', { http.post('/api/v1/treatment/sync', {
session_id: self.data.sessionId, session_id: self.data.sessionId,
device_id: ble.getDeviceId(), device_id: self._deviceId || '',
start_time: new Date(Date.now() - self.data.duration).toISOString(), start_time: new Date(Date.now() - self.data.duration).toISOString(),
end_time: new Date().toISOString(), end_time: new Date().toISOString(),
regions: self.data.regions, regions: self.data.regions,
@@ -39,7 +39,7 @@ Page({
var self = this var self = this
http.get('/api/v1/subscription').then(function (sub) { http.get('/api/v1/subscription').then(function (sub) {
self.setData({ self.setData({
subExpired: sub.status !== 1, subExpired: sub.status !== 'active',
subDays: sub.remaining_days || 0 subDays: sub.remaining_days || 0
}) })
}).catch(function () {}) }).catch(function () {})
+86 -10
查看文件
@@ -76,7 +76,9 @@ function register(router) {
if (!admin) return fail(1002, '未授权,请重新登录') 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 }) 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') 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 => { router.post('/api/v1/admin/devices/:device_id/unbind', async ctx => {
@@ -116,8 +118,23 @@ function register(router) {
const admin = await requireAdmin(ctx) const admin = await requireAdmin(ctx)
if (!admin) return fail(1002, '未授权,请重新登录') if (!admin) return fail(1002, '未授权,请重新登录')
const p = pageParams(ctx) const p = pageParams(ctx)
const total = await query('SELECT COUNT(*) AS total FROM users', {}) const keyword = (ctx.query.keyword || '').trim()
const records = await query('SELECT * FROM users ORDER BY created_at DESC' + limitClause(p.pageSize, p.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(p.pageSize, p.offset),
params
)
return ok({ records, total: total[0].total }) return ok({ records, total: total[0].total })
}) })
@@ -128,15 +145,39 @@ function register(router) {
if (!user) return fail(1004, 'USER_NOT_FOUND') 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 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 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 => { router.get('/api/v1/admin/subscriptions', async ctx => {
const admin = await requireAdmin(ctx) const admin = await requireAdmin(ctx)
if (!admin) return fail(1002, '未授权,请重新登录') if (!admin) return fail(1002, '未授权,请重新登录')
const p = pageParams(ctx) const p = pageParams(ctx)
const total = await query('SELECT COUNT(*) AS total FROM subscriptions', {}) const tab = (ctx.query.tab || '').trim()
const records = await query('SELECT * FROM subscriptions ORDER BY created_at DESC' + limitClause(p.pageSize, p.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(p.pageSize, p.offset),
params
)
return ok({ records, total: total[0].total }) return ok({ records, total: total[0].total })
}) })
@@ -145,6 +186,7 @@ function register(router) {
if (!admin) return fail(1002, '未授权,请重新登录') 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 }) 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') 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))', { 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, user_id: ctx.body.user_id,
plan: ctx.body.plan || 'monthly', plan: ctx.body.plan || 'monthly',
@@ -159,8 +201,29 @@ function register(router) {
const admin = await requireAdmin(ctx) const admin = await requireAdmin(ctx)
if (!admin) return fail(1002, '未授权,请重新登录') if (!admin) return fail(1002, '未授权,请重新登录')
const p = pageParams(ctx) const p = pageParams(ctx)
const total = await query('SELECT COUNT(*) AS total FROM treatment_records', {}) const keyword = (ctx.query.keyword || '').trim()
const records = await query('SELECT * FROM treatment_records ORDER BY created_at DESC' + limitClause(p.pageSize, p.offset), {}) 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 }) return ok({ records, total: total[0].total })
}) })
@@ -168,8 +231,21 @@ function register(router) {
const admin = await requireAdmin(ctx) const admin = await requireAdmin(ctx)
if (!admin) return fail(1002, '未授权,请重新登录') if (!admin) return fail(1002, '未授权,请重新登录')
const p = pageParams(ctx) const p = pageParams(ctx)
const total = await query('SELECT COUNT(*) AS total FROM operation_logs', {}) const type = (ctx.query.type || '').trim()
const records = await query('SELECT * FROM operation_logs ORDER BY created_at DESC' + limitClause(p.pageSize, p.offset), {}) 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 }) return ok({ records, total: total[0].total })
}) })