refactor: restructure entire project for human maintainability

Server:
- Add Express framework, replace custom router/request parser
- Create DAO layer (12 files) centralizing all 73 SQL queries
- Rewrite 7 route files as thin Express controllers calling DAOs
- Add SCF-to-Express adapter (lib/serverless.js)
- Add auth middleware (middleware/auth.js)
- Remove dead code from lib/auth.js

Admin console:
- Extract DataTable component (table + pagination)
- Extract ConfirmModal component (modal + form styles)
- Create listMixin for paginated list pages
- Move form styles to common.css for slot compatibility
- Refactor device + subscription pages as examples

Miniprogram:
- Split 734-line BLE monolith into 4 focused modules
  (protocol, connection, commands, barrel index)
- Create API module (utils/api.js) with named functions
- Create page utilities (utils/page.js)
- Refactor index + profile pages to use API module
这个提交包含在:
Guoguo
2026-04-29 05:58:20 -07:00
父节点 9f0e629c82
当前提交 bb4b80f867
修改 46 个文件,包含 4306 行新增2020 行删除
+69 -146
查看文件
@@ -16,61 +16,46 @@
</view>
</view>
<view class="page-card">
<view class="data-table">
<view class="t-header">
<view class="t-row">
<text class="t-th flex2">设备编号</text>
<text class="t-th flex2">产品编号</text>
<text class="t-th flex2">绑定用户</text>
<text class="t-th flex1">电量</text>
<text class="t-th flex1">固件版本</text>
<text class="t-th flex2">绑定时间</text>
<text class="t-th flex1">状态</text>
<text class="t-th flex1">操作</text>
</view>
<DataTable
:columns="columns"
:records="records"
:page="page"
:totalPages="totalPages"
@page="onPage"
>
<template #rows>
<view class="t-row" v-for="item in records" :key="item.device_id">
<text class="t-td flex2">{{ item.device_id }}</text>
<text class="t-td flex2">{{ item.product_id || 'HOX_LIGHT_MASK' }}</text>
<text class="t-td flex2">{{ item.bound_user || '-' }}</text>
<text class="t-td flex1">{{ item.battery != null ? item.battery + '%' : '-' }}</text>
<text class="t-td flex1">{{ item.firmware_version || item.fw_version || '-' }}</text>
<text class="t-td flex2">{{ formatDate(item.activated_at) }}</text>
<text class="t-td flex1">
<text :class="statusBadge(item.status)">{{ statusMap[item.status] || '未知' }}</text>
</text>
<text class="t-td flex1">
<text class="action-link" @click="onDetail(item.device_id)">详情</text>
<text class="action-link" v-if="item.bound_user" @click="onUnbind(item)">解绑</text>
</text>
</view>
<view class="t-body">
<view class="t-row" v-for="item in devices" :key="item.device_id">
<text class="t-td flex2">{{ item.device_id }}</text>
<text class="t-td flex2">{{ item.product_id || 'HOX_LIGHT_MASK' }}</text>
<text class="t-td flex2">{{ item.bound_user || '-' }}</text>
<text class="t-td flex1">{{ item.battery != null ? item.battery + '%' : '-' }}</text>
<text class="t-td flex1">{{ item.firmware_version || item.fw_version || '-' }}</text>
<text class="t-td flex2">{{ formatDate(item.activated_at) }}</text>
<text class="t-td flex1">
<text :class="statusBadge(item.status)">{{ statusMap[item.status] || '未知' }}</text>
</text>
<text class="t-td flex1">
<text class="action-link" @click="onDetail(item.device_id)">详情</text>
<text class="action-link" v-if="item.bound_user" @click="onUnbind(item)">解绑</text>
</text>
</view>
</view>
</view>
</template>
</DataTable>
<view class="pagination">
<button class="btn-page" :disabled="page <= 1" @click="onPage(page - 1)"></button>
<button class="btn-page active">{{ page }}</button>
<button class="btn-page" :disabled="page >= totalPages" @click="onPage(page + 1)"></button>
<text class="page-info"> {{ totalPages }} </text>
<ConfirmModal
:visible="showBatchImport"
title="批量导入设备"
confirmText="导入"
:loading="batchImporting"
@close="showBatchImport = false"
@confirm="onBatchImport"
>
<view class="form-group">
<text class="form-label">设备编号每行一个</text>
<textarea class="form-textarea" v-model="batchDeviceIds" placeholder="输入设备编号,每行一个&#10;例如:&#10;HOX001&#10;HOX002&#10;HOX003" :maxlength="-1"></textarea>
</view>
</view>
<view class="modal-mask" v-if="showBatchImport" @click="showBatchImport = false">
<view class="modal-content" @click.stop>
<view class="modal-title">批量导入设备</view>
<view class="form-group">
<text class="form-label">设备编号每行一个</text>
<textarea class="form-textarea" v-model="batchDeviceIds" placeholder="输入设备编号,每行一个&#10;例如:&#10;HOX001&#10;HOX002&#10;HOX003" :maxlength="-1"></textarea>
</view>
<view class="batch-hint">最多 500 个设备</view>
<view class="modal-actions">
<button class="btn-default" @click="showBatchImport = false">取消</button>
<button class="btn-primary" @click="onBatchImport" :loading="batchImporting">导入</button>
</view>
</view>
</view>
<view class="batch-hint">最多 500 个设备</view>
</ConfirmModal>
</AdminLayout>
</template>
@@ -78,54 +63,48 @@
import { get, post } from '../../utils/request'
import { exportCSV } from '../../utils/export'
import { formatDateShort } from '../../utils/format'
import { listMixin } from '../../utils/useList'
import AdminLayout from '../../components/AdminLayout.vue'
import DataTable from '../../components/DataTable.vue'
import ConfirmModal from '../../components/ConfirmModal.vue'
var STATUS_MAP = { 1: '未激活', 2: '在线', 3: '离线', 4: '故障' }
var STATUS_BADGE = { 2: 'badge badge-success', 3: 'badge badge-warning', 1: 'badge badge-blue', 4: 'badge badge-error' }
export default {
components: { AdminLayout },
components: { AdminLayout, DataTable, ConfirmModal },
mixins: [listMixin('/api/v1/admin/devices')],
data() {
return {
devices: [],
total: 0,
page: 1,
pageSize: 20,
keyword: '',
statusMap: { 1: '未激活', 2: '在线', 3: '离线', 4: '故障' },
statusMap: STATUS_MAP,
columns: [
{ key: 'device_id', label: '设备编号', flex: 2 },
{ key: 'product_id', label: '产品编号', flex: 2 },
{ key: 'bound_user', label: '绑定用户', flex: 2 },
{ key: 'battery', label: '电量', flex: 1 },
{ key: 'fw', label: '固件版本', flex: 1 },
{ key: 'activated_at', label: '绑定时间', flex: 2 },
{ key: 'status', label: '状态', flex: 1 },
{ key: 'actions', label: '操作', flex: 1 }
],
showBatchImport: false,
batchDeviceIds: '',
batchImporting: false
}
},
computed: {
totalPages() {
return Math.ceil(this.total / this.pageSize) || 1
}
},
onShow() {
this.loadDevices()
this.reload()
},
methods: {
async loadDevices() {
try {
const data = await get('/api/v1/admin/devices', { page: this.page, page_size: this.pageSize, keyword: this.keyword })
this.devices = data.records || []
this.total = data.total || 0
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
},
onSearch() {
this.page = 1
this.loadDevices()
},
onPage(p) {
this.page = p
this.loadDevices()
reload() {
this.loadList({ keyword: this.keyword })
},
onDetail(deviceId) {
uni.navigateTo({ url: '/pages/device-detail/index?device_id=' + deviceId })
},
onCreateDevice() {
const self = this
var self = this
uni.showModal({
title: '预生成产品码',
editable: true,
@@ -135,7 +114,7 @@ export default {
try {
await post('/api/v1/admin/devices', { device_id: res.content.trim(), product_id: 'HOX_LIGHT_MASK' })
uni.showToast({ title: '创建成功', icon: 'success' })
self.loadDevices()
self.reload()
} catch (e) {
uni.showToast({ title: '创建失败', icon: 'none' })
}
@@ -143,7 +122,7 @@ export default {
})
},
onUnbind(item) {
const self = this
var self = this
uni.showModal({
title: '确认解绑',
content: '确定要解绑设备 ' + item.device_id + ' 吗?',
@@ -152,7 +131,7 @@ export default {
try {
await post('/api/v1/admin/devices/' + item.device_id + '/unbind', {})
uni.showToast({ title: '解绑成功', icon: 'success' })
self.loadDevices()
self.reload()
} catch (e) {
uni.showToast({ title: '解绑失败', icon: 'none' })
}
@@ -161,12 +140,11 @@ export default {
})
},
statusBadge(status) {
const map = { 2: 'badge badge-success', 3: 'badge badge-warning', 1: 'badge badge-blue', 4: 'badge badge-error' }
return map[status] || 'badge badge-default'
return STATUS_BADGE[status] || 'badge badge-default'
},
formatDate: formatDateShort,
async onBatchImport() {
const ids = this.batchDeviceIds.split('\n').map(s => s.trim()).filter(Boolean)
var ids = this.batchDeviceIds.split('\n').map(function (s) { return s.trim() }).filter(Boolean)
if (ids.length === 0) {
uni.showToast({ title: '请输入设备编号', icon: 'none' })
return
@@ -177,11 +155,11 @@ export default {
}
this.batchImporting = true
try {
const result = await post('/api/v1/admin/devices/batch', { device_ids: ids })
var result = await post('/api/v1/admin/devices/batch', { device_ids: ids })
uni.showToast({ title: '导入 ' + result.created + ' 个设备', icon: 'success' })
this.showBatchImport = false
this.batchDeviceIds = ''
this.loadDevices()
this.reload()
} catch (e) {
uni.showToast({ title: '导入失败', icon: 'none' })
} finally {
@@ -190,8 +168,8 @@ export default {
},
async onExport() {
try {
const data = await get('/api/v1/admin/devices', { page: 1, page_size: 9999, keyword: this.keyword })
const records = data.records || []
var data = await get('/api/v1/admin/devices', { page: 1, page_size: 9999, keyword: this.keyword })
var records = data.records || []
exportCSV('devices_' + new Date().toISOString().slice(0, 10) + '.csv',
['设备编号', '产品编号', '绑定用户', '电量', '固件版本', '绑定时间', '状态'],
records.map(function (r) {
@@ -210,64 +188,9 @@ export default {
<style scoped>
@import '../../styles/common.css';
.pagination {
justify-content: flex-end;
}
.modal-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 480px;
background: #fff;
border-radius: 8px;
padding: 24px;
}
.modal-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 14px;
color: #333;
margin-bottom: 6px;
}
.form-textarea {
width: 100%;
height: 160px;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 12px;
font-size: 14px;
box-sizing: border-box;
resize: vertical;
}
.batch-hint {
font-size: 12px;
color: #999;
margin-bottom: 16px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>
+89 -166
查看文件
@@ -28,76 +28,64 @@
</view>
</view>
<view class="page-card">
<view class="tabs">
<view class="tab" :class="{ active: activeTab === 'all' }" @click="onTabFilter('all')">全部订阅</view>
<view class="tab" :class="{ active: activeTab === 'monthly' }" @click="onTabFilter('monthly')">月卡</view>
<view class="tab" :class="{ active: activeTab === 'yearly' }" @click="onTabFilter('yearly')">年卡</view>
<view class="tab" :class="{ active: activeTab === 'trial' }" @click="onTabFilter('trial')">试用中</view>
<view class="tab" :class="{ active: activeTab === 'expired' }" @click="onTabFilter('expired')">已过期</view>
</view>
<DataTable
:columns="columns"
:records="records"
:page="page"
:totalPages="totalPages"
@page="onPage"
>
<template #toolbar>
<view class="tabs">
<view class="tab" :class="{ active: activeTab === 'all' }" @click="onTabFilter('all')">全部订阅</view>
<view class="tab" :class="{ active: activeTab === 'monthly' }" @click="onTabFilter('monthly')">月卡</view>
<view class="tab" :class="{ active: activeTab === 'yearly' }" @click="onTabFilter('yearly')">年卡</view>
<view class="tab" :class="{ active: activeTab === 'trial' }" @click="onTabFilter('trial')">试用中</view>
<view class="tab" :class="{ active: activeTab === 'expired' }" @click="onTabFilter('expired')">已过期</view>
</view>
</template>
<view class="data-table">
<view class="t-header">
<view class="t-row">
<text class="t-th flex2">用户</text>
<text class="t-th flex1">订阅类型</text>
<text class="t-th flex1">订单金额</text>
<text class="t-th flex2">开始日期</text>
<text class="t-th flex2">到期日期</text>
<text class="t-th flex1">状态</text>
<text class="t-th flex1">操作</text>
</view>
<template #rows>
<view class="t-row" v-for="item in records" :key="item.id">
<text class="t-td flex2">{{ item.nickname || item.user_id }}</text>
<text class="t-td flex1">{{ planText(item.plan) }}</text>
<text class="t-td flex1">¥{{ item.amount || '-' }}</text>
<text class="t-td flex2">{{ formatDate(item.start_time) }}</text>
<text class="t-td flex2">{{ formatDate(item.expire_time) }}</text>
<text class="t-td flex1">
<text :class="statusBadge(item.status)">{{ statusText(item.status) }}</text>
</text>
<text class="t-td flex1">
<text class="action-link" @click="onDetail(item)">详情</text>
<text class="action-link" v-if="item.status === 1" @click="onExtend(item)">延期</text>
<text class="action-link" v-if="item.status === 1" @click="onCancel(item)">取消</text>
<text class="action-link" v-if="item.status === 2 || item.status === 3" @click="onRenew(item)">续费</text>
</text>
</view>
<view class="t-body">
<view class="t-row" v-for="item in subscriptions" :key="item.id">
<text class="t-td flex2">{{ item.nickname || item.user_id }}</text>
<text class="t-td flex1">{{ planText(item.plan) }}</text>
<text class="t-td flex1">¥{{ item.amount || '-' }}</text>
<text class="t-td flex2">{{ formatDate(item.start_time) }}</text>
<text class="t-td flex2">{{ formatDate(item.expire_time) }}</text>
<text class="t-td flex1">
<text :class="statusBadge(item.status)">{{ statusText(item.status) }}</text>
</text>
<text class="t-td flex1">
<text class="action-link" @click="onDetail(item)">详情</text>
<text class="action-link" v-if="item.status === 1" @click="onExtend(item)">延期</text>
<text class="action-link" v-if="item.status === 1" @click="onCancel(item)">取消</text>
<text class="action-link" v-if="item.status === 2 || item.status === 3" @click="onRenew(item)">续费</text>
</text>
</view>
</view>
</view>
</template>
</DataTable>
<view class="pagination">
<button class="btn-page" :disabled="page <= 1" @click="onPage(page - 1)"></button>
<button class="btn-page active">{{ page }}</button>
<button class="btn-page" :disabled="page >= totalPages" @click="onPage(page + 1)"></button>
<text class="page-info"> {{ totalPages }} </text>
<ConfirmModal
:visible="showCreate"
title="创建订阅"
confirmText="创建"
:loading="creating"
@close="showCreate = false"
@confirm="onCreate"
>
<view class="form-group">
<text class="form-label">用户ID</text>
<input class="form-input" v-model="createForm.user_id" placeholder="输入用户ID" />
</view>
</view>
<view class="modal-mask" v-if="showCreate" @click="showCreate = false">
<view class="modal-content" @click.stop>
<view class="modal-title">创建订阅</view>
<view class="form-group">
<text class="form-label">用户ID</text>
<input class="form-input" v-model="createForm.user_id" placeholder="输入用户ID" />
</view>
<view class="form-group">
<text class="form-label">方案</text>
<input class="form-input" v-model="createForm.plan" placeholder="monthly/quarterly/yearly" />
</view>
<view class="form-group">
<text class="form-label">天数</text>
<input class="form-input" v-model="createForm.days" placeholder="订阅天数" type="number" />
</view>
<view class="modal-actions">
<button class="btn-default" @click="showCreate = false">取消</button>
<button class="btn-primary" @click="onCreate" :loading="creating">创建</button>
</view>
<view class="form-group">
<text class="form-label">方案</text>
<input class="form-input" v-model="createForm.plan" placeholder="monthly/quarterly/yearly" />
</view>
</view>
<view class="form-group">
<text class="form-label">天数</text>
<input class="form-input" v-model="createForm.days" placeholder="订阅天数" type="number" />
</view>
</ConfirmModal>
</AdminLayout>
</template>
@@ -105,50 +93,50 @@
import { get, post } from '../../utils/request'
import { exportCSV } from '../../utils/export'
import { formatDateShort } from '../../utils/format'
import { listMixin } from '../../utils/useList'
import AdminLayout from '../../components/AdminLayout.vue'
import DataTable from '../../components/DataTable.vue'
import ConfirmModal from '../../components/ConfirmModal.vue'
var PLAN_MAP = { monthly: '月卡', quarterly: '季卡', yearly: '年卡', trial: '试用' }
var STATUS_TEXT = { 1: '生效中', 2: '已过期', 3: '已取消' }
var STATUS_BADGE = { 1: 'badge badge-success', 2: 'badge badge-error', 3: 'badge badge-default' }
export default {
components: { AdminLayout },
components: { AdminLayout, DataTable, ConfirmModal },
mixins: [listMixin('/api/v1/admin/subscriptions')],
data() {
return {
subscriptions: [],
total: 0,
page: 1,
pageSize: 20,
stats: {},
activeTab: 'all',
columns: [
{ key: 'user', label: '用户', flex: 2 },
{ key: 'plan', label: '订阅类型', flex: 1 },
{ key: 'amount', label: '订单金额', flex: 1 },
{ key: 'start', label: '开始日期', flex: 2 },
{ key: 'expire', label: '到期日期', flex: 2 },
{ key: 'status', label: '状态', flex: 1 },
{ key: 'actions', label: '操作', flex: 1 }
],
showCreate: false,
creating: false,
createForm: { user_id: '', plan: 'monthly', days: 30 },
activeTab: 'all'
}
},
computed: {
totalPages() {
return Math.ceil(this.total / this.pageSize) || 1
createForm: { user_id: '', plan: 'monthly', days: 30 }
}
},
onShow() {
this.loadSubscriptions()
this.reload()
},
methods: {
async loadSubscriptions() {
try {
const data = await get('/api/v1/admin/subscriptions', { page: this.page, page_size: this.pageSize, tab: this.activeTab })
this.subscriptions = data.records || []
this.total = data.total || 0
if (data.stats) this.stats = data.stats
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
reload() {
this.loadList({ tab: this.activeTab })
},
onListLoaded(data) {
if (data.stats) this.stats = data.stats
},
onTabFilter(tab) {
this.activeTab = tab
this.page = 1
this.loadSubscriptions()
},
onPage(p) {
this.page = p
this.loadSubscriptions()
this.reload()
},
async onCreate() {
this.creating = true
@@ -159,7 +147,7 @@ export default {
days: parseInt(this.createForm.days)
})
this.showCreate = false
this.loadSubscriptions()
this.reload()
uni.showToast({ title: '创建成功', icon: 'success' })
} catch (e) {
uni.showToast({ title: '创建失败', icon: 'none' })
@@ -186,7 +174,7 @@ export default {
days: 30
})
uni.showToast({ title: '延期成功', icon: 'success' })
self.loadSubscriptions()
self.reload()
} catch (e) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
@@ -206,7 +194,7 @@ export default {
subscription_id: item.subscription_id
})
uni.showToast({ title: '已取消', icon: 'success' })
self.loadSubscriptions()
self.reload()
} catch (e) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
@@ -220,29 +208,18 @@ export default {
this.createForm.days = 30
this.showCreate = true
},
planText(plan) {
const map = { monthly: '月卡', quarterly: '季卡', yearly: '年卡', trial: '试用' }
return map[plan] || plan || '-'
},
statusText(status) {
const map = { 1: '生效中', 2: '已过期', 3: '已取消' }
return map[status] || '-'
},
statusBadge(status) {
const map = { 1: 'badge badge-success', 2: 'badge badge-error', 3: 'badge badge-default' }
return map[status] || 'badge badge-default'
},
planText(plan) { return PLAN_MAP[plan] || plan || '-' },
statusText(status) { return STATUS_TEXT[status] || '-' },
statusBadge(status) { return STATUS_BADGE[status] || 'badge badge-default' },
formatDate: formatDateShort,
async onExport() {
try {
const data = await get('/api/v1/admin/subscriptions', { page: 1, page_size: 9999, tab: this.activeTab })
const records = data.records || []
var planMap = { monthly: '月卡', quarterly: '季卡', yearly: '年卡', trial: '试用' }
var statusMap = { 1: '生效中', 2: '已过期', 3: '已取消' }
var data = await get('/api/v1/admin/subscriptions', { page: 1, page_size: 9999, tab: this.activeTab })
var records = data.records || []
exportCSV('subscriptions_' + new Date().toISOString().slice(0, 10) + '.csv',
['用户', '订阅类型', '订单金额', '开始日期', '到期日期', '状态'],
records.map(function (r) {
return [r.nickname || r.user_id || '', planMap[r.plan] || r.plan || '', r.amount || '', r.start_time ? String(r.start_time).slice(0, 10) : '', r.expire_time ? String(r.expire_time).slice(0, 10) : '', statusMap[r.status] || '']
return [r.nickname || r.user_id || '', PLAN_MAP[r.plan] || r.plan || '', r.amount || '', r.start_time ? String(r.start_time).slice(0, 10) : '', r.expire_time ? String(r.expire_time).slice(0, 10) : '', STATUS_TEXT[r.status] || '']
})
)
uni.showToast({ title: '导出成功', icon: 'success' })
@@ -308,58 +285,4 @@ export default {
.action-link {
margin-right: 8px;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 440px;
background: #fff;
border-radius: 8px;
padding: 24px;
}
.modal-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 14px;
color: #333;
margin-bottom: 6px;
}
.form-input {
width: 100%;
height: 36px;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 0 12px;
font-size: 14px;
box-sizing: border-box;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 20px;
}
</style>