# AR智能巡检管理系统 - 技术设计文档

## 一、项目概述

### 1.1 项目简介

AR智能巡检管理系统是一个基于 RuoYi-Vue-Plus 5.5.1 框架开发的电力设施智能巡检管理平台，通过AR设备实现智能化巡检作业流程管理。

### 1.2 技术栈

**后端技术栈：**
- Spring Boot 3.5.7
- JDK 17/21
- MyBatis-Plus 3.5.x
- Sa-Token 1.38.0（权限认证）
- MySQL 5.7+
- Redisson（分布式缓存）
- MapStruct-Plus（对象映射）

**前端技术栈：**
- Vue 3
- TypeScript
- Element Plus
- Vite

### 1.3 核心特性

- ✅ **无多租户架构** - 简化数据模型，适用于单租户场景
- ✅ **树形步骤管理** - 支持任意深度的步骤层级结构
- ✅ **JSON灵活存储** - 区域数据、点位坐标、AI配置使用JSON格式
- ✅ **执行状态追踪** - 完整的任务执行生命周期管理
- ✅ **媒体文件管理** - 支持图片、视频、音频等多媒体文件

---

## 二、需求分析

### 2.1 功能模块

| 模块名称 | 功能描述 | 核心实体 |
|---------|---------|---------|
| AR设备管理 | 管理AR巡检设备基础信息 | ArDevice |
| 区域管理 | 管理巡检区域及区域数据 | ArRegion |
| 点位管理 | 管理巡检点位及位置坐标 | ArPoint |
| 任务模板管理 | 管理巡检任务模板 | ArTask |
| 步骤管理 | 管理任务步骤树形结构 | ArStep |
| 任务执行管理 | 管理任务执行记录及状态 | ArExecution |
| 步骤记录管理 | 管理步骤执行详细记录 | ArStepRecord |
| 媒体文件管理 | 管理步骤执行过程中的媒体文件 | ArStepMedia |

### 2.2 关键需求

#### 2.2.1 设计决策

**问题1：步骤层级结构**
- **决策**：支持任意深度的树形结构
- **实现**：使用 parent_id + ancestors 字段实现

**问题2：坐标数据存储**
- **决策**：使用JSON字符串存储
- **实现**：使用 JacksonTypeHandler 实现自动序列化/反序列化

**问题3：语音/AI字段性质**
- **决策**：作为步骤配置项（非执行记录）
- **实现**：字段设计在 ar_step 表中

**问题4：执行记录追踪**
- **决策**：追踪状态、执行数据、AI识别结果、语音文本
- **实现**：在 ar_step_record 表中设计对应字段

#### 2.2.2 数据库调整需求

根据实际需求，数据库设计进行了以下调整：

| 表名 | 调整内容 |
|------|---------|
| ar_device | 新增 device_model 字段 |
| ar_region | 新增 region_data (JSON) 和 remark 字段 |
| ar_point | 字段 position 改名为 position_data (JSON)，移除 direction，新增 remark |
| ar_task | 新增 remark 字段 |
| ar_step | 移除UI预设字段（从点位获取），新增 ai_data (JSON) |
| ar_step_media | 移除 oss_id 字段 |

#### 2.2.3 多租户处理

**明确要求：不考虑多租户**

实施措施：
1. 所有数据表均不包含 tenant_id 字段
2. 所有实体类继承 BaseEntity（非 TenantEntity）
3. 配置文件设置 `tenant.enable: false`
4. 所有业务表加入 `tenant.excludes` 列表

---

## 三、数据库设计

### 3.1 数据库表清单

系统共包含 8 张业务表，所有表均使用雪花ID作为主键，支持逻辑删除。

### 3.2 表结构设计

#### 3.2.1 AR设备表 (ar_device)

```sql
CREATE TABLE `ar_device` (
  `id` bigint NOT NULL COMMENT '设备ID',
  `device_name` varchar(100) NOT NULL COMMENT '设备名称',
  `device_no` varchar(50) NOT NULL COMMENT '设备编号',
  `device_model` varchar(100) DEFAULT NULL COMMENT '设备型号',
  `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_dept` bigint DEFAULT NULL COMMENT '创建部门',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` tinyint DEFAULT '0' COMMENT '删除标志(0存在 1删除)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_device_no` (`device_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AR设备表';
