fix: resolve quality audit issues
- utils.js: force UTC+8 output to match MySQL timezone on SCF - admin subscriptions: return stats (plan counts + revenue) in response - admin devices: implement keyword search filter - device-detail: fix binding history field names (bind_time/unbind_time/bind_status) - user-detail: fix treatment time field (start_time not started_at) - dashboard: show actual mode per treatment, use sub_stats from dashboard API
这个提交包含在:
@@ -64,7 +64,7 @@
|
|||||||
<text class="t-td" style="flex:2">{{ item.nickname || item.user_id || '-' }}</text>
|
<text class="t-td" style="flex:2">{{ item.nickname || item.user_id || '-' }}</text>
|
||||||
<text class="t-td" style="flex:2">{{ item.device_id || '-' }}</text>
|
<text class="t-td" style="flex:2">{{ item.device_id || '-' }}</text>
|
||||||
<text class="t-td" style="flex:1">
|
<text class="t-td" style="flex:1">
|
||||||
<text class="badge badge-blue">✨ 智能模式</text>
|
<text :class="item.mode === 1 ? 'badge badge-blue' : 'badge badge-warning'">{{ item.mode === 1 ? '✨ 智能' : '🔄 普通' }}</text>
|
||||||
</text>
|
</text>
|
||||||
<text class="t-td" style="flex:1">{{ Math.floor((item.total_duration_ms || 0) / 60000) }}分</text>
|
<text class="t-td" style="flex:1">{{ Math.floor((item.total_duration_ms || 0) / 60000) }}分</text>
|
||||||
<text class="t-td" style="flex:2">{{ formatDate(item.start_time || item.started_at) }}</text>
|
<text class="t-td" style="flex:2">{{ formatDate(item.start_time || item.started_at) }}</text>
|
||||||
@@ -128,7 +128,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async loadDashboard() {
|
async loadDashboard() {
|
||||||
try {
|
try {
|
||||||
this.stats = await get('/api/v1/admin/dashboard')
|
const dashboard = await get('/api/v1/admin/dashboard')
|
||||||
|
this.stats = dashboard
|
||||||
|
if (dashboard.sub_stats) this.subStats = dashboard.sub_stats
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
@@ -138,10 +140,6 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.recentTreatments = []
|
this.recentTreatments = []
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const subData = await get('/api/v1/admin/subscriptions', { page: 1, page_size: 1, tab: 'all' })
|
|
||||||
if (subData.stats) this.subStats = subData.stats
|
|
||||||
} catch (e) {}
|
|
||||||
},
|
},
|
||||||
formatDate: formatDateUtil,
|
formatDate: formatDateUtil,
|
||||||
onNav(path) {
|
onNav(path) {
|
||||||
|
|||||||
@@ -61,11 +61,11 @@
|
|||||||
<view class="t-body">
|
<view class="t-body">
|
||||||
<view class="t-row" v-for="(b, idx) in device.binding_history" :key="idx">
|
<view class="t-row" v-for="(b, idx) in device.binding_history" :key="idx">
|
||||||
<text class="t-td flex2">{{ b.nickname || '-' }}</text>
|
<text class="t-td flex2">{{ b.nickname || '-' }}</text>
|
||||||
<text class="t-td flex2">{{ formatDate(b.bound_at) }}</text>
|
<text class="t-td flex2">{{ formatDate(b.bind_time) }}</text>
|
||||||
<text class="t-td flex2">{{ b.unbound_at ? formatDate(b.unbound_at) : '-' }}</text>
|
<text class="t-td flex2">{{ b.unbind_time ? formatDate(b.unbind_time) : '-' }}</text>
|
||||||
<text class="t-td flex1">
|
<text class="t-td flex1">
|
||||||
<text :class="b.status === 1 ? 'badge badge-success' : 'badge badge-default'">
|
<text :class="b.bind_status === 1 ? 'badge badge-success' : 'badge badge-default'">
|
||||||
{{ b.status === 1 ? '有效' : '已解绑' }}
|
{{ b.bind_status === 1 ? '有效' : '已解绑' }}
|
||||||
</text>
|
</text>
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="t-body">
|
<view class="t-body">
|
||||||
<view class="t-row" v-for="(t, idx) in user.recent_treatments" :key="idx">
|
<view class="t-row" v-for="(t, idx) in user.recent_treatments" :key="idx">
|
||||||
<text class="t-td flex2">{{ formatDate(t.started_at) }}</text>
|
<text class="t-td flex2">{{ formatDate(t.start_time) }}</text>
|
||||||
<text class="t-td flex1">{{ Math.floor((t.total_duration_ms || 0) / 60000) }}分钟</text>
|
<text class="t-td flex1">{{ Math.floor((t.total_duration_ms || 0) / 60000) }}分钟</text>
|
||||||
<text class="t-td flex2">{{ t.device_id || '-' }}</text>
|
<text class="t-td flex2">{{ t.device_id || '-' }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
+3
-1
@@ -3,7 +3,9 @@ function toMysqlDate(value) {
|
|||||||
const d = new Date(value)
|
const d = new Date(value)
|
||||||
if (Number.isNaN(d.getTime())) return null
|
if (Number.isNaN(d.getTime())) return null
|
||||||
const pad = n => String(n).padStart(2, '0')
|
const pad = n => String(n).padStart(2, '0')
|
||||||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
|
const offset = 8 * 60
|
||||||
|
const local = new Date(d.getTime() + offset * 60 * 1000)
|
||||||
|
return local.getUTCFullYear() + '-' + pad(local.getUTCMonth() + 1) + '-' + pad(local.getUTCDate()) + ' ' + pad(local.getUTCHours()) + ':' + pad(local.getUTCMinutes()) + ':' + pad(local.getUTCSeconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(date) {
|
function formatDate(date) {
|
||||||
|
|||||||
+16
-3
@@ -78,8 +78,15 @@ 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 devices', {})
|
const keyword = (ctx.query.keyword || '').trim()
|
||||||
const records = await query('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 ORDER BY d.created_at DESC' + limitClause(p.pageSize, p.offset), {})
|
let where = ''
|
||||||
|
const params = {}
|
||||||
|
if (keyword) {
|
||||||
|
where = ' WHERE d.device_id LIKE :kw OR d.device_name LIKE :kw'
|
||||||
|
params.kw = '%' + keyword + '%'
|
||||||
|
}
|
||||||
|
const total = await query('SELECT COUNT(*) AS total FROM devices d' + where, params)
|
||||||
|
const records = await query('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 + ' ORDER BY d.created_at DESC' + limitClause(p.pageSize, p.offset), params)
|
||||||
return ok({ records, total: total[0].total })
|
return ok({ records, total: total[0].total })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -239,7 +246,13 @@ function register(router) {
|
|||||||
'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),
|
'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
|
params
|
||||||
)
|
)
|
||||||
return ok({ records, total: total[0].total })
|
const statsRow = await query('SELECT ' +
|
||||||
|
'SUM(CASE WHEN plan = \'monthly\' AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS monthly_count, ' +
|
||||||
|
'SUM(CASE WHEN plan = \'yearly\' AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS yearly_count, ' +
|
||||||
|
'SUM(CASE WHEN plan = \'trial\' AND status = 1 AND expire_time > NOW() THEN 1 ELSE 0 END) AS trial_count, ' +
|
||||||
|
'COALESCE(SUM(CASE WHEN MONTH(start_time) = MONTH(NOW()) AND YEAR(start_time) = YEAR(NOW()) THEN amount ELSE 0 END), 0) AS monthly_revenue ' +
|
||||||
|
'FROM subscriptions', {})
|
||||||
|
return ok({ records, total: total[0].total, stats: statsRow[0] || {} })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.post('/api/v1/admin/subscriptions', async ctx => {
|
router.post('/api/v1/admin/subscriptions', async ctx => {
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户