refactor: migrate to Tencent Cloud backend

这个提交包含在:
Guoguo
2026-04-28 22:56:47 +08:00
父节点 267c75718b
当前提交 444c91c0b0
修改 102 个文件,包含 17927 行新增1997 行删除
+15 -14
查看文件
@@ -12,14 +12,6 @@ App({
onLaunch: function () {
var sysInfo = wx.getSystemInfoSync()
this.globalData.statusBarHeight = sysInfo.statusBarHeight || 44
if (!wx.cloud) {
console.error('请使用 2.2.3 或以上的基础库以使用云能力')
return
}
wx.cloud.init({
traceUser: true
})
this.checkLogin()
},
@@ -43,12 +35,21 @@ App({
},
doLogin: function () {
return http.post('/api/v1/auth/login', {}).then(function (data) {
wx.setStorageSync('token', data.token)
var app = getApp()
app.globalData.userInfo = data.user_info
app.globalData.userId = data.user_id
return data
return new Promise(function (resolve, reject) {
wx.login({
success: function (res) {
http.post('/api/v1/auth/login', { code: res.code }).then(function (data) {
wx.setStorageSync('token', data.token)
var app = getApp()
app.globalData.userInfo = data.user_info
app.globalData.userId = data.user_id
resolve(data)
}).catch(reject)
},
fail: function (err) {
reject({ code: 2002, message: err.errMsg || '微信登录失败' })
}
})
})
}
})
-184
查看文件
@@ -1,184 +0,0 @@
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, data } = event
if (action === 'dashboard') {
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,
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: '未知操作' }
}
@@ -1,9 +0,0 @@
{
"name": "admin",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "~2.6.3"
}
}
-51
查看文件
@@ -1,51 +0,0 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()
const openid = wxContext.OPENID
if (event.action === 'login') {
let userRecord = await db.collection('users').where({ openid }).get()
if (userRecord.data.length === 0) {
const newUser = {
openid,
nickname: '',
avatar_url: '',
phone: '',
gender: 0,
created_at: db.serverDate()
}
await db.collection('users').add({ data: newUser })
userRecord = await db.collection('users').where({ openid }).get()
}
const user = userRecord.data[0]
return {
code: 0,
data: {
token: openid + '_' + Date.now(),
user_id: user._id,
user_info: {
user_id: user._id,
nickname: user.nickname || '用户' + openid.slice(-4),
avatar_url: user.avatar_url || '',
phone: user.phone || '',
gender: user.gender || 0
}
}
}
}
if (event.action === 'refresh') {
const userRecord = await db.collection('users').where({ openid }).get()
if (userRecord.data.length === 0) {
return { code: 1001, message: '用户不存在' }
}
return { code: 0, data: { token: openid + '_' + Date.now() } }
}
return { code: -1, message: '未知操作' }
}
@@ -1,9 +0,0 @@
{
"name": "auth",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "~2.6.3"
}
}
@@ -1,89 +0,0 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()
const openid = wxContext.OPENID
const { action } = event
if (action === 'bind') {
const { device_id } = event.data
if (!device_id) return { code: -1, message: '缺少设备ID' }
const existBind = await db.collection('bindings').where({ openid, status: 'active' }).count()
if (existBind.total > 0) {
const exist = await db.collection('bindings').where({ openid, status: 'active' }).get()
return { code: 2001, message: '已绑定设备', data: { device_id: exist.data[0].device_id } }
}
await db.collection('bindings').add({
data: {
openid,
device_id,
status: 'active',
bind_time: db.serverDate()
}
})
const subExist = await db.collection('subscriptions').where({ openid, status: 'active' }).count()
if (subExist.total === 0) {
await db.collection('subscriptions').add({
data: {
openid,
plan_type: 'trial',
status: 'active',
start_time: db.serverDate(),
end_time: new Date(Date.now() + 7 * 24 * 3600 * 1000),
remaining_days: 7
}
})
}
return {
code: 0,
data: {
device_id,
bind_token: 'bt_' + Date.now(),
subscription: { plan_type: 'trial', remaining_days: 7 }
}
}
}
if (action === 'unbind') {
await db.collection('bindings').where({ openid, status: 'active' }).update({ data: { status: 'inactive' } })
return { code: 0, data: {} }
}
if (action === 'list') {
const bindings = await db.collection('bindings').where({ openid, status: 'active' }).get()
const devices = bindings.data.map(b => ({
device_id: b.device_id,
name: '我的光面膜',
status: 'online',
bind_time: b.bind_time
}))
return { code: 0, data: { devices, total: devices.length } }
}
if (action === 'detail') {
const { device_id } = event.data
const bindings = await db.collection('bindings').where({ openid, 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,
name: '我的光面膜',
firmware_version: '1.0.0',
battery_level: 85,
status: 'online',
bind_time: b.bind_time
}
}
}
return { code: -1, message: '未知操作' }
}
@@ -1,9 +0,0 @@
{
"name": "device",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "~2.6.3"
}
}
@@ -1,58 +0,0 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()
const openid = wxContext.OPENID
const { action } = event
if (action === 'status') {
const subs = await db.collection('subscriptions').where({ openid, status: 'active' }).get()
if (subs.data.length === 0) {
return { code: 0, data: { plan_type: 'none', status: 'inactive', remaining_days: 0 } }
}
const sub = subs.data[0]
const now = new Date()
const end = new Date(sub.end_time)
const remain = Math.max(0, Math.ceil((end - now) / (24 * 3600 * 1000)))
return {
code: 0,
data: {
plan_type: sub.plan_type,
status: remain > 0 ? 'active' : 'expired',
remaining_days: remain,
start_time: sub.start_time,
end_time: sub.end_time
}
}
}
if (action === 'purchase') {
const { plan_type } = event.data
const price = plan_type === 'monthly' ? 99 : 899
const days = plan_type === 'monthly' ? 30 : 365
await db.collection('subscriptions').where({ openid, status: 'active' }).update({ data: { status: 'expired' } })
await db.collection('subscriptions').add({
data: {
openid,
plan_type,
status: 'active',
start_time: db.serverDate(),
end_time: new Date(Date.now() + days * 24 * 3600 * 1000),
remaining_days: days,
price
}
})
return { code: 0, data: { order_id: 'ORD' + Date.now(), plan_type, status: 'paid' } }
}
if (action === 'verify') {
return { code: 0, data: { valid: true } }
}
return { code: -1, message: '未知操作' }
}
@@ -1,9 +0,0 @@
{
"name": "subscription",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "~2.6.3"
}
}
@@ -1,42 +0,0 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()
const openid = wxContext.OPENID
const { action } = event
if (action === 'sync') {
const d = event.data
await db.collection('treatment_records').add({
data: {
openid,
session_id: d.session_id || 'S' + Date.now(),
device_id: d.device_id || '',
regions: d.regions || 0,
total_duration_ms: d.total_duration_ms || 0,
mode: d.mode || 0,
avg_pd: d.avg_pd || 0,
created_at: db.serverDate()
}
})
return { code: 0, data: { session_id: d.session_id || 'S' + Date.now() } }
}
if (action === 'history') {
const { page = 1, page_size = 20 } = event.data || {}
const totalRes = await db.collection('treatment_records').where({ openid }).count()
const records = await db.collection('treatment_records')
.where({ openid })
.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: '未知操作' }
}
@@ -1,9 +0,0 @@
{
"name": "treatment",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "~2.6.3"
}
}
-39
查看文件
@@ -1,39 +0,0 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()
const openid = wxContext.OPENID
const { action } = event
if (action === 'profile') {
const users = await db.collection('users').where({ openid }).get()
if (users.data.length === 0) return { code: 1001, message: '用户不存在' }
const user = users.data[0]
const binds = await db.collection('bindings').where({ openid, status: 'active' }).count()
return {
code: 0,
data: {
user_id: user._id,
nickname: user.nickname || '用户' + openid.slice(-4),
avatar_url: user.avatar_url || '',
phone: user.phone || '',
gender: user.gender || 0,
device_count: binds.total,
created_at: user.created_at
}
}
}
if (action === 'update') {
const updates = {}
if (event.data.nickname) updates.nickname = event.data.nickname
if (event.data.avatar_url) updates.avatar_url = event.data.avatar_url
if (event.data.gender !== undefined) updates.gender = event.data.gender
await db.collection('users').where({ openid }).update({ data: updates })
return { code: 0, data: {} }
}
return { code: -1, message: '未知操作' }
}
@@ -1,9 +0,0 @@
{
"name": "user",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "~2.6.3"
}
}
+12
查看文件
@@ -0,0 +1,12 @@
var ENV = 'test'
var API_BASES = {
local: 'http://localhost:3000',
test: 'https://1426323813-ilxkhlxf4p.ap-guangzhou.tencentscf.com',
prod: 'https://replace-with-scf-prod-url'
}
module.exports = {
ENV: ENV,
API_BASE: API_BASES[ENV]
}
+29 -3
查看文件
@@ -5,13 +5,17 @@ Page({
statusBarHeight: 44,
scanning: true,
scanProgress: 0,
regions: 0x7F,
regionData: [],
error: ''
},
onLoad: function () {
onLoad: function (options) {
var app = getApp()
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
regions: parseInt(options.regions) || 0x7F
})
this.startScan()
},
@@ -38,6 +42,13 @@ Page({
})
ble.queryStatus().catch(function () {})
setTimeout(function () {
clearInterval(progressTimer)
if (!self.data.scanning) return
self.setData({ scanning: false, scanProgress: 100 })
self.parseScanResults({ region_mask: self.data.regions })
}, 5000)
},
parseScanResults: function (status) {
@@ -49,6 +60,21 @@ Page({
},
onNext: function () {
wx.navigateTo({ url: '/pages/treatment-setup/treatment-setup' })
var mask = this.data.regions || 0x7F
ble.setParams({
region_mask: mask,
wavelength: 2,
brightness: 200,
duration_ms: 600000,
mode: 1
}).then(function () {
return ble.startTreatment(mask)
}).then(function () {
wx.redirectTo({
url: '/pages/treating/treating?regions=' + mask + '&wavelength=2&duration=600000&mode=1'
})
}).catch(function (err) {
wx.showToast({ title: err.error_msg || '启动失败', icon: 'none' })
})
}
})
+11 -3
查看文件
@@ -1,4 +1,5 @@
var ble = require('../../services/ble')
var http = require('../../utils/request')
var app = getApp()
var SCAN_TIMEOUT = 15000
@@ -56,9 +57,16 @@ Page({
var userId = app.globalData.userId || ''
ble.on('bind_result', function (result) {
if (result.success) {
self.setData({ state: 'done' })
wx.redirectTo({
url: '/pages/bind-success/bind-success?device_id=' + self.data.deviceId
http.post('/api/v1/device/bind/confirm', {
device_id: self.data.deviceId,
bind_token: self.data.bindToken
}).then(function () {
self.setData({ state: 'done' })
wx.redirectTo({
url: '/pages/bind-success/bind-success?device_id=' + self.data.deviceId
})
}).catch(function (err) {
self.setData({ state: 'error', error: err.message || '后台确认绑定失败' })
})
} else {
self.setData({ state: 'error', error: '设备绑定失败' })
+1 -1
查看文件
@@ -1,5 +1,4 @@
var ble = require('../../services/ble')
var mqtt = require('../../services/mqtt')
var http = require('../../utils/request')
var app = getApp()
@@ -43,6 +42,7 @@ Page({
var devices = data.devices || []
self.setData({ hasDevice: devices.length > 0 })
if (devices.length > 0) {
app.globalData.currentDevice = devices[0]
self.setData({
deviceName: devices[0].name || devices[0].device_id || '我的光面膜',
deviceInfo: devices[0]
+86 -2
查看文件
@@ -1,9 +1,11 @@
var app = getApp()
var http = require('../../utils/request')
Page({
data: {
statusBarHeight: 44,
loading: false
loading: false,
userProfile: null
},
onLoad: function () {
@@ -15,12 +17,94 @@ Page({
var self = this
self.setData({ loading: true })
app.doLogin().then(function () {
self.getWechatProfile().then(function (profile) {
self.setData({ userProfile: profile })
return app.doLogin()
}).then(function () {
return self.saveProfile()
}).then(function () {
self.setData({ loading: false })
wx.switchTab({ url: '/pages/index/index' })
}).catch(function (err) {
self.setData({ loading: false })
wx.showToast({ title: err.message || '登录失败', icon: 'none' })
})
},
getWechatProfile: function () {
return new Promise(function (resolve) {
if (!wx.getUserProfile) {
resolve(null)
return
}
wx.getUserProfile({
desc: '用于完善会员资料',
success: function (res) {
resolve(res.userInfo || null)
},
fail: function () {
resolve(null)
}
})
})
},
saveProfile: function () {
var profile = this.data.userProfile || {}
if (!profile.nickName && !profile.avatarUrl) return Promise.resolve()
return this.updateProfile({
nickname: profile.nickName || '',
avatar: profile.avatarUrl || '',
gender: profile.gender || 0
})
},
updateProfile: function (payload) {
return http.put('/api/v1/user/profile', payload).then(function () {
return app.loadProfile()
})
},
uploadAvatar: function (filePath) {
var ext = 'jpg'
var match = String(filePath).match(/\.([a-zA-Z0-9]+)$/)
if (match && match[1]) ext = match[1].toLowerCase()
var contentType = ext === 'png' ? 'image/png' : 'image/jpeg'
return http.post('/api/v1/user/avatar/upload-url', {
ext: ext,
content_type: contentType
}).then(function (data) {
return new Promise(function (resolve, reject) {
wx.uploadFile({
url: data.upload_url,
filePath: filePath,
name: 'file',
header: { 'Content-Type': contentType },
success: function (res) {
if (res.statusCode >= 200 && res.statusCode < 300) resolve(data.public_url)
else reject({ message: '头像上传失败' })
},
fail: function () { reject({ message: '头像上传失败' }) }
})
})
})
},
onGetPhoneNumber: function (e) {
var self = this
if (!e.detail || !e.detail.code) {
wx.showToast({ title: '已跳过手机号授权', icon: 'none' })
return
}
self.setData({ loading: true })
app.doLogin().then(function () {
return http.post('/api/v1/user/phone', { code: e.detail.code })
}).then(function () {
self.setData({ loading: false })
wx.switchTab({ url: '/pages/index/index' })
}).catch(function (err) {
self.setData({ loading: false })
wx.showToast({ title: err.message || '手机号授权失败', icon: 'none' })
})
}
})
+7 -2
查看文件
@@ -9,14 +9,19 @@
<view class="flower-area">
<text class="flower">🌸</text>
<view class="desc">获取你的手机号</view>
<view class="desc">提供更好的服务</view>
<view class="desc">授权获取微信昵称和头像</view>
<view class="desc">用于完善会员资料</view>
</view>
<button class="login-btn" loading="{{loading}}" bindtap="onLogin" disabled="{{loading}}">
微信授权登录
</button>
<!-- 手机号授权已预留。需要时展示该按钮并隐藏上面的普通登录按钮。 -->
<button class="phone-btn disabled-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber" disabled="{{loading}}">
授权手机号登录(预留)
</button>
<view class="agreement">登录即表示同意《用户协议》和《隐私政策》</view>
</view>
</view>
+21
查看文件
@@ -46,6 +46,27 @@
border: none;
}
.phone-btn {
display: block;
width: 100%;
padding: 28rpx;
background: #ffffff;
color: #E6508C;
border: 2rpx solid #E6508C;
border-radius: 20rpx;
font-size: 30rpx;
text-align: center;
margin-top: 24rpx;
}
.phone-btn::after {
border: none;
}
.disabled-phone-btn {
display: none;
}
.agreement {
text-align: center;
font-size: 24rpx;
+1 -1
查看文件
@@ -51,7 +51,7 @@ Page({
http.post('/api/v1/device/bind', { device_id: deviceId }).then(function (data) {
self.setData({ scanning: false })
wx.redirectTo({
url: '/pages/bind-success/bind-success?device_id=' + deviceId
url: '/pages/ble-connect/ble-connect?device_id=' + encodeURIComponent(deviceId) + '&bind_token=' + encodeURIComponent(data.bind_token || '')
})
}).catch(function (err) {
self.setData({ scanning: false })
+54 -10
查看文件
@@ -1,4 +1,5 @@
var ble = require('../../services/ble')
var commandSync = require('../../services/command-sync')
Page({
data: {
@@ -13,7 +14,9 @@ Page({
temperature: 0,
progress: 0,
paused: false,
completed: false
completed: false,
startedAt: 0,
localTimer: null
},
onLoad: function (options) {
@@ -24,36 +27,67 @@ Page({
wavelength: parseInt(options.wavelength) || 2,
duration: parseInt(options.duration) || 600000,
mode: parseInt(options.mode) || 0,
remainingMs: parseInt(options.duration) || 600000
remainingMs: parseInt(options.duration) || 600000,
startedAt: Date.now()
})
ble.on('status', this.onStatus.bind(this))
ble.on('treatment_complete', this.onComplete.bind(this))
ble.on('exception', this.onException.bind(this))
this.startLocalTimer()
this.syncCommands()
},
onUnload: function () {
ble.off('status')
ble.off('treatment_complete')
ble.off('exception')
if (this.data.localTimer) clearInterval(this.data.localTimer)
},
onStatus: function (status) {
var remaining = status.remaining_ms || 0
startLocalTimer: function () {
var self = this
var timer = setInterval(function () {
if (self.data.completed || self.data.paused) return
var elapsed = Date.now() - self.data.startedAt
var remaining = Math.max(0, self.data.duration - elapsed)
self.updateProgress(remaining)
if (remaining <= 0) {
clearInterval(timer)
self.finishAsComplete()
}
}, 1000)
this.setData({ localTimer: timer })
},
updateProgress: function (remaining) {
var total = this.data.duration
var progress = total > 0 ? Math.round(((total - remaining) / total) * 100) : 0
var mins = Math.floor(remaining / 60000)
var secs = Math.floor((remaining % 60000) / 1000)
var text = ('0' + mins).slice(-2) + ':' + ('0' + secs).slice(-2)
this.setData({
remainingMs: remaining,
remainingText: text,
progress: progress,
remainingText: ('0' + mins).slice(-2) + ':' + ('0' + secs).slice(-2),
progress: progress
})
},
onStatus: function (status) {
var remaining = status.remaining_ms || 0
this.updateProgress(remaining)
this.setData({
battery: status.battery,
temperature: status.temperature,
paused: status.mode_state === 0x03
})
this.syncCommands()
},
syncCommands: function () {
var app = getApp()
var device = app.globalData.currentDevice || {}
var deviceId = device.device_id || this.options.device_id
if (deviceId) commandSync.sync(deviceId)
},
onComplete: function (result) {
@@ -71,6 +105,16 @@ Page({
}, 1000)
},
finishAsComplete: function () {
var elapsed = Math.min(this.data.duration, Date.now() - this.data.startedAt)
this.onComplete({
session_id: 'SESS' + Date.now(),
regions: this.data.regions,
total_duration_ms: elapsed,
avg_pd: 0
})
},
onException: function (err) {
wx.showModal({
title: '设备异常',
@@ -89,8 +133,8 @@ Page({
content: '确定要提前结束本次护理吗?',
success: function (res) {
if (res.confirm) {
ble.stopTreatment().then(function () {
wx.navigateBack()
ble.stopTreatment().catch(function () {}).then(function () {
self.finishAsComplete()
})
}
}
@@ -13,6 +13,7 @@ Page({
selectedMode: 0,
subExpired: false,
subDays: 6,
fixedDurationMs: 600000,
submitting: false,
error: ''
},
@@ -26,7 +27,7 @@ Page({
checkSubscription: function () {
var http = require('../../utils/request')
var self = this
http.get('/api/v1/subscription/status').then(function (sub) {
http.get('/api/v1/subscription').then(function (sub) {
self.setData({
subExpired: sub.status !== 'active',
subDays: sub.remaining_days || 0
@@ -35,6 +36,7 @@ Page({
},
onToggleRegion: function (e) {
if (this.data.selectedMode === 0) return
var idx = e.currentTarget.dataset.idx
var regions = this.data.regions
regions[idx].checked = !regions[idx].checked
@@ -47,7 +49,13 @@ Page({
wx.navigateTo({ url: '/pages/subscribe-prompt/subscribe-prompt' })
return
}
this.setData({ selectedMode: mode })
var data = { selectedMode: mode }
if (mode === 0) {
var regions = this.data.regions
regions.forEach(function (r) { r.checked = true })
data.regions = regions
}
this.setData(data)
},
onStart: function () {
@@ -66,7 +74,7 @@ Page({
if (self.data.selectedMode === 1) {
self.setData({ submitting: false })
wx.navigateTo({ url: '/pages/auto-scan/auto-scan' })
wx.navigateTo({ url: '/pages/auto-scan/auto-scan?regions=' + mask })
return
}
@@ -74,7 +82,7 @@ Page({
region_mask: mask,
wavelength: 2,
brightness: 200,
duration_ms: 600000,
duration_ms: self.data.fixedDurationMs,
mode: self.data.selectedMode
}).then(function () {
return ble.startTreatment(mask)
@@ -83,7 +91,7 @@ Page({
wx.redirectTo({
url: '/pages/treating/treating?regions=' + mask +
'&wavelength=2' +
'&duration=600000' +
'&duration=' + self.data.fixedDurationMs +
'&mode=' + self.data.selectedMode
})
}).catch(function (err) {
@@ -22,13 +22,14 @@
</view>
<view class="page-title" style="font-size:28rpx;">选择护理区域</view>
<view class="mode-tip" wx:if="{{selectedMode === 0}}">普通模式固定 10 分钟,护理区域默认全脸,不可调整</view>
<view class="face-map">
<view class="face-region forehead {{regions[2].checked ? 'active' : ''}}" bindtap="onToggleRegion" data-idx="2">额头</view>
<view class="face-region left-cheek {{regions[0].checked ? 'active' : ''}}" bindtap="onToggleRegion" data-idx="0">左脸</view>
<view class="face-region right-cheek {{regions[1].checked ? 'active' : ''}}" bindtap="onToggleRegion" data-idx="1">右脸</view>
<view class="face-region nose {{regions[4].checked ? 'active' : ''}}" bindtap="onToggleRegion" data-idx="4">鼻唇</view>
<view class="face-region chin {{regions[3].checked ? 'active' : ''}}" bindtap="onToggleRegion" data-idx="3">下巴</view>
<view class="face-region forehead {{regions[2].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="2">额头</view>
<view class="face-region left-cheek {{regions[0].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="0">左脸</view>
<view class="face-region right-cheek {{regions[1].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="1">右脸</view>
<view class="face-region nose {{regions[4].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="4">鼻唇</view>
<view class="face-region chin {{regions[3].checked ? 'active' : ''}} {{selectedMode === 0 ? 'disabled' : ''}}" bindtap="onToggleRegion" data-idx="3">下巴</view>
</view>
<button class="btn-primary" bindtap="onStart" disabled="{{submitting}}" loading="{{submitting}}">
@@ -74,6 +74,19 @@
color: #E6508C;
}
.face-region.disabled {
border-color: #d8d8d8;
background: #eeeeee;
color: #999999;
}
.mode-tip {
color: #999999;
font-size: 24rpx;
text-align: center;
margin: -8rpx 0 20rpx;
}
.face-region.forehead {
top: 5%;
left: 25%;
+1 -2
查看文件
@@ -50,8 +50,7 @@
"libVersion": "3.15.2",
"appid": "wxc4045074ef298510",
"projectname": "hox-beauty",
"cloudfunctionRoot": "cloud-functions/",
"condition": {},
"simulatorPluginLibVersion": {},
"editorSetting": {}
}
}
+47
查看文件
@@ -0,0 +1,47 @@
const ble = require('../services/ble')
function assert(condition, message) {
if (!condition) throw new Error(message)
}
function bytes(buffer) {
return Array.from(new Uint8Array(buffer))
}
function xor(arr) {
return arr.reduce((acc, item) => acc ^ item, 0)
}
function testBuildFrameChecksum() {
const frame = bytes(ble.buildFrame(0x02, [0x7F, 0x01]))
assert(frame[0] === 0xAA && frame[1] === 0x55, 'frame header mismatch')
assert(frame[2] === 0x02, 'frame length mismatch')
assert(frame[3] === 0x02, 'frame type mismatch')
assert(frame[frame.length - 1] === xor(frame.slice(0, -1)), 'checksum must include header, len, type, payload')
}
function testParseStatusFrame() {
const payload = [0x02, 0x7F, 0x02, 200, 0x00, 0x09, 0x27, 0xC0, 0x00, 0x01, 86, 36, 1, 2]
const frame = ble.buildFrame(0x21, payload)
const parsed = ble.parseFrame(frame)
assert(parsed.type === 0x21, 'status type mismatch')
const status = ble.parseStatusReport(parsed.payload)
assert(status.mode_state === 0x02, 'mode_state mismatch')
assert(status.region_mask === 0x7F, 'region mask mismatch')
assert(status.remaining_ms === 600000, 'remaining_ms mismatch')
assert(status.battery === 86, 'battery mismatch')
assert(status.temperature === 36, 'temperature mismatch')
}
function testBindPayloadLength() {
const userBytes = ble.hexToBytes('0000000000000001')
const tokenBytes = ble.hexToBytes('0011223344556677')
assert(userBytes.length === 8, 'user id must be 8 bytes')
assert(tokenBytes.length === 8, 'bind token must be 8 bytes')
}
testBuildFrameChecksum()
testParseStatusFrame()
testBindPayloadLength()
console.log('BLE frame tests passed')
+3 -3
查看文件
@@ -126,8 +126,8 @@ function buildFrame(type, payload) {
if (payload && payload.length > 0) {
frame = frame.concat(payload)
}
var checkBytes = frame.slice(2)
frame.push(xorChecksum(checkBytes))
var checkBytes = frame.slice(0)
frame.push(xorChecksum(checkBytes))
return bytesToBuffer(frame)
}
@@ -140,7 +140,7 @@ function parseFrame(buffer) {
var type = bytes[3]
var payload = bytes.slice(4, 4 + len)
var checksum = bytes[4 + len]
var expected = xorChecksum(bytes.slice(2, 4 + len))
var expected = xorChecksum(bytes.slice(0, 4 + len))
if (checksum !== expected) return null
return { type: type, payload: payload, seq: payload.length > 0 ? payload[payload.length - 1] : 0 }
}
+68
查看文件
@@ -0,0 +1,68 @@
var ble = require('./ble')
var http = require('../utils/request')
var syncing = false
function commandPayload(command) {
return command.payload || {}
}
function execute(command) {
var payload = commandPayload(command)
switch (Number(command.opcode)) {
case ble.CMD.SET_PARAMS:
return ble.setParams({
region_mask: payload.region_mask,
wavelength: payload.wavelength,
brightness: payload.brightness,
duration_ms: payload.duration_ms,
mode: payload.mode
})
case ble.CMD.START:
return ble.startTreatment(payload.region_mask)
case ble.CMD.STOP:
return ble.stopTreatment()
case ble.CMD.QUERY_STATUS:
return ble.queryStatus()
default:
return Promise.reject({ error_code: 0xFE, error_msg: 'unsupported opcode' })
}
}
function report(command, success, result) {
return http.post('/api/v1/device/command/result', {
command_id: command.seq || command.command_id,
seq: command.seq || command.command_id,
success: success,
opcode: command.opcode,
result: result || null
}).catch(function () {})
}
function runOne(command) {
return execute(command).then(function (result) {
return report(command, true, result)
}).catch(function (err) {
return report(command, false, err)
})
}
function sync(deviceId) {
if (syncing || !deviceId || !ble.isConnected()) return Promise.resolve()
syncing = true
return http.get('/api/v1/device/command/pending', { device_id: deviceId }).then(function (data) {
var chain = Promise.resolve()
var commands = data.commands || []
commands.forEach(function (command) {
chain = chain.then(function () { return runOne(command) })
})
return chain
}).catch(function () {}).then(function () {
syncing = false
})
}
module.exports = {
sync: sync,
execute: execute
}
-172
查看文件
@@ -1,172 +0,0 @@
var request = require('../utils/request')
var MQTT_BROKER = 'wxs://iotcloud.tencent.com/socket/mqtt'
var _socketTask = null
var _connected = false
var _subscriptions = {}
var _listeners = {}
var _reconnectTimer = null
var _heartbeatTimer = null
var _userId = null
function on(event, callback) {
if (!_listeners[event]) _listeners[event] = []
_listeners[event].push(callback)
}
function off(event, callback) {
if (!_listeners[event]) return
if (callback) {
_listeners[event] = _listeners[event].filter(function (cb) { return cb !== callback })
} else {
_listeners[event] = []
}
}
function emit(event, data) {
if (!_listeners[event]) return
_listeners[event].forEach(function (cb) {
try { cb(data) } catch (e) { console.error('mqtt emit error:', e) }
})
}
function connect(userId) {
_userId = userId
var clientId = 'user_' + userId
var token = wx.getStorageSync('token')
_socketTask = wx.connectSocket({
url: MQTT_BROKER,
header: {
'Authorization': 'Bearer ' + token
},
success: function () {
console.log('mqtt connecting...')
},
fail: function () {
emit('error', { msg: 'MQTT连接失败' })
scheduleReconnect()
}
})
_socketTask.onOpen(function () {
_connected = true
startHeartbeat()
subscribeUserTopics()
emit('connected')
})
_socketTask.onMessage(function (res) {
handleMessage(res.data)
})
_socketTask.onClose(function () {
_connected = false
stopHeartbeat()
emit('disconnected')
scheduleReconnect()
})
_socketTask.onError(function () {
_connected = false
emit('error', { msg: 'MQTT连接异常' })
})
}
function subscribeUserTopics() {
if (!_userId) return
subscribe('users/' + _userId + '/subscription')
subscribe('users/' + _userId + '/devices')
subscribe('users/' + _userId + '/notification')
}
function subscribe(topic) {
_subscriptions[topic] = true
}
function unsubscribe(topic) {
delete _subscriptions[topic]
}
function handleMessage(data) {
try {
var msg = JSON.parse(data)
var topic = msg.topic
if (topic && _subscriptions[topic]) {
if (topic.indexOf('/subscription') !== -1) {
emit('subscription_change', msg.payload || msg)
} else if (topic.indexOf('/devices') !== -1) {
emit('devices_change', msg.payload || msg)
} else if (topic.indexOf('/notification') !== -1) {
emit('notification', msg.payload || msg)
} else {
emit('message', msg.payload || msg)
}
}
} catch (e) {
console.error('mqtt parse error:', e)
}
}
function publish(topic, payload) {
if (!_connected || !_socketTask) return
_socketTask.send({
data: JSON.stringify({ topic: topic, payload: payload })
})
}
function startHeartbeat() {
stopHeartbeat()
_heartbeatTimer = setInterval(function () {
if (_connected && _socketTask) {
_socketTask.send({ data: JSON.stringify({ type: 'ping' }) })
}
}, 30000)
}
function stopHeartbeat() {
if (_heartbeatTimer) {
clearInterval(_heartbeatTimer)
_heartbeatTimer = null
}
}
function scheduleReconnect() {
if (_reconnectTimer) return
_reconnectTimer = setTimeout(function () {
_reconnectTimer = null
if (_userId) connect(_userId)
}, 5000)
}
function disconnect() {
if (_reconnectTimer) {
clearTimeout(_reconnectTimer)
_reconnectTimer = null
}
stopHeartbeat()
if (_socketTask) {
_socketTask.close({})
_socketTask = null
}
_connected = false
_subscriptions = {}
_listeners = {}
_userId = null
}
function isConnected() {
return _connected
}
module.exports = {
connect: connect,
disconnect: disconnect,
subscribe: subscribe,
unsubscribe: unsubscribe,
publish: publish,
on: on,
off: off,
isConnected: isConnected
}
+2 -64
查看文件
@@ -1,45 +1,7 @@
var USE_CLOUD = true
function callCloud(funcName, action, data) {
return new Promise(function (resolve, reject) {
wx.cloud.callFunction({
name: funcName,
data: { action: action, data: data }
}).then(function (res) {
if (res.result && res.result.code === 0) {
resolve(res.result.data)
} else if (res.result && (res.result.code === 1001 || res.result.code === 1002)) {
wx.removeStorageSync('token')
wx.reLaunch({ url: '/pages/login/login' })
reject(res.result)
} else {
reject(res.result || { code: -1, message: '请求失败' })
}
}).catch(function (err) {
reject({ code: 2002, message: err.errMsg || '网络异常' })
})
})
}
var mock = null
if (!USE_CLOUD) {
mock = require('./mock')
}
var API_BASE = 'https://api.lightmask.com'
var config = require('../config/env')
var API_BASE = config.API_BASE
function httpRequest(options) {
if (mock && mock.enabled) {
return new Promise(function (resolve) {
setTimeout(function () {
var result = mock.handle(options.method || 'GET', options.path, options.data)
if (result.code === 0) {
resolve(result.data)
}
}, 200)
})
}
var token = wx.getStorageSync('token')
return new Promise(function (resolve, reject) {
@@ -71,31 +33,7 @@ function httpRequest(options) {
})
}
var FUNC_MAP = {
'/api/v1/auth/login': { fn: 'auth', action: 'login' },
'/api/v1/auth/refresh': { fn: 'auth', action: 'refresh' },
'/api/v1/user/profile': { fn: 'user', action: 'profile' },
'/api/v1/user/update': { fn: 'user', action: 'update' },
'/api/v1/device/bind': { fn: 'device', action: 'bind' },
'/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' },
'/api/v1/treatment/sync': { fn: 'treatment', action: 'sync' },
'/api/v1/treatment/history': { fn: 'treatment', action: 'history' }
}
function request(options) {
if (USE_CLOUD) {
var mapping = FUNC_MAP[options.path]
if (mapping) {
return callCloud(mapping.fn, mapping.action, options.data)
}
return Promise.reject({ code: -1, message: '未映射的接口' })
}
return httpRequest(options)
}