```

**核心字段说明：**
- `device_no`: 设备编号，全局唯一
- `device_model`: 设备型号（新增需求）
- `status`: 设备状态（0正常 1停用）

#### 3.2.2 巡检区域表 (ar_region)

```sql
CREATE TABLE `ar_region` (
  `id` bigint NOT NULL COMMENT '区域ID',
  `region_name` varchar(100) NOT NULL COMMENT '区域名称',
  `region_code` varchar(50) NOT NULL COMMENT '区域代码',
  `region_data` json DEFAULT NULL COMMENT '区域数据(JSON格式)',
  `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_dept` bigint DEFAULT NULL COMMENT '创建部门',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` tinyint DEFAULT '0' COMMENT '删除标志(0存在 1删除)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_region_code` (`region_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='巡检区域表';
```

**核心字段说明：**
- `region_code`: 区域代码，全局唯一
- `region_data`: 区域数据，JSON格式存储（新增需求）
- `remark`: 备注信息（新增需求）

#### 3.2.3 巡检点位表 (ar_point)

```sql
CREATE TABLE `ar_point` (
  `id` bigint NOT NULL COMMENT '点位ID',
  `region_id` bigint NOT NULL COMMENT '所属区域ID',
  `point_name` varchar(100) NOT NULL COMMENT '点位名称',
  `point_code` varchar(50) NOT NULL COMMENT '点位代码',
  `position_data` json DEFAULT NULL COMMENT '位置数据(JSON格式,包含坐标等)',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_dept` bigint DEFAULT NULL COMMENT '创建部门',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` tinyint DEFAULT '0' COMMENT '删除标志(0存在 1删除)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_region_point` (`region_id`, `point_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='巡检点位表';
```

**核心字段说明：**
- `point_code`: 点位代码，同一区域内唯一
- `position_data`: 位置数据，JSON格式（字段改名需求）
- 移除了 `direction` 字段（按需求调整）
- `remark`: 备注信息（新增需求）

#### 3.2.4 巡检任务模板表 (ar_task)

```sql
CREATE TABLE `ar_task` (
  `id` bigint NOT NULL COMMENT '任务ID',
  `task_name` varchar(100) NOT NULL COMMENT '任务名称',
  `task_code` varchar(50) NOT NULL COMMENT '任务代码',
  `region_id` bigint NOT NULL COMMENT '关联区域ID',
  `task_type` varchar(50) DEFAULT NULL COMMENT '任务类型',
  `status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_dept` bigint DEFAULT NULL COMMENT '创建部门',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` tinyint DEFAULT '0' COMMENT '删除标志(0存在 1删除)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_task_code` (`task_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='巡检任务模板表';
```

**核心字段说明：**
- `task_code`: 任务代码，全局唯一
- `region_id`: 关联区域ID
- `remark`: 备注信息（新增需求）

#### 3.2.5 巡检步骤表 (ar_step)

```sql
CREATE TABLE `ar_step` (
  `id` bigint NOT NULL COMMENT '步骤ID',
  `task_id` bigint NOT NULL COMMENT '所属任务ID',
  `parent_id` bigint DEFAULT '0' COMMENT '父步骤ID(0表示根节点)',
  `ancestors` varchar(500) DEFAULT '0' COMMENT '祖级列表',
  `step_name` varchar(100) NOT NULL COMMENT '步骤名称',
  `step_content` text COMMENT '步骤内容',
  `content_voice` varchar(255) DEFAULT NULL COMMENT '内容语音URL',
  `order_num` int DEFAULT '0' COMMENT '显示顺序',
  `point_id` bigint DEFAULT NULL COMMENT '关联点位ID',

  -- 语音交互配置
  `need_voice_read` char(1) DEFAULT '0' COMMENT '是否需要语音播报(0否 1是)',
  `need_voice_rephrase` char(1) DEFAULT '0' COMMENT '是否需要复述(0否 1是)',
  `rephrase_content` text COMMENT '复述内容',
  `rephrase_voice` varchar(255) DEFAULT NULL COMMENT '复述语音URL',
  `need_voice_confirm` char(1) DEFAULT '0' COMMENT '是否需要语音确认(0否 1是)',
  `confirm_content` text COMMENT '确认内容',
  `confirm_voice` varchar(255) DEFAULT NULL COMMENT '确认语音URL',
  `confirm_word` varchar(100) DEFAULT NULL COMMENT '确认关键词',

  -- AI识别配置
  `need_ai` char(1) DEFAULT '0' COMMENT '是否需要AI识别(0否 1是)',
  `ai_target_name` varchar(100) DEFAULT NULL COMMENT 'AI识别目标名称',
  `ai_data` json DEFAULT NULL COMMENT 'AI配置数据(JSON格式)',

  `is_operation` char(1) DEFAULT '0' COMMENT '是否需要操作(0否 1是)',
  `is_leaf` char(1) DEFAULT '1' COMMENT '是否叶子节点(0否 1是)',

  `create_dept` bigint DEFAULT NULL COMMENT '创建部门',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` tinyint DEFAULT '0' COMMENT '删除标志(0存在 1删除)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='巡检步骤表';
```

**核心字段说明：**
- `parent_id` + `ancestors`: 实现树形结构
- 支持任意深度的步骤层级
- 语音交互配置字段：复述、确认等
- `ai_data`: AI配置数据，JSON格式（新增需求）
- **移除了UI预设字段**（birth_point, birth_direction, prefab 等，按需求调整）

#### 3.2.6 任务执行记录表 (ar_execution)

```sql
CREATE TABLE `ar_execution` (
  `id` bigint NOT NULL COMMENT '执行ID',
  `task_id` bigint NOT NULL COMMENT '任务模板ID',
  `execution_code` varchar(50) NOT NULL COMMENT '执行编号',
  `region_id` bigint NOT NULL COMMENT '区域ID',
  `device_id` bigint DEFAULT NULL COMMENT '使用的AR设备ID',

  -- 执行角色
  `operator_id` bigint DEFAULT NULL COMMENT '操作人ID',
  `operator_name` varchar(50) DEFAULT NULL COMMENT '操作人姓名',
  `custodian_id` bigint DEFAULT NULL COMMENT '监护人ID',
  `custodian_name` varchar(50) DEFAULT NULL COMMENT '监护人姓名',
  `sender_id` bigint DEFAULT NULL COMMENT '送电人ID',
  `sender_name` varchar(50) DEFAULT NULL COMMENT '送电人姓名',
  `recipient_id` bigint DEFAULT NULL COMMENT '受电人ID',
  `recipient_name` varchar(50) DEFAULT NULL COMMENT '受电人姓名',
  `commander_id` bigint DEFAULT NULL COMMENT '指挥人ID',
  `commander_name` varchar(50) DEFAULT NULL COMMENT '指挥人姓名',

  `status` varchar(20) DEFAULT 'pending' COMMENT '执行状态(pending待执行 in_progress执行中 completed已完成 cancelled已取消)',
  `start_time` datetime DEFAULT NULL COMMENT '开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '结束时间',
  `total_steps` int DEFAULT '0' COMMENT '总步骤数',
  `completed_steps` int DEFAULT '0' COMMENT '已完成步骤数',

  `create_dept` bigint DEFAULT NULL COMMENT '创建部门',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` tinyint DEFAULT '0' COMMENT '删除标志(0存在 1删除)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_execution_code` (`execution_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务执行记录表';
```

**核心字段说明：**
- `execution_code`: 执行编号，自动生成（格式：EXE-{timestamp}）
- 5种执行角色：操作人、监护人、送电人、受电人、指挥人
- `status`: 执行状态（pending/in_progress/completed/cancelled）
- 自动设置 `start_time` 和 `end_time`

#### 3.2.7 步骤执行记录表 (ar_step_record)

```sql
CREATE TABLE `ar_step_record` (
  `id` bigint NOT NULL COMMENT '记录ID',
  `execution_id` bigint NOT NULL COMMENT '任务执行ID',
  `step_id` bigint NOT NULL COMMENT '步骤ID',
  `status` varchar(20) DEFAULT 'pending' COMMENT '状态(pending待执行 completed已完成 skipped已跳过)',
  `is_done` char(1) DEFAULT '0' COMMENT '是否完成(0否 1是)',
  `start_time` datetime DEFAULT NULL COMMENT '开始时间',
  `completion_time` datetime DEFAULT NULL COMMENT '完成时间',
  `duration` int DEFAULT NULL COMMENT '耗时(秒)',
  `text_feedback` text COMMENT '文本反馈',
  `voice_text` text COMMENT '语音识别文本',
  `ai_result` json DEFAULT NULL COMMENT 'AI识别结果(JSON格式)',
  `executor_id` bigint DEFAULT NULL COMMENT '执行人ID',
  `executor_name` varchar(50) DEFAULT NULL COMMENT '执行人姓名',

  `create_dept` bigint DEFAULT NULL COMMENT '创建部门',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` tinyint DEFAULT '0' COMMENT '删除标志(0存在 1删除)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='步骤执行记录表';
```

**核心字段说明：**
- `status`: 步骤执行状态（pending/completed/skipped）
- `duration`: 耗时，自动计算（完成时间 - 开始时间）
- `text_feedback`: 文本反馈
- `voice_text`: 语音识别文本
- `ai_result`: AI识别结果，JSON格式

#### 3.2.8 步骤媒体文件表 (ar_step_media)

```sql
CREATE TABLE `ar_step_media` (
  `id` bigint NOT NULL COMMENT '媒体ID',
  `step_record_id` bigint NOT NULL COMMENT '步骤记录ID',
  `media_type` varchar(20) NOT NULL COMMENT '媒体类型(image图片 video视频 audio音频)',
  `file_url` varchar(500) NOT NULL COMMENT '文件URL',
  `file_name` varchar(200) NOT NULL COMMENT '文件名称',
  `file_size` bigint DEFAULT NULL COMMENT '文件大小(字节)',
  `upload_time` datetime DEFAULT NULL COMMENT '上传时间',

  `create_dept` bigint DEFAULT NULL COMMENT '创建部门',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` tinyint DEFAULT '0' COMMENT '删除标志(0存在 1删除)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='步骤媒体文件表';
```

**核心字段说明：**
- `media_type`: 媒体类型（image/video/audio）
- **移除了 oss_id 字段**（按需求调整）
- `upload_time`: 上传时间，自动设置

### 3.3 表关系说明

```
ar_region (区域)
    ↓ 1:N
ar_point (点位)

ar_region (区域)
    ↓ 1:N
ar_task (任务模板)
    ↓ 1:N
ar_step (步骤) - 树形结构

ar_task (任务模板)
    ↓ 1:N
ar_execution (任务执行)
    ↓ 1:N
ar_step_record (步骤记录)
    ↓ 1:N
ar_step_media (媒体文件)

ar_device (设备)
    ↓ 1:N
ar_execution (任务执行)
```

---

## 四、技术架构设计

### 4.1 分层架构

```
┌─────────────────────────────────────┐
│         Controller Layer            │  REST API控制层
│   (ArDeviceController, etc.)        │
└─────────────────────────────────────┘
              ↓
┌─────────────────────────────────────┐
│          Service Layer              │  业务逻辑层
│  (IArDeviceService, Impl, etc.)     │
└─────────────────────────────────────┘
              ↓
┌─────────────────────────────────────┐
│          Mapper Layer               │  数据访问层
│    (ArDeviceMapper, etc.)           │
└─────────────────────────────────────┘
              ↓
┌─────────────────────────────────────┐
│         Database Layer              │  MySQL数据库
│         (ar_device, etc.)           │
└─────────────────────────────────────┘
```

### 4.2 对象转换流程

```
Client Request (JSON)
      ↓
  Controller
      ↓ @RequestBody
     Bo (Business Object)
      ↓ MapStructUtils.convert()
   Entity
      ↓ MyBatis-Plus
   Database
      ↓ MyBatis-Plus
     Vo (View Object)
      ↓ JSON Response
    Client
```

### 4.3 核心技术组件

#### 4.3.1 MyBatis-Plus 配置

**主键策略：** 雪花ID（ASSIGN_ID）

```java
@TableId(value = "id")
private Long id;
```

**逻辑删除：**

```java
@TableLogic
private Integer delFlag;
```

**JSON字段处理：**

```java
@TableName(value = "ar_region", autoResultMap = true)
public class ArRegion extends BaseEntity {
    @TableField(typeHandler = JacksonTypeHandler.class)
    private Map<String, Object> regionData;
}
```

#### 4.3.2 MapStruct-Plus 对象映射

**实体与Bo互转：**

```java
@Data
@AutoMapper(target = ArDevice.class, reverseConvertGenerate = false)
public class ArDeviceBo extends BaseEntity {
    // fields...
}
```

**实体转Vo：**

```java
@Data
@AutoMapper(target = ArDevice.class)
public class ArDeviceVo implements Serializable {
    // fields...
}
```

**使用方式：**

```java
ArDevice entity = MapstructUtils.convert(bo, ArDevice.class);
```

#### 4.3.3 Sa-Token 权限控制

**权限注解：**

```java
@SaCheckPermission("inspection:device:list")
@GetMapping("/list")
public TableDataInfo<ArDeviceVo> list(ArDeviceBo bo, PageQuery pageQuery) {
    return arDeviceService.queryPageList(bo, pageQuery);
}
```

**权限标识格式：** `模块:功能:操作`

#### 4.3.4 数据验证

**分组验证：**

```java
// 新增时验证
@NotBlank(message = "设备名称不能为空", groups = {AddGroup.class})
private String deviceName;

// 编辑时验证
@NotNull(message = "设备ID不能为空", groups = {EditGroup.class})
private Long id;
```

---

## 五、模块设计

### 5.1 模块结构

```
ruoyi-modules/
└── ruoyi-inspection/
    ├── pom.xml
    └── src/main/java/org/dromara/inspection/
        ├── controller/         # 控制器层
        │   ├── ArDeviceController.java
        │   ├── ArRegionController.java
        │   ├── ArPointController.java
        │   ├── ArTaskController.java
        │   ├── ArStepController.java
        │   ├── ArExecutionController.java
        │   ├── ArStepRecordController.java
        │   └── ArStepMediaController.java
        ├── service/            # 服务接口
        │   ├── IArDeviceService.java
        │   └── impl/           # 服务实现
        │       └── ArDeviceServiceImpl.java
        ├── mapper/             # Mapper接口
        │   └── ArDeviceMapper.java
        └── domain/             # 领域对象
            ├── ArDevice.java           # 实体
            ├── bo/                      # 业务对象
            │   └── ArDeviceBo.java
            └── vo/                      # 视图对象
                └── ArDeviceVo.java
```

### 5.2 标准CRUD实现模式

#### 5.2.1 Controller层标准实现

```java
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/inspection/device")
public class ArDeviceController extends BaseController {

    private final IArDeviceService arDeviceService;

    // 1. 分页查询列表
    @SaCheckPermission("inspection:device:list")
    @GetMapping("/list")
    public TableDataInfo<ArDeviceVo> list(
        @Validated(QueryGroup.class) ArDeviceBo bo,
        PageQuery pageQuery) {
        return arDeviceService.queryPageList(bo, pageQuery);
    }

    // 2. 导出Excel
    @SaCheckPermission("inspection:device:export")
    @Log(title = "AR设备", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(ArDeviceBo bo, HttpServletResponse response) {
        List<ArDeviceVo> list = arDeviceService.queryList(bo);
        ExcelUtil.exportExcel(list, "AR设备", ArDeviceVo.class, response);
    }

    // 3. 查询详情
    @SaCheckPermission("inspection:device:query")
    @GetMapping("/{id}")
    public R<ArDeviceVo> getInfo(@PathVariable("id") Long id) {
        return R.ok(arDeviceService.queryById(id));
    }

    // 4. 新增
    @SaCheckPermission("inspection:device:add")
    @Log(title = "AR设备", businessType = BusinessType.INSERT)
    @RepeatSubmit
    @PostMapping()
    public R<Void> add(@Validated(AddGroup.class) @RequestBody ArDeviceBo bo) {
        return toAjax(arDeviceService.insertByBo(bo));
    }

    // 5. 修改
    @SaCheckPermission("inspection:device:edit")
    @Log(title = "AR设备", businessType = BusinessType.UPDATE)
    @RepeatSubmit
    @PutMapping()
    public R<Void> edit(@Validated(EditGroup.class) @RequestBody ArDeviceBo bo) {
        return toAjax(arDeviceService.updateByBo(bo));
    }

    // 6. 删除
    @SaCheckPermission("inspection:device:remove")
    @Log(title = "AR设备", businessType = BusinessType.DELETE)
    @DeleteMapping("/{ids}")
    public R<Void> remove(@PathVariable Long[] ids) {
        return toAjax(arDeviceService.deleteWithValidByIds(Arrays.asList(ids), true));
    }
}
```

#### 5.2.2 Service层标准实现

```java
@RequiredArgsConstructor
@Service
public class ArDeviceServiceImpl implements IArDeviceService {

    private final ArDeviceMapper baseMapper;

    @Override
    public ArDeviceVo queryById(Long id) {
        return baseMapper.selectVoById(id);
    }

    @Override
    public TableDataInfo<ArDeviceVo> queryPageList(ArDeviceBo bo, PageQuery pageQuery) {
        LambdaQueryWrapper<ArDevice> lqw = buildQueryWrapper(bo);
        Page<ArDeviceVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
        return TableDataInfo.build(result);
    }

    @Override
    public List<ArDeviceVo> queryList(ArDeviceBo bo) {
        return baseMapper.selectVoList(buildQueryWrapper(bo));
    }

    private LambdaQueryWrapper<ArDevice> buildQueryWrapper(ArDeviceBo bo) {
        LambdaQueryWrapper<ArDevice> lqw = Wrappers.lambdaQuery();
        lqw.like(StringUtils.isNotBlank(bo.getDeviceName()),
                 ArDevice::getDeviceName, bo.getDeviceName());
        lqw.eq(StringUtils.isNotBlank(bo.getDeviceNo()),
               ArDevice::getDeviceNo, bo.getDeviceNo());
        lqw.eq(StringUtils.isNotBlank(bo.getStatus()),
               ArDevice::getStatus, bo.getStatus());
        return lqw;
    }

    @Override
    public Boolean insertByBo(ArDeviceBo bo) {
        ArDevice add = MapstructUtils.convert(bo, ArDevice.class);
        validEntityBeforeSave(add);
        boolean flag = baseMapper.insert(add) > 0;
        if (flag) {
            bo.setId(add.getId());
        }
        return flag;
    }

    @Override
    public Boolean updateByBo(ArDeviceBo bo) {
        ArDevice update = MapstructUtils.convert(bo, ArDevice.class);
        validEntityBeforeSave(update);
        return baseMapper.updateById(update) > 0;
    }

    private void validEntityBeforeSave(ArDevice entity) {
        // 唯一性校验：设备编号
        if (StringUtils.isNotBlank(entity.getDeviceNo())) {
            LambdaQueryWrapper<ArDevice> lqw = Wrappers.lambdaQuery();
            lqw.eq(ArDevice::getDeviceNo, entity.getDeviceNo());
            lqw.ne(entity.getId() != null, ArDevice::getId, entity.getId());
            long count = baseMapper.selectCount(lqw);
            if (count > 0) {
                throw new ServiceException("设备编号已存在!");
            }
        }
    }

    @Override
    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
        if (isValid) {
            List<ArDevice> list = baseMapper.selectByIds(ids);
            if (list.size() != ids.size()) {
                throw new ServiceException("您没有删除权限!");
            }
        }
        return baseMapper.deleteByIds(ids) > 0;
    }
}
```

### 5.3 特殊模块设计

#### 5.3.1 步骤管理（树形结构）

**树形VO设计：**

```java
@Data
public class ArStepTreeVo implements Serializable {
    private Long id;
    private Long taskId;
    private Long parentId;
    private String stepName;
    // ... 其他字段

    // 核心：子节点列表
    private List<ArStepTreeVo> children;
}
```

**树形查询接口：**

```java
@GetMapping("/tree/{taskId}")
public R<List<ArStepTreeVo>> tree(@PathVariable("taskId") Long taskId) {
    return R.ok(arStepService.queryStepTree(taskId));
}
```

**递归构建树形结构：**

```java
@Override
public List<ArStepTreeVo> queryStepTree(Long taskId) {
    // 1. 查询所有步骤
    LambdaQueryWrapper<ArStep> lqw = Wrappers.lambdaQuery();
    lqw.eq(ArStep::getTaskId, taskId);
    lqw.orderByAsc(ArStep::getOrderNum);
    List<ArStepVo> allSteps = baseMapper.selectVoList(lqw);

    // 2. 递归构建树
    return buildStepTree(allSteps, 0L);
}

private List<ArStepTreeVo> buildStepTree(List<ArStepVo> allSteps, Long parentId) {
    List<ArStepTreeVo> tree = new ArrayList<>();
    for (ArStepVo step : allSteps) {
        if (step.getParentId().equals(parentId)) {
            ArStepTreeVo treeNode = new ArStepTreeVo();
            BeanUtils.copyProperties(step, treeNode);

            // 递归查找子节点
            List<ArStepTreeVo> children = buildStepTree(allSteps, step.getId());
            treeNode.setChildren(children);

            tree.add(treeNode);
        }
    }
    return tree;
}
```

**自动维护ancestors字段：**

```java
@Override
public Boolean insertByBo(ArStepBo bo) {
    ArStep add = MapstructUtils.convert(bo, ArStep.class);

    // 自动设置ancestors
    if (add.getParentId() != null && add.getParentId() != 0) {
        ArStep parent = baseMapper.selectById(add.getParentId());
        if (parent != null) {
            add.setAncestors(parent.getAncestors() + "," + add.getParentId());
        } else {
            add.setAncestors("0," + add.getParentId());
        }
    } else {
        add.setAncestors("0");
    }

    // 设置初始状态
    if (StringUtils.isBlank(add.getIsLeaf())) {
        add.setIsLeaf("1");
    }

    boolean flag = baseMapper.insert(add) > 0;

    // 如果插入成功且有父节点，更新父节点的isLeaf
    if (flag && add.getParentId() != null && add.getParentId() != 0) {
        ArStep parent = new ArStep();
        parent.setId(add.getParentId());
        parent.setIsLeaf("0");
        baseMapper.updateById(parent);
    }

    if (flag) {
        bo.setId(add.getId());
    }
    return flag;
}
```

**级联删除：**

```java
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
    if (isValid) {
        List<ArStep> list = baseMapper.selectByIds(ids);
        if (list.size() != ids.size()) {
            throw new ServiceException("您没有删除权限!");
        }
    }

    // 级联删除子步骤
    for (Long id : ids) {
        LambdaQueryWrapper<ArStep> lqw = Wrappers.lambdaQuery();
        lqw.eq(ArStep::getParentId, id);
        long count = baseMapper.selectCount(lqw);
        if (count > 0) {
            List<ArStep> children = baseMapper.selectList(lqw);
            List<Long> childrenIds = children.stream()
                .map(ArStep::getId)
                .collect(Collectors.toList());
            // 递归删除子节点
            deleteWithValidByIds(childrenIds, false);
        }
    }

    return baseMapper.deleteByIds(ids) > 0;
}
```

#### 5.3.2 任务执行管理（状态管理）

**自动生成执行编号：**

```java
@Override
public Boolean insertByBo(ArExecutionBo bo) {
    ArExecution add = MapstructUtils.convert(bo, ArExecution.class);

    // 自动生成执行编号
    if (StringUtils.isBlank(add.getExecutionCode())) {
        add.setExecutionCode("EXE-" + System.currentTimeMillis());
    }

    // 设置初始状态
    if (StringUtils.isBlank(add.getStatus())) {
        add.setStatus("pending");
    }

    boolean flag = baseMapper.insert(add) > 0;
    if (flag) {
        bo.setId(add.getId());
    }
    return flag;
}
```

**状态变化自动设置时间戳：**

```java
@Override
public Boolean updateByBo(ArExecutionBo bo) {
    ArExecution update = MapstructUtils.convert(bo, ArExecution.class);

    // 状态变为in_progress时，自动设置开始时间
    if ("in_progress".equals(update.getStatus()) && update.getStartTime() == null) {
        update.setStartTime(new Date());
    }

    // 状态变为completed或cancelled时，自动设置结束时间
    if (("completed".equals(update.getStatus()) || "cancelled".equals(update.getStatus()))
        && update.getEndTime() == null) {
        update.setEndTime(new Date());
    }

    return baseMapper.updateById(update) > 0;
}
```

#### 5.3.3 步骤记录管理（自动计算耗时）

```java
@Override
public Boolean updateByBo(ArStepRecordBo bo) {
    ArStepRecord update = MapstructUtils.convert(bo, ArStepRecord.class);

    // 状态变为completed时，自动设置完成时间并计算耗时
    if ("completed".equals(update.getStatus()) && update.getCompletionTime() == null) {
        update.setCompletionTime(new Date());
        update.setIsDone("1");

        // 自动计算耗时（秒）
        if (update.getStartTime() != null) {
            long duration = (update.getCompletionTime().getTime()
                           - update.getStartTime().getTime()) / 1000;
            update.setDuration((int) duration);
        }
    }

    return baseMapper.updateById(update) > 0;
}
```

#### 5.3.4 媒体文件管理（自动设置上传时间）

```java
@Override
public Boolean insertByBo(ArStepMediaBo bo) {
    ArStepMedia add = MapstructUtils.convert(bo, ArStepMedia.class);
    validEntityBeforeSave(add);

    // 自动设置上传时间
    if (add.getUploadTime() == null) {
        add.setUploadTime(new Date());
    }

    boolean flag = baseMapper.insert(add) > 0;
    if (flag) {
        bo.setId(add.getId());
    }
    return flag;
}
```

---

## 六、API接口设计

### 6.1 RESTful API规范

所有API遵循统一的RESTful风格：

| HTTP方法 | 路径 | 功能 | 权限标识 |
|---------|------|-----|---------|
| GET | /inspection/{module}/list | 分页查询列表 | {module}:list |
| POST | /inspection/{module}/export | 导出Excel | {module}:export |
| GET | /inspection/{module}/{id} | 查询详情 | {module}:query |
| POST | /inspection/{module} | 新增 | {module}:add |
| PUT | /inspection/{module} | 修改 | {module}:edit |
| DELETE | /inspection/{module}/{ids} | 批量删除 | {module}:remove |

### 6.2 模块API列表

#### 6.2.1 AR设备管理

```
GET    /inspection/device/list          # 分页查询设备列表
POST   /inspection/device/export        # 导出设备列表
GET    /inspection/device/{id}          # 查询设备详情
POST   /inspection/device               # 新增设备
PUT    /inspection/device               # 修改设备
DELETE /inspection/device/{ids}         # 删除设备
```

**查询参数示例：**
```json
{
  "deviceName": "设备1",
  "deviceNo": "DEV001",
  "status": "0",
  "pageNum": 1,
  "pageSize": 10
}
```

#### 6.2.2 区域管理

```
GET    /inspection/region/list          # 分页查询区域列表
POST   /inspection/region/export        # 导出区域列表
GET    /inspection/region/{id}          # 查询区域详情
POST   /inspection/region               # 新增区域
PUT    /inspection/region               # 修改区域
DELETE /inspection/region/{ids}         # 删除区域
```

**新增请求体示例：**
```json
{
  "regionName": "A区",
  "regionCode": "REGION-A",
  "regionData": {
    "area": 1000,
    "building": "主楼",
    "floor": 3
  },
  "status": "0",
  "remark": "主要巡检区域"
}
```

#### 6.2.3 点位管理

```
GET    /inspection/point/list           # 分页查询点位列表
POST   /inspection/point/export         # 导出点位列表
GET    /inspection/point/{id}           # 查询点位详情
POST   /inspection/point                # 新增点位
PUT    /inspection/point                # 修改点位
DELETE /inspection/point/{ids}          # 删除点位
```

**新增请求体示例：**
```json
{
  "regionId": 1,
  "pointName": "配电柜A1",
  "pointCode": "POINT-A1",
  "positionData": {
    "x": 10.5,
    "y": 20.3,
    "z": 1.5,
    "rotation": {
      "x": 0,
      "y": 90,
      "z": 0
    }
  },
  "remark": "主配电柜"
}
```

#### 6.2.4 任务模板管理

```
GET    /inspection/task/list            # 分页查询任务列表
POST   /inspection/task/export          # 导出任务列表
GET    /inspection/task/{id}            # 查询任务详情
POST   /inspection/task                 # 新增任务
PUT    /inspection/task                 # 修改任务
DELETE /inspection/task/{ids}           # 删除任务
```

#### 6.2.5 步骤管理

```
GET    /inspection/step/list            # 分页查询步骤列表
GET    /inspection/step/tree/{taskId}   # 查询任务的步骤树
POST   /inspection/step/export          # 导出步骤列表
GET    /inspection/step/{id}            # 查询步骤详情
POST   /inspection/step                 # 新增步骤
PUT    /inspection/step                 # 修改步骤
DELETE /inspection/step/{ids}           # 删除步骤（级联）
```

**树形结构返回示例：**
```json
[
  {
    "id": 1,
    "taskId": 1,
    "parentId": 0,
    "stepName": "开始巡检",
    "orderNum": 1,
    "children": [
      {
        "id": 2,
        "parentId": 1,
        "stepName": "检查电压",
        "orderNum": 1,
        "children": []
      },
      {
        "id": 3,
        "parentId": 1,
        "stepName": "检查电流",
        "orderNum": 2,
        "children": []
      }
    ]
  }
]
```

**新增步骤请求体示例：**
```json
{
  "taskId": 1,
  "parentId": 0,
  "stepName": "检查配电柜",
  "stepContent": "检查配电柜外观及指示灯状态",
  "orderNum": 1,
  "pointId": 1,
  "needVoiceRead": "1",
  "needAi": "1",
  "aiTargetName": "配电柜指示灯",
  "aiData": {
    "modelName": "yolov8",
    "confidence": 0.8,
    "classes": ["红灯", "绿灯", "黄灯"]
  }
}
```

#### 6.2.6 任务执行管理

```
GET    /inspection/execution/list       # 分页查询执行记录列表
POST   /inspection/execution/export     # 导出执行记录
GET    /inspection/execution/{id}       # 查询执行记录详情
POST   /inspection/execution            # 新增执行记录
PUT    /inspection/execution            # 修改执行记录
DELETE /inspection/execution/{ids}      # 删除执行记录
```

**新增执行记录示例：**
```json
{
  "taskId": 1,
  "regionId": 1,
  "deviceId": 1,
  "operatorId": 1001,
  "operatorName": "张三",
  "custodianId": 1002,
  "custodianName": "李四",
  "status": "pending"
}
```

**更新状态示例：**
```json
{
  "id": 1,
  "status": "in_progress"
  // 自动设置startTime
}
```

#### 6.2.7 步骤记录管理

```
GET    /inspection/stepRecord/list      # 分页查询步骤记录列表
POST   /inspection/stepRecord/export    # 导出步骤记录
GET    /inspection/stepRecord/{id}      # 查询步骤记录详情
POST   /inspection/stepRecord           # 新增步骤记录
PUT    /inspection/stepRecord           # 修改步骤记录
DELETE /inspection/stepRecord/{ids}     # 删除步骤记录
```

**新增步骤记录示例：**
```json
{
  "executionId": 1,
  "stepId": 1,
  "startTime": "2025-01-13 10:00:00"
}
```

**完成步骤示例：**
```json
{
  "id": 1,
  "status": "completed",
  "textFeedback": "检查完成，设备状态正常",
  "voiceText": "设备状态正常",
  "aiResult": {
    "detected": true,
    "confidence": 0.95,
    "status": "正常"
  }
  // 自动设置completionTime和计算duration
}
```

#### 6.2.8 媒体文件管理

```
GET    /inspection/stepMedia/list       # 分页查询媒体文件列表
POST   /inspection/stepMedia/export     # 导出媒体文件列表
GET    /inspection/stepMedia/{id}       # 查询媒体文件详情
POST   /inspection/stepMedia            # 新增媒体文件
PUT    /inspection/stepMedia            # 修改媒体文件
DELETE /inspection/stepMedia/{ids}      # 删除媒体文件
```

**新增媒体文件示例：**
```json
{
  "stepRecordId": 1,
  "mediaType": "image",
  "fileUrl": "https://oss.example.com/inspection/20250113/abc123.jpg",
  "fileName": "配电柜照片.jpg",
  "fileSize": 2048576
  // 自动设置uploadTime
}
```

### 6.3 统一响应格式

**成功响应：**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    // 响应数据
  }
}
```

**分页响应：**
```json
{
  "code": 200,
  "msg": "查询成功",
  "rows": [
    // 数据列表
  ],
  "total": 100
}
```

**失败响应：**
```json
{
  "code": 500,
  "msg": "设备编号已存在!"
}
```

---

## 七、关键技术实现

### 7.1 多租户禁用方案

#### 7.1.1 配置文件修改

**application.yml：**
```yaml
# 多租户配置
tenant:
  # 是否开启
  enable: false
  # 排除表（不进行多租户处理的表）
  excludes:
    - sys_menu
    - sys_tenant
    - sys_tenant_package
    - ar_device
    - ar_region
    - ar_point
    - ar_task
    - ar_step
    - ar_execution
    - ar_step_record
    - ar_step_media
```

**application-dev.yml, application-prod.yml：**
同样设置 `tenant.enable: false`

#### 7.1.2 实体类设计

所有实体类继承 **BaseEntity** 而非 TenantEntity：

```java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("ar_device")
public class ArDevice extends BaseEntity {
    // 不包含tenantId字段
}
```

### 7.2 JSON字段处理方案

#### 7.2.1 实体类配置

```java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "ar_region", autoResultMap = true)  // 必须设置autoResultMap
public class ArRegion extends BaseEntity {

    @TableField(typeHandler = JacksonTypeHandler.class)  // 指定类型处理器
    private Map<String, Object> regionData;
}
```

**关键点：**
1. `@TableName(autoResultMap = true)` - 启用自动结果映射
2. `@TableField(typeHandler = JacksonTypeHandler.class)` - 使用Jackson处理器
3. 字段类型使用 `Map<String, Object>` 或自定义POJO

#### 7.2.2 数据库字段类型

MySQL使用 `json` 类型：
```sql
`region_data` json DEFAULT NULL COMMENT '区域数据(JSON格式)'
```

#### 7.2.3 使用示例

**新增数据：**
```java
ArRegion region = new ArRegion();
region.setRegionName("A区");
Map<String, Object> data = new HashMap<>();
data.put("area", 1000);
data.put("building", "主楼");
region.setRegionData(data);
baseMapper.insert(region);
```

**查询数据：**
```java
ArRegion region = baseMapper.selectById(1L);
Map<String, Object> data = region.getRegionData();
Integer area = (Integer) data.get("area");
```

### 7.3 树形结构实现方案

#### 7.3.1 数据库字段设计

```sql
`parent_id` bigint DEFAULT '0' COMMENT '父步骤ID(0表示根节点)',
`ancestors` varchar(500) DEFAULT '0' COMMENT '祖级列表',
`is_leaf` char(1) DEFAULT '1' COMMENT '是否叶子节点(0否 1是)'
```

#### 7.3.2 ancestors字段说明

ancestors存储从根节点到父节点的完整路径：
- 根节点：`"0"`
- 一级节点：`"0,1"`
- 二级节点：`"0,1,2"`
- 三级节点：`"0,1,2,3"`

**优势：**
- 快速查询所有祖先节点
- 快速查询所有子孙节点
- 支持任意深度

#### 7.3.3 新增节点自动维护ancestors

```java
public Boolean insertByBo(ArStepBo bo) {
    ArStep add = MapstructUtils.convert(bo, ArStep.class);

    // 自动设置ancestors
    if (add.getParentId() != null && add.getParentId() != 0) {
        ArStep parent = baseMapper.selectById(add.getParentId());
        if (parent != null) {
            // 继承父节点的ancestors + 父节点ID
            add.setAncestors(parent.getAncestors() + "," + add.getParentId());
        } else {
            add.setAncestors("0," + add.getParentId());
        }
    } else {
        add.setAncestors("0");
    }

    boolean flag = baseMapper.insert(add) > 0;

    // 更新父节点的isLeaf标识
    if (flag && add.getParentId() != null && add.getParentId() != 0) {
        ArStep parent = new ArStep();
        parent.setId(add.getParentId());
        parent.setIsLeaf("0");  // 有子节点，不是叶子节点
        baseMapper.updateById(parent);
    }

    return flag;
}
```

#### 7.3.4 递归构建树形结构

```java
public List<ArStepTreeVo> queryStepTree(Long taskId) {
    // 1. 一次性查询所有步骤
    LambdaQueryWrapper<ArStep> lqw = Wrappers.lambdaQuery();
    lqw.eq(ArStep::getTaskId, taskId);
    lqw.orderByAsc(ArStep::getOrderNum);
    List<ArStepVo> allSteps = baseMapper.selectVoList(lqw);

    // 2. 递归构建树（从根节点parentId=0开始）
    return buildStepTree(allSteps, 0L);
}

private List<ArStepTreeVo> buildStepTree(List<ArStepVo> allSteps, Long parentId) {
    List<ArStepTreeVo> tree = new ArrayList<>();

    for (ArStepVo step : allSteps) {
        if (step.getParentId().equals(parentId)) {
            // 找到当前parentId的子节点
            ArStepTreeVo treeNode = new ArStepTreeVo();
            BeanUtils.copyProperties(step, treeNode);

            // 递归查找子节点的子节点
            List<ArStepTreeVo> children = buildStepTree(allSteps, step.getId());
            treeNode.setChildren(children);

            tree.add(treeNode);
        }
    }

    return tree;
}
```

#### 7.3.5 级联删除

```java
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
    // 权限校验...

    // 级联删除所有子节点
    for (Long id : ids) {
        LambdaQueryWrapper<ArStep> lqw = Wrappers.lambdaQuery();
        lqw.eq(ArStep::getParentId, id);
        long count = baseMapper.selectCount(lqw);

        if (count > 0) {
            // 查询所有子节点
            List<ArStep> children = baseMapper.selectList(lqw);
            List<Long> childrenIds = children.stream()
                .map(ArStep::getId)
                .collect(Collectors.toList());

            // 递归删除子节点
            deleteWithValidByIds(childrenIds, false);
        }
    }

    return baseMapper.deleteByIds(ids) > 0;
}
```

### 7.4 自动字段管理方案

#### 7.4.1 自动生成执行编号

```java
@Override
public Boolean insertByBo(ArExecutionBo bo) {
    ArExecution add = MapstructUtils.convert(bo, ArExecution.class);

    // 自动生成执行编号：EXE-{时间戳}
    if (StringUtils.isBlank(add.getExecutionCode())) {
        add.setExecutionCode("EXE-" + System.currentTimeMillis());
    }

    // 设置初始状态
    if (StringUtils.isBlank(add.getStatus())) {
        add.setStatus("pending");
    }

    boolean flag = baseMapper.insert(add) > 0;
    if (flag) {
        bo.setId(add.getId());
    }
    return flag;
}
```

#### 7.4.2 状态变化自动设置时间戳

```java
@Override
public Boolean updateByBo(ArExecutionBo bo) {
    ArExecution update = MapstructUtils.convert(bo, ArExecution.class);

    // pending -> in_progress：设置开始时间
    if ("in_progress".equals(update.getStatus()) && update.getStartTime() == null) {
        update.setStartTime(new Date());
    }

    // -> completed/cancelled：设置结束时间
    if (("completed".equals(update.getStatus()) || "cancelled".equals(update.getStatus()))
        && update.getEndTime() == null) {
        update.setEndTime(new Date());
    }

    return baseMapper.updateById(update) > 0;
}
```

#### 7.4.3 自动计算耗时

```java
@Override
public Boolean updateByBo(ArStepRecordBo bo) {
    ArStepRecord update = MapstructUtils.convert(bo, ArStepRecord.class);

    // 状态变为completed：自动设置完成时间、isDone标识、计算耗时
    if ("completed".equals(update.getStatus()) && update.getCompletionTime() == null) {
        update.setCompletionTime(new Date());
        update.setIsDone("1");

        // 计算耗时（秒）
        if (update.getStartTime() != null) {
            long durationMs = update.getCompletionTime().getTime()
                            - update.getStartTime().getTime();
            update.setDuration((int) (durationMs / 1000));
        }
    }

    return baseMapper.updateById(update) > 0;
}
```

#### 7.4.4 自动设置上传时间

```java
@Override
public Boolean insertByBo(ArStepMediaBo bo) {
    ArStepMedia add = MapstructUtils.convert(bo, ArStepMedia.class);

    // 自动设置上传时间
    if (add.getUploadTime() == null) {
        add.setUploadTime(new Date());
    }

    boolean flag = baseMapper.insert(add) > 0;
    if (flag) {
        bo.setId(add.getId());
    }
    return flag;
}
```

### 7.5 唯一性校验方案

#### 7.5.1 单字段唯一性校验

```java
private void validEntityBeforeSave(ArDevice entity) {
    // 校验设备编号唯一性
    if (StringUtils.isNotBlank(entity.getDeviceNo())) {
        LambdaQueryWrapper<ArDevice> lqw = Wrappers.lambdaQuery();
        lqw.eq(ArDevice::getDeviceNo, entity.getDeviceNo());
        // 编辑时排除自身
        lqw.ne(entity.getId() != null, ArDevice::getId, entity.getId());

        long count = baseMapper.selectCount(lqw);
        if (count > 0) {
            throw new ServiceException("设备编号已存在!");
        }
    }
}
```

#### 7.5.2 联合唯一性校验

```java
private void validEntityBeforeSave(ArPoint entity) {
    // 校验点位代码在同一区域内唯一
    if (entity.getRegionId() != null && StringUtils.isNotBlank(entity.getPointCode())) {
        LambdaQueryWrapper<ArPoint> lqw = Wrappers.lambdaQuery();
        lqw.eq(ArPoint::getRegionId, entity.getRegionId());
        lqw.eq(ArPoint::getPointCode, entity.getPointCode());
        lqw.ne(entity.getId() != null, ArPoint::getId, entity.getId());

        long count = baseMapper.selectCount(lqw);
        if (count > 0) {
            throw new ServiceException("该区域下点位代码已存在!");
        }
    }
}
```

---

## 八、部署说明

### 8.1 开发环境部署

#### 8.1.1 后端启动

```bash
# 1. 编译项目
mvn clean install -DskipTests

# 2. 运行主应用（开发环境）
cd ruoyi-admin
mvn spring-boot:run

# 或者使用IDE运行
# 主类：org.dromara.DromaraApplication
```

访问地址：`http://localhost:8080`

#### 8.1.2 前端启动

```bash
# 1. 进入前端目录
cd plus-ui

# 2. 安装依赖（首次）
npm install --registry=https://registry.npmmirror.com

# 3. 启动开发服务器
npm run dev
```

访问地址：`http://localhost:80`

默认账号：`admin / admin123`

#### 8.1.3 数据库初始化

1. 创建数据库：`ry-vue-plus`
2. 执行脚本：`script/sql/mysql/ry_all_5.5.1.sql`
3. 执行业务表创建语句（已通过代码自动创建）

### 8.2 生产环境部署

#### 8.2.1 后端打包

```bash
# 使用生产环境配置打包
mvn clean package -Pprod -DskipTests

# 生成的jar包位置
# ruoyi-admin/target/ruoyi-admin.jar
```

#### 8.2.2 后端运行

```bash
# 启动应用
java -jar ruoyi-admin.jar

# 指定配置文件
java -jar ruoyi-admin.jar --spring.profiles.active=prod

# 后台运行
nohup java -jar ruoyi-admin.jar > server.log 2>&1 &
```

#### 8.2.3 前端打包

```bash
cd plus-ui

# 构建生产环境
npm run build:prod

# 生成的静态文件位置
# plus-ui/dist/
```

#### 8.2.4 Nginx配置

```nginx
server {
    listen 80;
    server_name your-domain.com;

    # 前端静态文件
    location / {
        root /var/www/ar-inspection/dist;
        try_files $uri $uri/ /index.html;
    }

    # 后端API代理
    location /prod-api/ {
        proxy_pass http://localhost:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
```

### 8.3 Docker部署

#### 8.3.1 Dockerfile（后端）

```dockerfile
FROM openjdk:17-jdk-alpine
WORKDIR /app
COPY ruoyi-admin/target/ruoyi-admin.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
```

#### 8.3.2 Dockerfile（前端）

```dockerfile
FROM nginx:alpine
COPY plus-ui/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```

#### 8.3.3 docker-compose.yml

```yaml
version: '3'
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: ry-vue-plus
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  backend:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - mysql
      - redis
    environment:
      SPRING_PROFILES_ACTIVE: prod

  frontend:
    build:
      context: .
      dockerfile: Dockerfile.frontend
    ports:
      - "80:80"
    depends_on:
      - backend

volumes:
  mysql-data:
```

### 8.4 环境配置检查清单

- [ ] MySQL 5.7+ 已安装并启动
- [ ] Redis 已安装并启动
- [ ] JDK 17/21 已安装
- [ ] Maven 3.6+ 已安装（开发环境）
- [ ] Node.js 18+ 已安装（开发环境）
- [ ] 数据库连接信息已配置
- [ ] Redis连接信息已配置
- [ ] 多租户已禁用（tenant.enable: false）
- [ ] 所有业务表已加入tenant.excludes
- [ ] 文件上传路径已配置
- [ ] 日志路径已配置

---

## 九、系统测试

### 9.1 功能测试清单

#### 9.1.1 AR设备管理
- [ ] 新增设备（设备编号唯一性校验）
- [ ] 修改设备信息
- [ ] 删除设备
- [ ] 查询设备列表（分页、条件查询）
- [ ] 导出设备列表Excel

#### 9.1.2 区域管理
- [ ] 新增区域（区域代码唯一性校验）
- [ ] 修改区域信息（包含region_data JSON）
- [ ] 删除区域
- [ ] 查询区域列表
- [ ] 导出区域列表

#### 9.1.3 点位管理
- [ ] 新增点位（点位代码区域内唯一性校验）
- [ ] 修改点位（包含position_data JSON）
- [ ] 删除点位
- [ ] 查询点位列表
- [ ] 导出点位列表

#### 9.1.4 任务模板管理
- [ ] 新增任务（任务代码唯一性校验）
- [ ] 修改任务
- [ ] 删除任务
- [ ] 查询任务列表
- [ ] 导出任务列表

#### 9.1.5 步骤管理
- [ ] 新增根步骤（parentId=0）
- [ ] 新增子步骤（ancestors自动维护）
- [ ] 修改步骤（包含ai_data JSON）
- [ ] 删除步骤（级联删除子步骤）
- [ ] 查询步骤树形结构
- [ ] 查询步骤列表
- [ ] 导出步骤列表

#### 9.1.6 任务执行管理
- [ ] 新增执行记录（execution_code自动生成）
- [ ] 修改状态为in_progress（start_time自动设置）
- [ ] 修改状态为completed（end_time自动设置）
- [ ] 修改状态为cancelled
- [ ] 删除执行记录
- [ ] 查询执行记录列表
- [ ] 导出执行记录

#### 9.1.7 步骤记录管理
- [ ] 新增步骤记录
- [ ] 修改步骤记录
- [ ] 完成步骤（completion_time和duration自动计算）
- [ ] 删除步骤记录
- [ ] 查询步骤记录列表
- [ ] 导出步骤记录

#### 9.1.8 媒体文件管理
- [ ] 新增媒体文件（upload_time自动设置）
- [ ] 修改媒体文件信息
- [ ] 删除媒体文件
- [ ] 查询媒体文件列表
- [ ] 导出媒体文件列表

### 9.2 性能测试建议

- 步骤树形结构查询性能（大数据量）
- 分页查询性能
- JSON字段查询性能
- 级联删除性能

### 9.3 安全测试建议

- 权限控制测试（Sa-Token）
- SQL注入防护测试
- XSS防护测试
- 文件上传安全测试

---

## 十、附录

### 10.1 权限标识清单

| 模块 | 权限标识 |
|------|---------|
| AR设备管理 | inspection:device:list / query / add / edit / remove / export |
| 区域管理 | inspection:region:list / query / add / edit / remove / export |
| 点位管理 | inspection:point:list / query / add / edit / remove / export |
| 任务模板管理 | inspection:task:list / query / add / edit / remove / export |
| 步骤管理 | inspection:step:list / query / add / edit / remove / export |
| 任务执行管理 | inspection:execution:list / query / add / edit / remove / export |
| 步骤记录管理 | inspection:stepRecord:list / query / add / edit / remove / export |
| 媒体文件管理 | inspection:stepMedia:list / query / add / edit / remove / export |

### 10.2 数据字典

#### 设备状态（device.status）
- 0：正常
- 1：停用

#### 执行状态（execution.status）
- pending：待执行
- in_progress：执行中
- completed：已完成
- cancelled：已取消

#### 步骤记录状态（step_record.status）
- pending：待执行
- completed：已完成
- skipped：已跳过

#### 媒体类型（step_media.media_type）
- image：图片
- video：视频
- audio：音频

#### 是否标识（通用）
- 0：否
- 1：是

### 10.3 常见问题FAQ

**Q1: 为什么不使用多租户？**
A: 根据项目实际需求，系统面向单一组织使用，不需要多租户隔离，禁用多租户可以简化数据模型和查询逻辑。

**Q2: JSON字段如何进行查询？**
A: MyBatis-Plus支持JSON字段的基本查询，复杂查询可以使用原生SQL或MySQL的JSON函数。

**Q3: 步骤树形结构支持多深？**
A: 理论上支持无限深度，但建议不超过5层，过深的层级会影响查询性能和用户体验。

**Q4: 如何批量导入设备/区域等数据？**
A: 使用Excel导入功能（需要前端实现），或通过API批量调用新增接口。

**Q5: 媒体文件存储在哪里？**
A: 系统支持MinIO、阿里云OSS等对象存储，file_url存储访问地址。

### 10.4 后续优化建议

1. **性能优化**
   - 为常用查询字段添加索引
   - 步骤树形结构考虑使用缓存
   - 大数据量导出使用异步任务

2. **功能增强**
   - 步骤执行过程的WebSocket实时推送
   - 任务执行进度可视化
   - 执行报告自动生成
   - 数据统计分析

3. **安全加固**
   - 文件上传类型限制
   - 文件大小限制
   - 敏感数据加密
   - 操作审计日志

4. **运维监控**
   - 接口性能监控
   - 异常告警
   - 数据备份策略
   - 日志归档策略

---

## 文档变更记录

| 版本 | 日期 | 变更内容 | 作者 |
|------|------|---------|------|
| 1.0 | 2025-01-13 | 初始版本，完成全部设计文档 | Claude Code |

---

**文档结束**
