docs: 完善前端开发指南与平台支撑文档

- 新增 04-前端开发/04-常见坑点与调试指南.md
  - 高频踩坑记录(路由、Pinia、表格分页、权限指令)
  - 调试技巧与性能优化建议
  - 开发环境配置指南

- 扩展 06-平台支撑/07-API 文档/01-接口分域与维护原则.md
  - 接口分域架构(system/infra/ops/iot)
  - Swagger 注解规范与变更流程
  - 接口版本管理与跨域处理

- 扩展 06-平台支撑/08-数据库/01-数据域划分与表关系思路.md
  - 三大核心数据域(SYSTEM/OPS/IoT)
  - 核心表结构 SQL(用户、工单、设备、物模型)
  - 跨域关联原则与索引设计规范

- 扩展 06-平台支撑/09-DevOps 运维/01-部署运行与排障视角.md
  - 系统分层架构图
  - 排障决策树与各层检查清单
  - 常用诊断命令速查

- 扩展 06-平台支撑/09-DevOps 运维/02-环境部署指南.md
  - 四环境规划(Dev/Test/Staging/Prod)
  - 本地开发环境部署详解
  - Jenkins 自动部署流程与 Docker 配置
  - 生产部署检查清单与回滚流程
This commit is contained in:
lzh
2026-04-07 12:35:49 +08:00
parent 21126ab49d
commit 8a4aafd71f
5 changed files with 1865 additions and 23 deletions

View File

