文件
jw-beauty/docs/architecture/03-管理后台架构.md
Guoguo b754926937 fix: clear stale token expiry on auth failure, correct doc inaccuracies
- request.js: also remove admin_token_expiry when clearing auth on
  1001/1002 response, preventing stale expiry value in storage
- 01-服务端架构.md: fix table count from 10 to 11
- 03-管理后台架构.md: add /api/v1 prefix to all API endpoint paths
2026-04-28 18:49:00 -07:00

387 行
12 KiB
Markdown

此文件含有不可见的 Unicode 字符
此文件含有人类无法区分的不可见的 Unicode 字符,但可以由计算机进行不同的处理。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 管理后台架构
## 概述
管理后台是一个基于 uni-app + Vue 3 的单页应用,编译目标为 H5(浏览器),部署在腾讯云 COS 上作为静态站点。通过调用服务端 Admin API 实现设备管理、用户管理、订阅管理等后台功能。
**技术栈:** uni-app + Vue 3 + Pinia + Vite
**AppID** `__UNI__HOXADMIN`
**路由模式:** Hash`#/pages/dashboard/index`
---
## 目录结构
```
admin-console/
├── index.html # SPA 入口 HTML
├── package.json
├── vite.config.js # Vite 配置(仅引入 uni 插件)
├── scripts/
│ └── deploy-cos.js # COS 静态站点部署脚本
└── src/
├── App.vue # 根组件(登录状态检查)
├── main.js # 应用入口(createSSRApp + Pinia
├── manifest.json # uni-app 配置
├── pages.json # 页面路由定义
├── config/
│ └── env.js # 环境配置(API 地址)
├── components/
│ └── AdminLayout.vue # 侧边栏 + 顶栏布局组件
├── store/
│ └── user.js # Pinia 用户状态(token、管理员信息)
├── utils/
│ ├── request.js # HTTP 请求封装(Bearer token 注入)
│ ├── export.js # CSV 导出工具
│ └── format.js # 日期格式化
├── styles/
│ └── common.css # 表格、徽标、按钮等公共样式
└── pages/
├── login/index.vue # 登录页
├── dashboard/index.vue # 仪表盘
├── device/index.vue # 设备列表
├── device-detail/index.vue # 设备详情
├── user/index.vue # 用户列表
├── user-detail/index.vue # 用户详情
├── subscription/index.vue# 订阅管理
├── record/index.vue # 护理记录
├── log/index.vue # 操作日志
└── settings/index.vue # 系统设置
```
共 10 个页面,1 个全局组件,1 个 Store。
---
## 应用启动流程
```
index.html → main.js
├── createSSRApp(App)
├── app.use(createPinia())
└── App.vue onLaunch()
├── useUserStore().isLoggedIn()
│ ├── 检查 Storage 中 admin_token 是否存在
│ └── 检查 admin_token_expiry 是否未过期(7天有效)
├── 已登录 → 停留在当前页
└── 未登录 → uni.reLaunch('/pages/login/index')
```
---
## 页面一览
### 登录页
| 项目 | 说明 |
|------|------|
| 路径 | `/pages/login/index` |
| 导航栏 | 自定义(无系统导航栏) |
| API | `POST /api/v1/admin/login` |
| 功能 | 用户名+密码登录,成功后跳转到仪表盘 |
登录成功后:
1. `userStore.login()` 保存 token 和管理员信息
2. `uni.reLaunch('/pages/dashboard/index')`
### 仪表盘
| 项目 | 说明 |
|------|------|
| 路径 | `/pages/dashboard/index` |
| API | `GET /api/v1/admin/dashboard`, `GET /api/v1/admin/records?page=1&page_size=5` |
| 功能 | 4 个统计卡片 + 最近护理表格 + 快捷导航 |
4 个统计指标:
| 指标 | 字段 | 图标 |
|------|------|------|
| 绑定设备数 | device_count | 📱 |
| 注册用户数 | user_count | 👥 |
| 护理次数 | treatment_count | 💆 |
| 活跃订阅 | subscription_count | 💳 |
### 设备管理
| 项目 | 说明 |
|------|------|
| 路径 | `/pages/device/index` |
| API | `GET /api/v1/admin/devices`, `POST /api/v1/admin/devices` |
| 功能 | 设备列表(分页、搜索)、预生成产品码、CSV 导出 |
状态显示:未激活、在线、离线、禁用
### 设备详情
| 项目 | 说明 |
|------|------|
| 路径 | `/pages/device-detail/index` |
| API | `GET /api/v1/admin/devices/:id`, `POST .../unbind`, `POST .../command` |
| 功能 | 设备信息网格、绑定历史、护理记录、解绑、远程指令 |
### 用户管理
| 项目 | 说明 |
|------|------|
| 路径 | `/pages/user/index` |
| API | `GET /api/v1/admin/users` |
| 功能 | 用户列表(分页、搜索)、订阅状态徽标、CSV 导出 |
### 用户详情
| 项目 | 说明 |
|------|------|
| 路径 | `/pages/user-detail/index` |
| API | `GET /api/v1/admin/users/:user_id` |
| 功能 | 用户信息、统计行(护理次数/时长/消费)、最近护理记录 |
### 订阅管理
| 项目 | 说明 |
|------|------|
| 路径 | `/pages/subscription/index` |
| API | `GET /api/v1/admin/subscriptions`, `POST /api/v1/admin/subscriptions` |
| 功能 | 统计行、Tab 过滤(全部/月度/年度/试用/过期)、创建订阅弹窗、CSV 导出 |
创建订阅表单字段:user_id, plan, amount, days
### 护理记录
| 项目 | 说明 |
|------|------|
| 路径 | `/pages/record/index` |
| API | `GET /api/v1/admin/records` |
| 功能 | 记录列表(分页、日期范围筛选、关键字搜索)、区域位掩码解码显示、CSV 导出 |
区域解码映射:1=左脸 2=右脸 4=额头 8=下巴 16=鼻 32=左眼 64=右眼
### 操作日志
| 项目 | 说明 |
|------|------|
| 路径 | `/pages/log/index` |
| API | `GET /api/v1/admin/logs` |
| 功能 | 时间线样式日志列表、类型过滤、角色徽标(管理员/系统/用户)、CSV 导出 |
### 系统设置
| 项目 | 说明 |
|------|------|
| 路径 | `/pages/settings/index` |
| API | `GET /api/v1/admin/settings`, `POST /api/v1/admin/settings` |
| 功能 | 三组配置 + 保存/重置按钮 |
配置项分组:
| 分组 | 字段 |
|------|------|
| 基本配置 | system_name, admin_email, timezone |
| 订阅定价 | monthly_price, yearly_price, trial_days |
| 功能开关 | enable_register, enable_binding, enable_free_mode, maintenance_mode |
---
## 全局布局组件 — AdminLayout
每个页面(除登录页)都包裹在 `<AdminLayout>` 组件中:
```
┌─────────────────────────────────────────────┐
│ Sidebar (220px) │ Header │
│ │ 页面标题 管理员名 退出 │
│ 🌸 光子美容仪 ├──────────────────────────┤
│ 管理后台 │ │
│ │ Content (<slot>) │
│ ▸ 仪表盘 │ │
│ ▸ 设备管理 │ │
│ ▸ 用户管理 │ │
│ ▸ 订阅管理 │ │
│ ▸ 护理记录 │ │
│ ▸ 操作日志 │ │
│ ▸ 系统设置 │ │
│ │ │
└─────────────────────────────────────────────┘
```
- **Props**`currentPage` — 当前页面路径,用于高亮侧边栏菜单项
- **侧边栏导航**:使用 `uni.redirectTo`(替换页面,不累积页面栈)
- **退出登录**`store.clearToken()``uni.reLaunch('/pages/login/index')`
- **管理员名称**:从 `userStore.adminInfo.real_name` 读取
---
## 状态管理 — Pinia Store
### user.js
```js
// State
token: ref('') // 从 Storage 初始化
adminInfo: ref(null) // { admin_id, username, real_name, role }
// Actions
setToken(val) // 保存 token + 计算过期时间(7天后)
clearToken() // 清除 token + adminInfo + Storage
login({ username, password }) // POST /admin/login → setToken + adminInfo
isLoggedIn() // 检查 token 非空 && 未过期
```
Token 存储位置:`uni.setStorageSync('admin_token')`
过期时间存储:`uni.setStorageSync('admin_token_expiry')`
有效期:7 天(客户端计算)
---
## HTTP 请求层(request.js
### 请求流程
```
页面调用 get/post(url, data)
request(options)
├── 从 Storage 读取 admin_token
├── 注入 Authorization: Bearer <token>
├── 拼接 BASE_URL + url
uni.request()
├── code === 0 → resolve(res.data.data)
├── code === 1001/1002 → 清除 token → reLaunch 到 login → reject
└── 其他 code → reject(res.data)
```
### 注意事项
- Token 直接从 `uni.getStorageSync('admin_token')` 读取,不通过 Pinia store
- 没有 token 刷新机制(不同于小程序端),token 过期后走 1001/1002 重定向登录
- PUT 方法已定义但目前没有页面使用
---
## CSV 导出(export.js
所有列表页都有"导出"按钮,调用 `exportCSV(filename, headers, rows)`
1. 构建 CSV 字符串(带 BOM `` 确保 Excel 正确识别编码)
2. 创建 Blob → 临时 `<a>` 元素 → 触发下载
3. 仅 H5 端可用(使用 `document.createElement`
---
## 样式体系
### 品牌色
| 色值 | 用途 |
|------|------|
| #E6508C | 主色(按钮、活跃状态、链接) |
| #001529 | 侧边栏背景(深蓝) |
| #f0f2f5 | 页面背景 |
| #52c41a | 成功状态徽标 |
| #faad14 | 警告状态徽标 |
| #ff4d4f | 错误状态徽标 |
### 公共样式(common.css
被 device / user / subscription / record / log / device-detail / user-detail 7 个页面引用,包含:
- **Flex 表格**`.data-table`, `.t-header`, `.t-row`, `.t-th`, `.t-td`, `.flex1/.flex2/.flex3`
- **徽标**`.badge`, `.badge-success/.warning/.error/.default/.blue`
- **按钮**`.btn-primary`(粉色), `.btn-default`(白色边框), `.btn-sm`
- **分页**`.pagination`, `.btn-page`, `.page-info`
- **工具栏**`.toolbar`, `.header-actions`, `.search-input`
- **卡片**`.page-card`
- **操作链接**`.action-link`(粉色文字)
Dashboard 和 Login 各自独立定义样式,不引用 common.css。
---
## 路由与导航
### pages.json 页面注册顺序
1. login(入口页)
2. dashboard
3. device → device-detail
4. user → user-detail
5. subscription
6. record
7. log
8. settings
没有 tabBar。登录页使用自定义导航栏,其他页面使用系统导航栏(白底黑字)。
### 导航方式
| 方法 | 使用场景 |
|------|---------|
| `uni.reLaunch` | 登录跳转、退出登录(清空页面栈) |
| `uni.redirectTo` | 侧边栏菜单切换(替换当前页) |
| `uni.navigateTo` | 进入详情页(压栈,可返回) |
| `uni.navigateBack` | 详情页返回列表 |
---
## 鉴权流程
```
1. 启动 App.vue onLaunch()
└── isLoggedIn() → 检查 admin_token 存在 + admin_token_expiry 未过期
├── 否 → uni.reLaunch('/pages/login/index')
└── 是 → 正常显示
2. 登录 POST /admin/login
└── 成功 → setToken(token)
├── Storage: admin_token = token
├── Storage: admin_token_expiry = now + 7天
└── 内存: adminInfo = { admin_id, username, real_name, role }
3. 每次 API 请求
└── request.js 从 Storage 读取 admin_token → Bearer header
├── 服务端返回 1001/1002 → 清 token → 跳登录
└── 正常响应 → 继续
4. 退出
└── AdminLayout 退出按钮 → clearToken() → reLaunch 登录
```
---
## 部署
### 构建
```bash
npm run build:h5 # uni build -p h5,输出到 dist/build/h5/
```
### 部署到 COS
```bash
npm run deploy:cos # 构建 + 上传到 COS
```
`scripts/deploy-cos.js`
-`server/.env` 读取 COS 凭证
- 上传 `dist/build/h5/` 下所有文件到 COS
- 文件路径前缀:`admin/`(可通过 `ADMIN_COS_PREFIX` 环境变量配置)
- 自动设置正确的 `Content-Type`
### 环境配置
| 环境 | API 地址 |
|------|---------|
| local | `http://localhost:3000` |
| test | `https://1426323813-ilxkhlxf4p.ap-guangzhou.tencentscf.com` |
| prod | 待配置 |
当前设置:`ENV = 'test'`