refactor: migrate to Tencent Cloud backend
这个提交包含在:
+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 || '微信登录失败' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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' })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,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]
|
||||
|
||||
@@ -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' })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -50,8 +50,7 @@
|
||||
"libVersion": "3.15.2",
|
||||
"appid": "wxc4045074ef298510",
|
||||
"projectname": "hox-beauty",
|
||||
"cloudfunctionRoot": "cloud-functions/",
|
||||
"condition": {},
|
||||
"simulatorPluginLibVersion": {},
|
||||
"editorSetting": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户