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
这个提交包含在:
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户