fix: resolve multiple miniprogram bugs and extend admin console

- fix manual bind stuck at BLE scan, redirect to bind-success
- add 15s timeout and stopScan to ble-connect
- add stopScan method to ble.js
- add back-to-home button on bind-success page
- fix field name in subscribe-plans (plan -> plan_type)
- fix history.js to handle created_at field
- fix index.js to handle device name field
- add subscription route alias in request.js
- extend admin cloud function from 1 to 9 actions
- add mock mode to admin-console request.js
- remove unused discover page and legacy record function

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
这个提交包含在:
Guoguo
2026-04-24 23:28:53 +08:00
共同撰写人 Claude Opus 4.6
父节点 52cb1f72dc
当前提交 267c75718b
修改 18 个文件,包含 332 行新增169 行删除
+166 -5
查看文件
@@ -1,23 +1,184 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
const PAGE_SIZE = 20
exports.main = async (event, context) => {
const { action } = event
const { action, data } = event
if (action === 'dashboard') {
const deviceCount = await db.collection('bindings').where({ status: 'active' }).count()
const userCount = await db.collection('users').count()
const treatmentCount = await db.collection('treatment_records').count()
const [deviceCount, userCount, treatmentCount, subCount] = await Promise.all([
db.collection('bindings').where({ status: 'active' }).count(),
db.collection('users').count(),
db.collection('treatment_records').count(),
db.collection('subscriptions').where({ status: 'active' }).count()
])
return {
code: 0,
data: {
device_count: deviceCount.total,
user_count: userCount.total,
treatment_count: treatmentCount.total
treatment_count: treatmentCount.total,
subscription_count: subCount.total
}
}
}
if (action === 'devices') {
const { page = 1, page_size = PAGE_SIZE, keyword } = data || {}
let query = db.collection('bindings')
const conditions = { status: 'active' }
if (keyword) {
conditions.device_id = db.RegExp({ regexp: keyword, options: 'i' })
}
query = query.where(conditions)
const [totalRes, records] = await Promise.all([
query.count(),
query.orderBy('bind_time', 'desc').skip((page - 1) * page_size).limit(page_size).get()
])
const devices = records.data.map(b => ({
device_id: b.device_id,
bound_user: b.openid ? b.openid.slice(-8) : '-',
battery: null,
fw_version: '1.0.0',
activated_at: b.bind_time,
status: 2
}))
return { code: 0, data: { records: devices, total: totalRes.total } }
}
if (action === 'device_detail') {
const { device_id } = data
const bindings = await db.collection('bindings').where({ device_id, status: 'active' }).get()
if (bindings.data.length === 0) return { code: -1, message: '设备不存在' }
const b = bindings.data[0]
return {
code: 0,
data: {
device_id: b.device_id,
bound_user: b.openid ? b.openid.slice(-8) : '-',
battery: null,
fw_version: '1.0.0',
activated_at: b.bind_time,
status: 2
}
}
}
if (action === 'device_unbind') {
const { device_id } = data
await db.collection('bindings').where({ device_id, status: 'active' }).update({
data: { status: 'inactive', unbind_time: db.serverDate() }
})
return { code: 0, data: {} }
}
if (action === 'users') {
const { page = 1, page_size = PAGE_SIZE, keyword } = data || {}
let conditions = {}
if (keyword) {
conditions = _.or([
{ openid: db.RegExp({ regexp: keyword, options: 'i' }) },
{ nickname: db.RegExp({ regexp: keyword, options: 'i' }) }
])
}
const [totalRes, records] = await Promise.all([
db.collection('users').where(conditions).count(),
db.collection('users').where(conditions).orderBy('created_at', 'desc').skip((page - 1) * page_size).limit(page_size).get()
])
return { code: 0, data: { records: records.data, total: totalRes.total } }
}
if (action === 'user_detail') {
const { user_id } = data
const userRes = await db.collection('users').doc(user_id).get()
const user = userRes.data
const [bindings, subs, treatments] = await Promise.all([
db.collection('bindings').where({ openid: user.openid, status: 'active' }).get(),
db.collection('subscriptions').where({ openid: user.openid }).orderBy('start_time', 'desc').limit(1).get(),
db.collection('treatment_records').where({ openid: user.openid }).orderBy('created_at', 'desc').limit(5).get()
])
const devices = bindings.data.map(b => ({ device_id: b.device_id, device_name: '我的光面膜' }))
const sub = subs.data[0] || {}
const treatmentCount = await db.collection('treatment_records').where({ openid: user.openid }).count()
return {
code: 0,
data: {
...user,
user_id: user._id,
devices,
subscription_type: sub.plan_type || 'none',
subscription_status: sub.status || 'none',
subscription_expire: sub.end_time || null,
treatment_count: treatmentCount.total,
recent_treatments: treatments.data
}
}
}
if (action === 'subscriptions') {
const { page = 1, page_size = PAGE_SIZE, tab } = data || {}
let conditions = {}
if (tab && tab !== 'all') {
if (tab === 'expired') {
conditions = { status: 'expired' }
} else {
conditions = { plan_type: tab, status: 'active' }
}
}
const [totalRes, records] = await Promise.all([
db.collection('subscriptions').where(conditions).count(),
db.collection('subscriptions').where(conditions).orderBy('start_time', 'desc').skip((page - 1) * page_size).limit(page_size).get()
])
const monthlyCount = await db.collection('subscriptions').where({ plan_type: 'monthly', status: 'active' }).count()
const yearlyCount = await db.collection('subscriptions').where({ plan_type: 'yearly', status: 'active' }).count()
const trialCount = await db.collection('subscriptions').where({ plan_type: 'trial', status: 'active' }).count()
return {
code: 0,
data: {
records: records.data.map(s => ({
...s,
id: s._id,
user_id: s.openid ? s.openid.slice(-8) : '-',
plan: s.plan_type,
amount: s.price || '-',
started_at: s.start_time,
expired_at: s.end_time,
status: s.status === 'active' ? 1 : 3
})),
total: totalRes.total,
stats: {
monthly_count: monthlyCount.total,
yearly_count: yearlyCount.total,
trial_count: trialCount.total
}
}
}
}
if (action === 'records') {
const { page = 1, page_size = PAGE_SIZE, user_id } = data || {}
let conditions = {}
if (user_id) {
const userRes = await db.collection('users').doc(user_id).get()
conditions = { openid: userRes.data.openid }
}
const [totalRes, records] = await Promise.all([
db.collection('treatment_records').where(conditions).count(),
db.collection('treatment_records').where(conditions).orderBy('created_at', 'desc').skip((page - 1) * page_size).limit(page_size).get()
])
return { code: 0, data: { records: records.data, total: totalRes.total } }
}
if (action === 'logs') {
const { page = 1, page_size = PAGE_SIZE } = data || {}
const [totalRes, records] = await Promise.all([
db.collection('operation_logs').count(),
db.collection('operation_logs').orderBy('created_at', 'desc').skip((page - 1) * page_size).limit(page_size).get()
])
return { code: 0, data: { records: records.data, total: totalRes.total } }
}
return { code: -1, message: '未知操作' }
}
@@ -13,5 +13,9 @@ Page({
onStart: function () {
wx.redirectTo({ url: '/pages/wear-check/wear-check' })
},
onGoHome: function () {
wx.reLaunch({ url: '/pages/index/index' })
}
})
@@ -19,5 +19,6 @@
</view>
<button class="btn-primary" bindtap="onStart">开始使用</button>
<button class="btn-secondary" bindtap="onGoHome">返回首页</button>
</view>
</view>
@@ -1,5 +1,6 @@
var ble = require('../../services/ble')
var app = getApp()
var SCAN_TIMEOUT = 15000
Page({
data: {
@@ -19,19 +20,32 @@ Page({
this.startBleConnect()
},
onUnload: function () {
clearTimeout(this._scanTimer)
ble.stopScan()
},
startBleConnect: function () {
var self = this
self.setData({ state: 'scanning', error: '' })
self._scanTimer = setTimeout(function () {
ble.stopScan()
self.setData({ state: 'error', error: '搜索超时,请确认设备已开机并在附近' })
}, SCAN_TIMEOUT)
ble.startScan({
onFound: function (device) {
clearTimeout(self._scanTimer)
self.setData({ state: 'connecting' })
},
onConnected: function (device) {
clearTimeout(self._scanTimer)
self.setData({ state: 'binding' })
self.doBind()
},
onError: function (err) {
clearTimeout(self._scanTimer)
self.setData({ state: 'error', error: err.msg || '连接失败' })
}
})
-8
查看文件
@@ -1,8 +0,0 @@
Page({
data: {
articles: []
},
onLoad: function () {
}
})
-3
查看文件
@@ -1,3 +0,0 @@
{
"navigationBarTitleText": "发现"
}
-4
查看文件
@@ -1,4 +0,0 @@
<view class="container">
<view class="section-title">发现</view>
<view class="text-center text-muted mt-20">暂无内容</view>
</view>
-1
查看文件
@@ -1 +0,0 @@
{}
+1 -1
查看文件
@@ -54,7 +54,7 @@ Page({
r.duration_text = durationMin + '分钟'
r.region_names = ble.getRegionName(r.regions || 0).join('、')
r.wavelength_name = ble.getWavelengthName(r.wavelength || 2)
r.date_text = self.formatDate(r.start_time)
r.date_text = self.formatDate(r.start_time || r.created_at)
if (r.start_time && new Date(r.start_time) >= monthStart) {
monthCount++
+40 -1
查看文件
@@ -44,7 +44,7 @@ Page({
self.setData({ hasDevice: devices.length > 0 })
if (devices.length > 0) {
self.setData({
deviceName: devices[0].device_name || '光子美容仪',
deviceName: devices[0].name || devices[0].device_id || '我的光面膜',
deviceInfo: devices[0]
})
}
@@ -90,6 +90,45 @@ Page({
wx.navigateTo({ url: '/pages/scan/scan' })
},
onManageDevice: function () {
var self = this
wx.showActionSheet({
itemList: ['解绑当前设备'],
success: function (res) {
if (res.tapIndex === 0) {
wx.showModal({
title: '确认解绑',
content: '解绑后将无法使用该设备,确定要解绑吗?',
success: function (modalRes) {
if (modalRes.confirm) {
self.doUnbind()
}
}
})
}
}
})
},
doUnbind: function () {
var self = this
wx.showLoading({ title: '解绑中...' })
ble.disconnect()
http.post('/api/v1/device/unbind', {}).then(function () {
wx.hideLoading()
self.setData({
hasDevice: false,
deviceName: '',
deviceInfo: null,
connected: false
})
wx.showToast({ title: '已解绑', icon: 'success' })
}).catch(function (err) {
wx.hideLoading()
wx.showToast({ title: err.message || '解绑失败', icon: 'none' })
})
},
onStartTreatment: function () {
if (!this.data.subscription || this.data.subscription.status === 0) {
wx.navigateTo({ url: '/pages/subscribe-prompt/subscribe-prompt' })
+1 -1
查看文件
@@ -20,6 +20,6 @@
</view>
<button class="btn-primary" bindtap="onStartTreatment">开始护理</button>
<button class="btn-secondary" bindtap="onAddDevice">设备管理</button>
<button class="btn-secondary" bindtap="onManageDevice">设备管理</button>
</view>
</view>
+12 -6
查看文件
@@ -47,16 +47,22 @@ Page({
bindDevice: function (deviceId) {
var self = this
self.setData({ scanning: true, error: '' })
http.post('/api/v1/device/bind', { device_id: deviceId }).then(function (data) {
self.setData({ scanning: false })
wx.navigateTo({
url: '/pages/ble-connect/ble-connect?device_id=' + deviceId + '&bind_token=' + data.bind_token
wx.redirectTo({
url: '/pages/bind-success/bind-success?device_id=' + deviceId
})
}).catch(function (err) {
self.setData({
scanning: false,
error: err.message || '绑定失败'
})
self.setData({ scanning: false })
if (err && err.code === 2001) {
wx.showToast({ title: '已绑定设备', icon: 'none' })
setTimeout(function () {
wx.navigateBack()
}, 1500)
} else {
self.setData({ error: err.message || '绑定失败' })
}
})
},
@@ -29,12 +29,12 @@ Page({
self.setData({ purchasing: true })
http.post('/api/v1/subscription/purchase', {
plan: self.data.selected,
plan_type: self.data.selected,
payment_method: 'wechat'
}).then(function (data) {
return http.post('/api/v1/subscription/verify', {
order_id: data.order_id,
plan: self.data.selected
plan_type: self.data.selected
})
}).then(function () {
self.setData({ purchasing: false })
+6
查看文件
@@ -538,6 +538,11 @@ function unbindDevice(userId) {
return writeCommand(CMD.UNBIND, payload)
}
function stopScan() {
wx.stopBluetoothDevicesDiscovery({})
wx.offBluetoothDeviceFound()
}
function disconnect() {
if (_deviceId) {
wx.closeBLEConnection({ deviceId: _deviceId })
@@ -589,6 +594,7 @@ module.exports = {
isConnected: isConnected,
getDeviceId: getDeviceId,
startScan: startScan,
stopScan: stopScan,
connect: connect,
disconnect: disconnect,
on: on,
+1
查看文件
@@ -80,6 +80,7 @@ var FUNC_MAP = {
'/api/v1/device/unbind': { fn: 'device', action: 'unbind' },
'/api/v1/device/list': { fn: 'device', action: 'list' },
'/api/v1/device/detail': { fn: 'device', action: 'detail' },
'/api/v1/subscription': { fn: 'subscription', action: 'status' },
'/api/v1/subscription/status': { fn: 'subscription', action: 'status' },
'/api/v1/subscription/purchase': { fn: 'subscription', action: 'purchase' },
'/api/v1/subscription/verify': { fn: 'subscription', action: 'verify' },