From 44d55d2a6e4d896c4e31d2b63f0697aa0fc1b52a Mon Sep 17 00:00:00 2001 From: YANGJIANKUAN Date: Thu, 13 Nov 2025 17:15:27 +0800 Subject: [PATCH] feat: add design md file --- AR-INSPECTION-DESIGN.md | 1930 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1930 insertions(+) create mode 100644 AR-INSPECTION-DESIGN.md diff --git a/AR-INSPECTION-DESIGN.md b/AR-INSPECTION-DESIGN.md new file mode 100644 index 0000000..f86a603 --- /dev/null +++ b/AR-INSPECTION-DESIGN.md @@ -0,0 +1,1930 @@ +# 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 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 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 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 list = arDeviceService.queryList(bo); + ExcelUtil.exportExcel(list, "AR设备", ArDeviceVo.class, response); + } + + // 3. 查询详情 + @SaCheckPermission("inspection:device:query") + @GetMapping("/{id}") + public R 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 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 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 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 queryPageList(ArDeviceBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + @Override + public List queryList(ArDeviceBo bo) { + return baseMapper.selectVoList(buildQueryWrapper(bo)); + } + + private LambdaQueryWrapper buildQueryWrapper(ArDeviceBo bo) { + LambdaQueryWrapper 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 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 ids, Boolean isValid) { + if (isValid) { + List 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 children; +} +``` + +**树形查询接口:** + +```java +@GetMapping("/tree/{taskId}") +public R> tree(@PathVariable("taskId") Long taskId) { + return R.ok(arStepService.queryStepTree(taskId)); +} +``` + +**递归构建树形结构:** + +```java +@Override +public List queryStepTree(Long taskId) { + // 1. 查询所有步骤 + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(ArStep::getTaskId, taskId); + lqw.orderByAsc(ArStep::getOrderNum); + List allSteps = baseMapper.selectVoList(lqw); + + // 2. 递归构建树 + return buildStepTree(allSteps, 0L); +} + +private List buildStepTree(List allSteps, Long parentId) { + List tree = new ArrayList<>(); + for (ArStepVo step : allSteps) { + if (step.getParentId().equals(parentId)) { + ArStepTreeVo treeNode = new ArStepTreeVo(); + BeanUtils.copyProperties(step, treeNode); + + // 递归查找子节点 + List 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 ids, Boolean isValid) { + if (isValid) { + List list = baseMapper.selectByIds(ids); + if (list.size() != ids.size()) { + throw new ServiceException("您没有删除权限!"); + } + } + + // 级联删除子步骤 + for (Long id : ids) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(ArStep::getParentId, id); + long count = baseMapper.selectCount(lqw); + if (count > 0) { + List children = baseMapper.selectList(lqw); + List 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 regionData; +} +``` + +**关键点:** +1. `@TableName(autoResultMap = true)` - 启用自动结果映射 +2. `@TableField(typeHandler = JacksonTypeHandler.class)` - 使用Jackson处理器 +3. 字段类型使用 `Map` 或自定义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 data = new HashMap<>(); +data.put("area", 1000); +data.put("building", "主楼"); +region.setRegionData(data); +baseMapper.insert(region); +``` + +**查询数据:** +```java +ArRegion region = baseMapper.selectById(1L); +Map 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 queryStepTree(Long taskId) { + // 1. 一次性查询所有步骤 + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(ArStep::getTaskId, taskId); + lqw.orderByAsc(ArStep::getOrderNum); + List allSteps = baseMapper.selectVoList(lqw); + + // 2. 递归构建树(从根节点parentId=0开始) + return buildStepTree(allSteps, 0L); +} + +private List buildStepTree(List allSteps, Long parentId) { + List tree = new ArrayList<>(); + + for (ArStepVo step : allSteps) { + if (step.getParentId().equals(parentId)) { + // 找到当前parentId的子节点 + ArStepTreeVo treeNode = new ArStepTreeVo(); + BeanUtils.copyProperties(step, treeNode); + + // 递归查找子节点的子节点 + List children = buildStepTree(allSteps, step.getId()); + treeNode.setChildren(children); + + tree.add(treeNode); + } + } + + return tree; +} +``` + +#### 7.3.5 级联删除 + +```java +public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + // 权限校验... + + // 级联删除所有子节点 + for (Long id : ids) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(ArStep::getParentId, id); + long count = baseMapper.selectCount(lqw); + + if (count > 0) { + // 查询所有子节点 + List children = baseMapper.selectList(lqw); + List 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 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 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 | + +--- + +**文档结束**