feat: complete feature gaps across all modules

Server:
- Add settings-cache with 60s TTL for feature toggle checks
- Enforce maintenance_mode on login, enable_binding on device bind

Admin console:
- Remove dead "发送通知" button from user detail
- Firmware check calls real API and compares versions

Miniprogram:
- Wear-check: dynamic battery/connected from BLE state
- Login: hide non-functional phone auth button
- Treating: show actual selected regions instead of hardcoded "全脸"
- Index: display subscription status with tap to manage
- Auto-scan: show "待检测" instead of "--" for PD data
- Agreements: tap shows "内容建设中" modal
这个提交包含在:
Guoguo
2026-05-02 08:48:26 -07:00
父节点 1017fb5dae
当前提交 031678c03f
修改 17 个文件,包含 116 行新增12 行删除
+18 -1
查看文件
@@ -162,8 +162,25 @@ export default {
onViewLogs() { onViewLogs() {
this.$emit('navigate', 'log', { device_id: this.deviceId }) this.$emit('navigate', 'log', { device_id: this.deviceId })
}, },
onCheckUpdate() { async onCheckUpdate() {
uni.showToast({ title: '正在检查更新...', icon: 'none' }) uni.showToast({ title: '正在检查更新...', icon: 'none' })
try {
const data = await get('/api/v1/admin/firmware')
const records = data.records || []
const latest = records.filter(r => r.status === 1).sort((a, b) => b.firmware_id - a.firmware_id)[0]
if (!latest) {
uni.showToast({ title: '暂无固件信息', icon: 'none' })
return
}
const current = this.device && this.device.firmware_version
if (current && current === latest.version) {
uni.showToast({ title: '当前已是最新版本', icon: 'success' })
} else {
uni.showToast({ title: '发现新版本 v' + latest.version, icon: 'none', duration: 3000 })
}
} catch (e) {
uni.showToast({ title: '检查更新失败', icon: 'none' })
}
}, },
formatDate: formatDateUtil formatDate: formatDateUtil
} }
@@ -5,7 +5,6 @@
<view class="page-card" v-if="user"> <view class="page-card" v-if="user">
<view class="page-header"> <view class="page-header">
<text class="page-title">👥 用户详情 - {{ user.nickname || user.user_id }}</text> <text class="page-title">👥 用户详情 - {{ user.nickname || user.user_id }}</text>
<button class="btn-primary btn-sm">发送通知</button>
</view> </view>
<view class="user-profile"> <view class="user-profile">
+1 -1
查看文件
@@ -65,7 +65,7 @@ Page({
parseScanResults: function (regions) { parseScanResults: function (regions) {
return regions.map(function (name) { return regions.map(function (name) {
return { region: name, pd: '--' } return { region: name, pd: '待检测' }
}) })
}, },
@@ -15,6 +15,7 @@
</view> </view>
<view class="scan-hint">请将面罩对准面部</view> <view class="scan-hint">请将面罩对准面部</view>
<view class="scan-hint" style="font-size:24rpx;color:#999;margin-top:8rpx;">连接设备后可获取皮肤数据</view>
<view class="debug-actions" wx:if="{{devMode}}"> <view class="debug-actions" wx:if="{{devMode}}">
<view class="debug-label">🔧 调试工具</view> <view class="debug-label">🔧 调试工具</view>
+9
查看文件
@@ -23,6 +23,15 @@
</view> </view>
</view> </view>
<view class="sub-info" bindtap="onViewSubscription">
<view class="sub-info-left">
<text class="sub-info-icon">💳</text>
<text wx:if="{{subscription && subscription.status === 'active'}}">智能模式 · 剩余{{subRemaining}}天</text>
<text wx:else>未订阅智能模式</text>
</view>
<text class="sub-info-arrow"></text>
</view>
<button class="btn-primary" bindtap="onStartTreatment">开始护理</button> <button class="btn-primary" bindtap="onStartTreatment">开始护理</button>
<button class="btn-secondary" bindtap="onManageDevice">设备管理</button> <button class="btn-secondary" bindtap="onManageDevice">设备管理</button>
</view> </view>
+24
查看文件
@@ -18,3 +18,27 @@
.btn-secondary::after { .btn-secondary::after {
border: none; border: none;
} }
.sub-info {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-top: 20rpx;
}
.sub-info-left {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
color: #333;
}
.sub-info-icon {
font-size: 32rpx;
}
.sub-info-arrow {
color: #999;
font-size: 32rpx;
}
+4
查看文件
@@ -65,6 +65,10 @@ Page({
}) })
}, },
onAgreement: function () {
wx.showModal({ title: '提示', content: '用户协议和隐私政策内容建设中', showCancel: false })
},
onGetPhoneNumber: function (e) { onGetPhoneNumber: function (e) {
var self = this var self = this
if (!e.detail || !e.detail.code) { if (!e.detail || !e.detail.code) {
+2 -2
查看文件
@@ -18,10 +18,10 @@
</button> </button>
<!-- 手机号授权已预留。需要时展示该按钮并隐藏上面的普通登录按钮。 --> <!-- 手机号授权已预留。需要时展示该按钮并隐藏上面的普通登录按钮。 -->
<button class="phone-btn disabled-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber" disabled="{{loading}}"> <button wx:if="{{false}}" class="phone-btn disabled-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber" disabled="{{loading}}">
授权手机号登录(预留) 授权手机号登录(预留)
</button> </button>
<view class="agreement">登录即表示同意《用户协议》和《隐私政策》</view> <view class="agreement" bindtap="onAgreement">登录即表示同意《用户协议》和《隐私政策》</view>
</view> </view>
</view> </view>
@@ -91,6 +91,10 @@ Page({
}).catch(function () {}) }).catch(function () {})
}, },
onAgreement: function () {
wx.showModal({ title: '提示', content: '订阅协议内容建设中', showCancel: false })
},
onSelect: function (e) { onSelect: function (e) {
this.setData({ selected: e.currentTarget.dataset.plan }) this.setData({ selected: e.currentTarget.dataset.plan })
}, },
@@ -40,6 +40,6 @@
<button class="btn-primary btn-primary-gold" bindtap="onPurchase" disabled="{{purchasing}}" loading="{{purchasing}}"> <button class="btn-primary btn-primary-gold" bindtap="onPurchase" disabled="{{purchasing}}" loading="{{purchasing}}">
{{subscription && subscription.status === 'active' && subRemaining > 0 ? '续费' : '确认支付'}} {{subscription && subscription.status === 'active' && subRemaining > 0 ? '续费' : '确认支付'}}
</button> </button>
<view class="agreement">支付即表示同意《订阅协议》</view> <view class="agreement" bindtap="onAgreement">支付即表示同意《订阅协议》</view>
</view> </view>
</view> </view>
+15 -2
查看文件
@@ -18,19 +18,32 @@ Page({
completed: false, completed: false,
startedAt: 0, startedAt: 0,
devMode: false, devMode: false,
regionText: '全脸护理',
},
_getRegionText: function (regionMask) {
var REGION_MAP = {1:'左脸', 2:'右脸', 4:'额头', 8:'下巴', 16:'鼻', 32:'左眼', 64:'右眼'}
if (regionMask === 0x7F) return '全脸护理'
var names = []
for (var bit in REGION_MAP) {
if (regionMask & parseInt(bit)) names.push(REGION_MAP[bit])
}
return names.length > 0 ? names.join('+') + '护理' : '全脸护理'
}, },
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._deviceId = ble.getDeviceId() || (app.globalData.currentDevice && app.globalData.currentDevice.device_id) || ''
var regions = parseInt(options.regions) || 0x7F
this.setData({ this.setData({
regions: parseInt(options.regions) || 0x7F, regions: regions,
wavelength: parseInt(options.wavelength) || 2, wavelength: parseInt(options.wavelength) || 2,
duration: parseInt(options.duration) || 600000, duration: parseInt(options.duration) || 600000,
mode: parseInt(options.mode) || 0, mode: parseInt(options.mode) || 0,
remainingMs: parseInt(options.duration) || 600000, remainingMs: parseInt(options.duration) || 600000,
startedAt: Date.now() startedAt: Date.now(),
regionText: this._getRegionText(regions)
}) })
this._onStatus = this.onStatus.bind(this) this._onStatus = this.onStatus.bind(this)
+1 -1
查看文件
@@ -16,7 +16,7 @@
</view> </view>
<view class="mode-tag-area"> <view class="mode-tag-area">
<text class="mode-tag">{{mode === 1 ? '✨ 智能模式' : '🔄 普通模式'}} · 全脸护理</text> <text class="mode-tag">{{mode === 1 ? '✨ 智能模式' : '🔄 普通模式'}} · {{regionText}}</text>
</view> </view>
<view class="relax-card"> <view class="relax-card">
+11 -1
查看文件
@@ -7,7 +7,9 @@ Page({
checking: false, checking: false,
result: null, result: null,
error: '', error: '',
devMode: false devMode: false,
battery: 0,
connected: false
}, },
onBack: function () { onBack: function () {
@@ -21,6 +23,11 @@ Page({
onLoad: function () { onLoad: function () {
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 })
var device = app.globalData.currentDevice || {}
this.setData({
battery: device.battery || 0,
connected: ble.isConnected()
})
}, },
onShow: function () { onShow: function () {
@@ -34,6 +41,9 @@ Page({
if (this._onStatus) ble.off('status', this._onStatus) if (this._onStatus) ble.off('status', this._onStatus)
this._onStatus = function (status) { this._onStatus = function (status) {
self.setData({ checking: false }) self.setData({ checking: false })
if (status.battery !== undefined) {
self.setData({ battery: status.battery, connected: true })
}
if (status.bind_status === 1) { if (status.bind_status === 1) {
self.setData({ result: 'ok' }) self.setData({ result: 'ok' })
} else { } else {
+3 -2
查看文件
@@ -11,9 +11,10 @@
<view class="device-row-info"> <view class="device-row-info">
<view class="device-row-name"> <view class="device-row-name">
光子美容仪 光子美容仪
<text class="status-badge status-badge-green">已连接</text> <text class="status-badge status-badge-green" wx:if="{{connected}}">已连接</text>
<text class="status-badge" style="background:#ccc;" wx:else>未连接</text>
</view> </view>
<view class="device-row-battery">电量 85%</view> <view class="device-row-battery">电量 {{battery}}%</view>
</view> </view>
</view> </view>
+14
查看文件
@@ -0,0 +1,14 @@
const settingsDao = require('../dao/settings.dao')
let cache = null
let cacheTime = 0
const TTL = 60000 // 1 minute
async function getSettings() {
if (cache && Date.now() - cacheTime < TTL) return cache
cache = await settingsDao.getAll()
cacheTime = Date.now()
return cache
}
module.exports = { getSettings }
+4
查看文件
@@ -3,6 +3,7 @@ const router = require('express').Router()
const { ok, fail } = require('../lib/response') const { ok, fail } = require('../lib/response')
const { signUser, readBearer } = require('../lib/auth') const { signUser, readBearer } = require('../lib/auth')
const { code2Session } = require('../lib/wechat') const { code2Session } = require('../lib/wechat')
const { getSettings } = require('../lib/settings-cache')
const config = require('../config') const config = require('../config')
const userDao = require('../dao/user.dao') const userDao = require('../dao/user.dao')
const logDao = require('../dao/log.dao') const logDao = require('../dao/log.dao')
@@ -10,6 +11,9 @@ const logDao = require('../dao/log.dao')
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next) const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
router.post('/auth/login', wrap(async (req, res) => { router.post('/auth/login', wrap(async (req, res) => {
const settings = await getSettings()
if (settings.maintenance_mode === true) return res.json(fail(2001, '系统维护中,请稍后再试'))
const session = await code2Session(req.body.code || '') const session = await code2Session(req.body.code || '')
let user = await userDao.findByOpenid(session.openid) let user = await userDao.findByOpenid(session.openid)
if (!user) { if (!user) {
+4
查看文件
@@ -2,6 +2,7 @@ const router = require('express').Router()
const { ok, fail } = require('../lib/response') const { ok, fail } = require('../lib/response')
const { requireUser } = require('../middleware/auth') const { requireUser } = require('../middleware/auth')
const { randomHex } = require('../lib/auth') const { randomHex } = require('../lib/auth')
const { getSettings } = require('../lib/settings-cache')
const bindingDao = require('../dao/binding.dao') const bindingDao = require('../dao/binding.dao')
const deviceDao = require('../dao/device.dao') const deviceDao = require('../dao/device.dao')
const commandDao = require('../dao/command.dao') const commandDao = require('../dao/command.dao')
@@ -12,6 +13,9 @@ const logDao = require('../dao/log.dao')
const wrap = fn => (req, res, next) => fn(req, res, next).catch(next) const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)
router.post('/device/bind', requireUser, wrap(async (req, res) => { router.post('/device/bind', requireUser, wrap(async (req, res) => {
const settings = await getSettings()
if (settings.enable_binding === false) return res.json(fail(2001, '设备绑定已关闭'))
const deviceId = String(req.body.device_id || '').trim() const deviceId = String(req.body.device_id || '').trim()
if (!deviceId) return res.json(fail(2001, 'device_id required')) if (!deviceId) return res.json(fail(2001, 'device_id required'))