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 @@
最后在线
- {{ 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 })
})