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.
这个提交包含在:
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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: '试用' }
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
|||||||
@@ -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' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
|||||||
'®ions=' + result.regions +
|
'®ions=' + 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 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户