@@ -0,0 +1,334 @@
# 04-常见坑点与调试指南
本文档收集前端开发过程中高频出现的坑点、错误案例和调试技巧。所有前端开发人员在遇到类似问题时应先查阅本文档。
---
## 一、高频踩坑记录
### 1.1 路由跳转后页面不刷新
**现象**从列表页点击进入详情页URL 已变但页面内容未更新。
**原因**Vue Router 复用了同一个组件实例,`onMounted` 不会再次触发。
**解决方案**
```vue
<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 方案 1监听 route.params 变化
watch(() => route.params.id, (newId) => {
fetchDetail(newId)
}, { immediate: true })
// 方案 2使用 key 强制重新渲染组件
// 在 router-view 上添加 :key="route.fullPath"
</script>
```
---
### 1.2 Pinia Store 数据响应式丢失
**现象**:修改 Store 中的数据后,页面未更新。
**错误写法**
```typescript
// ❌ 错误:直接替换整个对象,丢失响应式
const userStore = useUserStore()
userStore.userInfo = { name: 'new', age: 25 }
```
**正确写法**
```typescript
// ✅ 正确:使用 Object.assign 保持响应式
const userStore = useUserStore()
Object.assign(userStore.userInfo, { name: 'new', age: 25 })
// 或者在定义 Store 时使用 ref
export const useUserStore = defineStore('user', () => {
const userInfo = ref({ name: '', age: 0 })
const updateInfo = (newInfo: any) => {
userInfo.value = { ...userInfo.value, ...newInfo }
}
return { userInfo, updateInfo }
})
```
---
### 1.3 表格分页参数不生效
**现象**:切换页码后,表格数据未更新或总是返回第一页。
**原因**Pagination 组件未正确绑定 `v-model:current``v-model:pageSize`
**正确写法**
```vue
<template>
<a-table
:data-source="tableData"
:pagination="{
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
showSizeChanger: true,
showQuickJumper: true
}"
@change="handleTableChange"
/>
</template>
<script setup>
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0
})
const handleTableChange = (pag: any) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
pagination.total = pag.total
fetchList()
}
</script>
```
---
### 1.4 权限指令不生效
**现象**:添加了 `v-hasPermi` 但按钮仍然显示。
**排查步骤**
1. 检查后端返回的权限标识是否正确(`/system/user/get-permission`
2. 检查权限标识字符串是否与后端 `@PreAuthorize` 注解完全一致
3. 检查是否在登录完成后才注册指令(确保 `permissionStore` 已加载)
**调试命令**
```typescript
// 在浏览器控制台执行
window.$permission = usePermission()
window.$permission.hasPermission('ops:ticket:update')
// 返回 true/false
```
---
### 1.5 Axios 请求重复发送
**现象**:点击一次按钮,接口被调用多次。
**常见原因**
1. 按钮未添加 `loading` 状态防抖
2. 表单验证触发多次提交
3. Vue 3 的 `watch` 未正确配置 `immediate` 导致初始化时多触发一次
**解决方案**
```vue
<script setup>
const loading = ref(false)
const handleSubmit = async () => {
if (loading.value) return
loading.value = true
try {
await submitForm()
} finally {
loading.value = false
}
}
</script>
<template>
<a-button :loading="loading" @click="handleSubmit">提交</a-button>
</template>
```
---
## 二、调试技巧
### 2.1 快速定位组件来源
在浏览器 DevTools 中:
```javascript
// 在控制台执行,点击页面元素后自动定位到源码
import { inspect } from 'vue'
inspect()
```
---
### 2.2 查看 Pinia Store 状态
```javascript
// 浏览器控制台
window.$pinia = usePinia()
Object.keys(window.$pinia.state.value).forEach(key => {
console.log(key, window.$pinia.state.value[key])
})
```
---
### 2.3 网络请求调试
`src/utils/http/axios/index.ts` 中临时开启详细日志:
```typescript
axios.interceptors.request.use(config => {
console.log('[AXIOS REQUEST]', config.url, config.params, config.data)
return config
})
axios.interceptors.response.use(response => {
console.log('[AXIOS RESPONSE]', response.config.url, response.data)
return response
}, error => {
console.error('[AXIOS ERROR]', error.config?.url, error.response?.data)
return Promise.reject(error)
})
```
---
### 2.4 路由调试
```javascript
// 查看当前路由信息
console.log($route)
// 查看所有已注册路由
console.log($router.getRoutes())
// 动态添加路由后检查是否生效
console.log($router.hasRoute('ops-ticket-list'))
```
---
## 三、性能优化建议
### 3.1 大列表渲染优化
**问题**:一次性渲染 1000+ 条数据导致页面卡顿。
**解决方案**
1. 使用虚拟滚动(`vue-virtual-scroller`
2. 开启分页(推荐)
3. 使用 `v-memo` 缓存静态内容Vue 3.2+
```vue
<template>
<div v-memo="[item.id, item.status]" v-for="item in list" :key="item.id">
{{ item.name }}
</div>
</template>
```
---
### 3.2 组件懒加载
```typescript
// 路由懒加载
const routes = [
{
path: '/ops',
component: () => import('@/views/ops/index.vue'),
children: [
{
path: 'ticket',
component: () => import('@/views/ops/ticket/index.vue')
}
]
}
]
```
---
### 3.3 避免不必要的计算
```vue
<script setup>
import { computed } from 'vue'
// ❌ 错误:每次渲染都重新计算
const filteredList = list.value.filter(item => item.status === 'active')
// ✅ 正确:使用 computed 缓存结果
const filteredList = computed(() =>
list.value.filter(item => item.status === 'active')
)
</script>
```
---
## 四、开发环境配置
### 4.1 推荐 VS Code 插件
- VolarVue 3 官方插件)
- ESLint
- Prettier
- TypeScript Vue Plugin
### 4.2 本地调试配置
```bash
# 安装依赖
pnpm install
# 启动开发服务器
pnpm dev
# 构建生产版本
pnpm build
# 类型检查
pnpm type-check
# Lint 检查
pnpm lint
```
### 4.3 环境变量配置
```bash
# .env.development
VITE_API_BASE_URL=http://localhost:48080
VITE_APP_TITLE=AIOT 管理后台 - 开发环境
# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=AIOT 管理后台
```
---
## 五、相关文档
- [01-前端工程结构与协作边界.md](./01-前端工程结构与协作边界.md)
- [02-API 交互与状态管理规范.md](./02-API 交互与状态管理规范.md)
- [03-RBAC 权限控制与开发规范.md](./03-RBAC 权限控制与开发规范.md)

View File

@@ -0,0 +1,226 @@
# 01-接口分域与维护原则
本文档定义 AIOT 系统 API 接口的组织原则、文档维护责任边界,以及接口变更的协作流程。
**核心原则**:接口文档优先按**业务域**维护,而非按页面或客户端维护。因为页面会变、客户端会增,但领域边界相对稳定。
---
## 一、接口分域架构
### 1.1 四大核心域
| 域标识 | 对应微服务 | 职责范围 | 负责人 |
|--------|-----------|---------|--------|
| `system` | `module-system` | 用户、角色、菜单、部门、租户、字典 | 后端架构组 |
| `infra` | `module-infra` | 文件管理、定时任务、代码生成、消息推送 | 后端架构组 |
| `ops` | `module-ops` | 工单、巡检、保洁、安保、排班、考勤 | Ops 业务组 |
| `iot` | `module-iot` | 设备、物模型、规则引擎、告警、MQTT 桥接 | IoT 业务组 |
### 1.2 接口 URL 规范
所有接口必须遵循 RESTful 风格,并按域组织路径:
```
GET /api/system/users # 用户列表
POST /api/system/users # 创建用户
GET /api/system/users/{id} # 用户详情
PUT /api/system/users/{id} # 更新用户
DELETE /api/system/users/{id} # 删除用户
GET /api/ops/tickets # 工单列表
POST /api/ops/tickets/{id}/dispatch # 派单(业务操作)
POST /api/ops/tickets/{id}/confirm # 确认到岗(业务操作)
GET /api/iot/devices # 设备列表
POST /api/iot/devices/{id}/reboot # 重启设备(业务操作)
```
**禁止事项**
-`/api/getUserList` - 非 RESTful
-`/api/ticket/updateStatus` - 动词在 URL 中,应使用 `/api/tickets/{id}/status`
-`/api/opsAndIot/xxx` - 跨域接口应拆分或通过事件驱动
---
## 二、接口文档维护责任
### 2.1 Swagger 注解是唯一的真理源
**所有接口必须在 Controller 方法上完整标注 Swagger 注解**
```java
@RestController
@RequestMapping("/ops/tickets")
@Tag(name = "工单管理", description = "工单 CRUD 及状态流转")
public class TicketController {
@PostMapping("/dispatch")
@Operation(summary = "派单", description = "将工单派发给指定保洁员")
@PreAuthorize("@ss.hasPermi('ops:ticket:dispatch')")
public CommonResult<Long> dispatchTicket(@RequestBody @Valid TicketDispatchReqVO reqVO) {
// ...
}
}
```
**必填注解**
- `@Tag` - 接口分组(对应域)
- `@Operation(summary = ..., description = ...)` - 接口用途
- `@Parameter` / `@Schema` - 参数说明
- `@ApiResponse` - 返回码说明(特别是业务错误码)
### 2.2 接口变更流程
```
开发者修改接口
更新 Swagger 注解
提交 MR → 自动触发 Swagger 文档生成
前端/移动端负责人 Review 文档
确认无破坏性变更后合并
```
**破坏性变更定义**(需提前通知所有调用方):
- 删除或重命名已有字段
- 修改字段类型(如 `String``Integer`
- 增加必填参数
- 修改接口路径或 HTTP 方法
**非破坏性变更**(可直接发布):
- 新增可选参数
- 新增接口
- 增加返回字段
---
## 三、为什么不按页面维护接口文档
### 3.1 错误示例
```
❌ 按页面组织:
- 保洁管理页面接口
- 获取保洁员列表
- 创建保洁员
- 编辑保洁员
- 工单列表页面接口
- 获取工单列表
- 工单详情
```
**问题**
1. 同一个 `/api/ops/cleaners` 接口可能被保洁管理页面、工单派发页面、排班页面同时使用
2. 新增一个移动端页面时,接口文档需要重复维护
3. 页面重构或合并时,接口文档需要大量调整
### 3.2 正确示例
```
✅ 按业务域组织:
- ops 域
- 保洁员管理
- GET /api/ops/cleaners - 保洁员列表
- POST /api/ops/cleaners - 创建保洁员
- PUT /api/ops/cleaners/{id} - 更新保洁员
- 工单管理
- GET /api/ops/tickets - 工单列表
- POST /api/ops/tickets/{id}/dispatch - 派单
```
**优势**
1. 接口与页面解耦,一个接口服务多个客户端
2. 领域边界清晰,便于微服务拆分
3. 新增客户端(如小程序)时,直接引用已有域文档
---
## 四、接口版本管理
### 4.1 版本号位置
当需要发布不兼容的接口变更时,使用 URL 路径版本号:
```
GET /api/v1/ops/tickets # 旧版本
GET /api/v2/ops/tickets # 新版本(字段结构变化)
```
**禁止使用**
- ❌ Query 参数版本:`/api/ops/tickets?version=2`
- ❌ Header 版本:`X-API-Version: 2`(不利于缓存和调试)
### 4.2 版本共存策略
- 新版本发布后,旧版本至少保留 **3 个月** 过渡期
- 在网关层监控旧版本接口的调用量,提前通知调用方迁移
- 过渡期结束后,在网关层返回 `410 Gone` 并引导升级
---
## 五、跨域接口处理
### 5.1 禁止跨域直接调用
```java
// ❌ 错误ops 服务直接调用 iot 服务的 Feign Client
@Autowired
private IotDeviceClient deviceClient;
public void dispatchTicket() {
// 直接调用 IoT 服务
deviceClient.getDeviceStatus(deviceId);
}
```
**问题**
- 微服务之间产生强耦合
- 无法独立部署和扩展
- 故障传播IoT 服务宕机拖垮 Ops 服务)
### 5.2 正确做法:事件驱动
```java
// ✅ 正确:通过消息队列解耦
// Ops 服务发布事件
applicationEventPublisher.publishEvent(new TicketDispatchedEvent(ticketId, deviceId));
// IoT 服务监听事件并处理
@EventListener
public void onTicketDispatched(TicketDispatchedEvent event) {
// 处理设备相关逻辑
}
```
---
## 六、接口文档访问
### 6.1 Swagger UI 地址
| 环境 | 地址 |
|------|------|
| 开发环境 | `http://localhost:48080/swagger-ui.html` |
| 测试环境 | `http://test-api.example.com/swagger-ui.html` |
| 生产环境 | **不开放**(通过内部文档平台查看) |
### 6.2 导出 OpenAPI 规范
```bash
# 导出 YAML 格式
curl http://localhost:48080/v3/api-docs -o openapi.yaml
# 导出 JSON 格式
curl http://localhost:48080/v3/api-docs -o openapi.json
```
---
## 七、相关文档
- [00-支撑平台总览.md](../00-支撑平台总览.md)
- [01-统一网关入口规范.md](../01-统一网关入口规范.md)
- [08-数据库/01-数据域划分与表关系思路.md](../08-数据库/01-数据域划分与表关系思路.md)

View File

@@ -1,31 +1,415 @@
# 🗄️ 数据域划分与表关系思路
# 01-数据域划分与表关系思路
当前数据库文档最重要的不是表清单,而是先划分数据域
本文档定义 AIOT 系统 MySQL 数据库的逻辑分域原则、表命名规范,以及跨域关联查询的底线
## 1. 系统主数据
**核心原则**:数据库表按业务域划分,域内可自由关联,跨域关联必须通过冗余字段或事件驱动,禁止跨域 JOIN。
- 用户
- 角色
- 菜单
- 部门
- 租户
---
## 2. IoT 主数据与过程数据
## 一、数据库分域架构
- 产品
- 设备
- 设备分组
- 物模型
- 告警配置
- 告警记录
- 设备消息
### 1.1 三大核心数据域
## 3. Ops 主数据与过程数据
```
┌─────────────────────────────────────────────────────────────┐
│ AIOT Database │
├─────────────────┬─────────────────┬─────────────────────────┤
│ SYSTEM 域 │ OPS 域 │ IoT 域 │
│ (系统主数据) │ (业务过程数据) │ (设备与物联数据) │
├─────────────────┼─────────────────┼─────────────────────────┤
│ - sys_user │ - ops_ticket │ - iot_product │
│ - sys_role │ - ops_order │ - iot_device │
│ - sys_menu │ - ops_cleaner │ - iot_thing_model │
│ - sys_dept │ - ops_inspection│ - iot_rule │
│ - sys_dict │ - ops_security │ - iot_alarm │
│ - sys_tenant │ - ops_shift │ - iot_message_log │
└─────────────────┴─────────────────┴─────────────────────────┘
```
- 工单主记录
- 工单业务日志
- 工单事件
- 队列记录
- 执行人状态
- 统计结果
### 1.2 域职责边界
| 域 | 职责 | 数据特点 | 读写比例 |
|----|------|---------|---------|
| **SYSTEM** | 用户、角色、权限、组织架构、字典 | 低频变更、高一致性要求 | 读 95% / 写 5% |
| **OPS** | 工单、排班、考勤、巡检记录 | 高频写入、状态流转复杂 | 读 60% / 写 40% |
| **IoT** | 设备、物模型、消息、告警 | 超高频写入、时序性强 | 读 20% / 写 80% |
---
## 二、表命名规范
### 2.1 强制前缀规则
所有表名必须带域前缀,格式:`{域标识}_{模块名}_{实体名}`
```sql
-- ✅ 正确
sys_user -- 系统域 - 用户
sys_role -- 系统域 - 角色
ops_ticket -- Ops 域 - 工单
ops_order_queue -- Ops 域 - 工单队列
iot_device -- IoT 域 - 设备
iot_alarm_record -- IoT 域 - 告警记录
-- ❌ 错误
user -- 缺少域前缀
ticket -- 缺少域前缀
iot_device_info -- 冗余后缀device 本身就表示信息)
```
### 2.2 关联表命名
多对多关联表使用 `_{域}_关联实体 A_关联实体 B` 格式:
```sql
sys_user_role -- 用户 - 角色关联
sys_role_menu -- 角色 - 菜单关联
ops_ticket_cleaner -- 工单 - 保洁员关联(历史派单记录)
```
### 2.3 扩展表命名
当主表字段过多需要拆分时:
```sql
ops_ticket -- 主表(核心字段)
ops_ticket_ext -- 扩展表(低频访问的大字段)
ops_ticket_trace -- 追踪表(状态流转日志)
```
---
## 三、核心表结构概览
### 3.1 SYSTEM 域
```sql
-- 用户表
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL COMMENT '租户 ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
password VARCHAR(100) NOT NULL COMMENT '密码',
nickname VARCHAR(50) COMMENT '昵称',
email VARCHAR(255) COMMENT '邮箱',
phone VARCHAR(20) COMMENT '手机号',
status TINYINT DEFAULT 1 COMMENT '状态 (0-禁用 1-正常)',
dept_id BIGINT COMMENT '部门 ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_tenant_username (tenant_id, username),
INDEX idx_phone (phone)
);
-- 角色表
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(50) NOT NULL COMMENT '角色名称',
code VARCHAR(50) NOT NULL COMMENT '角色标识',
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_tenant_code (tenant_id, code)
);
-- 用户角色关联表
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);
```
### 3.2 OPS 域
```sql
-- 工单表
CREATE TABLE ops_ticket (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
ticket_no VARCHAR(32) NOT NULL COMMENT '工单号',
type TINYINT NOT NULL COMMENT '工单类型 (1-保洁 2-安保 3-巡检)',
status TINYINT NOT NULL COMMENT '状态 (见 WorkOrderStatusEnum)',
priority TINYINT DEFAULT 1 COMMENT '优先级 (1-P0 紧急 2-P1 高 3-P2 中 4-P3 低)',
-- 位置信息
location_name VARCHAR(100) COMMENT '位置名称',
location_address VARCHAR(255) COMMENT '详细地址',
beacon_id VARCHAR(50) COMMENT '关联信标 ID',
-- 派单信息
assigned_cleaner_id BIGINT COMMENT '指派保洁员 ID',
assigned_at DATETIME COMMENT '派单时间',
confirmed_at DATETIME COMMENT '确认时间',
arrived_at DATETIME COMMENT '到岗时间 (信标感应)',
completed_at DATETIME COMMENT '完成时间',
-- 队列评分 (用于智能派单排序)
queue_score DECIMAL(10,2) COMMENT '队列评分',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_ticket_no (tenant_id, ticket_no),
INDEX idx_status (status),
INDEX idx_assigned_cleaner (assigned_cleaner_id),
INDEX idx_beacon (beacon_id)
);
-- 工单状态流转日志表
CREATE TABLE ops_ticket_log (
id BIGINT PRIMARY KEY,
ticket_id BIGINT NOT NULL,
from_status TINYINT NOT NULL,
to_status TINYINT NOT NULL,
operator_id BIGINT COMMENT '操作人 ID',
operator_type TINYINT COMMENT '操作人类型 (1-人 2-系统 3-信标)',
remark VARCHAR(500) COMMENT '备注',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ticket (ticket_id),
INDEX idx_created (created_at)
);
-- 保洁员表
CREATE TABLE ops_cleaner (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL COMMENT '关联系统用户 ID',
name VARCHAR(50) NOT NULL,
phone VARCHAR(20) NOT NULL,
status TINYINT DEFAULT 1 COMMENT '状态 (1-空闲 2-工作中 3-离线)',
current_ticket_id BIGINT COMMENT '当前工单 ID',
badge_no VARCHAR(50) COMMENT '工牌编号',
-- 统计字段 (冗余,避免实时 COUNT)
total_tickets INT DEFAULT 0 COMMENT '累计完成工单数',
today_tickets INT DEFAULT 0 COMMENT '今日完成工单数',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user (tenant_id, user_id),
INDEX idx_status (status),
INDEX idx_badge (badge_no)
);
```
### 3.3 IoT 域
```sql
-- 产品表 (设备模板)
CREATE TABLE iot_product (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
product_key VARCHAR(50) NOT NULL COMMENT '产品唯一标识',
product_secret VARCHAR(100) COMMENT '产品密钥',
-- 物模型
thing_model_id BIGINT COMMENT '关联物模型 ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_product_key (tenant_id, product_key)
);
-- 设备表
CREATE TABLE iot_device (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
device_name VARCHAR(100) NOT NULL,
device_key VARCHAR(50) NOT NULL COMMENT '设备唯一标识',
device_secret VARCHAR(100) COMMENT '设备密钥',
-- 状态
status TINYINT DEFAULT 0 COMMENT '状态 (0-未激活 1-在线 2-离线 3-禁用)',
last_heartbeat_at DATETIME COMMENT '最后心跳时间',
-- 关联信息
beacon_id VARCHAR(50) COMMENT '绑定信标 ID',
cleaner_id BIGINT COMMENT '绑定保洁员 ID (工牌设备)',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_device_key (tenant_id, device_key),
INDEX idx_product (product_id),
INDEX idx_status (status),
INDEX idx_cleaner (cleaner_id)
);
-- 物模型定义表
CREATE TABLE iot_thing_model (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
-- 属性定义 (JSON 存储)
properties JSON COMMENT '属性列表',
events JSON COMMENT '事件列表',
services JSON COMMENT '服务列表',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 告警记录表
CREATE TABLE iot_alarm (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
device_id BIGINT NOT NULL,
alarm_type VARCHAR(50) NOT NULL COMMENT '告警类型',
alarm_level TINYINT NOT NULL COMMENT '告警级别 (1-紧急 2-重要 3-一般)',
content VARCHAR(500) COMMENT '告警内容',
status TINYINT DEFAULT 0 COMMENT '状态 (0-未处理 1-已处理)',
triggered_at DATETIME NOT NULL,
handled_at DATETIME COMMENT '处理时间',
handler_id BIGINT COMMENT '处理人 ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_device (device_id),
INDEX idx_status (status),
INDEX idx_triggered (triggered_at)
);
```
---
## 四、跨域关联原则
### 4.1 禁止跨域 JOIN
```sql
-- ❌ 错误:跨域 JOINOps 域直接 JOIN System 域)
SELECT t.*, u.nickname
FROM ops_ticket t
JOIN sys_user u ON t.assigned_cleaner_id = u.id
WHERE t.status = 1;
-- ❌ 错误:跨域 JOINIoT 域直接 JOIN Ops 域)
SELECT d.*, c.name
FROM iot_device d
JOIN ops_cleaner c ON d.cleaner_id = c.id;
```
**问题**
- 违反微服务边界,未来无法拆分数据库
- 跨域查询性能不可控
- 事务边界模糊
### 4.2 正确做法:冗余字段 + 应用层组装
```sql
-- ✅ 正确:在 Ops 域冗余 System 域的必要字段
CREATE TABLE ops_cleaner (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
name VARCHAR(50) NOT NULL, -- 冗余 sys_user.nickname
phone VARCHAR(20) NOT NULL, -- 冗余 sys_user.phone
-- ...
);
-- 应用层查询时:
-- 1. 先查 ops_ticket 获取 assigned_cleaner_id
-- 2. 批量查询 ops_cleaner 获取保洁员信息(含冗余的 name/phone
-- 3. 在内存中组装结果
```
### 4.3 必须跨域查询时的方案
**场景**:后台管理页面需要展示工单列表,包含保洁员姓名、部门等信息。
**方案 A应用层批量查询推荐**
```java
// 1. 查询工单列表
List<Ticket> tickets = ticketMapper.selectList(query);
// 2. 批量查询保洁员信息
List<Long> cleanerIds = tickets.stream()
.map(Ticket::getAssignedCleanerId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
List<Cleaner> cleaners = cleanerMapper.selectBatchIds(cleanerIds);
Map<Long, Cleaner> cleanerMap = cleaners.stream()
.collect(Collectors.toMap(Cleaner::getId, Function.identity()));
// 3. 批量查询用户信息System 域)
List<Long> userIds = cleaners.stream()
.map(Cleaner::getUserId)
.collect(Collectors.toList());
List<User> users = userMapper.selectBatchIds(userIds);
// 4. 内存组装
tickets.forEach(ticket -> {
Cleaner cleaner = cleanerMap.get(ticket.getAssignedCleanerId());
if (cleaner != null) {
ticket.setCleanerName(cleaner.getName());
// ...
}
});
```
**方案 B数据同步读多写少场景**
```java
// System 域的用户信息变更时,发布事件
applicationEventPublisher.publishEvent(new UserInfoChangedEvent(userId));
// Ops 域监听事件,更新冗余字段
@EventListener
public void onUserInfoChanged(UserInfoChangedEvent event) {
User user = userService.getById(event.getUserId());
cleanerMapper.updateByUserId(user.getId(),
CleanerUpdateParams.builder()
.name(user.getNickname())
.phone(user.getPhone())
.build()
);
}
```
---
## 五、索引设计规范
### 5.1 必建索引场景
| 场景 | 索引类型 | 示例 |
|------|---------|------|
| 主键 | PRIMARY KEY | `id` |
| 唯一业务键 | UNIQUE KEY | `tenant_id + ticket_no` |
| 外键关联 | INDEX | `assigned_cleaner_id` |
| 状态筛选 | INDEX | `status` |
| 时间范围查询 | INDEX | `created_at` |
| 组合查询 | 复合索引 | `tenant_id + status + created_at` |
### 5.2 复合索引顺序原则
```sql
-- ✅ 正确:高选择性字段在前
INDEX idx_tenant_status_created (tenant_id, status, created_at)
-- 查询时:
WHERE tenant_id = ? AND status = ? AND created_at > ?
WHERE tenant_id = ? AND status = ?
WHERE tenant_id = ?
-- ❌ 错误:低选择性字段在前
INDEX idx_status_tenant (status, tenant_id)
-- 当 status 只有 4 个值时,区分度极低
```
---
## 六、相关文档
- [00-支撑平台总览.md](../00-支撑平台总览.md)
- [02-中间件使用规约.md](../02-中间件使用规约.md)
- [07-API 文档/01-接口分域与维护原则.md](../07-API 文档/01-接口分域与维护原则.md)

View File

@@ -0,0 +1,350 @@
# 01-部署运行与排障视角
本文档建立 AIOT 系统的排障方法论,帮助开发和运维人员快速定位问题所属层次,而非堆砌命令。
**核心原则**:排障第一刀先判断问题属于哪一层,再逐层深入。禁止一上来就 `kubectl logs``docker exec` 盲目翻日志。
---
## 一、系统分层架构
```
┌─────────────────────────────────────────────────────────────┐
│ 客户端层 (Client) │
│ Web 管理后台 │ 移动端 App │ 小程序 │ 工牌/信标 │
└─────────────────────────────────────────────────────────────┘
↓ HTTPS / MQTT
┌─────────────────────────────────────────────────────────────┐
│ 网关入口层 (Gateway) │
│ viewsh-gateway (Spring Cloud Gateway + Sentinel) │
│ - 路由分发 - JWT 鉴权 - 限流熔断 - 黑白名单 │
└─────────────────────────────────────────────────────────────┘
↓ 内部 HTTP / Feign
┌─────────────────────────────────────────────────────────────┐
│ 主服务装配层 (Microservices) │
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
│ │module-system│ module-infra│ module-ops │ module-iot │ │
│ │ 用户角色权限 │ 文件任务消息│ 工单巡检保洁│ 设备物模型 │ │
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
│ Nacos (注册发现/配置中心) │
│ Redis (缓存/分布式锁) │
│ MQ (RabbitMQ/Kafka 消息队列) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ IoT 规则与设备层 (IoT Edge) │
│ MQTT Broker (EMQX) │ 规则引擎 │ 设备影子 │ 边缘网关 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Ops 状态与执行层 (Physical) │
│ 智能工牌 (Badge) │ 蓝牙信标 (Beacon) │ 现场工作人员 │
└─────────────────────────────────────────────────────────────┘
```
---
## 二、排障决策树
### 2.1 第一刀:问题现象归类
```
用户报告问题
┌──────────────────────────────────────────┐
│ 1. 所有用户都访问不了? │
│ → 网关入口层 / 基础设施层 │
│ → 检查网关健康度、Nacos、数据库连接池 │
└──────────────────────────────────────────┘
↓ 否
┌──────────────────────────────────────────┐
│ 2. 特定功能访问不了? │
│ → 主服务装配层 │
│ → 检查对应微服务状态、日志、依赖中间件 │
└──────────────────────────────────────────┘
↓ 否
┌──────────────────────────────────────────┐
│ 3. 设备数据不上报/不响应? │
│ → IoT 规则与设备层 │
│ → 检查 MQTT Broker、设备在线状态、规则引擎│
└──────────────────────────────────────────┘
↓ 否
┌──────────────────────────────────────────┐
│ 4. 工单状态不更新/信标无感应? │
│ → Ops 状态与执行层 │
│ → 检查工牌电量、信标广播、蓝牙连接日志 │
└──────────────────────────────────────────┘
```
---
## 三、各层排障清单
### 3.1 网关入口层
**典型症状**
- 所有接口返回 `502 Bad Gateway``503 Service Unavailable`
- 登录接口正常,业务接口全部 `401 Unauthorized`
- 部分用户访问正常,部分用户报错
**检查步骤**
```bash
# 1. 检查网关容器状态
docker ps | grep gateway
docker logs viewsh-gateway --tail 100
# 2. 检查网关健康端点
curl http://gateway-host:18080/actuator/health
# 3. 检查 Nacos 服务注册
curl http://nacos-host:8848/nacos/v1/ns/instance/list?serviceName=module-ops
# 4. 检查网关路由配置 (Nacos 配置中心)
# Data ID: viewsh-gateway.yaml
# 检查 route 配置是否指向正确的服务名
# 5. 检查 JWT 密钥配置
# 确认 gateway 和 各微服务使用相同的 jwt.secret
```
**常见问题**
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 全部 502 | 下游服务全部未注册 | 检查 Nacos 是否正常,微服务是否启动 |
| 全部 401 | JWT 密钥不一致 | 统一 Nacos 中的 `jwt.secret` 配置 |
| 部分路由 404 | 路由配置遗漏 | 在 Nacos 网关配置中补充 route |
| 限流报错 429 | 触发 Sentinel 限流 | 检查限流阈值,临时调高或扩容 |
---
### 3.2 主服务装配层
**典型症状**
- 特定功能报错(如工单列表打不开,但用户管理正常)
- 接口响应极慢(>5s
- 间歇性报错,重试后正常
**检查步骤**
```bash
# 1. 定位问题服务
# 根据功能确定所属微服务:
# - 工单/保洁/巡检 → module-ops
# - 设备/物模型/告警 → module-iot
# - 用户/角色/权限 → module-system
# 2. 检查服务容器状态
docker ps | grep module-ops
docker stats module-ops # 查看 CPU/内存使用率
# 3. 查看应用日志
docker logs module-ops --tail 200 | grep -E "ERROR|WARN"
# 4. 检查 JVM 状态
docker exec module-ops jstat -gcutil <pid> 1000 5
# 5. 检查数据库连接池
# 登录 MySQL 查看连接数
SHOW PROCESSLIST;
SHOW STATUS LIKE 'Threads_connected';
# 6. 检查 Redis 连接
redis-cli -h redis-host ping
redis-cli -h redis-host KEYS "aiot:ops:*" | head 20
# 7. 检查 MQ 队列积压
# RabbitMQ 管理界面http://mq-host:15672
# 查看队列消息数,确认是否有消费失败
```
**常见问题**
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 接口超时 | 数据库慢查询 | 检查慢查询日志,添加索引 |
| 内存溢出 OOM | 堆内存不足 | 调大 `-Xmx`,检查内存泄漏 |
| 数据库连接耗尽 | 连接池配置过小 | 调大 `maximum-pool-size` |
| Redis 连接失败 | Redis 宕机或密码错误 | 检查 Redis 状态和 Nacos 配置 |
| MQ 消息积压 | 消费者处理慢或宕机 | 检查消费者日志,增加并发数 |
---
### 3.3 IoT 规则与设备层
**典型症状**
- 设备显示离线,但实际已通电
- 设备上报数据,但系统未收到
- 规则引擎未触发预期动作
**检查步骤**
```bash
# 1. 检查 MQTT Broker 状态
docker ps | grep emqx
docker logs emqx --tail 100
# 2. 检查设备在线状态
# EMQX 管理界面http://emqx-host:18083
# 查看设备连接数、订阅主题
# 3. 检查设备认证日志
docker logs emqx | grep "device-xxx"
# 4. 检查规则引擎
# EMQX 规则界面:查看规则执行日志
# 确认规则 SQL 是否正确,动作是否配置
# 5. 检查设备消息日志
# 查询 iot_message_log 表
SELECT * FROM iot_message_log
WHERE device_id = xxx
ORDER BY created_at DESC
LIMIT 10;
# 6. 模拟设备上报测试
# 使用 MQTT.fx 或命令行工具
mosquitto_pub -h emqx-host -t "/sys/xxx/xxx/post" -m '{"temp": 25}'
```
**常见问题**
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 设备连不上 | 设备密钥错误或 Broker 宕机 | 检查设备三元组,重启 EMQX |
| 数据不上报 | 网络问题或主题错误 | 检查设备网络,确认发布主题 |
| 规则不触发 | 规则 SQL 语法错误 | 在 EMQX 控制台测试规则 SQL |
| 消息丢失 | QoS 级别过低 | 设备端使用 QoS 1 或 2 |
---
### 3.4 Ops 状态与执行层
**典型症状**
- 保洁员已到岗,但系统显示未到达
- 工单派发了,但工牌未收到通知
- 信标感应失败,无法自动完工
**检查步骤**
```bash
# 1. 检查工牌状态
# 查询 ops_cleaner 表
SELECT id, name, badge_no, status, current_ticket_id
FROM ops_cleaner
WHERE id = xxx;
# 2. 检查信标绑定关系
# 查询 iot_device 或 ops_location 表
SELECT beacon_id, location_name
FROM ops_location
WHERE id = xxx;
# 3. 检查工单状态流转日志
SELECT * FROM ops_ticket_log
WHERE ticket_id = xxx
ORDER BY created_at DESC;
# 4. 检查蓝牙信标广播
# 使用手机 App 或 nRF Connect 扫描信标
# 确认信标 UUID、Major、Minor 是否正确广播
# 5. 检查工牌电量
# 工牌管理界面查看电量,或询问现场人员
# 电量低于 20% 可能导致蓝牙断开
# 6. 检查信标感应日志
# 查看 IoT 服务日志中信标事件上报记录
docker logs module-iot | grep "beacon:xxx"
```
**常见问题**
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 未自动到岗 | 信标未广播或工牌未感应 | 检查信标电量,重新绑定 |
| 工牌未收到派单 | 工牌离线或 MQTT 断开 | 检查工牌网络,重启工牌 |
| 状态不更新 | 工牌按键损坏或固件 bug | 更换工牌,升级固件 |
| 误感应 | 信标位置过近或信号穿透 | 调整信标位置,降低发射功率 |
---
## 四、常用诊断命令速查
### 4.1 容器诊断
```bash
# 查看所有容器状态
docker ps -a
# 查看容器资源使用
docker stats
# 查看容器日志
docker logs <container> --tail 100 -f
# 进入容器调试
docker exec -it <container> bash
# 重启容器
docker restart <container>
```
### 4.2 数据库诊断
```bash
# 查看当前连接
SHOW PROCESSLIST;
# 查看慢查询
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
# 查看表锁
SHOW OPEN TABLES WHERE In_use > 0;
# 查看连接数
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';
```
### 4.3 Redis 诊断
```bash
# 连接测试
redis-cli -h <host> ping
# 查看内存使用
redis-cli info memory
# 查看慢查询
redis-cli slowlog get 10
# 查看 Key 数量
redis-cli dbsize
# 查看特定 Key
redis-cli get "aiot:ops:ticket:1001"
```
### 4.4 网络诊断
```bash
# 测试端口连通性
telnet <host> <port>
nc -zv <host> <port>
# DNS 解析
nslookup <domain>
dig <domain>
# 路由追踪
traceroute <host>
mtr <host>
```
---
## 五、相关文档
- [00-支撑平台总览.md](../00-支撑平台总览.md)
- [01-统一网关入口规范.md](../01-统一网关入口规范.md)
- [02-中间件使用规约.md](../02-中间件使用规约.md)
- [02-环境部署指南.md](./02-环境部署指南.md)

View File

@@ -0,0 +1,548 @@
# 02-环境部署指南
本文档描述 AIOT 项目从开发环境到生产环境的完整部署流程、配置规范和运维操作。
---
## 一、环境规划
### 1.1 环境清单
| 环境 | 用途 | 域名 | 数据库 | 更新频率 | 负责人 |
|------|------|------|--------|---------|--------|
| **开发 (Dev)** | 本地开发联调 | `dev-api.example.com` | 共享 Dev DB | 随时 | 开发者 |
| **测试 (Test)** | QA 功能测试 | `test-api.example.com` | 独立 Test DB | 每日 | QA |
| **预生产 (Staging)** | 上线前验证 | `staging-api.example.com` | 生产库只读副本 | 每周 | DevOps |
| **生产 (Prod)** | 正式用户 | `api.example.com` | 生产主库 | 计划性 | DevOps+PM |
### 1.2 环境隔离原则
```
┌─────────────────────────────────────────────────────────────┐
│ 开发环境 (Dev) │
│ - 开发者本地 Docker Compose 或 WSL2 运行 │
│ - 连接共享 Dev 数据库(多人共用) │
│ - Nacos 配置dev Profile │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 测试环境 (Test) │
│ - 独立服务器/容器集群 │
│ - 独立 Test 数据库(每日从生产脱敏同步) │
│ - Nacos 配置test Profile │
│ - Jenkins 自动部署test 分支触发) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 预生产环境 (Staging) │
│ - 独立服务器/容器集群(配置同生产) │
│ - 生产库只读副本(用于验证 SQL
│ - Nacos 配置prod Profile但指向测试 MQ/Redis
│ - 手动触发部署Release 分支) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 生产环境 (Prod) │
│ - 高可用集群(多副本 + 负载均衡) │
│ - 生产主数据库(主从复制) │
│ - Nacos 配置prod Profile │
│ - 严格审批流程PM+Tech Lead 签字) │
└─────────────────────────────────────────────────────────────┘
```
---
## 二、开发环境部署(本地)
### 2.1 前置条件
```bash
# 必需软件
- Docker 20.10+
- Docker Compose 2.0+
- JDK 11/17
- Maven 3.8+
- Node.js 18+
- pnpm 8+
# 可选工具
- MySQL Workbench / DBeaver
- Redis Desktop Manager
- Postman / Apifox
```
### 2.2 启动基础设施
```bash
# 进入项目根目录
cd /path/to/aiot-platform-cloud
# 启动核心中间件Nacos + MySQL + Redis + EMQX
docker-compose -f docker-compose.core.yml up -d
# 检查容器状态
docker-compose -f docker-compose.core.yml ps
# 查看 Nacos 日志(确认启动成功)
docker logs aiot-nacos --tail 50
```
**核心服务端口**
| 服务 | 端口 | 访问地址 |
|------|------|---------|
| Nacos | 8848 | `http://localhost:8848/nacos` |
| MySQL | 3306 | `localhost:3306` |
| Redis | 6379 | `localhost:6379` |
| EMQX | 1883/18083 | `localhost:18083` |
| RabbitMQ | 5672/15672 | `localhost:15672` |
**默认账号**
- Nacos: `nacos / nacos`
- MySQL: `root / aiot123456`
- Redis: 无密码
- RabbitMQ: `guest / guest`
- EMQX: `admin / public`
### 2.3 初始化数据库
```bash
# 1. 创建数据库
mysql -h localhost -u root -p << EOF
CREATE DATABASE IF NOT EXISTS aiot_platform
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_general_ci;
USE aiot_platform;
EOF
# 2. 导入表结构
mysql -h localhost -u root -p aiot_platform < sql/aiot_schema.sql
# 3. 导入初始数据(租户、管理员账号、菜单权限)
mysql -h localhost -u root -p aiot_platform < sql/aiot_data.sql
```
### 2.4 配置 Nacos
1. 访问 `http://localhost:8848/nacos`
2. 登录(`nacos / nacos`
3. 导入配置(`导入` → 选择 `config/` 目录下的配置文件)
- `viewsh-gateway.yaml`
- `module-system.yaml`
- `module-ops.yaml`
- `module-iot.yaml`
- `application-common.yaml`
**关键配置项检查**
```yaml
# application-common.yaml
spring:
datasource:
url: jdbc:mysql://host.docker.internal:3306/aiot_platform?useSSL=false
username: root
password: aiot123456
redis:
host: host.docker.internal
port: 6379
# 开发环境特殊配置
aiot:
dev-mode: true
swagger-enable: true
mock-iot-device: true # 启用模拟设备上报
```
> **注意**Docker 容器内访问宿主机服务需使用 `host.docker.internal` 而非 `localhost`。
### 2.5 启动后端服务
```bash
# 1. 编译网关
cd viewsh-gateway
mvn clean package -DskipTests -P dev
java -jar target/viewsh-gateway.jar --spring.profiles.active=dev
# 2. 编译主服务module-system / module-ops / module-iot
cd module-system
mvn clean package -DskipTests -P dev
java -jar target/module-system.jar --spring.profiles.active=dev
# 3. 检查服务注册
# 访问 Nacos 控制台,确认服务状态为「健康」
```
### 2.6 启动前端
```bash
# 1. 安装依赖
cd yudao-ui-admin-vben
pnpm install
# 2. 配置环境变量
# 复制 .env.development 并修改 API 地址
cp .env.development .env.development.local
echo "VITE_API_BASE_URL=http://localhost:18080" >> .env.development.local
# 3. 启动开发服务器
pnpm dev
# 4. 访问管理后台
# http://localhost:5173
# 默认管理员账号admin / admin123
```
---
## 三、测试环境部署Jenkins 自动)
### 3.1 部署流程
```
开发者推送代码到 test 分支
GitLab Webhook 触发 Jenkins Job
Jenkins 拉取代码 → Maven 编译 → 运行单元测试
构建 Docker 镜像Tag: test-{commit_hash}
推送到私有镜像仓库
SSH 登录测试服务器 → docker pull → docker stop → docker run
健康检查(/actuator/health
发送通知到飞书/钉钉群
```
### 3.2 Jenkinsfile 示例
```groovy
pipeline {
agent any
environment {
DOCKER_IMAGE = "registry.example.com/aiot/module-ops"
DOCKER_TAG = "test-${env.GIT_COMMIT.take(7)}"
}
stages {
stage('Checkout') {
steps {
git branch: 'test',
url: 'git@gitlab.example.com:aiot/aiot-platform-cloud.git'
}
}
stage('Build') {
steps {
sh 'cd module-ops && mvn clean package -DskipTests -P test'
}
}
stage('Unit Test') {
steps {
sh 'cd module-ops && mvn test'
}
}
stage('Docker Build') {
steps {
sh '''
cd module-ops
docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} .
docker push ${DOCKER_IMAGE}:${DOCKER_TAG}
'''
}
}
stage('Deploy') {
steps {
sh '''
ssh deploy@test-server "
docker pull ${DOCKER_IMAGE}:${DOCKER_TAG} &&
docker stop module-ops || true &&
docker rm module-ops || true &&
docker run -d --name module-ops \\
-p 18081:18080 \\
-e SPRING_PROFILES_ACTIVE=test \\
${DOCKER_IMAGE}:${DOCKER_TAG}
"
'''
}
}
stage('Health Check') {
steps {
sh '''
for i in {1..30}; do
if curl -s http://test-server:18081/actuator/health | grep -q UP; then
echo "Deploy successful!"
exit 0
fi
sleep 2
done
echo "Health check failed!"
exit 1
'''
}
}
}
post {
success {
// 发送飞书通知
sh '''
curl -X POST https://open.feishu.cn/open-apis/bot/v2/hook/xxx \\
-H "Content-Type: application/json" \\
-d '{"msg_type":"text","content":{"text":"✅ module-ops 测试环境部署成功\\n版本${DOCKER_TAG}"}}'
'''
}
failure {
// 发送失败通知
sh '''
curl -X POST https://open.feishu.cn/open-apis/bot/v2/hook/xxx \\
-H "Content-Type: application/json" \\
-d '{"msg_type":"text","content":{"text":"❌ module-ops 测试环境部署失败\\n请检查 Jenkins 日志"}}'
'''
}
}
}
```
---
## 四、生产环境部署(严格审批)
### 4.1 部署前检查清单
**必须全部勾选才能执行部署**
- [ ] **代码审查**MR 已获 2 人以上 Approve
- [ ] **测试报告**QA 已签署测试通过报告
- [ ] **SQL 审查**:涉及数据库变更的 SQL 已获 DBA 审核
- [ ] **回滚方案**:已制定明确回滚步骤并验证
- [ ] **备份确认**:生产数据库已完成全量备份
- [ ] **通知到位**:相关干系人(客服、运营)已收到部署通知
- [ ] **时间窗口**:部署时间在低峰期(凌晨 2:00-4:00
- [ ] **值班人员**DevOps 和业务负责人在线待命
### 4.2 部署流程
```bash
# 1. 创建 Release 分支(从 master 最新提交)
git checkout master
git pull
git checkout -b release/v1.2.0
# 2. 更新版本号
# 修改各模块 pom.xml 中的 <version>1.2.0</version>
# 修改 CHANGELOG.md
# 3. 提交并打 Tag
git add .
git commit -m "release: v1.2.0"
git tag -a v1.2.0 -m "Release version 1.2.0"
# 4. 推送 Release 分支和 Tag
git push origin release/v1.2.0
git push origin v1.2.0
# 5. 在 Jenkins 触发生产部署 Job
# 选择 Release 分支,确认版本号,点击「部署到生产」
# 6. 部署过程中实时监控
# - Grafana 仪表盘CPU、内存、QPS、错误率
# - 日志平台ELK
# - 业务监控(工单处理量、设备在线数)
# 7. 部署完成后验证
# - 冒烟测试(核心功能快速验证)
# - 确认无异常日志
# - 通知干系人部署完成
```
### 4.3 回滚流程
**当生产部署后出现严重 Bug 时**
```bash
# 1. 立即通知
# 飞书群通知:「生产环境出现异常,准备回滚到 v1.1.0」
# 2. 执行回滚
# Jenkins 选择「回滚」Job选择上一个稳定版本 v1.1.0
# 3. 验证回滚
# - 确认服务恢复正常
# - 检查数据一致性(是否有脏数据)
# 4. 事后复盘
# - 记录事故时间线
# - 分析根本原因
# - 制定改进措施
```
---
## 五、Docker 部署详解
### 5.1 Dockerfile 示例(后端)
```dockerfile
# 构建阶段
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /build
COPY pom.xml .
COPY module-ops/pom.xml module-ops/
RUN mvn -f module-ops/pom.xml dependency:go-offline -B
COPY module-ops/src module-ops/src
RUN mvn -f module-ops/pom.xml clean package -DskipTests
# 运行阶段
FROM openjdk:17-slim
WORKDIR /app
# 创建非 root 用户
RUN useradd -m -u 1000 appuser
# 复制 JAR
COPY --from=builder /build/module-ops/target/*.jar app.jar
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \\
CMD curl -f http://localhost:18080/actuator/health || exit 1
# 切换用户
USER appuser
EXPOSE 18080
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]
```
### 5.2 Docker Compose 示例(生产)
```yaml
version: '3.8'
services:
gateway:
image: registry.example.com/aiot/viewsh-gateway:v1.2.0
ports:
- "80:18080"
- "443:18443"
environment:
- SPRING_PROFILES_ACTIVE=prod
- NACOS_SERVER_ADDR=nacos:8848
depends_on:
- nacos
restart: always
deploy:
replicas: 2
resources:
limits:
cpus: '2'
memory: 2G
module-ops:
image: registry.example.com/aiot/module-ops:v1.2.0
environment:
- SPRING_PROFILES_ACTIVE=prod
- NACOS_SERVER_ADDR=nacos:8848
depends_on:
- nacos
- mysql
- redis
restart: always
deploy:
replicas: 3
resources:
limits:
cpus: '4'
memory: 4G
nacos:
image: nacos/nacos-server:2.2.0
environment:
- MODE=cluster
- NACOS_SERVERS=nacos1:8848 nacos2:8848 nacos3:8848
volumes:
- ./nacos/cluster-logs:/home/nacos/logs
restart: always
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
restart: always
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis-data:/data
restart: always
volumes:
mysql-data:
redis-data:
```
---
## 六、监控与告警
### 6.1 关键监控指标
| 指标类别 | 指标名称 | 告警阈值 | 告警级别 |
|---------|---------|---------|---------|
| **应用层** | HTTP 错误率 | > 1% | P1 |
| | API 响应时间 P99 | > 2000ms | P2 |
| | JVM 堆内存使用率 | > 85% | P2 |
| **中间件** | MySQL 连接数 | > 80% | P2 |
| | Redis 内存使用率 | > 80% | P2 |
| | MQ 消息积压 | > 10000 | P2 |
| **业务层** | 工单派发失败率 | > 5% | P1 |
| | 设备离线率 | > 10% | P2 |
| | 信标感应成功率 | < 95% | P2 |
### 6.2 告警通知渠道
```yaml
# Prometheus Alertmanager 配置
receivers:
- name: 'feishu-p1'
webhook_configs:
- url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx'
send_resolved: true
# P1 级别:电话 + 飞书
- name: 'feishu-p2'
webhook_configs:
- url: 'https://open.feishu.cn/open-apis/bot/v2/hook/yyy'
send_resolved: true
# P2 级别:飞书
- name: 'email'
email_configs:
- to: 'devops@example.com'
send_resolved: true
# P3 级别:邮件
```
---
## 七、相关文档
- [03-CICD 与部署流水线规范.md](../03-CICD 与部署流水线规范.md)
- [01-部署运行与排障视角.md](./01-部署运行与排障视角.md)
- [07-协作规范/02-开发工作流与-Git-规约.md](../../07-协作规范/02-开发工作流与-Git-规约.md)