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-header">
<text class="card-title">实时护理数据</text>
<text class="card-extra">查看全部 </text>
<text class="card-extra" @click="onNav('/pages/record/index')">查看全部 </text>
</view>
<view class="data-table">
<view class="t-header">
@@ -28,7 +28,7 @@
<view class="info-item">
<text class="info-label">固件版本</text>
<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>
</view>
</view>
@@ -42,7 +42,7 @@
</view>
<view class="info-item">
<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>
@@ -94,7 +94,7 @@
</view>
<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>
</view>
</AdminLayout>
+19 -1
查看文件
@@ -42,7 +42,7 @@
</text>
<text class="t-td flex1">
<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>
</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) {
const map = { 2: 'badge badge-success', 3: 'badge badge-warning', 1: 'badge badge-blue', 4: 'badge badge-error' }
return map[status] || 'badge badge-default'
+1 -1
查看文件
@@ -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' })
+1 -1
查看文件
@@ -41,7 +41,7 @@ import { useUserStore } from '../../store/user'
export default {
data() {
return {
form: { username: 'admin', password: 'admin' },
form: { username: '', password: '' },
loading: false
}
},
+4 -4
查看文件
@@ -33,12 +33,12 @@
</view>
<view class="t-body">
<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 flex2">{{ item.device_id || '-' }}</text>
<text class="t-td flex1">
<text :class="item.mode === 'smart' ? 'badge badge-blue' : 'badge badge-warning'">
{{ item.mode === 'smart' ? '✨ 智能模式' : '🔄 普通模式' }}
<text :class="item.mode === 1 ? 'badge badge-blue' : 'badge badge-warning'">
{{ item.mode === 1 ? '✨ 智能模式' : '🔄 普通模式' }}
</text>
</text>
<text class="t-td flex1">{{ formatRegions(item.regions) }}</text>
@@ -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' })
@@ -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) {
@@ -23,7 +23,7 @@
<text class="stat-label">护理次数</text>
</view>
<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>
</view>
<view class="stat-item">
@@ -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: '试用' }
+3 -10
查看文件
@@ -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' })