From 5eac583d61cb4e4f99ab5d60dc980c3cbc6b8e5b Mon Sep 17 00:00:00 2001 From: ChuXun <504257689@qq.com> Date: Thu, 29 Jan 2026 04:19:53 +0800 Subject: [PATCH] 1 --- .vscode/settings.json | 3 + PROJECT_DOCS.md | 48296 ++++++++-------- README.md | 192 +- docker-compose.hub.yml | 26 - docs/DOCKER_COMPOSE_USAGE.md | 89 + .../.mvn/wrapper/maven-wrapper.properties | 38 +- ems-backend/api测试文档.md | 886 +- ems-backend/json-db/aqi_records.json | 1632 +- ems-backend/json-db/assignments.json | 266 +- ems-backend/json-db/attachments.json | 98 +- ems-backend/json-db/feedbacks.json | 320 +- ems-backend/json-db/grids.json | 25600 ++++---- ems-backend/json-db/map_grids.json | 22400 +++---- ems-backend/json-db/operation_logs.json | 1620 +- ems-backend/json-db/pollutant_thresholds.json | 70 +- ems-backend/json-db/tasks.json | 592 +- ems-backend/json-db/users.json | 2436 +- ems-backend/mvnw.cmd | 298 +- ems-backend/pom.xml | 282 +- .../com/dne/ems/EmsBackendApplication.java | 72 +- .../com/dne/ems/config/DataInitializer.java | 1222 +- .../dne/ems/config/EmptyJpaAnnotations.java | 266 +- .../dne/ems/config/FileStorageProperties.java | 72 +- .../com/dne/ems/config/OpenApiConfig.java | 78 +- .../dne/ems/config/RestTemplateConfig.java | 70 +- .../com/dne/ems/config/WebClientConfig.java | 64 +- .../config/converter/StringListConverter.java | 100 +- .../dne/ems/controller/AuthController.java | 266 +- .../ems/controller/DashboardController.java | 472 +- .../ems/controller/FeedbackController.java | 424 +- .../dne/ems/controller/FileController.java | 204 +- .../dne/ems/controller/GridController.java | 684 +- .../controller/GridWorkerTaskController.java | 270 +- .../com/dne/ems/controller/MapController.java | 210 +- .../controller/OperationLogController.java | 176 +- .../ems/controller/PathfindingController.java | 122 +- .../ems/controller/PersonnelController.java | 312 +- .../dne/ems/controller/ProfileController.java | 136 +- .../dne/ems/controller/PublicController.java | 124 +- .../ems/controller/SupervisorController.java | 178 +- .../controller/TaskAssignmentController.java | 208 +- .../controller/TaskManagementController.java | 460 +- .../dne/ems/dto/AqiDataSubmissionRequest.java | 56 +- .../com/dne/ems/dto/AqiDistributionDTO.java | 104 +- .../com/dne/ems/dto/AqiHeatmapPointDTO.java | 68 +- .../java/com/dne/ems/dto/AssigneeInfoDTO.java | 66 +- .../java/com/dne/ems/dto/AttachmentDTO.java | 66 +- .../java/com/dne/ems/dto/CityBlueprint.java | 50 +- .../com/dne/ems/dto/DashboardStatsDTO.java | 32 +- .../com/dne/ems/dto/ErrorResponseDTO.java | 100 +- .../java/com/dne/ems/dto/FeedbackDTO.java | 100 +- .../com/dne/ems/dto/FeedbackResponseDTO.java | 300 +- .../dne/ems/dto/FeedbackStatsResponse.java | 110 +- .../ems/dto/FeedbackSubmissionRequest.java | 154 +- .../dne/ems/dto/GridAssignmentRequest.java | 36 +- .../java/com/dne/ems/dto/GridCoverageDTO.java | 38 +- .../com/dne/ems/dto/GridUpdateRequest.java | 42 +- .../java/com/dne/ems/dto/HeatmapPointDTO.java | 38 +- .../ems/dto/JwtAuthenticationResponse.java | 28 +- .../dne/ems/dto/LocationUpdateRequest.java | 66 +- .../java/com/dne/ems/dto/LoginRequest.java | 58 +- .../java/com/dne/ems/dto/OperationLogDTO.java | 200 +- .../main/java/com/dne/ems/dto/PageDTO.java | 94 +- .../com/dne/ems/dto/PasswordResetDto.java | 24 +- .../com/dne/ems/dto/PasswordResetRequest.java | 18 +- .../dne/ems/dto/PasswordResetWithCodeDto.java | 40 +- .../com/dne/ems/dto/PathfindingRequest.java | 58 +- .../src/main/java/com/dne/ems/dto/Point.java | 28 +- .../dne/ems/dto/PollutantThresholdDTO.java | 268 +- .../com/dne/ems/dto/PollutionStatsDTO.java | 26 +- .../dne/ems/dto/ProcessFeedbackRequest.java | 48 +- .../dne/ems/dto/PublicFeedbackRequest.java | 110 +- .../dne/ems/dto/RejectFeedbackRequest.java | 32 +- .../java/com/dne/ems/dto/SignUpRequest.java | 58 +- .../com/dne/ems/dto/TaskApprovalRequest.java | 46 +- .../dne/ems/dto/TaskAssignmentRequest.java | 48 +- .../com/dne/ems/dto/TaskCreationRequest.java | 146 +- .../main/java/com/dne/ems/dto/TaskDTO.java | 80 +- .../java/com/dne/ems/dto/TaskDetailDTO.java | 194 +- .../dne/ems/dto/TaskFromFeedbackRequest.java | 120 +- .../java/com/dne/ems/dto/TaskHistoryDTO.java | 30 +- .../java/com/dne/ems/dto/TaskInfoDTO.java | 28 +- .../com/dne/ems/dto/TaskRejectionRequest.java | 20 +- .../java/com/dne/ems/dto/TaskStatsDTO.java | 26 +- .../dne/ems/dto/TaskSubmissionRequest.java | 24 +- .../java/com/dne/ems/dto/TaskSummaryDTO.java | 70 +- .../com/dne/ems/dto/TrendDataPointDTO.java | 108 +- .../com/dne/ems/dto/UserCreationRequest.java | 58 +- .../dne/ems/dto/UserFeedbackSummaryDTO.java | 40 +- .../java/com/dne/ems/dto/UserInfoDTO.java | 108 +- .../dne/ems/dto/UserRegistrationRequest.java | 64 +- .../dne/ems/dto/UserRoleUpdateRequest.java | 36 +- .../java/com/dne/ems/dto/UserSummaryDTO.java | 54 +- .../com/dne/ems/dto/UserUpdateRequest.java | 76 +- .../dne/ems/dto/ai/VolcanoChatRequest.java | 288 +- .../dne/ems/dto/ai/VolcanoChatResponse.java | 168 +- .../FeedbackSubmittedForAiReviewEvent.java | 36 +- .../event/TaskReadyForAssignmentEvent.java | 34 +- .../dne/ems/exception/ErrorResponseDTO.java | 46 +- .../ems/exception/FileNotFoundException.java | 38 +- .../ems/exception/FileStorageException.java | 28 +- .../ems/exception/GlobalExceptionHandler.java | 196 +- .../exception/InvalidOperationException.java | 30 +- .../exception/ResourceNotFoundException.java | 30 +- .../exception/UserAlreadyExistsException.java | 22 +- .../AuthenticationFailureEventListener.java | 66 +- .../AuthenticationSuccessEventListener.java | 64 +- .../ems/listener/FeedbackEventListener.java | 50 +- .../main/java/com/dne/ems/model/AqiData.java | 222 +- .../java/com/dne/ems/model/AqiRecord.java | 330 +- .../java/com/dne/ems/model/Assignment.java | 162 +- .../com/dne/ems/model/AssignmentRecord.java | 198 +- .../java/com/dne/ems/model/Attachment.java | 182 +- .../main/java/com/dne/ems/model/Feedback.java | 370 +- .../src/main/java/com/dne/ems/model/Grid.java | 134 +- .../main/java/com/dne/ems/model/MapGrid.java | 130 +- .../java/com/dne/ems/model/OperationLog.java | 224 +- .../com/dne/ems/model/PasswordResetToken.java | 182 +- .../com/dne/ems/model/PollutantThreshold.java | 196 +- .../src/main/java/com/dne/ems/model/Task.java | 366 +- .../java/com/dne/ems/model/TaskHistory.java | 208 +- .../com/dne/ems/model/TaskSubmission.java | 90 +- .../java/com/dne/ems/model/UserAccount.java | 374 +- .../dne/ems/model/enums/AssignmentMethod.java | 44 +- .../dne/ems/model/enums/AssignmentStatus.java | 68 +- .../dne/ems/model/enums/FeedbackStatus.java | 164 +- .../java/com/dne/ems/model/enums/Gender.java | 46 +- .../java/com/dne/ems/model/enums/Level.java | 42 +- .../dne/ems/model/enums/OperationType.java | 148 +- .../dne/ems/model/enums/PollutionType.java | 76 +- .../java/com/dne/ems/model/enums/Role.java | 88 +- .../dne/ems/model/enums/SeverityLevel.java | 54 +- .../com/dne/ems/model/enums/TaskStatus.java | 84 +- .../com/dne/ems/model/enums/UserStatus.java | 54 +- .../java/com/dne/ems/model/package-info.java | 48 +- .../dne/ems/repository/AqiDataRepository.java | 54 +- .../ems/repository/AqiRecordRepository.java | 76 +- .../AssignmentRecordRepository.java | 34 +- .../ems/repository/AssignmentRepository.java | 24 +- .../ems/repository/AttachmentRepository.java | 36 +- .../ems/repository/FeedbackRepository.java | 86 +- .../dne/ems/repository/GridRepository.java | 160 +- .../dne/ems/repository/MapGridRepository.java | 50 +- .../repository/OperationLogRepository.java | 174 +- .../PasswordResetTokenRepository.java | 32 +- .../PollutantThresholdRepository.java | 30 +- .../ems/repository/TaskHistoryRepository.java | 26 +- .../dne/ems/repository/TaskRepository.java | 70 +- .../repository/TaskSubmissionRepository.java | 48 +- .../ems/repository/UserAccountRepository.java | 82 +- .../impl/JsonAqiDataRepositoryImpl.java | 300 +- .../impl/JsonAqiRecordRepositoryImpl.java | 354 +- .../JsonAssignmentRecordRepositoryImpl.java | 178 +- .../impl/JsonAssignmentRepositoryImpl.java | 144 +- .../impl/JsonAttachmentRepositoryImpl.java | 166 +- .../impl/JsonFeedbackRepositoryImpl.java | 392 +- .../impl/JsonGridRepositoryImpl.java | 256 +- .../impl/JsonMapGridRepositoryImpl.java | 242 +- .../impl/JsonOperationLogRepositoryImpl.java | 240 +- .../JsonPasswordResetTokenRepositoryImpl.java | 160 +- .../JsonPollutantThresholdRepositoryImpl.java | 176 +- .../impl/JsonTaskHistoryRepositoryImpl.java | 152 +- .../impl/JsonTaskRepositoryImpl.java | 310 +- .../JsonTaskSubmissionRepositoryImpl.java | 188 +- .../impl/JsonUserAccountRepositoryImpl.java | 366 +- .../dne/ems/security/CustomUserDetails.java | 196 +- .../ems/security/JwtAuthenticationFilter.java | 204 +- .../com/dne/ems/security/SecurityConfig.java | 308 +- .../ems/security/UserDetailsServiceImpl.java | 88 +- .../com/dne/ems/service/AiReviewService.java | 28 +- .../java/com/dne/ems/service/AuthService.java | 218 +- .../com/dne/ems/service/DashboardService.java | 290 +- .../com/dne/ems/service/FeedbackService.java | 262 +- .../dne/ems/service/FileStorageService.java | 138 +- .../java/com/dne/ems/service/GridService.java | 172 +- .../ems/service/GridWorkerTaskService.java | 214 +- .../dne/ems/service/JsonStorageService.java | 210 +- .../java/com/dne/ems/service/JwtService.java | 108 +- .../dne/ems/service/LoginAttemptService.java | 44 +- .../java/com/dne/ems/service/MailService.java | 96 +- .../dne/ems/service/OperationLogService.java | 160 +- .../com/dne/ems/service/PersonnelService.java | 96 +- .../dne/ems/service/SupervisorService.java | 62 +- .../ems/service/TaskAssignmentService.java | 74 +- .../ems/service/TaskManagementService.java | 296 +- .../dne/ems/service/UserAccountService.java | 90 +- .../dne/ems/service/UserFeedbackService.java | 38 +- .../ems/service/VerificationCodeService.java | 48 +- .../ems/service/impl/AiReviewServiceImpl.java | 416 +- .../dne/ems/service/impl/AuthServiceImpl.java | 618 +- .../service/impl/DashboardServiceImpl.java | 1260 +- .../ems/service/impl/FeedbackServiceImpl.java | 750 +- .../service/impl/FileStorageServiceImpl.java | 442 +- .../dne/ems/service/impl/GridServiceImpl.java | 338 +- .../impl/GridWorkerTaskServiceImpl.java | 764 +- .../dne/ems/service/impl/JwtServiceImpl.java | 364 +- .../service/impl/LoginAttemptServiceImpl.java | 118 +- .../dne/ems/service/impl/MailServiceImpl.java | 240 +- .../service/impl/OperationLogServiceImpl.java | 462 +- .../service/impl/PersonnelServiceImpl.java | 324 +- .../service/impl/SupervisorServiceImpl.java | 204 +- .../impl/TaskAssignmentServiceImpl.java | 346 +- .../impl/TaskManagementServiceImpl.java | 932 +- .../service/impl/UserAccountServiceImpl.java | 270 +- .../service/impl/UserFeedbackServiceImpl.java | 144 +- .../impl/VerificationCodeServiceImpl.java | 190 +- .../ems/service/pathfinding/AStarService.java | 322 +- .../GridWorkerLocationRequired.java | 36 +- .../GridWorkerLocationValidator.java | 46 +- .../dne/ems/validation/PasswordValidator.java | 84 +- .../com/dne/ems/validation/ValidPassword.java | 68 +- .../src/main/resources/application.properties | 132 +- .../target/classes/application.properties | 132 +- 213 files changed, 68860 insertions(+), 68794 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 docker-compose.hub.yml create mode 100644 docs/DOCKER_COMPOSE_USAGE.md diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3466b3f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/PROJECT_DOCS.md b/PROJECT_DOCS.md index c83bf72..14b5547 100644 --- a/PROJECT_DOCS.md +++ b/PROJECT_DOCS.md @@ -1,58 +1,58 @@ -# 项目文档汇总 - -此文档由脚本合并生成,包含原散落 Markdown 内容。 -生成时间: 2026-01-29 04:04 - +# 项目文档汇总 + +此文档由脚本合并生成,包含原散落 Markdown 内容。 +生成时间: 2026-01-29 04:04 + ## 项目与部署 - + ### README.md - -# 环境监督系统 (EMS) - -## 📖 项目简介 - -环境监督系统 (EMS) 是一个全栈的Web应用程序,旨在提供一个全面的平台,用于环境事件的报告、监控、管理和分析。系统支持多角色访问,包括管理员、决策者、网格员、监督员和公众监督员,每个角色都有特定的权限和定制化的用户界面。 - -## ✨ 主要功能 - -- **仪表盘**: 为决策者和管理员提供关键指标和数据的可视化概览。 -- **网格化地图**: 在地图上直观地展示和管理环境监督网格。 -- **任务管理**: 网格员可以接收、处理和汇报环境监督任务。 -- **反馈系统**: 公众监督员和监督员可以提交、查看和管理环境问题反馈。 -- **系统管理**: 管理员可以管理用户信息和查看系统操作日志。 -- **个性化设置**: 所有用户都可以查看个人信息和操作日志。 -- **动态主题**: 系统界面颜色会根据用户角色动态变化,提供更好的用户体验。 - -## 🛠️ 技术栈 - -本项目采用前后端分离的架构。 - -### 后端 (ems-backend) - -- **框架**: [Spring Boot 3](https://spring.io/projects/spring-boot) -- **语言**: Java 17 -- **文件存储**: json -- **API文档**: SpringDoc (Swagger UI) -- **身份认证**: Spring Security with JWT -- **构建工具**: Maven - -### 前端 (ems-frontend) - -- **框架**: [Vue 3](https://vuejs.org/) -- **构建工具**: [Vite](https://vitejs.dev/) -- **语言**: TypeScript -- **UI 组件库**: [Element Plus](https://element-plus.org/) -- **状态管理**: [Pinia](https://pinia.vuejs.org/) -- **路由**: [Vue Router](https://router.vuejs.org/) -- **HTTP客户端**: Axios -- **代码风格**: Prettier - -## 📁 项目结构 - -``` -. + +# 环境监督系统 (EMS) + +## 📖 项目简介 + +环境监督系统 (EMS) 是一个全栈的Web应用程序,旨在提供一个全面的平台,用于环境事件的报告、监控、管理和分析。系统支持多角色访问,包括管理员、决策者、网格员、监督员和公众监督员,每个角色都有特定的权限和定制化的用户界面。 + +## ✨ 主要功能 + +- **仪表盘**: 为决策者和管理员提供关键指标和数据的可视化概览。 +- **网格化地图**: 在地图上直观地展示和管理环境监督网格。 +- **任务管理**: 网格员可以接收、处理和汇报环境监督任务。 +- **反馈系统**: 公众监督员和监督员可以提交、查看和管理环境问题反馈。 +- **系统管理**: 管理员可以管理用户信息和查看系统操作日志。 +- **个性化设置**: 所有用户都可以查看个人信息和操作日志。 +- **动态主题**: 系统界面颜色会根据用户角色动态变化,提供更好的用户体验。 + +## 🛠️ 技术栈 + +本项目采用前后端分离的架构。 + +### 后端 (ems-backend) + +- **框架**: [Spring Boot 3](https://spring.io/projects/spring-boot) +- **语言**: Java 17 +- **文件存储**: json +- **API文档**: SpringDoc (Swagger UI) +- **身份认证**: Spring Security with JWT +- **构建工具**: Maven + +### 前端 (ems-frontend) + +- **框架**: [Vue 3](https://vuejs.org/) +- **构建工具**: [Vite](https://vitejs.dev/) +- **语言**: TypeScript +- **UI 组件库**: [Element Plus](https://element-plus.org/) +- **状态管理**: [Pinia](https://pinia.vuejs.org/) +- **路由**: [Vue Router](https://router.vuejs.org/) +- **HTTP客户端**: Axios +- **代码风格**: Prettier + +## 📁 项目结构 + +``` +. ├── ems-backend/ # 后端 Spring Boot 项目 │ ├── src/ │ └── pom.xml @@ -62,70 +62,70 @@ │ └── package.json └── README.md # 项目说明文件 ``` - -## 🚀 快速开始 - -### 环境准备 - -- [JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) 或更高版本 -- [Maven 3.8](https://maven.apache.org/download.cgi) 或更高版本 -- [Node.js 22.x](https://nodejs.org/) 或更高版本 - -### 后端启动 - -1. **进入后端目录**: - ```bash - cd ems-backend - ``` - -2. **安装依赖**: - ```bash - mvn install - ``` - -3. **运行项目**: - ```bash - mvn spring-boot:run - ``` - 后端服务将启动在默认端口 (通常是 `8080`)。 - -### 前端启动 - -1. **进入前端目录**: + +## 🚀 快速开始 + +### 环境准备 + +- [JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) 或更高版本 +- [Maven 3.8](https://maven.apache.org/download.cgi) 或更高版本 +- [Node.js 22.x](https://nodejs.org/) 或更高版本 + +### 后端启动 + +1. **进入后端目录**: + ```bash + cd ems-backend + ``` + +2. **安装依赖**: + ```bash + mvn install + ``` + +3. **运行项目**: + ```bash + mvn spring-boot:run + ``` + 后端服务将启动在默认端口 (通常是 `8080`)。 + +### 前端启动 + +1. **进入前端目录**: ```bash cd ems-frontend ``` - -2. **安装依赖**: - ```bash - npm install - ``` - -3. **启动开发服务器**: - ```bash - npm run dev - ``` - 前端应用将启动在 `http://localhost:5173` (或其他Vite指定的端口)。 - -## 👥 用户角色 - -系统预设了以下五种角色,拥有不同的职责和访问权限: - -1. **ADMIN (管理员)**: 拥有最高权限,可以访问所有功能,包括用户管理和系统设置。 -2. **DECISION_MAKER (决策者)**: 关注宏观数据,主要访问仪表盘和地图。 -3. **GRID_WORKER (网格员)**: 负责执行具体任务,主要使用任务管理和地图功能。 -4. **SUPERVISOR (监督员)**: 负责管理和审查收到的反馈信息。 -5. **PUBLIC_SUPERVISOR (公众监督员)**: 可以提交环境问题的反馈。 - + +2. **安装依赖**: + ```bash + npm install + ``` + +3. **启动开发服务器**: + ```bash + npm run dev + ``` + 前端应用将启动在 `http://localhost:5173` (或其他Vite指定的端口)。 + +## 👥 用户角色 + +系统预设了以下五种角色,拥有不同的职责和访问权限: + +1. **ADMIN (管理员)**: 拥有最高权限,可以访问所有功能,包括用户管理和系统设置。 +2. **DECISION_MAKER (决策者)**: 关注宏观数据,主要访问仪表盘和地图。 +3. **GRID_WORKER (网格员)**: 负责执行具体任务,主要使用任务管理和地图功能。 +4. **SUPERVISOR (监督员)**: 负责管理和审查收到的反馈信息。 +5. **PUBLIC_SUPERVISOR (公众监督员)**: 可以提交环境问题的反馈。 + 登录后,系统会根据您的角色,展示不同的菜单和界面主题。 - + --- - + ### DOCKER_PULL.md - + # Docker Pull & Run Guide (Local Deployment) This guide helps others run the system locally using prebuilt images. @@ -168,14 +168,14 @@ docker compose -f docker-compose.private.yml down - If login fails, verify registry URL and credentials. - If API requests fail with 401/403 after a previous login, clear browser localStorage token: - Open DevTools → Application → Local Storage → remove `token`. - + --- - + ### DOCKER_PUBLISH.md - + # Docker Build & Publish Guide This guide explains how to package the project into Docker images and push them to a registry. @@ -245,999 +245,999 @@ docker pull //ems-frontend:latest - Never commit `.env` with secrets. - If using a public registry, remove sensitive configs from images. - For local run with images, use `docker-compose.hub.yml` or `docker-compose.private.yml`. - + --- - + ## 后端文档 - + ### ems-backend/project_readme.md - -# Spring Boot 项目深度解析 - -本文档旨在深入解析当前 Spring Boot 项目的构建原理、核心技术以及业务逻辑,帮助您更好地理解和准备答疑。 - -## 一、Spring Boot 核心原理 - -Spring Boot 是一个用于简化新 Spring 应用的初始搭建以及开发过程的框架。其核心原理主要体现在以下几个方面: - -### 1. 自动配置 (Auto-Configuration) - 智能的“管家” - -您可以将 Spring Boot 的自动配置想象成一个非常智能的“管家”。在传统的 Spring 开发中,您需要手动配置很多东西,比如数据库连接、We你b 服务器等,就像您需要亲自告诉管家每一个细节:如何烧水、如何扫地。 - -而 Spring Boot 的“管家”则会观察您家里添置了什么新东西(即您在项目中加入了什么依赖),然后自动地把这些东西配置好,让它们能正常工作。 - -**它是如何工作的?** - -1. **观察您的“购物清单” (`pom.xml`)**:当您在项目的 `pom.xml` 文件中加入一个 `spring-boot-starter-*` 依赖,比如 `spring-boot-starter-web`,就等于告诉“管家”:“我需要开发一个网站。” - -2. **自动准备所需工具**:看到这个“购物清单”后,“管家”会立刻为您准备好一套完整的网站开发工具: - * 一个嵌入式的 **Tomcat 服务器**,这样您就不用自己安装和配置服务器了。 - * 配置好 **Spring MVC** 框架,这是处理网页请求的核心工具。 - * 设置好处理 JSON 数据的工具 (Jackson),方便前后端通信。 - -3. **背后的魔法 (`@EnableAutoConfiguration`)**:这一切的起点是您项目主启动类上的 `@SpringBootApplication` 注解,它内部包含了 `@EnableAutoConfiguration`。这个注解就是给“管家”下达“开始自动工作”命令的开关。它会去检查一个固定的清单(位于 `META-INF/spring.factories`),上面写着所有它认识的工具(自动配置类)以及何时应该启用它们。 - -**项目实例:** - -在本项目中,因为 `pom.xml` 包含了 `spring-boot-starter-web`,所以 Spring Boot 自动为您配置了处理 Web 请求的所有环境。这就是为什么您可以在 `com.dne.ems.controller` 包下的任何一个 Controller(例如 `AuthController`)中直接使用 `@RestController` 和 `@PostMapping` 这样的注解来定义 API 接口,而无需编写任何一行 XML 配置来启动 Web 服务或配置 Spring MVC。 - -简单来说,**自动配置就是 Spring Boot 替您完成了大部分繁琐、重复的配置工作,让您可以专注于编写业务代码。** - -### 2. 起步依赖 (Starter Dependencies) - “主题工具箱” - -如果说自动配置是“智能管家”,那么起步依赖就是“管家”使用的“主题工具箱”。您不需要告诉管家需要买钉子、锤子、螺丝刀……您只需要说:“我需要一个木工工具箱。” - -起步依赖 (`spring-boot-starter-*`) 就是这样的“主题工具箱”。每一个 starter 都包含了一整套用于某个特定任务的、经过测试、版本兼容的依赖。 - -**项目实例:** - -- **`spring-boot-starter-web`**: 这是“Web 开发工具箱”。它不仅包含了 Spring MVC,还包含了内嵌的 Tomcat 服务器和处理验证的库。您只需要引入这一个依赖,所有 Web 开发的基础环境就都准备好了。 - -- **`spring-boot-starter-data-jpa`**: 这是“数据库操作工具箱”。它打包了与数据库交互所需的一切,包括 Spring Data JPA、Hibernate (JPA 的一种实现) 和数据库连接池 (HikariCP)。您引入它之后,就可以直接在 `com.dne.ems.repository` 包下编写 `JpaRepository` 接口来进行数据库操作,而无需关心如何配置 Hibernate 或事务管理器。 - -- **`spring-boot-starter-security`**: 这是“安全管理工具箱”。引入它之后,Spring Security 框架就被集成进来,提供了认证和授权的基础功能。您可以在 `com.dne.ems.security.SecurityConfig` 中看到对这个“工具箱”的具体配置,比如定义哪些 API 需要登录才能访问。 - -**优势总结:** - -- **简化依赖**:您不必再手动添加一长串的单个依赖项。 -- **避免冲突**:Spring Boot 已经为您管理好了这些依赖之间的版本,您不必再担心因为版本不兼容而导致的各种奇怪错误。 - -通过使用这些“工具箱”,您可以非常快速地搭建起一个功能完备的应用程序框架。 - -### 3. 嵌入式 Web 服务器 - -Spring Boot 内嵌了常见的 Web 服务器,如 Tomcat、Jetty 或 Undertow。这意味着您不需要将应用程序打包成 WAR 文件部署到外部服务器,而是可以直接将应用程序打包成一个可执行的 JAR 文件,通过 `java -jar` 命令来运行。 - -- **优势**:简化了部署流程,便于持续集成和持续部署 (CI/CD)。 - -## 二、前后端分离与交互原理 - -本项目采用前后端分离的架构,前端(如 Vue.js)和后端(Spring Boot)是两个独立的项目,它们之间通过 API 进行通信。 - -### 1. RESTful API - -后端通过暴露一组 RESTful API 来提供服务。REST (Representational State Transfer) 是一种软件架构风格,它定义了一组用于创建 Web 服务的约束。核心思想是,将应用中的所有事物都看作资源,每个资源都有一个唯一的标识符 (URI)。 - -- **HTTP 方法**: - - `GET`:获取资源。 - - `POST`:创建新资源。 - - `PUT` / `PATCH`:更新资源。 - - `DELETE`:删除资源。 - -### 2. JSON (JavaScript Object Notation) - -前后端之间的数据交换格式通常是 JSON。当浏览器向后端发送请求时,它可能会在请求体中包含 JSON 数据。当后端响应时,它会将 Java 对象序列化为 JSON 字符串返回给前端。 - -- **Spring Boot 中的实现**:Spring Boot 默认使用 Jackson 库来处理 JSON 的序列化和反序列化。当 Controller 的方法参数有 `@RequestBody` 注解时,Spring 会自动将请求体中的 JSON 转换为 Java 对象。当方法有 `@ResponseBody` 或类有 `@RestController` 注解时,Spring 会自动将返回的 Java 对象转换为 JSON。 - -### 3. 认证与授权 (JWT) - -在前后端分离架构中,常用的认证机制是 JSON Web Token (JWT)。 - -- **流程**: - 1. 用户使用用户名和密码登录。 - 2. 后端验证凭据,如果成功,则生成一个 JWT 并返回给前端。 - 3. 前端将收到的 JWT 存储起来(例如,在 `localStorage` 或 `sessionStorage` 中)。 - 4. 在后续的每个请求中,前端都会在 HTTP 请求的 `Authorization` 头中携带这个 JWT。 - 5. 后端的安全过滤器会拦截每个请求,验证 JWT 的有效性。如果验证通过,则允许访问受保护的资源。 - -## 三、项目业务逻辑与分层结构分析 - -本项目是一个环境管理系统 (EMS),其代码结构遵循经典的分层架构模式。 - -### 1. 分层结构概述 - -``` -+----------------------------------------------------+ -| Controller (表现层) | -| (处理 HTTP 请求, 调用 Service, 返回 JSON 响应) | -+----------------------+-----------------------------+ - | (调用) -+----------------------v-----------------------------+ -| Service (业务逻辑层) | -| (实现核心业务逻辑, 事务管理) | -+----------------------+-----------------------------+ - | (调用) -+----------------------v-----------------------------+ -| Repository (数据访问层) | -| (通过 Spring Data JPA 与数据库交互) | -+----------------------+-----------------------------+ - | (操作) -+----------------------v-----------------------------+ -| Model (领域模型) | -| (定义数据结构, 对应数据库表) | -+----------------------------------------------------+ -``` - -### 2. 各层详细分析 - -#### a. Model (领域模型) - -位于 `com.dne.ems.model` 包下,是应用程序的核心。这些是简单的 Java 对象 (POJO),使用 JPA 注解(如 `@Entity`, `@Table`, `@Id`, `@Column`)映射到数据库中的表。 - -- **核心实体**: - - `UserAccount`: 用户账户信息,包含角色、状态等。 - - `Task`: 任务实体,包含任务状态、描述、位置等信息。 - - `Feedback`: 公众或用户提交的反馈信息。 - - `Grid`: 地理网格信息,用于管理和分配任务区域。 - - `Attachment`: 附件信息,如上传的图片。 - -#### b. Repository (数据访问层) - -位于 `com.dne.ems.repository` 包下。这些是接口,继承自 Spring Data JPA 的 `JpaRepository`。开发者只需要定义接口,Spring Data JPA 会在运行时自动为其提供实现,从而极大地简化了数据访问代码。 - -- **示例**:`TaskRepository` 提供了对 `Task` 实体的 CRUD (创建、读取、更新、删除) 操作,以及基于方法名约定的查询功能(如 `findByStatus(TaskStatus status)`)。 - -#### c. Service (业务逻辑层) - -位于 `com.dne.ems.service` 包下,是业务逻辑的核心实现。Service 层封装了应用程序的业务规则和流程,并负责协调多个 Repository 来完成一个完整的业务操作。事务管理也通常在这一层通过 `@Transactional` 注解来声明。 - -- **主要 Service 及其职责**: - - `AuthService`: 处理用户认证,如登录、注册。 - - `TaskManagementService`: 负责任务的创建、更新、查询等核心管理功能。 - - `TaskAssignmentService`: 负责任务的分配逻辑,如自动分配或手动指派。 - - `SupervisorService`: 主管相关的业务逻辑,如审核任务。 - - `GridWorkerTaskService`: 网格员的任务处理逻辑,如提交任务进度。 - - `FeedbackService`: 处理公众反馈,可能包括创建任务等后续操作。 - - `PersonnelService`: 员工管理,如添加、查询用户信息。 - -- **层间关系**:Service 通常会注入 (DI) 一个或多个 Repository 来操作数据。一个 Service 也可能调用另一个 Service 来复用业务逻辑。 - -#### d. Controller (表现层) - -位于 `com.dne.ems.controller` 包下。Controller 负责接收来自客户端的 HTTP 请求,解析请求参数,然后调用相应的 Service 来处理业务逻辑。最后,它将 Service 返回的数据(通常是 DTO)封装成 HTTP 响应(通常是 JSON 格式)返回给客户端。 - -- **主要 Controller 及其职责**: - - `AuthController`: 暴露 `/api/auth/login`, `/api/auth/register` 等认证相关的端点。 - - `TaskManagementController`: 提供任务管理的 RESTful API,如 `GET /api/tasks`, `POST /api/tasks`。 - - `SupervisorController`: 提供主管操作的 API,如审核、分配任务。 - - `FileController`: 处理文件上传和下载。 - -- **DTO (Data Transfer Object)**:位于 `com.dne.ems.dto` 包下。Controller 和 Service 之间,以及 Controller 和前端之间,通常使用 DTO 来传输数据。这样做的好处是可以避免直接暴露数据库实体,并且可以根据前端页面的需要来定制数据结构,避免传输不必要的信息。 - -### 3. 安全 (Security) - -位于 `com.dne.ems.security` 包下。使用 Spring Security 来提供全面的安全服务。 - -- `SecurityConfig`: 配置安全规则,如哪些 URL 是公开的,哪些需要认证,以及配置 JWT 过滤器。 -- `JwtAuthenticationFilter`: 一个自定义的过滤器,在每个请求到达 Controller 之前执行。它从请求头中提取 JWT,验证其有效性,并将用户信息加载到 Spring Security 的上下文中。 -- `UserDetailsServiceImpl`: 实现了 Spring Security 的 `UserDetailsService` 接口,负责根据用户名从数据库加载用户信息(包括密码和权限)。 - -## 四、总结 - -这个 Spring Boot 项目是一个典型的、结构清晰的、基于 RESTful API 的前后端分离应用。它有效地利用了 Spring Boot 的自动配置和起步依赖来简化开发,并通过分层架构将表现、业务逻辑和数据访问清晰地分离开来,使得项目易于理解、维护和扩展。 - -希望这份文档能帮助您在答疑中充满信心! - -## 五、为小白定制:项目代码结构与工作流程详解 - -为了让您彻底理解代码是如何组织的,我们把整个后端项目想象成一个分工明确的大公司。 - -### 1. 公司各部门职责 (包/文件夹的作用) - -- **`com.dne.ems.config` (后勤与配置部)** - - **职责**: 负责应用的各种基础配置。比如 `WebClientConfig` 是配置网络请求工具的,`WebSocketConfig` 是配置实时通讯的,`DataInitializer` 则是在项目启动时初始化一些默认数据(比如默认的管理员账号)。这个部门确保了其他所有部门的工具和环境都是准备好的。 - -- **`com.dne.ems.model` (产品设计部)** - - **职责**: 定义公司的核心“产品”是什么样的。这里的“产品”就是数据。例如,`UserAccount.java` 定义了“用户”有哪些属性(姓名、密码、角色等),`Task.java` 定义了“任务”有哪些属性(标题、状态、截止日期等)。它们是整个公司的业务基础,决定了公司要处理什么信息。这些文件直接对应数据库里的表结构。 - -- **`com.dne.ems.repository` (仓库管理部)** - - **职责**: 专门负责和数据库这个大“仓库”打交道。当需要存取数据时,其他部门不会直接去仓库,而是会通知仓库管理员。例如,`TaskRepository` 提供了所有操作 `Task` 表的方法,如保存一个新任务、根据ID查找一个任务。这个部门使用了 Spring Data JPA 技术,所以代码看起来很简单,但功能强大。 - -- **`com.dne.ems.service` (核心业务部)** - - **职责**: 这是公司的核心部门,负责处理所有实际的业务逻辑。一个业务操作可能很复杂,需要协调多个部门。例如,`TaskManagementService` (任务管理服务) 在创建一个新任务时,可能需要: - 1. 调用 `TaskRepository` 将任务信息存入数据库。 - 2. 调用 `UserAccountRepository` 查找合适的执行人。 - 3. 调用 `NotificationService` (如果存在的话) 发送通知。 - - **`impl` 子目录**: `service` 包下通常是接口 (定义了“能做什么”),而 `impl` (implementation) 包下是这些接口的具体实现 (定义了“具体怎么做”)。这是为了“面向接口编程”,是一种良好的编程习惯。 - -- **`com.dne.ems.controller` (客户服务与接待部)** - - **职责**: 这是公司的“前台”,直接面向客户(在这里就是前端浏览器或App)。它接收客户的请求(HTTP 请求),然后将请求传达给相应的业务部门 (`Service`) 去处理。处理完成后,它再把结果(通常是 JSON 格式的数据)返回给客户。 - - 例如,`TaskManagementController` 里的一个 `createTask` 方法,会接收前端发来的创建任务的请求,然后调用 `TaskManagementService` 的 `createTask` 方法来真正执行创建逻辑。 - -- **`com.dne.ems.dto` (数据包装与运输部)** - - **职责**: 负责数据的包装和运输。直接把数据库里的原始数据 (`Model`) 给客户看是不安全也不专业的。DTO (Data Transfer Object) 就是专门用于在各部门之间,特别是“前台”(`Controller`)和客户(前端)之间传递数据的“包装盒”。 - - 例如,`TaskDTO` 可能只包含任务的ID、标题和状态,而隐藏了创建时间、更新时间等前端不需要的敏感信息。 - -- **`com.dne.ems.security` (安保部)** - - **职责**: 负责整个公司的安全。`SecurityConfig` 定义了公司的安保规则(比如哪些地方需要门禁卡才能进),`JwtAuthenticationFilter` 就是门口的保安,每个请求进来都要检查它的“门禁卡”(JWT Token) 是否有效。 - -- **`com.dne.ems.exception` (风险与危机处理部)** - - **职责**: 处理各种突发异常情况。当业务流程中出现错误时(比如找不到用户、数据库连接失败),`GlobalExceptionHandler` 会捕获这些错误,并给出一个统一、友好的错误提示给前端,而不是让程序崩溃。 - -### 2. 一次典型的请求流程:用户登录 - -让我们通过“用户登录”这个简单的例子,看看这些部门是如何协同工作的: - -1. **客户上门 (前端 -> Controller)**: 用户在前端页面输入用户名和密码,点击登录。前端将这些信息打包成一个 JSON 对象,发送一个 HTTP POST 请求到后端的 `/api/auth/login` 地址。 - -2. **前台接待 (Controller)**: `AuthController` 接收到这个请求。它看到地址是 `/login`,于是调用 `login` 方法。它从请求中取出 JSON 数据,并将其转换为 `LoginRequest` 这个 DTO 对象。 - -3. **转交业务部门 (Controller -> Service)**: `AuthController` 自己不处理登录逻辑,它调用 `AuthService` 的 `login` 方法,并将 `LoginRequest` 这个 DTO 传递过去。 - -4. **核心业务处理 (Service)**: `AuthService` 开始处理登录: - a. 它首先需要验证用户名是否存在,于是它请求 **仓库管理部** (`UserAccountRepository`):“请帮我根据这个用户名 (`loginRequest.getUsername()`) 查一下有没有这个人。” - b. `UserAccountRepository` 从数据库中查找到对应的 `UserAccount` (Model) 信息,返回给 `AuthService`。 - c. `AuthService` 拿到用户信息后,会使用密码加密工具,验证用户提交的密码和数据库中存储的加密密码是否匹配。 - d. 如果验证成功,`AuthService` 会请求 **安保部** (`JwtService`):“请为这位用户生成一张有时效的门禁卡 (JWT Token)。” - -5. **返回处理结果 (Service -> Controller -> 前端)**: `JwtService` 生成 Token 后返回给 `AuthService`。`AuthService` 将 Token 包装在一个 `JwtAuthenticationResponse` DTO 中,返回给 `AuthController`。`AuthController` 再将这个包含 Token 的 DTO 转换成 JSON 格式,作为 HTTP 响应返回给前端。 - -6. **客户拿到凭证 (前端)**: 前端收到包含 Token 的响应,就知道登录成功了。它会把这个 Token 保存起来,在之后访问需要权限的页面时,都会带上这张“门禁卡”。 - -通过这个流程,您可以看到,一个简单的请求背后是多个“部门”按照明确的职责划分,层层调用、协同工作的结果。这种结构使得代码逻辑清晰,易于维护和扩展。 -嗯 -## 六、核心业务流程与关键机制分析 - -下面,我们将梳理出本项目的几个核心业务流程和支撑这些流程的关键技术机制,并详细说明它们涉及的层级和文件,让您清晰地看到数据和指令是如何在系统中流转的。 - -### (一) 核心业务流程 - -#### 流程一:用户认证与授权 (Authentication & Authorization) - -**目标**:保障系统安全,确保只有授权用户才能访问受保护的资源。 - -**执行路径**: - -1. **注册 (`signup`)** - * **入口**: `AuthController.signup()` - * **业务逻辑**: `AuthService.signup()` - * 验证邮箱和验证码的有效性 (`VerificationCodeService`)。 - * 检查邮箱是否已注册 (`UserAccountRepository`)。 - * 对密码进行加密 (`PasswordEncoder`)。 - * 创建并保存新的 `UserAccount` 实体。 - -2. **登录 (`login`)** - * **入口**: `AuthController.login()` - * **业务逻辑**: `AuthService.login()` - * 通过 Spring Security 的 `AuthenticationManager` 验证用户名和密码。 - * 如果认证成功,调用 `JwtService` 生成 JWT。 - * 返回包含 JWT 的响应给前端。 - -3. **访问受保护资源** - * **安全过滤器**: `JwtAuthenticationFilter` - * 拦截所有请求,从 `Authorization` 请求头中提取 JWT。 - * 验证 JWT 的有效性 (`JwtService`)。 - * 如果有效,从 JWT 中解析出用户信息,并构建一个 `UsernamePasswordAuthenticationToken`,设置到 Spring Security 的 `SecurityContextHolder` 中,完成授权。 - -#### 流程二:公众反馈与任务转化 (Feedback to Task) - -**目标**:将公众通过开放接口提交的反馈(如环境问题)转化为系统内部的可追踪任务。 - -**执行路径**: - -1. **提交反馈** - * **入口**: `PublicController.submitFeedback()` - * **业务逻辑**: `FeedbackService.createFeedback()` - * 接收 `FeedbackDTO`,包含反馈内容和可选的附件。 - * 处理文件上传(如果存在),调用 `FileStorageService` 保存文件,并将附件信息与反馈关联。 - * 保存 `Feedback` 实体到数据库。 - -2. **反馈审核与任务创建 (由管理员操作)** - * **入口**: `SupervisorController.approveFeedbackAndCreateTask()` (假设存在此接口) - * **业务逻辑**: `SupervisorService.processFeedback()` - * 管理员审核反馈内容。 - * 如果反馈有效,调用 `TaskManagementService.createTaskFromFeedback()`,基于反馈信息创建一个新的 `Task`。 - -#### 流程三:任务生命周期管理 (Task Lifecycle) - -**目标**:完整地管理一个任务从创建、分配、执行到完成的全过程。 - -**执行路径**: - -1. **任务创建** - * **入口**: `TaskManagementController.createTask()` - * **业务逻辑**: `TaskManagementService.createTask()` - * 创建 `Task` 实体并设置初始状态(如 `PENDING_ASSIGNMENT`)。 - -2. **任务分配** - * **入口**: `SupervisorController.assignTask()` - * **业务逻辑**: `TaskAssignmentService.assignTaskToWorker()` - * 将任务 (`Task`) 与一个网格员 (`GridWorker`) 关联起来,创建 `Assignment` 记录。 - * 更新任务状态为 `ASSIGNED`。 - -3. **任务执行与更新** - * **入口**: `GridWorkerTaskController.updateTaskStatus()` - * **业务逻辑**: `GridWorkerTaskService.updateTaskProgress()` - * 网格员提交任务进度或结果,可附带图片等附件 (`FileStorageService`)。 - * 更新任务状态(如 `IN_PROGRESS`, `COMPLETED`)。 - -4. **任务审核与关闭** - * **入口**: `SupervisorController.reviewTask()` - * **业务逻辑**: `SupervisorService.reviewAndCloseTask()` - * 主管审核已完成的任务。 - * 如果合格,将任务状态更新为 `CLOSED`。如果不合格,可打回 (`REJECTED`)。 - -#### 流程四:数据可视化与决策支持 (Dashboard & AI) - -**目标**:为管理层提供数据洞察,并通过 AI 功能辅助决策。 - -**执行路径**: - -1. **仪表盘数据** - * **入口**: `DashboardController` 中的各个接口,如 `getTaskStats()`, `getRecentActivities()`。 - * **业务逻辑**: `DashboardService` - * 调用多个 `Repository`(如 `TaskRepository`, `FeedbackRepository`)进行数据聚合查询,统计任务状态、反馈趋势等。 - * 返回 DTO 给前端进行图表展示。 - -2. **AI 辅助功能** - * **文本审核**: `AiReviewService.reviewText()` 可能被用于自动审核反馈内容或评论,识别敏感信息。 - * **路径规划**: `AStarService.findPath()` 用于计算两点之间的最优路径,可辅助网格员规划任务路线。 - -### (二) 关键支撑机制 - -除了核心业务流程,系统还包含一系列关键的技术机制来保证其健壮、安全和可维护性。 - -1. **文件上传与管理** - * **位置**: `FileStorageService`, `FileController` - * **作用**: 提供统一的文件存储和访问服务。无论是用户头像、反馈附件还是任务报告,都通过此服务进行处理,实现了与具体存储位置(本地文件系统或云存储)的解耦。 - -2. **邮件与验证码服务** - * **位置**: `MailService`, `VerificationCodeService` - * **作用**: 负责发送邮件(如注册验证、密码重置)和管理验证码的生成与校验,是保障账户安全的重要环节。 - -3. **登录安全与尝试锁定** - * **位置**: `LoginAttemptService`, `AuthenticationFailureEventListener` - * **作用**: 监听登录失败事件 (`AuthenticationFailureListener`),并通过 `LoginAttemptService` 记录特定 IP 的失败次数。当超过阈值时,会暂时锁定该 IP 的登录权限,有效防止暴力破解攻击。 - -4. **A* 路径规划算法** - * **位置**: `AStarService`, `PathfindingController` - * **作用**: 实现了 A* 寻路算法,这是一个独立的功能模块,可以根据地图数据(网格 `Grid`)计算最优路径,为其他业务(如任务路线规划)提供支持。 - -5. **系统事件处理机制** - * **位置**: `event` 和 `listener` 包 - * **作用**: 基于 Spring 的事件驱动模型,实现系统内各组件的解耦。例如,当一个用户注册成功后,可以发布一个 `UserRegistrationEvent` 事件,而邮件发送监听器 (`EmailNotificationListener`) 和新用户欢迎监听器 (`WelcomeBonusListener`) 可以分别监听并处理此事件,而无需在注册服务中硬编码这些逻辑。 - -6. **全局异常处理** - * **位置**: `GlobalExceptionHandler` - * **作用**: 捕获整个应用中抛出的未处理异常,并根据异常类型返回统一、规范的错误响应给前端。这避免了将原始的、可能包含敏感信息的堆栈跟踪暴露给用户,并提升了用户体验。 - -7. **自定义数据校验** - * **位置**: `validation` 包 (如 `PasswordValidator`) - * **作用**: 实现自定义的、复杂的校验逻辑。例如,`@ValidPassword` 注解可以确保用户设置的密码符合特定的强度要求(如长度、包含数字和特殊字符等)。 - -8. **数据持久化与查询 (JPA)** - * **位置**: `repository` 包 - * **作用**: 利用 Spring Data JPA,通过定义接口 (`extends JpaRepository`) 的方式,极大地简化了数据库的 CRUD 操作和查询。开发者无需编写 SQL 语句,JPA 会自动根据方法名或注解生成相应的查询。 - -9. **API 数据契约 (DTO)** - * **位置**: `dto` 包 - * **作用**: 数据传输对象 (Data Transfer Object) 是前后端分离架构中的关键实践。它作为 API 的“数据契约”,明确了接口需要什么数据、返回什么数据,实现了表现层与领域模型的解耦,使得双方可以独立演进,同时避免了敏感信息的泄露。 - -10. **应用配置与初始化** - * **位置**: `config` 包, `DataInitializer` - * **作用**: `config` 包集中管理了应用的所有配置类,如安全配置 (`SecurityConfig`)、Web 配置等。`DataInitializer` 则利用 `CommandLineRunner` 接口,在应用启动后执行一次性任务,如创建默认管理员账户、初始化基础数据等,确保了应用开箱即用的能力。 - * **密码加密**: 使用 `PasswordEncoder` 对用户密码进行加密,增强安全性。 - * **创建用户实体**: 创建一个新的 `UserAccount` 对象 (`Model`),并填充信息。 - -3. **Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.UserAccountRepository.java` - * **方法**: `save(UserAccount user)` - * **作用**: `AuthService` 调用此方法,将新创建的 `UserAccount` 对象持久化到数据库中。 - -4. **Model (领域模型)** - * **文件**: `com.dne.ems.model.UserAccount.java` - * **作用**: 定义了用户账户的数据结构,是数据库中 `user_accounts` 表的映射。 - -### 流程二:任务创建与分配 - -**目标**:主管或管理员根据一个已批准的反馈,创建一个新任务,并将其分配给一个网格员。 - -**执行路径**: - -1. **Controller (表现层)** - * **文件**: `com.dne.ems.controller.TaskManagementController.java` - * **方法**: `createTaskFromFeedback(long feedbackId, TaskFromFeedbackRequest request)` - * **作用**: 接收 `/api/management/tasks/feedback/{feedbackId}/create-task` POST 请求。它将反馈ID和任务创建请求 (`TaskFromFeedbackRequest` DTO) 传递给 Service 层。 - -2. **Service (业务逻辑层)** - * **文件**: `com.dne.ems.service.impl.TaskManagementServiceImpl.java` - * **方法**: `createTaskFromFeedback(long feedbackId, TaskFromFeedbackRequest request)` - * **作用**: 实现核心的创建和分配逻辑: - * **查找反馈**: 调用 `FeedbackRepository` 找到对应的 `Feedback` (`Model`)。 - * **查找负责人**: 调用 `UserAccountRepository` 找到指定的负责人 `UserAccount` (`Model`)。 - * **创建任务**: 创建一个新的 `Task` 对象 (`Model`),并从 `Feedback` 和请求 DTO 中填充任务信息(如描述、截止日期、严重性等)。 - * **设置状态**: 将任务的初始状态设置为 `ASSIGNED`。 - -3. **Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.TaskRepository.java` - * **方法**: `save(Task task)` - * **作用**: `TaskManagementService` 调用此方法,将新创建的 `Task` 对象保存到数据库。 - * **涉及的其他 Repository**: `FeedbackRepository` 和 `UserAccountRepository` 用于在业务逻辑中获取必要的数据。 - -4. **Model (领域模型)** - * **文件**: `com.dne.ems.model.Task.java` - * **作用**: 定义了任务的数据结构。 - * **涉及的其他 Model**: `Feedback.java` 和 `UserAccount.java` 作为数据来源。 - -### 流程三:网格员处理任务与提交反馈 - -**目标**:网格员查看自己的任务,执行任务,并提交处理结果(包括评论和附件)。 - -**执行路径**: - -1. **Controller (表现层)** - * **文件**: `com.dne.ems.controller.GridWorkerTaskController.java` - * **方法**: `submitTask(long taskId, String comments, List files)` - * **作用**: 接收 `/api/worker/{taskId}/submit` POST 请求。这是一个 `multipart/form-data` 请求,因为它同时包含了文本(评论)和文件。Controller 将这些信息传递给 Service 层。 - -2. **Service (业务逻辑层)** - * **文件**: `com.dne.ems.service.impl.GridWorkerTaskServiceImpl.java` - * **方法**: `submitTask(long taskId, String comments, List files)` - * **作用**: 处理任务提交的业务逻辑: - * **查找任务**: 调用 `TaskRepository` 找到当前需要处理的 `Task` (`Model`)。 - * **验证权限**: 确保当前登录的用户就是这个任务的负责人。 - * **处理文件上传**: 如果有附件,调用 `FileStorageService` 将文件保存到服务器,并创建 `Attachment` (`Model`) 对象。 - * **更新任务状态**: 将任务状态更新为 `SUBMITTED` 或 `PENDING_REVIEW`。 - * **保存评论和附件信息**: 将评论和附件信息关联到任务上。 - -3. **Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.TaskRepository.java` - * **方法**: `save(Task task)` - * **作用**: `GridWorkerTaskService` 在更新了任务状态和信息后,调用此方法将 `Task` 对象的变化持久化到数据库。 - * **涉及的其他 Repository**: `AttachmentRepository` (如果适用) 用于保存附件信息。 - -4. **Model (领域模型)** - * **文件**: `com.dne.ems.model.Task.java` - * **作用**: 任务数据在此流程中被更新。 - * **涉及的其他 Model**: `Attachment.java` 用于表示上传的文件。 - * **密码加密**: 使用 `PasswordEncoder` 对用户提交的明文密码进行加密,确保数据库中存储的是密文,保障安全。 - * **创建用户实体**: 创建一个新的 `UserAccount` (Model) 对象,并将注册信息(包括加密后的密码和角色)设置到该对象中。 - -3. **Service -> Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.UserAccountRepository.java` - * **方法**: `save(UserAccount userAccount)` - * **作用**: Service 层调用此方法,将填充好数据的 `UserAccount` 实体对象持久化到数据库中。Spring Data JPA 会自动生成对应的 SQL `INSERT` 语句来完成操作。 - -4. **返回结果**: 数据成功存入数据库后,操作结果会逐层返回,最终 `AuthController` 会向前端返回一个成功的响应消息。 - -### 流程二:任务的创建与自动分配 - -**目标**:系统管理员或主管创建一个新的环境问题任务,并由系统自动分配给相应网格内的网格员。 - -**执行路径**: - -1. **前端 -> Controller (表现层)** - * **文件**: `com.dne.ems.controller.TaskManagementController.java` - * **方法**: `createTask(TaskCreateRequest taskCreateRequest)` - * **作用**: 接收 `/api/tasks` POST 请求,请求体中包含了任务的详细信息(如描述、位置、图片附件ID等),封装在 `TaskCreateRequest` DTO 中。Controller 调用 `TaskManagementService` 来处理创建逻辑。 - -2. **Controller -> Service (业务逻辑层)** - * **文件**: `com.dne.ems.service.impl.TaskManagementServiceImpl.java` - * **方法**: `createTask(TaskCreateRequest taskCreateRequest)` - * **作用**: 创建任务的核心业务逻辑。 - * 创建一个新的 `Task` (Model) 实体。 - * 根据请求中的附件ID,调用 `AttachmentRepository` 查找并关联附件。 - * 设置任务的初始状态为 `PENDING` (待处理)。 - * 调用 `TaskRepository` 将新任务保存到数据库。 - * **触发任务分配**: 保存任务后,调用 `TaskAssignmentService` 来执行自动分配逻辑。 - -3. **Service -> Service (业务逻辑层内部调用)** - * **文件**: `com.dne.ems.service.impl.TaskAssignmentServiceImpl.java` - * **方法**: `assignTask(Task task)` - * **作用**: 处理任务的分配逻辑。 - * 根据任务的地理位置信息,调用 `GridRepository` 找到对应的地理网格 (`Grid`)。 - * 根据网格信息,调用 `UserAccountRepository` 找到负责该网格的网格员 (`GridWorker`)。 - * 如果找到合适的网格员,则更新 `Task` 实体的 `assignee` (执行人) 字段。 - * 更新任务状态为 `ASSIGNED` (已分配)。 - * 调用 `TaskRepository` 将更新后的任务信息保存回数据库。 - * (可选) 调用通知服务,向被分配的网格员发送新任务通知。 - -4. **Service -> Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.TaskRepository.java`, `com.dne.ems.repository.GridRepository.java`, `com.dne.ems.repository.UserAccountRepository.java` - * **作用**: 在整个流程中,Service 层会频繁地与这些 Repository 交互,以查询网格信息、查询用户信息、保存和更新任务数据。 - -### 流程三:网格员处理任务并提交反馈 - -**目标**:网格员在完成任务后,通过 App 或前端页面提交任务处理结果,包括文字描述和现场图片。 - -**执行路径**: - -1. **前端 -> Controller (表现层)** - * **文件**: `com.dne.ems.controller.GridWorkerTaskController.java` - * **方法**: `submitTaskFeedback(Long taskId, TaskFeedbackRequest feedbackRequest)` - * **作用**: 接收 `/api/worker/tasks/{taskId}/feedback` POST 请求。路径中的 `taskId` 指明了要为哪个任务提交反馈,请求体 `TaskFeedbackRequest` DTO 中包含了反馈内容和附件ID。 - -2. **Controller -> Service (业务逻辑层)** - * **文件**: `com.dne.ems.service.impl.GridWorkerTaskServiceImpl.java` - * **方法**: `submitTaskFeedback(Long taskId, TaskFeedbackRequest feedbackRequest)` - * **作用**: 处理网格员提交反馈的业务逻辑。 - * 调用 `TaskRepository` 检查任务是否存在以及当前用户是否有权限操作该任务。 - * 更新 `Task` 实体的状态为 `COMPLETED` (已完成) 或 `PENDING_REVIEW` (待审核)。 - * 创建一个新的 `Feedback` (Model) 实体,将反馈内容和关联的 `Task` 设置进去。 - * 调用 `FeedbackRepository` 将新的反馈信息保存到数据库。 - * 调用 `TaskRepository` 更新任务状态。 - -3. **Service -> Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.TaskRepository.java`, `com.dne.ems.repository.FeedbackRepository.java` - * **作用**: `TaskRepository` 用于查询和更新任务信息,`FeedbackRepository` 用于保存新的反馈记录。 - -通过以上对核心业务流程的梳理,您可以清晰地看到,用户的每一个操作是如何触发后端系统中不同层、不同文件之间的一系列连锁反应,最终完成一个完整的业务闭环。这种清晰的、分层的架构是保证项目可维护性和扩展性的关键。 + +# Spring Boot 项目深度解析 + +本文档旨在深入解析当前 Spring Boot 项目的构建原理、核心技术以及业务逻辑,帮助您更好地理解和准备答疑。 + +## 一、Spring Boot 核心原理 + +Spring Boot 是一个用于简化新 Spring 应用的初始搭建以及开发过程的框架。其核心原理主要体现在以下几个方面: + +### 1. 自动配置 (Auto-Configuration) - 智能的“管家” + +您可以将 Spring Boot 的自动配置想象成一个非常智能的“管家”。在传统的 Spring 开发中,您需要手动配置很多东西,比如数据库连接、We你b 服务器等,就像您需要亲自告诉管家每一个细节:如何烧水、如何扫地。 + +而 Spring Boot 的“管家”则会观察您家里添置了什么新东西(即您在项目中加入了什么依赖),然后自动地把这些东西配置好,让它们能正常工作。 + +**它是如何工作的?** + +1. **观察您的“购物清单” (`pom.xml`)**:当您在项目的 `pom.xml` 文件中加入一个 `spring-boot-starter-*` 依赖,比如 `spring-boot-starter-web`,就等于告诉“管家”:“我需要开发一个网站。” + +2. **自动准备所需工具**:看到这个“购物清单”后,“管家”会立刻为您准备好一套完整的网站开发工具: + * 一个嵌入式的 **Tomcat 服务器**,这样您就不用自己安装和配置服务器了。 + * 配置好 **Spring MVC** 框架,这是处理网页请求的核心工具。 + * 设置好处理 JSON 数据的工具 (Jackson),方便前后端通信。 + +3. **背后的魔法 (`@EnableAutoConfiguration`)**:这一切的起点是您项目主启动类上的 `@SpringBootApplication` 注解,它内部包含了 `@EnableAutoConfiguration`。这个注解就是给“管家”下达“开始自动工作”命令的开关。它会去检查一个固定的清单(位于 `META-INF/spring.factories`),上面写着所有它认识的工具(自动配置类)以及何时应该启用它们。 + +**项目实例:** + +在本项目中,因为 `pom.xml` 包含了 `spring-boot-starter-web`,所以 Spring Boot 自动为您配置了处理 Web 请求的所有环境。这就是为什么您可以在 `com.dne.ems.controller` 包下的任何一个 Controller(例如 `AuthController`)中直接使用 `@RestController` 和 `@PostMapping` 这样的注解来定义 API 接口,而无需编写任何一行 XML 配置来启动 Web 服务或配置 Spring MVC。 + +简单来说,**自动配置就是 Spring Boot 替您完成了大部分繁琐、重复的配置工作,让您可以专注于编写业务代码。** + +### 2. 起步依赖 (Starter Dependencies) - “主题工具箱” + +如果说自动配置是“智能管家”,那么起步依赖就是“管家”使用的“主题工具箱”。您不需要告诉管家需要买钉子、锤子、螺丝刀……您只需要说:“我需要一个木工工具箱。” + +起步依赖 (`spring-boot-starter-*`) 就是这样的“主题工具箱”。每一个 starter 都包含了一整套用于某个特定任务的、经过测试、版本兼容的依赖。 + +**项目实例:** + +- **`spring-boot-starter-web`**: 这是“Web 开发工具箱”。它不仅包含了 Spring MVC,还包含了内嵌的 Tomcat 服务器和处理验证的库。您只需要引入这一个依赖,所有 Web 开发的基础环境就都准备好了。 + +- **`spring-boot-starter-data-jpa`**: 这是“数据库操作工具箱”。它打包了与数据库交互所需的一切,包括 Spring Data JPA、Hibernate (JPA 的一种实现) 和数据库连接池 (HikariCP)。您引入它之后,就可以直接在 `com.dne.ems.repository` 包下编写 `JpaRepository` 接口来进行数据库操作,而无需关心如何配置 Hibernate 或事务管理器。 + +- **`spring-boot-starter-security`**: 这是“安全管理工具箱”。引入它之后,Spring Security 框架就被集成进来,提供了认证和授权的基础功能。您可以在 `com.dne.ems.security.SecurityConfig` 中看到对这个“工具箱”的具体配置,比如定义哪些 API 需要登录才能访问。 + +**优势总结:** + +- **简化依赖**:您不必再手动添加一长串的单个依赖项。 +- **避免冲突**:Spring Boot 已经为您管理好了这些依赖之间的版本,您不必再担心因为版本不兼容而导致的各种奇怪错误。 + +通过使用这些“工具箱”,您可以非常快速地搭建起一个功能完备的应用程序框架。 + +### 3. 嵌入式 Web 服务器 + +Spring Boot 内嵌了常见的 Web 服务器,如 Tomcat、Jetty 或 Undertow。这意味着您不需要将应用程序打包成 WAR 文件部署到外部服务器,而是可以直接将应用程序打包成一个可执行的 JAR 文件,通过 `java -jar` 命令来运行。 + +- **优势**:简化了部署流程,便于持续集成和持续部署 (CI/CD)。 + +## 二、前后端分离与交互原理 + +本项目采用前后端分离的架构,前端(如 Vue.js)和后端(Spring Boot)是两个独立的项目,它们之间通过 API 进行通信。 + +### 1. RESTful API + +后端通过暴露一组 RESTful API 来提供服务。REST (Representational State Transfer) 是一种软件架构风格,它定义了一组用于创建 Web 服务的约束。核心思想是,将应用中的所有事物都看作资源,每个资源都有一个唯一的标识符 (URI)。 + +- **HTTP 方法**: + - `GET`:获取资源。 + - `POST`:创建新资源。 + - `PUT` / `PATCH`:更新资源。 + - `DELETE`:删除资源。 + +### 2. JSON (JavaScript Object Notation) + +前后端之间的数据交换格式通常是 JSON。当浏览器向后端发送请求时,它可能会在请求体中包含 JSON 数据。当后端响应时,它会将 Java 对象序列化为 JSON 字符串返回给前端。 + +- **Spring Boot 中的实现**:Spring Boot 默认使用 Jackson 库来处理 JSON 的序列化和反序列化。当 Controller 的方法参数有 `@RequestBody` 注解时,Spring 会自动将请求体中的 JSON 转换为 Java 对象。当方法有 `@ResponseBody` 或类有 `@RestController` 注解时,Spring 会自动将返回的 Java 对象转换为 JSON。 + +### 3. 认证与授权 (JWT) + +在前后端分离架构中,常用的认证机制是 JSON Web Token (JWT)。 + +- **流程**: + 1. 用户使用用户名和密码登录。 + 2. 后端验证凭据,如果成功,则生成一个 JWT 并返回给前端。 + 3. 前端将收到的 JWT 存储起来(例如,在 `localStorage` 或 `sessionStorage` 中)。 + 4. 在后续的每个请求中,前端都会在 HTTP 请求的 `Authorization` 头中携带这个 JWT。 + 5. 后端的安全过滤器会拦截每个请求,验证 JWT 的有效性。如果验证通过,则允许访问受保护的资源。 + +## 三、项目业务逻辑与分层结构分析 + +本项目是一个环境管理系统 (EMS),其代码结构遵循经典的分层架构模式。 + +### 1. 分层结构概述 + +``` ++----------------------------------------------------+ +| Controller (表现层) | +| (处理 HTTP 请求, 调用 Service, 返回 JSON 响应) | ++----------------------+-----------------------------+ + | (调用) ++----------------------v-----------------------------+ +| Service (业务逻辑层) | +| (实现核心业务逻辑, 事务管理) | ++----------------------+-----------------------------+ + | (调用) ++----------------------v-----------------------------+ +| Repository (数据访问层) | +| (通过 Spring Data JPA 与数据库交互) | ++----------------------+-----------------------------+ + | (操作) ++----------------------v-----------------------------+ +| Model (领域模型) | +| (定义数据结构, 对应数据库表) | ++----------------------------------------------------+ +``` + +### 2. 各层详细分析 + +#### a. Model (领域模型) + +位于 `com.dne.ems.model` 包下,是应用程序的核心。这些是简单的 Java 对象 (POJO),使用 JPA 注解(如 `@Entity`, `@Table`, `@Id`, `@Column`)映射到数据库中的表。 + +- **核心实体**: + - `UserAccount`: 用户账户信息,包含角色、状态等。 + - `Task`: 任务实体,包含任务状态、描述、位置等信息。 + - `Feedback`: 公众或用户提交的反馈信息。 + - `Grid`: 地理网格信息,用于管理和分配任务区域。 + - `Attachment`: 附件信息,如上传的图片。 + +#### b. Repository (数据访问层) + +位于 `com.dne.ems.repository` 包下。这些是接口,继承自 Spring Data JPA 的 `JpaRepository`。开发者只需要定义接口,Spring Data JPA 会在运行时自动为其提供实现,从而极大地简化了数据访问代码。 + +- **示例**:`TaskRepository` 提供了对 `Task` 实体的 CRUD (创建、读取、更新、删除) 操作,以及基于方法名约定的查询功能(如 `findByStatus(TaskStatus status)`)。 + +#### c. Service (业务逻辑层) + +位于 `com.dne.ems.service` 包下,是业务逻辑的核心实现。Service 层封装了应用程序的业务规则和流程,并负责协调多个 Repository 来完成一个完整的业务操作。事务管理也通常在这一层通过 `@Transactional` 注解来声明。 + +- **主要 Service 及其职责**: + - `AuthService`: 处理用户认证,如登录、注册。 + - `TaskManagementService`: 负责任务的创建、更新、查询等核心管理功能。 + - `TaskAssignmentService`: 负责任务的分配逻辑,如自动分配或手动指派。 + - `SupervisorService`: 主管相关的业务逻辑,如审核任务。 + - `GridWorkerTaskService`: 网格员的任务处理逻辑,如提交任务进度。 + - `FeedbackService`: 处理公众反馈,可能包括创建任务等后续操作。 + - `PersonnelService`: 员工管理,如添加、查询用户信息。 + +- **层间关系**:Service 通常会注入 (DI) 一个或多个 Repository 来操作数据。一个 Service 也可能调用另一个 Service 来复用业务逻辑。 + +#### d. Controller (表现层) + +位于 `com.dne.ems.controller` 包下。Controller 负责接收来自客户端的 HTTP 请求,解析请求参数,然后调用相应的 Service 来处理业务逻辑。最后,它将 Service 返回的数据(通常是 DTO)封装成 HTTP 响应(通常是 JSON 格式)返回给客户端。 + +- **主要 Controller 及其职责**: + - `AuthController`: 暴露 `/api/auth/login`, `/api/auth/register` 等认证相关的端点。 + - `TaskManagementController`: 提供任务管理的 RESTful API,如 `GET /api/tasks`, `POST /api/tasks`。 + - `SupervisorController`: 提供主管操作的 API,如审核、分配任务。 + - `FileController`: 处理文件上传和下载。 + +- **DTO (Data Transfer Object)**:位于 `com.dne.ems.dto` 包下。Controller 和 Service 之间,以及 Controller 和前端之间,通常使用 DTO 来传输数据。这样做的好处是可以避免直接暴露数据库实体,并且可以根据前端页面的需要来定制数据结构,避免传输不必要的信息。 + +### 3. 安全 (Security) + +位于 `com.dne.ems.security` 包下。使用 Spring Security 来提供全面的安全服务。 + +- `SecurityConfig`: 配置安全规则,如哪些 URL 是公开的,哪些需要认证,以及配置 JWT 过滤器。 +- `JwtAuthenticationFilter`: 一个自定义的过滤器,在每个请求到达 Controller 之前执行。它从请求头中提取 JWT,验证其有效性,并将用户信息加载到 Spring Security 的上下文中。 +- `UserDetailsServiceImpl`: 实现了 Spring Security 的 `UserDetailsService` 接口,负责根据用户名从数据库加载用户信息(包括密码和权限)。 + +## 四、总结 + +这个 Spring Boot 项目是一个典型的、结构清晰的、基于 RESTful API 的前后端分离应用。它有效地利用了 Spring Boot 的自动配置和起步依赖来简化开发,并通过分层架构将表现、业务逻辑和数据访问清晰地分离开来,使得项目易于理解、维护和扩展。 + +希望这份文档能帮助您在答疑中充满信心! + +## 五、为小白定制:项目代码结构与工作流程详解 + +为了让您彻底理解代码是如何组织的,我们把整个后端项目想象成一个分工明确的大公司。 + +### 1. 公司各部门职责 (包/文件夹的作用) + +- **`com.dne.ems.config` (后勤与配置部)** + - **职责**: 负责应用的各种基础配置。比如 `WebClientConfig` 是配置网络请求工具的,`WebSocketConfig` 是配置实时通讯的,`DataInitializer` 则是在项目启动时初始化一些默认数据(比如默认的管理员账号)。这个部门确保了其他所有部门的工具和环境都是准备好的。 + +- **`com.dne.ems.model` (产品设计部)** + - **职责**: 定义公司的核心“产品”是什么样的。这里的“产品”就是数据。例如,`UserAccount.java` 定义了“用户”有哪些属性(姓名、密码、角色等),`Task.java` 定义了“任务”有哪些属性(标题、状态、截止日期等)。它们是整个公司的业务基础,决定了公司要处理什么信息。这些文件直接对应数据库里的表结构。 + +- **`com.dne.ems.repository` (仓库管理部)** + - **职责**: 专门负责和数据库这个大“仓库”打交道。当需要存取数据时,其他部门不会直接去仓库,而是会通知仓库管理员。例如,`TaskRepository` 提供了所有操作 `Task` 表的方法,如保存一个新任务、根据ID查找一个任务。这个部门使用了 Spring Data JPA 技术,所以代码看起来很简单,但功能强大。 + +- **`com.dne.ems.service` (核心业务部)** + - **职责**: 这是公司的核心部门,负责处理所有实际的业务逻辑。一个业务操作可能很复杂,需要协调多个部门。例如,`TaskManagementService` (任务管理服务) 在创建一个新任务时,可能需要: + 1. 调用 `TaskRepository` 将任务信息存入数据库。 + 2. 调用 `UserAccountRepository` 查找合适的执行人。 + 3. 调用 `NotificationService` (如果存在的话) 发送通知。 + - **`impl` 子目录**: `service` 包下通常是接口 (定义了“能做什么”),而 `impl` (implementation) 包下是这些接口的具体实现 (定义了“具体怎么做”)。这是为了“面向接口编程”,是一种良好的编程习惯。 + +- **`com.dne.ems.controller` (客户服务与接待部)** + - **职责**: 这是公司的“前台”,直接面向客户(在这里就是前端浏览器或App)。它接收客户的请求(HTTP 请求),然后将请求传达给相应的业务部门 (`Service`) 去处理。处理完成后,它再把结果(通常是 JSON 格式的数据)返回给客户。 + - 例如,`TaskManagementController` 里的一个 `createTask` 方法,会接收前端发来的创建任务的请求,然后调用 `TaskManagementService` 的 `createTask` 方法来真正执行创建逻辑。 + +- **`com.dne.ems.dto` (数据包装与运输部)** + - **职责**: 负责数据的包装和运输。直接把数据库里的原始数据 (`Model`) 给客户看是不安全也不专业的。DTO (Data Transfer Object) 就是专门用于在各部门之间,特别是“前台”(`Controller`)和客户(前端)之间传递数据的“包装盒”。 + - 例如,`TaskDTO` 可能只包含任务的ID、标题和状态,而隐藏了创建时间、更新时间等前端不需要的敏感信息。 + +- **`com.dne.ems.security` (安保部)** + - **职责**: 负责整个公司的安全。`SecurityConfig` 定义了公司的安保规则(比如哪些地方需要门禁卡才能进),`JwtAuthenticationFilter` 就是门口的保安,每个请求进来都要检查它的“门禁卡”(JWT Token) 是否有效。 + +- **`com.dne.ems.exception` (风险与危机处理部)** + - **职责**: 处理各种突发异常情况。当业务流程中出现错误时(比如找不到用户、数据库连接失败),`GlobalExceptionHandler` 会捕获这些错误,并给出一个统一、友好的错误提示给前端,而不是让程序崩溃。 + +### 2. 一次典型的请求流程:用户登录 + +让我们通过“用户登录”这个简单的例子,看看这些部门是如何协同工作的: + +1. **客户上门 (前端 -> Controller)**: 用户在前端页面输入用户名和密码,点击登录。前端将这些信息打包成一个 JSON 对象,发送一个 HTTP POST 请求到后端的 `/api/auth/login` 地址。 + +2. **前台接待 (Controller)**: `AuthController` 接收到这个请求。它看到地址是 `/login`,于是调用 `login` 方法。它从请求中取出 JSON 数据,并将其转换为 `LoginRequest` 这个 DTO 对象。 + +3. **转交业务部门 (Controller -> Service)**: `AuthController` 自己不处理登录逻辑,它调用 `AuthService` 的 `login` 方法,并将 `LoginRequest` 这个 DTO 传递过去。 + +4. **核心业务处理 (Service)**: `AuthService` 开始处理登录: + a. 它首先需要验证用户名是否存在,于是它请求 **仓库管理部** (`UserAccountRepository`):“请帮我根据这个用户名 (`loginRequest.getUsername()`) 查一下有没有这个人。” + b. `UserAccountRepository` 从数据库中查找到对应的 `UserAccount` (Model) 信息,返回给 `AuthService`。 + c. `AuthService` 拿到用户信息后,会使用密码加密工具,验证用户提交的密码和数据库中存储的加密密码是否匹配。 + d. 如果验证成功,`AuthService` 会请求 **安保部** (`JwtService`):“请为这位用户生成一张有时效的门禁卡 (JWT Token)。” + +5. **返回处理结果 (Service -> Controller -> 前端)**: `JwtService` 生成 Token 后返回给 `AuthService`。`AuthService` 将 Token 包装在一个 `JwtAuthenticationResponse` DTO 中,返回给 `AuthController`。`AuthController` 再将这个包含 Token 的 DTO 转换成 JSON 格式,作为 HTTP 响应返回给前端。 + +6. **客户拿到凭证 (前端)**: 前端收到包含 Token 的响应,就知道登录成功了。它会把这个 Token 保存起来,在之后访问需要权限的页面时,都会带上这张“门禁卡”。 + +通过这个流程,您可以看到,一个简单的请求背后是多个“部门”按照明确的职责划分,层层调用、协同工作的结果。这种结构使得代码逻辑清晰,易于维护和扩展。 +嗯 +## 六、核心业务流程与关键机制分析 + +下面,我们将梳理出本项目的几个核心业务流程和支撑这些流程的关键技术机制,并详细说明它们涉及的层级和文件,让您清晰地看到数据和指令是如何在系统中流转的。 + +### (一) 核心业务流程 + +#### 流程一:用户认证与授权 (Authentication & Authorization) + +**目标**:保障系统安全,确保只有授权用户才能访问受保护的资源。 + +**执行路径**: + +1. **注册 (`signup`)** + * **入口**: `AuthController.signup()` + * **业务逻辑**: `AuthService.signup()` + * 验证邮箱和验证码的有效性 (`VerificationCodeService`)。 + * 检查邮箱是否已注册 (`UserAccountRepository`)。 + * 对密码进行加密 (`PasswordEncoder`)。 + * 创建并保存新的 `UserAccount` 实体。 + +2. **登录 (`login`)** + * **入口**: `AuthController.login()` + * **业务逻辑**: `AuthService.login()` + * 通过 Spring Security 的 `AuthenticationManager` 验证用户名和密码。 + * 如果认证成功,调用 `JwtService` 生成 JWT。 + * 返回包含 JWT 的响应给前端。 + +3. **访问受保护资源** + * **安全过滤器**: `JwtAuthenticationFilter` + * 拦截所有请求,从 `Authorization` 请求头中提取 JWT。 + * 验证 JWT 的有效性 (`JwtService`)。 + * 如果有效,从 JWT 中解析出用户信息,并构建一个 `UsernamePasswordAuthenticationToken`,设置到 Spring Security 的 `SecurityContextHolder` 中,完成授权。 + +#### 流程二:公众反馈与任务转化 (Feedback to Task) + +**目标**:将公众通过开放接口提交的反馈(如环境问题)转化为系统内部的可追踪任务。 + +**执行路径**: + +1. **提交反馈** + * **入口**: `PublicController.submitFeedback()` + * **业务逻辑**: `FeedbackService.createFeedback()` + * 接收 `FeedbackDTO`,包含反馈内容和可选的附件。 + * 处理文件上传(如果存在),调用 `FileStorageService` 保存文件,并将附件信息与反馈关联。 + * 保存 `Feedback` 实体到数据库。 + +2. **反馈审核与任务创建 (由管理员操作)** + * **入口**: `SupervisorController.approveFeedbackAndCreateTask()` (假设存在此接口) + * **业务逻辑**: `SupervisorService.processFeedback()` + * 管理员审核反馈内容。 + * 如果反馈有效,调用 `TaskManagementService.createTaskFromFeedback()`,基于反馈信息创建一个新的 `Task`。 + +#### 流程三:任务生命周期管理 (Task Lifecycle) + +**目标**:完整地管理一个任务从创建、分配、执行到完成的全过程。 + +**执行路径**: + +1. **任务创建** + * **入口**: `TaskManagementController.createTask()` + * **业务逻辑**: `TaskManagementService.createTask()` + * 创建 `Task` 实体并设置初始状态(如 `PENDING_ASSIGNMENT`)。 + +2. **任务分配** + * **入口**: `SupervisorController.assignTask()` + * **业务逻辑**: `TaskAssignmentService.assignTaskToWorker()` + * 将任务 (`Task`) 与一个网格员 (`GridWorker`) 关联起来,创建 `Assignment` 记录。 + * 更新任务状态为 `ASSIGNED`。 + +3. **任务执行与更新** + * **入口**: `GridWorkerTaskController.updateTaskStatus()` + * **业务逻辑**: `GridWorkerTaskService.updateTaskProgress()` + * 网格员提交任务进度或结果,可附带图片等附件 (`FileStorageService`)。 + * 更新任务状态(如 `IN_PROGRESS`, `COMPLETED`)。 + +4. **任务审核与关闭** + * **入口**: `SupervisorController.reviewTask()` + * **业务逻辑**: `SupervisorService.reviewAndCloseTask()` + * 主管审核已完成的任务。 + * 如果合格,将任务状态更新为 `CLOSED`。如果不合格,可打回 (`REJECTED`)。 + +#### 流程四:数据可视化与决策支持 (Dashboard & AI) + +**目标**:为管理层提供数据洞察,并通过 AI 功能辅助决策。 + +**执行路径**: + +1. **仪表盘数据** + * **入口**: `DashboardController` 中的各个接口,如 `getTaskStats()`, `getRecentActivities()`。 + * **业务逻辑**: `DashboardService` + * 调用多个 `Repository`(如 `TaskRepository`, `FeedbackRepository`)进行数据聚合查询,统计任务状态、反馈趋势等。 + * 返回 DTO 给前端进行图表展示。 + +2. **AI 辅助功能** + * **文本审核**: `AiReviewService.reviewText()` 可能被用于自动审核反馈内容或评论,识别敏感信息。 + * **路径规划**: `AStarService.findPath()` 用于计算两点之间的最优路径,可辅助网格员规划任务路线。 + +### (二) 关键支撑机制 + +除了核心业务流程,系统还包含一系列关键的技术机制来保证其健壮、安全和可维护性。 + +1. **文件上传与管理** + * **位置**: `FileStorageService`, `FileController` + * **作用**: 提供统一的文件存储和访问服务。无论是用户头像、反馈附件还是任务报告,都通过此服务进行处理,实现了与具体存储位置(本地文件系统或云存储)的解耦。 + +2. **邮件与验证码服务** + * **位置**: `MailService`, `VerificationCodeService` + * **作用**: 负责发送邮件(如注册验证、密码重置)和管理验证码的生成与校验,是保障账户安全的重要环节。 + +3. **登录安全与尝试锁定** + * **位置**: `LoginAttemptService`, `AuthenticationFailureEventListener` + * **作用**: 监听登录失败事件 (`AuthenticationFailureListener`),并通过 `LoginAttemptService` 记录特定 IP 的失败次数。当超过阈值时,会暂时锁定该 IP 的登录权限,有效防止暴力破解攻击。 + +4. **A* 路径规划算法** + * **位置**: `AStarService`, `PathfindingController` + * **作用**: 实现了 A* 寻路算法,这是一个独立的功能模块,可以根据地图数据(网格 `Grid`)计算最优路径,为其他业务(如任务路线规划)提供支持。 + +5. **系统事件处理机制** + * **位置**: `event` 和 `listener` 包 + * **作用**: 基于 Spring 的事件驱动模型,实现系统内各组件的解耦。例如,当一个用户注册成功后,可以发布一个 `UserRegistrationEvent` 事件,而邮件发送监听器 (`EmailNotificationListener`) 和新用户欢迎监听器 (`WelcomeBonusListener`) 可以分别监听并处理此事件,而无需在注册服务中硬编码这些逻辑。 + +6. **全局异常处理** + * **位置**: `GlobalExceptionHandler` + * **作用**: 捕获整个应用中抛出的未处理异常,并根据异常类型返回统一、规范的错误响应给前端。这避免了将原始的、可能包含敏感信息的堆栈跟踪暴露给用户,并提升了用户体验。 + +7. **自定义数据校验** + * **位置**: `validation` 包 (如 `PasswordValidator`) + * **作用**: 实现自定义的、复杂的校验逻辑。例如,`@ValidPassword` 注解可以确保用户设置的密码符合特定的强度要求(如长度、包含数字和特殊字符等)。 + +8. **数据持久化与查询 (JPA)** + * **位置**: `repository` 包 + * **作用**: 利用 Spring Data JPA,通过定义接口 (`extends JpaRepository`) 的方式,极大地简化了数据库的 CRUD 操作和查询。开发者无需编写 SQL 语句,JPA 会自动根据方法名或注解生成相应的查询。 + +9. **API 数据契约 (DTO)** + * **位置**: `dto` 包 + * **作用**: 数据传输对象 (Data Transfer Object) 是前后端分离架构中的关键实践。它作为 API 的“数据契约”,明确了接口需要什么数据、返回什么数据,实现了表现层与领域模型的解耦,使得双方可以独立演进,同时避免了敏感信息的泄露。 + +10. **应用配置与初始化** + * **位置**: `config` 包, `DataInitializer` + * **作用**: `config` 包集中管理了应用的所有配置类,如安全配置 (`SecurityConfig`)、Web 配置等。`DataInitializer` 则利用 `CommandLineRunner` 接口,在应用启动后执行一次性任务,如创建默认管理员账户、初始化基础数据等,确保了应用开箱即用的能力。 + * **密码加密**: 使用 `PasswordEncoder` 对用户密码进行加密,增强安全性。 + * **创建用户实体**: 创建一个新的 `UserAccount` 对象 (`Model`),并填充信息。 + +3. **Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.UserAccountRepository.java` + * **方法**: `save(UserAccount user)` + * **作用**: `AuthService` 调用此方法,将新创建的 `UserAccount` 对象持久化到数据库中。 + +4. **Model (领域模型)** + * **文件**: `com.dne.ems.model.UserAccount.java` + * **作用**: 定义了用户账户的数据结构,是数据库中 `user_accounts` 表的映射。 + +### 流程二:任务创建与分配 + +**目标**:主管或管理员根据一个已批准的反馈,创建一个新任务,并将其分配给一个网格员。 + +**执行路径**: + +1. **Controller (表现层)** + * **文件**: `com.dne.ems.controller.TaskManagementController.java` + * **方法**: `createTaskFromFeedback(long feedbackId, TaskFromFeedbackRequest request)` + * **作用**: 接收 `/api/management/tasks/feedback/{feedbackId}/create-task` POST 请求。它将反馈ID和任务创建请求 (`TaskFromFeedbackRequest` DTO) 传递给 Service 层。 + +2. **Service (业务逻辑层)** + * **文件**: `com.dne.ems.service.impl.TaskManagementServiceImpl.java` + * **方法**: `createTaskFromFeedback(long feedbackId, TaskFromFeedbackRequest request)` + * **作用**: 实现核心的创建和分配逻辑: + * **查找反馈**: 调用 `FeedbackRepository` 找到对应的 `Feedback` (`Model`)。 + * **查找负责人**: 调用 `UserAccountRepository` 找到指定的负责人 `UserAccount` (`Model`)。 + * **创建任务**: 创建一个新的 `Task` 对象 (`Model`),并从 `Feedback` 和请求 DTO 中填充任务信息(如描述、截止日期、严重性等)。 + * **设置状态**: 将任务的初始状态设置为 `ASSIGNED`。 + +3. **Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.TaskRepository.java` + * **方法**: `save(Task task)` + * **作用**: `TaskManagementService` 调用此方法,将新创建的 `Task` 对象保存到数据库。 + * **涉及的其他 Repository**: `FeedbackRepository` 和 `UserAccountRepository` 用于在业务逻辑中获取必要的数据。 + +4. **Model (领域模型)** + * **文件**: `com.dne.ems.model.Task.java` + * **作用**: 定义了任务的数据结构。 + * **涉及的其他 Model**: `Feedback.java` 和 `UserAccount.java` 作为数据来源。 + +### 流程三:网格员处理任务与提交反馈 + +**目标**:网格员查看自己的任务,执行任务,并提交处理结果(包括评论和附件)。 + +**执行路径**: + +1. **Controller (表现层)** + * **文件**: `com.dne.ems.controller.GridWorkerTaskController.java` + * **方法**: `submitTask(long taskId, String comments, List files)` + * **作用**: 接收 `/api/worker/{taskId}/submit` POST 请求。这是一个 `multipart/form-data` 请求,因为它同时包含了文本(评论)和文件。Controller 将这些信息传递给 Service 层。 + +2. **Service (业务逻辑层)** + * **文件**: `com.dne.ems.service.impl.GridWorkerTaskServiceImpl.java` + * **方法**: `submitTask(long taskId, String comments, List files)` + * **作用**: 处理任务提交的业务逻辑: + * **查找任务**: 调用 `TaskRepository` 找到当前需要处理的 `Task` (`Model`)。 + * **验证权限**: 确保当前登录的用户就是这个任务的负责人。 + * **处理文件上传**: 如果有附件,调用 `FileStorageService` 将文件保存到服务器,并创建 `Attachment` (`Model`) 对象。 + * **更新任务状态**: 将任务状态更新为 `SUBMITTED` 或 `PENDING_REVIEW`。 + * **保存评论和附件信息**: 将评论和附件信息关联到任务上。 + +3. **Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.TaskRepository.java` + * **方法**: `save(Task task)` + * **作用**: `GridWorkerTaskService` 在更新了任务状态和信息后,调用此方法将 `Task` 对象的变化持久化到数据库。 + * **涉及的其他 Repository**: `AttachmentRepository` (如果适用) 用于保存附件信息。 + +4. **Model (领域模型)** + * **文件**: `com.dne.ems.model.Task.java` + * **作用**: 任务数据在此流程中被更新。 + * **涉及的其他 Model**: `Attachment.java` 用于表示上传的文件。 + * **密码加密**: 使用 `PasswordEncoder` 对用户提交的明文密码进行加密,确保数据库中存储的是密文,保障安全。 + * **创建用户实体**: 创建一个新的 `UserAccount` (Model) 对象,并将注册信息(包括加密后的密码和角色)设置到该对象中。 + +3. **Service -> Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.UserAccountRepository.java` + * **方法**: `save(UserAccount userAccount)` + * **作用**: Service 层调用此方法,将填充好数据的 `UserAccount` 实体对象持久化到数据库中。Spring Data JPA 会自动生成对应的 SQL `INSERT` 语句来完成操作。 + +4. **返回结果**: 数据成功存入数据库后,操作结果会逐层返回,最终 `AuthController` 会向前端返回一个成功的响应消息。 + +### 流程二:任务的创建与自动分配 + +**目标**:系统管理员或主管创建一个新的环境问题任务,并由系统自动分配给相应网格内的网格员。 + +**执行路径**: + +1. **前端 -> Controller (表现层)** + * **文件**: `com.dne.ems.controller.TaskManagementController.java` + * **方法**: `createTask(TaskCreateRequest taskCreateRequest)` + * **作用**: 接收 `/api/tasks` POST 请求,请求体中包含了任务的详细信息(如描述、位置、图片附件ID等),封装在 `TaskCreateRequest` DTO 中。Controller 调用 `TaskManagementService` 来处理创建逻辑。 + +2. **Controller -> Service (业务逻辑层)** + * **文件**: `com.dne.ems.service.impl.TaskManagementServiceImpl.java` + * **方法**: `createTask(TaskCreateRequest taskCreateRequest)` + * **作用**: 创建任务的核心业务逻辑。 + * 创建一个新的 `Task` (Model) 实体。 + * 根据请求中的附件ID,调用 `AttachmentRepository` 查找并关联附件。 + * 设置任务的初始状态为 `PENDING` (待处理)。 + * 调用 `TaskRepository` 将新任务保存到数据库。 + * **触发任务分配**: 保存任务后,调用 `TaskAssignmentService` 来执行自动分配逻辑。 + +3. **Service -> Service (业务逻辑层内部调用)** + * **文件**: `com.dne.ems.service.impl.TaskAssignmentServiceImpl.java` + * **方法**: `assignTask(Task task)` + * **作用**: 处理任务的分配逻辑。 + * 根据任务的地理位置信息,调用 `GridRepository` 找到对应的地理网格 (`Grid`)。 + * 根据网格信息,调用 `UserAccountRepository` 找到负责该网格的网格员 (`GridWorker`)。 + * 如果找到合适的网格员,则更新 `Task` 实体的 `assignee` (执行人) 字段。 + * 更新任务状态为 `ASSIGNED` (已分配)。 + * 调用 `TaskRepository` 将更新后的任务信息保存回数据库。 + * (可选) 调用通知服务,向被分配的网格员发送新任务通知。 + +4. **Service -> Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.TaskRepository.java`, `com.dne.ems.repository.GridRepository.java`, `com.dne.ems.repository.UserAccountRepository.java` + * **作用**: 在整个流程中,Service 层会频繁地与这些 Repository 交互,以查询网格信息、查询用户信息、保存和更新任务数据。 + +### 流程三:网格员处理任务并提交反馈 + +**目标**:网格员在完成任务后,通过 App 或前端页面提交任务处理结果,包括文字描述和现场图片。 + +**执行路径**: + +1. **前端 -> Controller (表现层)** + * **文件**: `com.dne.ems.controller.GridWorkerTaskController.java` + * **方法**: `submitTaskFeedback(Long taskId, TaskFeedbackRequest feedbackRequest)` + * **作用**: 接收 `/api/worker/tasks/{taskId}/feedback` POST 请求。路径中的 `taskId` 指明了要为哪个任务提交反馈,请求体 `TaskFeedbackRequest` DTO 中包含了反馈内容和附件ID。 + +2. **Controller -> Service (业务逻辑层)** + * **文件**: `com.dne.ems.service.impl.GridWorkerTaskServiceImpl.java` + * **方法**: `submitTaskFeedback(Long taskId, TaskFeedbackRequest feedbackRequest)` + * **作用**: 处理网格员提交反馈的业务逻辑。 + * 调用 `TaskRepository` 检查任务是否存在以及当前用户是否有权限操作该任务。 + * 更新 `Task` 实体的状态为 `COMPLETED` (已完成) 或 `PENDING_REVIEW` (待审核)。 + * 创建一个新的 `Feedback` (Model) 实体,将反馈内容和关联的 `Task` 设置进去。 + * 调用 `FeedbackRepository` 将新的反馈信息保存到数据库。 + * 调用 `TaskRepository` 更新任务状态。 + +3. **Service -> Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.TaskRepository.java`, `com.dne.ems.repository.FeedbackRepository.java` + * **作用**: `TaskRepository` 用于查询和更新任务信息,`FeedbackRepository` 用于保存新的反馈记录。 + +通过以上对核心业务流程的梳理,您可以清晰地看到,用户的每一个操作是如何触发后端系统中不同层、不同文件之间的一系列连锁反应,最终完成一个完整的业务闭环。这种清晰的、分层的架构是保证项目可维护性和扩展性的关键。 --- - + ### ems-backend/api测试文档.md - -# EMS 后端 API 测试文档 - -本文档基于项目源代码生成,包含了所有后端 API 接口的详细信息,用于开发和测试。 - ---- - -## 目录 -1. [认证模块 (Auth)](#1-认证模块-auth) -2. [仪表盘模块 (Dashboard)](#2-仪表盘模块-dashboard) -3. [反馈模块 (Feedback)](#3-反馈模块-feedback) -4. [文件模块 (File)](#4-文件模块-file) -5. [网格模块 (Grid)](#5-网格模块-grid) -6. [网格员任务模块 (Worker)](#6-网格员任务模块-worker) -7. [地图模块 (Map)](#7-地图模块-map) -8. [路径规划模块 (Pathfinding)](#8-路径规划模块-pathfinding) -9. [人员管理模块 (Personnel)](#9-人员管理模块-personnel) -10. [个人资料模块 (Me)](#10-个人资料模块-me) -11. [公共接口模块 (Public)](#11-公共接口模块-public) -12. [主管模块 (Supervisor)](#12-主管模块-supervisor) -13. [任务分配模块 (Tasks)](#13-任务分配模块-tasks) -14. [任务管理模块 (Management)](#14-任务管理模块-management) - ---- - -## 1. 认证模块 (Auth) -**基础路径**: `/api/auth` - -### 1.1 用户注册 -- **功能描述**: 注册一个新用户。 -- **请求路径**: `/api/auth/signup` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `SignUpRequest` -- **示例请求**: - ```json - { - "name": "测试用户", - "email": "test@example.com", - "phone": "13800138000", - "password": "password123", - "verificationCode": "123456", - "role": "GRID_WORKER" - } - ``` - -### 1.2 用户登录 -- **功能描述**: 用户登录并获取 JWT Token。 -- **请求路径**: `/api/auth/login` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `LoginRequest` -- **示例请求**: - ```json - { - "email": "admin@aizhangz.top", - "password": "Admin@123" - } - ``` - -### 1.3 发送验证码 -- **功能描述**: 请求发送验证码到指定邮箱。 -- **请求路径**: `/api/auth/send-verification-code` -- **请求方法**: `POST` -- **所需权限**: 无 -- **查询参数**: `email` (string, 必需) -- **示例请求**: `POST /api/auth/send-verification-code?email=test@example.com` - -### 1.4 请求重置密码 -- **功能描述**: 请求发送重置密码的链接或令牌。 -- **请求路径**: `/api/auth/request-password-reset` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `PasswordResetRequest` -- **示例请求**: - ```json - { - "email": "test@example.com" - } - ``` - -### 1.5 重置密码 -- **功能描述**: 使用令牌重置密码。 -- **请求路径**: `/api/auth/reset-password` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `PasswordResetDto` -- **示例请求**: - ```json - { - "token": "some-reset-token", - "newPassword": "newPassword123" - } - ``` - ---- - -## 2. 仪表盘模块 (Dashboard) -**基础路径**: `/api/dashboard` -**所需权限**: `DECISION_MAKER` - -### 2.1 获取仪表盘统计数据 -- **请求路径**: `/api/dashboard/stats` -- **请求方法**: `GET` - -### 2.2 获取 AQI 分布 -- **请求路径**: `/api/dashboard/reports/aqi-distribution` -- **请求方法**: `GET` - -### 2.3 获取污染超标月度趋势 -- **请求路径**: `/api/dashboard/reports/pollution-trend` -- **请求方法**: `GET` - -### 2.4 获取网格覆盖率 -- **请求路径**: `/api/dashboard/reports/grid-coverage` -- **请求方法**: `GET` - -### 2.5 获取反馈热力图数据 -- **请求路径**: `/api/dashboard/map/heatmap` -- **请求方法**: `GET` - -### 2.6 获取污染类型统计 -- **请求路径**: `/api/dashboard/reports/pollution-stats` -- **请求方法**: `GET` - -### 2.7 获取任务完成情况统计 -- **请求路径**: `/api/dashboard/reports/task-completion-stats` -- **请求方法**: `GET` - -### 2.8 获取 AQI 热力图数据 -- **请求路径**: `/api/dashboard/map/aqi-heatmap` -- **请求方法**: `GET` - ---- - -## 3. 反馈模块 (Feedback) -**基础路径**: `/api/feedback` - -### 3.1 提交反馈 (JSON) -- **功能描述**: 用于测试,使用 JSON 提交反馈。 -- **请求路径**: `/api/feedback/submit-json` -- **请求方法**: `POST` -- **所需权限**: 已认证用户 -- **请求体**: `FeedbackSubmissionRequest` - -### 3.2 提交反馈 (Multipart) -- **功能描述**: 提交反馈,可包含文件。 -- **请求路径**: `/api/feedback/submit` -- **请求方法**: `POST` -- **所需权限**: 已认证用户 -- **请求体**: `multipart/form-data` - - `feedback` (part): `FeedbackSubmissionRequest` (JSON) - - `files` (part, optional): `MultipartFile[]` - -### 3.3 获取所有反馈 -- **功能描述**: 获取所有反馈(分页)。 -- **请求路径**: `/api/feedback` -- **请求方法**: `GET` -- **所需权限**: `ADMIN` -- **查询参数**: `page`, `size`, `sort` - ---- - -## 4. 文件模块 (File) -**基础路径**: `/api/files` -**所需权限**: 公开访问 - -### 4.1 下载文件 -- **请求路径**: `/api/files/download/{fileName}` -- **请求方法**: `GET` -- **路径参数**: `fileName` (string, 必需) - -### 4.2 查看文件 -- **请求路径**: `/api/files/view/{fileName}` -- **请求方法**: `GET` -- **路径参数**: `fileName` (string, 必需) - ---- - -## 5. 网格模块 (Grid) -**基础路径**: `/api/grids` -**所需权限**: `ADMIN` 或 `DECISION_MAKER` - -### 5.1 获取网格列表 -- **请求路径**: `/api/grids` -- **请求方法**: `GET` -- **查询参数**: - - `cityName` (string, 可选) - - `districtName` (string, 可选) - - `page`, `size`, `sort` - -### 5.2 更新网格信息 -- **请求路径**: `/api/grids/{gridId}` -- **请求方法**: `PATCH` -- **所需权限**: `ADMIN` -- **路径参数**: `gridId` (long, 必需) -- **请求体**: `GridUpdateRequest` - ---- - -## 6. 网格员任务模块 (Worker) -**基础路径**: `/api/worker` -**所需权限**: `GRID_WORKER` - -### 6.1 获取我的任务 -- **请求路径**: `/api/worker` -- **请求方法**: `GET` -- **查询参数**: - - `status` (enum: `TaskStatus`, 可选) - - `page`, `size`, `sort` - -### 6.2 获取任务详情 -- **请求路径**: `/api/worker/{taskId}` -- **请求方法**: `GET` -- **路径参数**: `taskId` (long, 必需) - -### 6.3 接受任务 -- **请求路径**: `/api/worker/{taskId}/accept` -- **请求方法**: `POST` -- **路径参数**: `taskId` (long, 必需) - -### 6.4 提交任务 -- **请求路径**: `/api/worker/{taskId}/submit` -- **请求方法**: `POST` -- **路径参数**: `taskId` (long, 必需) -- **请求体**: `TaskSubmissionRequest` - ---- - -## 7. 地图模块 (Map) -**基础路径**: `/api/map` -**所需权限**: 公开访问 - -### 7.1 获取完整地图网格 -- **请求路径**: `/api/map/grid` -- **请求方法**: `GET` - -### 7.2 创建或更新地图单元格 -- **请求路径**: `/api/map/grid` -- **请求方法**: `POST` -- **请求体**: `MapGrid` - -### 7.3 初始化地图 -- **请求路径**: `/api/map/initialize` -- **请求方法**: `POST` -- **查询参数**: - - `width` (int, 可选, 默认 20) - - `height` (int, 可选, 默认 20) - ---- - -## 8. 路径规划模块 (Pathfinding) -**基础路径**: `/api/pathfinding` -**所需权限**: 公开访问 - -### 8.1 寻找路径 -- **功能描述**: 使用 A* 算法在两点之间寻找最短路径。 -- **请求路径**: `/api/pathfinding/find` -- **请求方法**: `POST` -- **请求体**: `PathfindingRequest` -- **示例请求**: - ```json - { - "startX": 0, - "startY": 0, - "endX": 19, - "endY": 19 - } - ``` - ---- - -## 9. 人员管理模块 (Personnel) -**基础路径**: `/api/personnel` -**所需权限**: `ADMIN` - -### 9.1 创建用户 -- **请求路径**: `/api/personnel/users` -- **请求方法**: `POST` -- **请求体**: `UserCreationRequest` - -### 9.2 获取用户列表 -- **请求路径**: `/api/personnel/users` -- **请求方法**: `GET` -- **查询参数**: - - `role` (enum: `Role`, 可选) - - `name` (string, 可选) - - `page`, `size`, `sort` - -### 9.3 获取单个用户 -- **请求路径**: `/api/personnel/users/{userId}` -- **请求方法**: `GET` -- **路径参数**: `userId` (long, 必需) - -### 9.4 更新用户信息 -- **请求路径**: `/api/personnel/users/{userId}` -- **请求方法**: `PATCH` -- **路径参数**: `userId` (long, 必需) -- **请求体**: `UserUpdateRequest` - -### 9.5 更新用户角色 -- **请求路径**: `/api/personnel/users/{userId}/role` -- **请求方法**: `PUT` -- **路径参数**: `userId` (long, 必需) -- **请求体**: `UserRoleUpdateRequest` - -### 9.6 删除用户 -- **请求路径**: `/api/personnel/users/{userId}` -- **请求方法**: `DELETE` -- **路径参数**: `userId` (long, 必需) - ---- - -## 10. 个人资料模块 (Me) -**基础路径**: `/api/me` -**所需权限**: 已认证用户 - -### 10.1 更新个人资料 -- **请求路径**: `/api/me` -- **请求方法**: `PATCH` -- **请求体**: `UserUpdateRequest` - -### 10.2 更新我的位置 -- **请求路径**: `/api/me/location` -- **请求方法**: `POST` -- **请求体**: `LocationUpdateRequest` - -### 10.3 获取我的反馈历史 -- **请求路径**: `/api/me/feedback` -- **请求方法**: `GET` -- **查询参数**: `page`, `size`, `sort` - ---- - -## 11. 公共接口模块 (Public) -**基础路径**: `/api/public` -**所需权限**: 公开访问 - -### 11.1 公众提交反馈 -- **请求路径**: `/api/public/feedback` -- **请求方法**: `POST` -- **请求体**: `multipart/form-data` - - `feedback` (part): `PublicFeedbackRequest` (JSON) - - `files` (part, optional): `MultipartFile[]` - ---- - -## 12. 主管模块 (Supervisor) -**基础路径**: `/api/supervisor` -**所需权限**: `SUPERVISOR` 或 `ADMIN` - -### 12.1 获取待审核的反馈列表 -- **请求路径**: `/api/supervisor/reviews` -- **请求方法**: `GET` - -### 12.2 批准反馈 -- **请求路径**: `/api/supervisor/reviews/{feedbackId}/approve` -- **请求方法**: `POST` -- **路径参数**: `feedbackId` (long, 必需) - -### 12.3 拒绝反馈 -- **请求路径**: `/api/supervisor/reviews/{feedbackId}/reject` -- **请求方法**: `POST` -- **路径参数**: `feedbackId` (long, 必需) - ---- - -## 13. 任务分配模块 (Tasks) -**基础路径**: `/api/tasks` -**所需权限**: `ADMIN` 或 `SUPERVISOR` - -### 13.1 获取未分配的反馈 -- **请求路径**: `/api/tasks/unassigned` -- **请求方法**: `GET` - -### 13.2 获取可用的网格员 -- **请求路径**: `/api/tasks/grid-workers` -- **请求方法**: `GET` - -### 13.3 分配任务 -- **请求路径**: `/api/tasks/assign` -- **请求方法**: `POST` -- **请求体**: - ```json - { - "feedbackId": 1, - "assigneeId": 2 - } - ``` - ---- - -## 14. 任务管理模块 (Management) -**基础路径**: `/api/management/tasks` -**所需权限**: `SUPERVISOR` - -### 14.1 获取任务列表 -- **请求路径**: `/api/management/tasks` -- **请求方法**: `GET` -- **查询参数**: - - `status` (enum: `TaskStatus`, 可选) - - `assigneeId` (long, 可选) - - `severity` (enum: `SeverityLevel`, 可选) - - `pollutionType` (enum: `PollutionType`, 可选) - - `startDate` (date, 可选, 格式: YYYY-MM-DD) - - `endDate` (date, 可选, 格式: YYYY-MM-DD) - - `page`, `size`, `sort` - -### 14.2 分配任务 -- **请求路径**: `/api/management/tasks/{taskId}/assign` -- **请求方法**: `POST` -- **路径参数**: `taskId` (long, 必需) -- **请求体**: `TaskAssignmentRequest` - -### 14.3 获取任务详情 -- **请求路径**: `/api/management/tasks/{taskId}` -- **请求方法**: `GET` -- **路径参数**: `taskId` (long, 必需) - -### 14.4 审核任务 -- **请求路径**: `/api/management/tasks/{taskId}/review` -- **请求方法**: `POST` -- **路径参数**: `taskId` (long, 必需) -- **请求体**: `TaskApprovalRequest` - -### 14.5 取消任务 -- **请求路径**: `/api/management/tasks/{taskId}/cancel` -- **请求方法**: `POST` -- **路径参数**: `taskId` (long, 必需) - -### 14.6 直接创建任务 -- **请求路径**: `/api/management/tasks` -- **请求方法**: `POST` -- **请求体**: `TaskCreationRequest` - -### 14.7 获取待处理的反馈 -- **请求路径**: `/api/management/tasks/feedback` -- **请求方法**: `GET` -- **所需权限**: `SUPERVISOR` 或 `ADMIN` - -### 14.8 从反馈创建任务 -- **请求路径**: `/api/management/tasks/feedback/{feedbackId}/create-task` -- **请求方法**: `POST` -- **路径参数**: `feedbackId` (long, 必需) -- **请求体**: `TaskFromFeedbackRequest` + +# EMS 后端 API 测试文档 + +本文档基于项目源代码生成,包含了所有后端 API 接口的详细信息,用于开发和测试。 + +--- + +## 目录 +1. [认证模块 (Auth)](#1-认证模块-auth) +2. [仪表盘模块 (Dashboard)](#2-仪表盘模块-dashboard) +3. [反馈模块 (Feedback)](#3-反馈模块-feedback) +4. [文件模块 (File)](#4-文件模块-file) +5. [网格模块 (Grid)](#5-网格模块-grid) +6. [网格员任务模块 (Worker)](#6-网格员任务模块-worker) +7. [地图模块 (Map)](#7-地图模块-map) +8. [路径规划模块 (Pathfinding)](#8-路径规划模块-pathfinding) +9. [人员管理模块 (Personnel)](#9-人员管理模块-personnel) +10. [个人资料模块 (Me)](#10-个人资料模块-me) +11. [公共接口模块 (Public)](#11-公共接口模块-public) +12. [主管模块 (Supervisor)](#12-主管模块-supervisor) +13. [任务分配模块 (Tasks)](#13-任务分配模块-tasks) +14. [任务管理模块 (Management)](#14-任务管理模块-management) + +--- + +## 1. 认证模块 (Auth) +**基础路径**: `/api/auth` + +### 1.1 用户注册 +- **功能描述**: 注册一个新用户。 +- **请求路径**: `/api/auth/signup` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `SignUpRequest` +- **示例请求**: + ```json + { + "name": "测试用户", + "email": "test@example.com", + "phone": "13800138000", + "password": "password123", + "verificationCode": "123456", + "role": "GRID_WORKER" + } + ``` + +### 1.2 用户登录 +- **功能描述**: 用户登录并获取 JWT Token。 +- **请求路径**: `/api/auth/login` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `LoginRequest` +- **示例请求**: + ```json + { + "email": "admin@aizhangz.top", + "password": "Admin@123" + } + ``` + +### 1.3 发送验证码 +- **功能描述**: 请求发送验证码到指定邮箱。 +- **请求路径**: `/api/auth/send-verification-code` +- **请求方法**: `POST` +- **所需权限**: 无 +- **查询参数**: `email` (string, 必需) +- **示例请求**: `POST /api/auth/send-verification-code?email=test@example.com` + +### 1.4 请求重置密码 +- **功能描述**: 请求发送重置密码的链接或令牌。 +- **请求路径**: `/api/auth/request-password-reset` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `PasswordResetRequest` +- **示例请求**: + ```json + { + "email": "test@example.com" + } + ``` + +### 1.5 重置密码 +- **功能描述**: 使用令牌重置密码。 +- **请求路径**: `/api/auth/reset-password` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `PasswordResetDto` +- **示例请求**: + ```json + { + "token": "some-reset-token", + "newPassword": "newPassword123" + } + ``` + +--- + +## 2. 仪表盘模块 (Dashboard) +**基础路径**: `/api/dashboard` +**所需权限**: `DECISION_MAKER` + +### 2.1 获取仪表盘统计数据 +- **请求路径**: `/api/dashboard/stats` +- **请求方法**: `GET` + +### 2.2 获取 AQI 分布 +- **请求路径**: `/api/dashboard/reports/aqi-distribution` +- **请求方法**: `GET` + +### 2.3 获取污染超标月度趋势 +- **请求路径**: `/api/dashboard/reports/pollution-trend` +- **请求方法**: `GET` + +### 2.4 获取网格覆盖率 +- **请求路径**: `/api/dashboard/reports/grid-coverage` +- **请求方法**: `GET` + +### 2.5 获取反馈热力图数据 +- **请求路径**: `/api/dashboard/map/heatmap` +- **请求方法**: `GET` + +### 2.6 获取污染类型统计 +- **请求路径**: `/api/dashboard/reports/pollution-stats` +- **请求方法**: `GET` + +### 2.7 获取任务完成情况统计 +- **请求路径**: `/api/dashboard/reports/task-completion-stats` +- **请求方法**: `GET` + +### 2.8 获取 AQI 热力图数据 +- **请求路径**: `/api/dashboard/map/aqi-heatmap` +- **请求方法**: `GET` + +--- + +## 3. 反馈模块 (Feedback) +**基础路径**: `/api/feedback` + +### 3.1 提交反馈 (JSON) +- **功能描述**: 用于测试,使用 JSON 提交反馈。 +- **请求路径**: `/api/feedback/submit-json` +- **请求方法**: `POST` +- **所需权限**: 已认证用户 +- **请求体**: `FeedbackSubmissionRequest` + +### 3.2 提交反馈 (Multipart) +- **功能描述**: 提交反馈,可包含文件。 +- **请求路径**: `/api/feedback/submit` +- **请求方法**: `POST` +- **所需权限**: 已认证用户 +- **请求体**: `multipart/form-data` + - `feedback` (part): `FeedbackSubmissionRequest` (JSON) + - `files` (part, optional): `MultipartFile[]` + +### 3.3 获取所有反馈 +- **功能描述**: 获取所有反馈(分页)。 +- **请求路径**: `/api/feedback` +- **请求方法**: `GET` +- **所需权限**: `ADMIN` +- **查询参数**: `page`, `size`, `sort` + +--- + +## 4. 文件模块 (File) +**基础路径**: `/api/files` +**所需权限**: 公开访问 + +### 4.1 下载文件 +- **请求路径**: `/api/files/download/{fileName}` +- **请求方法**: `GET` +- **路径参数**: `fileName` (string, 必需) + +### 4.2 查看文件 +- **请求路径**: `/api/files/view/{fileName}` +- **请求方法**: `GET` +- **路径参数**: `fileName` (string, 必需) + +--- + +## 5. 网格模块 (Grid) +**基础路径**: `/api/grids` +**所需权限**: `ADMIN` 或 `DECISION_MAKER` + +### 5.1 获取网格列表 +- **请求路径**: `/api/grids` +- **请求方法**: `GET` +- **查询参数**: + - `cityName` (string, 可选) + - `districtName` (string, 可选) + - `page`, `size`, `sort` + +### 5.2 更新网格信息 +- **请求路径**: `/api/grids/{gridId}` +- **请求方法**: `PATCH` +- **所需权限**: `ADMIN` +- **路径参数**: `gridId` (long, 必需) +- **请求体**: `GridUpdateRequest` + +--- + +## 6. 网格员任务模块 (Worker) +**基础路径**: `/api/worker` +**所需权限**: `GRID_WORKER` + +### 6.1 获取我的任务 +- **请求路径**: `/api/worker` +- **请求方法**: `GET` +- **查询参数**: + - `status` (enum: `TaskStatus`, 可选) + - `page`, `size`, `sort` + +### 6.2 获取任务详情 +- **请求路径**: `/api/worker/{taskId}` +- **请求方法**: `GET` +- **路径参数**: `taskId` (long, 必需) + +### 6.3 接受任务 +- **请求路径**: `/api/worker/{taskId}/accept` +- **请求方法**: `POST` +- **路径参数**: `taskId` (long, 必需) + +### 6.4 提交任务 +- **请求路径**: `/api/worker/{taskId}/submit` +- **请求方法**: `POST` +- **路径参数**: `taskId` (long, 必需) +- **请求体**: `TaskSubmissionRequest` + +--- + +## 7. 地图模块 (Map) +**基础路径**: `/api/map` +**所需权限**: 公开访问 + +### 7.1 获取完整地图网格 +- **请求路径**: `/api/map/grid` +- **请求方法**: `GET` + +### 7.2 创建或更新地图单元格 +- **请求路径**: `/api/map/grid` +- **请求方法**: `POST` +- **请求体**: `MapGrid` + +### 7.3 初始化地图 +- **请求路径**: `/api/map/initialize` +- **请求方法**: `POST` +- **查询参数**: + - `width` (int, 可选, 默认 20) + - `height` (int, 可选, 默认 20) + +--- + +## 8. 路径规划模块 (Pathfinding) +**基础路径**: `/api/pathfinding` +**所需权限**: 公开访问 + +### 8.1 寻找路径 +- **功能描述**: 使用 A* 算法在两点之间寻找最短路径。 +- **请求路径**: `/api/pathfinding/find` +- **请求方法**: `POST` +- **请求体**: `PathfindingRequest` +- **示例请求**: + ```json + { + "startX": 0, + "startY": 0, + "endX": 19, + "endY": 19 + } + ``` + +--- + +## 9. 人员管理模块 (Personnel) +**基础路径**: `/api/personnel` +**所需权限**: `ADMIN` + +### 9.1 创建用户 +- **请求路径**: `/api/personnel/users` +- **请求方法**: `POST` +- **请求体**: `UserCreationRequest` + +### 9.2 获取用户列表 +- **请求路径**: `/api/personnel/users` +- **请求方法**: `GET` +- **查询参数**: + - `role` (enum: `Role`, 可选) + - `name` (string, 可选) + - `page`, `size`, `sort` + +### 9.3 获取单个用户 +- **请求路径**: `/api/personnel/users/{userId}` +- **请求方法**: `GET` +- **路径参数**: `userId` (long, 必需) + +### 9.4 更新用户信息 +- **请求路径**: `/api/personnel/users/{userId}` +- **请求方法**: `PATCH` +- **路径参数**: `userId` (long, 必需) +- **请求体**: `UserUpdateRequest` + +### 9.5 更新用户角色 +- **请求路径**: `/api/personnel/users/{userId}/role` +- **请求方法**: `PUT` +- **路径参数**: `userId` (long, 必需) +- **请求体**: `UserRoleUpdateRequest` + +### 9.6 删除用户 +- **请求路径**: `/api/personnel/users/{userId}` +- **请求方法**: `DELETE` +- **路径参数**: `userId` (long, 必需) + +--- + +## 10. 个人资料模块 (Me) +**基础路径**: `/api/me` +**所需权限**: 已认证用户 + +### 10.1 更新个人资料 +- **请求路径**: `/api/me` +- **请求方法**: `PATCH` +- **请求体**: `UserUpdateRequest` + +### 10.2 更新我的位置 +- **请求路径**: `/api/me/location` +- **请求方法**: `POST` +- **请求体**: `LocationUpdateRequest` + +### 10.3 获取我的反馈历史 +- **请求路径**: `/api/me/feedback` +- **请求方法**: `GET` +- **查询参数**: `page`, `size`, `sort` + +--- + +## 11. 公共接口模块 (Public) +**基础路径**: `/api/public` +**所需权限**: 公开访问 + +### 11.1 公众提交反馈 +- **请求路径**: `/api/public/feedback` +- **请求方法**: `POST` +- **请求体**: `multipart/form-data` + - `feedback` (part): `PublicFeedbackRequest` (JSON) + - `files` (part, optional): `MultipartFile[]` + +--- + +## 12. 主管模块 (Supervisor) +**基础路径**: `/api/supervisor` +**所需权限**: `SUPERVISOR` 或 `ADMIN` + +### 12.1 获取待审核的反馈列表 +- **请求路径**: `/api/supervisor/reviews` +- **请求方法**: `GET` + +### 12.2 批准反馈 +- **请求路径**: `/api/supervisor/reviews/{feedbackId}/approve` +- **请求方法**: `POST` +- **路径参数**: `feedbackId` (long, 必需) + +### 12.3 拒绝反馈 +- **请求路径**: `/api/supervisor/reviews/{feedbackId}/reject` +- **请求方法**: `POST` +- **路径参数**: `feedbackId` (long, 必需) + +--- + +## 13. 任务分配模块 (Tasks) +**基础路径**: `/api/tasks` +**所需权限**: `ADMIN` 或 `SUPERVISOR` + +### 13.1 获取未分配的反馈 +- **请求路径**: `/api/tasks/unassigned` +- **请求方法**: `GET` + +### 13.2 获取可用的网格员 +- **请求路径**: `/api/tasks/grid-workers` +- **请求方法**: `GET` + +### 13.3 分配任务 +- **请求路径**: `/api/tasks/assign` +- **请求方法**: `POST` +- **请求体**: + ```json + { + "feedbackId": 1, + "assigneeId": 2 + } + ``` + +--- + +## 14. 任务管理模块 (Management) +**基础路径**: `/api/management/tasks` +**所需权限**: `SUPERVISOR` + +### 14.1 获取任务列表 +- **请求路径**: `/api/management/tasks` +- **请求方法**: `GET` +- **查询参数**: + - `status` (enum: `TaskStatus`, 可选) + - `assigneeId` (long, 可选) + - `severity` (enum: `SeverityLevel`, 可选) + - `pollutionType` (enum: `PollutionType`, 可选) + - `startDate` (date, 可选, 格式: YYYY-MM-DD) + - `endDate` (date, 可选, 格式: YYYY-MM-DD) + - `page`, `size`, `sort` + +### 14.2 分配任务 +- **请求路径**: `/api/management/tasks/{taskId}/assign` +- **请求方法**: `POST` +- **路径参数**: `taskId` (long, 必需) +- **请求体**: `TaskAssignmentRequest` + +### 14.3 获取任务详情 +- **请求路径**: `/api/management/tasks/{taskId}` +- **请求方法**: `GET` +- **路径参数**: `taskId` (long, 必需) + +### 14.4 审核任务 +- **请求路径**: `/api/management/tasks/{taskId}/review` +- **请求方法**: `POST` +- **路径参数**: `taskId` (long, 必需) +- **请求体**: `TaskApprovalRequest` + +### 14.5 取消任务 +- **请求路径**: `/api/management/tasks/{taskId}/cancel` +- **请求方法**: `POST` +- **路径参数**: `taskId` (long, 必需) + +### 14.6 直接创建任务 +- **请求路径**: `/api/management/tasks` +- **请求方法**: `POST` +- **请求体**: `TaskCreationRequest` + +### 14.7 获取待处理的反馈 +- **请求路径**: `/api/management/tasks/feedback` +- **请求方法**: `GET` +- **所需权限**: `SUPERVISOR` 或 `ADMIN` + +### 14.8 从反馈创建任务 +- **请求路径**: `/api/management/tasks/feedback/{feedbackId}/create-task` +- **请求方法**: `POST` +- **路径参数**: `feedbackId` (long, 必需) +- **请求体**: `TaskFromFeedbackRequest` --- - + ## 前端文档 - + ### ems-frontend/DESIGN.md - + # 环境监督系统 - 前端详细设计说明书 (V1.0) 本文档旨在为环境监督系统前端应用的开发提供一个全面、具体、可执行的详细设计方案。 @@ -1362,14 +1362,14 @@ graph TD ### 4.6 系统管理 - **路径**: `/system` - **设计**: 使用二级路由和嵌套布局。顶层为 `el-tabs`,切换用户管理、角色管理等。 -- **角色管理**: 核心是权限分配,使用 `el-tree`,`show-checkbox`,`node-key="id"`,通过 `getCheckedKeys` 和 `setCheckedKeys` 方法来获取和设置角色的权限。 +- **角色管理**: 核心是权限分配,使用 `el-tree`,`show-checkbox`,`node-key="id"`,通过 `getCheckedKeys` 和 `setCheckedKeys` 方法来获取和设置角色的权限。 --- - + ### ems-frontend/README.md - + # ems-monitoring-system This template should help get you started developing with Vue 3 in Vite. @@ -1403,23419 +1403,23419 @@ npm run dev ```sh npm run build ``` - + --- - + ## 设计文档 - + ### Design/答辩稿.md - -# EMS环境监测系统技术答辩稿 - -## 开场白 - -各位评委老师好!今天我将为大家展示一个基于Spring Boot微服务架构的智能环境监测系统(EMS)。这个系统不仅仅是一个简单的CRUD应用,而是一个融合了现代企业级开发最佳实践的复杂分布式系统。让我们一起探索这个系统背后的技术奥秘。 - ---- - -## 第一部分:架构设计的哲学思考 - -### 为什么选择分层架构?解耦的艺术 - -**设问**:在面对复杂的业务需求时,我们如何确保代码的可维护性和可扩展性? - -答案就在于**分层架构**的设计哲学。我们的系统采用了经典的四层架构模式: - -``` -Controller Layer (控制层) → Service Layer (业务层) → Repository Layer (数据访问层) → Model Layer (数据模型层) -``` - -这种设计遵循了**单一职责原则**和**依赖倒置原则**,实现了真正的**高内聚、低耦合**。每一层都有明确的职责边界,层与层之间通过接口进行通信,这样的设计使得我们可以独立地修改某一层的实现,而不会影响到其他层。 - -**技术亮点**:我们使用了Spring的**依赖注入(DI)**容器来管理对象的生命周期,通过`@Autowired`注解实现了**控制反转(IoC)**,这是企业级应用开发的核心设计模式。 - ---- - -## 第二部分:Controller层的RESTful设计艺术 - -### 为什么我们需要14个专业化的Controller? - -**设问**:传统的单体应用往往将所有功能堆砌在一个Controller中,这样做有什么问题? - -我们的系统包含了14个专业化的Controller,每个Controller都专注于特定的业务领域: - -#### AuthController - 认证控制器的安全哲学 - -```java -@RestController -@RequestMapping("/api/auth") -@Validated -public class AuthController { - - @PostMapping("/login") - public ResponseEntity> authenticateUser( - @Valid @RequestBody LoginRequest loginRequest) { - // JWT令牌生成逻辑 - } -} -``` - -**技术深度解析**: -- **@RestController**:这是Spring Boot的组合注解,等价于`@Controller + @ResponseBody`,自动将返回值序列化为JSON -- **@Validated**:启用方法级别的参数验证,配合`@Valid`实现数据校验的**AOP切面编程** -- **ResponseEntity**:提供了对HTTP响应的完全控制,包括状态码、头部信息和响应体 - -**设计思考**:为什么不直接返回对象,而要包装在`ApiResponse`中?这体现了**统一响应格式**的设计模式,确保前端能够以一致的方式处理所有API响应。 - -#### DashboardController - 数据可视化的技术挑战 - -**设问**:如何在不影响系统性能的前提下,为决策者提供实时的数据洞察? - -```java -@GetMapping("/stats") -@PreAuthorize("hasRole('DECISION_MAKER')") -public ResponseEntity> getDashboardStats() { - // 复杂的数据聚合逻辑 -} -``` - -**技术亮点**: -- **@PreAuthorize**:这是Spring Security的方法级安全注解,实现了**基于角色的访问控制(RBAC)** -- **数据聚合优化**:我们在Repository层使用了复杂的JPQL查询和原生SQL,将计算下推到数据库层,避免了在应用层进行大量数据处理 - ---- - -## 第三部分:Service层的业务编排艺术 - -### 为什么业务逻辑需要如此复杂的编排? - -**设问**:简单的CRUD操作为什么需要这么多Service类?这不是过度设计吗? - -让我们看看`TaskManagementServiceImpl`的复杂性: - -```java -@Service -@Transactional -public class TaskManagementServiceImpl implements TaskManagementService { - - @Autowired - private TaskRepository taskRepository; - - @Autowired - private FeedbackRepository feedbackRepository; - - @Autowired - private ApplicationEventPublisher eventPublisher; - - @Override - public TaskDTO createTaskFromFeedback(Long feedbackId, TaskFromFeedbackDTO dto) { - // 1. 获取并验证反馈 - Feedback feedback = feedbackRepository.findById(feedbackId) - .orElseThrow(() -> new ResourceNotFoundException("Feedback not found")); - - // 2. 业务规则验证 - if (feedback.getStatus() == FeedbackStatus.PROCESSED) { - throw new BusinessException("Feedback already processed"); - } - - // 3. 创建任务实体 - Task task = Task.builder() - .title(dto.getTitle()) - .description(feedback.getDescription()) - .location(feedback.getLocation()) - .feedback(feedback) - .status(TaskStatus.PENDING) - .build(); - - // 4. 事务性操作 - Task savedTask = taskRepository.save(task); - feedback.setStatus(FeedbackStatus.PROCESSED); - feedbackRepository.save(feedback); - - // 5. 发布领域事件 - eventPublisher.publishEvent(new TaskCreatedEvent(savedTask)); - - return modelMapper.map(savedTask, TaskDTO.class); - } -} -``` - -**技术深度解析**: - -1. **@Transactional**:这是Spring的声明式事务管理,确保方法内的所有数据库操作要么全部成功,要么全部回滚,保证了数据的**ACID特性** - -2. **Builder模式**:使用Lombok的`@Builder`注解生成建造者模式代码,提高了对象创建的可读性和安全性 - -3. **事件驱动架构**:通过`ApplicationEventPublisher`发布领域事件,实现了模块间的**松耦合**通信 - -4. **ModelMapper**:使用对象映射框架避免手动的属性拷贝,减少了样板代码 - -**设计哲学**:这种复杂性是必要的,因为它体现了**领域驱动设计(DDD)**的思想,每个Service都是一个业务能力的封装,确保了业务逻辑的完整性和一致性。 - -### AuthServiceImpl - 安全认证的技术堡垒 - -**设问**:在当今网络安全威胁日益严重的环境下,我们如何构建一个既安全又用户友好的认证系统? - -```java -@Service -public class AuthServiceImpl implements AuthService { - - @Autowired - private AuthenticationManager authenticationManager; - - @Autowired - private JwtTokenProvider tokenProvider; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Override - public JwtAuthenticationResponse authenticateUser(LoginRequest loginRequest) { - // 1. Spring Security认证 - Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - loginRequest.getUsername(), - loginRequest.getPassword() - ) - ); - - // 2. 设置安全上下文 - SecurityContextHolder.getContext().setAuthentication(authentication); - - // 3. 生成JWT令牌 - String jwt = tokenProvider.generateToken(authentication); - - return new JwtAuthenticationResponse(jwt); - } -} -``` - -**技术亮点**: - -1. **AuthenticationManager**:Spring Security的核心认证管理器,支持多种认证提供者的**策略模式** - -2. **JWT(JSON Web Token)**:无状态的令牌认证机制,支持分布式系统的**水平扩展** - -3. **BCrypt密码编码**:使用自适应哈希算法,具有**盐值**和**工作因子**,抵御彩虹表攻击 - ---- - -## 第四部分:Repository层的数据访问艺术 - -### 为什么我们需要如此复杂的数据访问层? - -**设问**:直接使用SQL不是更简单吗?为什么要引入这么多抽象层? - -让我们看看`FeedbackRepository`的设计: - -```java -@Repository -public interface FeedbackRepository extends JpaRepository, - JpaSpecificationExecutor { - - // 方法名查询 - Spring Data JPA的魔法 - List findByStatus(FeedbackStatus status); - Page findBySubmitterId(Long submitterId, Pageable pageable); - - // 自定义JPQL查询 - 面向对象的查询语言 - @Query("SELECT new com.dne.ems.dto.HeatmapPointDTO(f.gridX, f.gridY, COUNT(f.id)) " + - "FROM Feedback f WHERE f.status = 'CONFIRMED' " + - "AND f.gridX IS NOT NULL AND f.gridY IS NOT NULL " + - "GROUP BY f.gridX, f.gridY") - List getHeatmapData(); - - // 性能优化 - EntityGraph解决N+1问题 - @Override - @EntityGraph(attributePaths = {"user", "attachments"}) - Page findAll(Specification spec, Pageable pageable); -} -``` - -**技术深度解析**: - -1. **Spring Data JPA**:这是Spring生态系统中的数据访问抽象层,它通过**代理模式**自动生成Repository实现类 - -2. **方法名查询**:基于方法名的**约定优于配置**原则,Spring会自动解析方法名并生成相应的查询 - -3. **JPQL(Java Persistence Query Language)**:面向对象的查询语言,具有数据库无关性 - -4. **EntityGraph**:JPA 2.1引入的性能优化特性,通过**预加载**策略解决N+1查询问题 - -5. **Specification模式**:实现了**动态查询**的构建,支持复杂的条件组合 - -### AqiDataRepository - 大数据处理的技术挑战 - -**设问**:面对海量的环境监测数据,我们如何确保查询性能? - -```java -@Repository -public interface AqiDataRepository extends JpaRepository { - - // 复杂的统计查询 - @Query("SELECT new com.dne.ems.dto.AqiDistributionDTO(" + - "CASE WHEN a.aqiValue <= 50 THEN 'Good' " + - "WHEN a.aqiValue <= 100 THEN 'Moderate' " + - "WHEN a.aqiValue <= 150 THEN 'Unhealthy for Sensitive Groups' " + - "WHEN a.aqiValue <= 200 THEN 'Unhealthy' " + - "WHEN a.aqiValue <= 300 THEN 'Very Unhealthy' " + - "ELSE 'Hazardous' END, COUNT(a.id)) " + - "FROM AqiData a GROUP BY 1") - List getAqiDistribution(); - - // 原生SQL查询 - 性能优化 - @Query(value = "SELECT DATE_FORMAT(record_time, '%Y-%m') as yearMonth, " + - "COUNT(id) as count FROM aqi_data " + - "WHERE aqi_value > 100 AND record_time >= :startDate " + - "GROUP BY DATE_FORMAT(record_time, '%Y-%m') ORDER BY 1", - nativeQuery = true) - List getMonthlyExceedanceTrendRaw(@Param("startDate") LocalDateTime startDate); - - // 默认方法 - 数据转换 - default List getMonthlyExceedanceTrend(LocalDateTime startDate) { - List rawResults = getMonthlyExceedanceTrendRaw(startDate); - return rawResults.stream() - .map(row -> new TrendDataPointDTO((String) row[0], ((Number) row[1]).longValue())) - .toList(); - } -} -``` - -**技术亮点**: - -1. **原生SQL查询**:在需要极致性能时,我们使用原生SQL直接操作数据库 - -2. **默认方法**:Java 8的接口默认方法特性,允许我们在接口中提供数据转换逻辑 - -3. **Stream API**:函数式编程范式,提供了优雅的数据处理方式 - -4. **DTO投影**:直接将查询结果映射到DTO对象,避免了不必要的数据传输 - ---- - -## 第五部分:Security模块的安全防护体系 - -### 为什么安全性需要如此复杂的设计? - -**设问**:在微服务架构中,我们如何构建一个既安全又高效的认证授权体系? - -我们的安全模块包含四个核心组件: - -#### JwtAuthenticationFilter - 无状态认证的守护者 - -```java -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - @Autowired - private JwtService jwtService; - - @Autowired - private UserDetailsService userDetailsService; - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - // 1. 提取JWT令牌 - String authHeader = request.getHeader("Authorization"); - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - String jwt = authHeader.substring(7); - String username = jwtService.extractUsername(jwt); - - // 2. 验证令牌并设置安全上下文 - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = userDetailsService.loadUserByUsername(username); - - if (jwtService.isTokenValid(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); - - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); - } - } - - filterChain.doFilter(request, response); - } -} -``` - -**技术深度解析**: - -1. **OncePerRequestFilter**:确保过滤器在每个请求中只执行一次,避免重复处理 - -2. **责任链模式**:FilterChain体现了责任链设计模式,每个过滤器处理特定的关注点 - -3. **ThreadLocal安全上下文**:SecurityContextHolder使用ThreadLocal存储认证信息,确保线程安全 - -4. **无状态设计**:JWT令牌包含了所有必要的用户信息,支持水平扩展 - -#### SecurityConfig - 安全策略的总指挥 - -```java -@Configuration -@EnableWebSecurity -@EnableMethodSecurity(prePostEnabled = true) -public class SecurityConfig { - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) // 禁用CSRF(无状态应用) - .sessionManagement(session -> - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态会话 - .authorizeHttpRequests(authz -> authz - .requestMatchers("/api/auth/**", "/api/public/**").permitAll() - .requestMatchers("/api/dashboard/**").hasRole("DECISION_MAKER") - .requestMatchers("/api/supervisor/**").hasAnyRole("SUPERVISOR", "ADMIN") - .requestMatchers("/api/worker/**").hasRole("GRID_WORKER") - .anyRequest().authenticated() - ) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(12); // 工作因子12,平衡安全性和性能 - } -} -``` - -**技术亮点**: - -1. **@EnableMethodSecurity**:启用方法级安全控制,支持`@PreAuthorize`等注解 - -2. **流式API配置**:Spring Security 5.7+的新配置方式,更加直观和类型安全 - -3. **细粒度权限控制**:基于URL模式和角色的访问控制 - -4. **BCrypt算法**:自适应哈希算法,工作因子可调,抵御暴力破解 - ---- - -## 第六部分:Model层的领域建模艺术 - -### 为什么需要如此复杂的实体关系? - -**设问**:简单的数据表不就够了吗?为什么要设计这么复杂的实体关系? - -让我们看看`UserAccount`实体的设计: - -```java -@Entity -@Table(name = "user_accounts") -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class UserAccount { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(unique = true, nullable = false) - @Email - private String email; - - @Column(nullable = false) - @Size(min = 8) - private String password; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private Role role; - - @Enumerated(EnumType.STRING) - @Builder.Default - private UserStatus status = UserStatus.ACTIVE; - - // 网格坐标(用于网格员) - private Integer gridX; - private Integer gridY; - - // 审计字段 - @CreationTimestamp - @Column(updatable = false) - private LocalDateTime createdAt; - - @UpdateTimestamp - private LocalDateTime updatedAt; - - // 关系映射 - @OneToMany(mappedBy = "submitter", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private List submittedFeedbacks = new ArrayList<>(); - - @OneToMany(mappedBy = "assignee", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private List assignedTasks = new ArrayList<>(); -} -``` - -**技术深度解析**: - -1. **JPA注解体系**: - - `@Entity`:标识为JPA实体 - - `@Table`:指定数据库表名 - - `@Id`和`@GeneratedValue`:主键策略 - - `@Column`:列约束和属性 - -2. **Lombok注解魔法**: - - `@Data`:自动生成getter/setter/toString/equals/hashCode - - `@Builder`:建造者模式,提高对象创建的可读性 - - `@NoArgsConstructor`和`@AllArgsConstructor`:构造器生成 - -3. **枚举映射**:`@Enumerated(EnumType.STRING)`确保数据库存储的是枚举的字符串值,提高可读性 - -4. **审计功能**:`@CreationTimestamp`和`@UpdateTimestamp`自动管理时间戳 - -5. **关系映射**:`@OneToMany`建立了实体间的关联关系,支持对象导航 - -### Feedback实体 - 复杂业务的数据载体 - -```java -@Entity -@Table(name = "feedbacks") -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Feedback { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(unique = true, nullable = false) - private String eventId; // 业务唯一标识 - - @Column(columnDefinition = "TEXT") - private String description; - - @Enumerated(EnumType.STRING) - @Builder.Default - private FeedbackStatus status = FeedbackStatus.PENDING; - - @Enumerated(EnumType.STRING) - private PollutionType pollutionType; - - // 地理信息 - private String location; - private Integer gridX; - private Integer gridY; - - // 关联关系 - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "submitter_id") - private UserAccount submitter; - - @OneToOne(mappedBy = "feedback", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private Task relatedTask; - - @OneToMany(mappedBy = "feedback", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private List attachments = new ArrayList<>(); - - // 自定义转换器 - @Convert(converter = StringListConverter.class) - private List tags = new ArrayList<>(); -} -``` - -**设计亮点**: - -1. **业务标识符**:`eventId`作为业务唯一标识,与技术主键`id`分离 - -2. **状态机模式**:`FeedbackStatus`枚举定义了反馈的生命周期状态 - -3. **地理信息建模**:支持文本地址和网格坐标两种定位方式 - -4. **自定义转换器**:`StringListConverter`将List转换为数据库的JSON字段 - ---- - -## 第七部分:高级特性与算法实现 - -### A*寻路算法 - 智能路径规划的核心 - -**设问**:在复杂的城市环境中,我们如何为网格员规划最优的任务执行路径? - -```java -@Service -public class AStarService { - - public List findPath(Point start, Point goal, int[][] grid) { - PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingDouble(Node::getF)); - Set closedSet = new HashSet<>(); - Map allNodes = new HashMap<>(); - - Node startNode = new Node(start, 0, heuristic(start, goal), null); - openSet.add(startNode); - allNodes.put(start, startNode); - - while (!openSet.isEmpty()) { - Node current = openSet.poll(); - - if (current.getPoint().equals(goal)) { - return reconstructPath(current); - } - - closedSet.add(current.getPoint()); - - for (Point neighbor : getNeighbors(current.getPoint(), grid)) { - if (closedSet.contains(neighbor) || isObstacle(neighbor, grid)) { - continue; - } - - double tentativeG = current.getG() + distance(current.getPoint(), neighbor); - - Node neighborNode = allNodes.get(neighbor); - if (neighborNode == null) { - neighborNode = new Node(neighbor, tentativeG, heuristic(neighbor, goal), current); - allNodes.put(neighbor, neighborNode); - openSet.add(neighborNode); - } else if (tentativeG < neighborNode.getG()) { - neighborNode.setG(tentativeG); - neighborNode.setF(tentativeG + neighborNode.getH()); - neighborNode.setParent(current); - } - } - } - - return Collections.emptyList(); // 无路径 - } - - private double heuristic(Point a, Point b) { - // 曼哈顿距离启发式函数 - return Math.abs(a.getX() - b.getX()) + Math.abs(a.getY() - b.getY()); - } -} -``` - -**算法亮点**: - -1. **优先队列**:使用`PriorityQueue`实现开放集合,确保总是选择f值最小的节点 - -2. **启发式函数**:曼哈顿距离作为启发式函数,保证算法的最优性 - -3. **路径重构**:通过父节点指针重构最优路径 - -4. **空间复杂度优化**:使用HashMap存储所有节点,避免重复创建 - -### 智能任务推荐算法 - -**设问**:面对众多的网格员,我们如何智能地推荐最合适的任务执行者? - -```java -@Service -public class TaskRecommendationService { - - public List recommendWorkersForTask(Long taskId) { - Task task = taskRepository.findById(taskId) - .orElseThrow(() -> new ResourceNotFoundException("Task not found")); - - List availableWorkers = userRepository - .findByRoleAndStatus(Role.GRID_WORKER, UserStatus.ACTIVE); - - return availableWorkers.stream() - .map(worker -> calculateWorkerScore(worker, task)) - .sorted(Comparator.comparingDouble(WorkerRecommendationDTO::getScore).reversed()) - .collect(Collectors.toList()); - } - - private WorkerRecommendationDTO calculateWorkerScore(UserAccount worker, Task task) { - double distanceScore = calculateDistanceScore(worker, task); // 40%权重 - double workloadScore = calculateWorkloadScore(worker); // 30%权重 - double skillScore = calculateSkillScore(worker, task); // 20%权重 - double performanceScore = calculatePerformanceScore(worker); // 10%权重 - - double totalScore = distanceScore * 0.4 + workloadScore * 0.3 + - skillScore * 0.2 + performanceScore * 0.1; - - return WorkerRecommendationDTO.builder() - .workerId(worker.getId()) - .workerName(worker.getName()) - .score(totalScore) - .distanceScore(distanceScore) - .workloadScore(workloadScore) - .skillScore(skillScore) - .performanceScore(performanceScore) - .build(); - } - - private double calculateDistanceScore(UserAccount worker, Task task) { - if (worker.getGridX() == null || worker.getGridY() == null) { - return 0.0; - } - - double distance = Math.sqrt( - Math.pow(worker.getGridX() - task.getGridX(), 2) + - Math.pow(worker.getGridY() - task.getGridY(), 2) - ); - - double maxDistance = 50.0; // 最大考虑距离 - return Math.max(0, (maxDistance - distance) / maxDistance); - } -} -``` - -**算法特点**: - -1. **多维度评分**:综合考虑距离、工作负载、技能匹配和历史表现 - -2. **加权评分模型**:不同维度采用不同权重,可根据业务需求调整 - -3. **流式处理**:使用Stream API进行函数式编程,代码简洁高效 - -4. **可扩展性**:评分算法可以轻松添加新的评分维度 - ---- - -## 第八部分:事件驱动架构的异步魅力 - -### 为什么选择事件驱动架构? - -**设问**:传统的同步调用方式有什么问题?为什么要引入事件驱动的复杂性? - -让我们看看事件驱动架构的实现: - -```java -// 事件定义 -@Getter -@AllArgsConstructor -public class TaskCreatedEvent extends ApplicationEvent { - private final Task task; - - public TaskCreatedEvent(Object source, Task task) { - super(source); - this.task = task; - } -} - -// 事件发布 -@Service -public class TaskManagementServiceImpl { - - @Autowired - private ApplicationEventPublisher eventPublisher; - - public TaskDTO createTask(TaskCreateDTO dto) { - Task task = // ... 创建任务逻辑 - Task savedTask = taskRepository.save(task); - - // 发布事件 - 异步处理 - eventPublisher.publishEvent(new TaskCreatedEvent(this, savedTask)); - - return modelMapper.map(savedTask, TaskDTO.class); - } -} - -// 事件监听 -@Component -public class TaskEventListener { - - @Autowired - private NotificationService notificationService; - - @Autowired - private AuditService auditService; - - @EventListener - @Async - public void handleTaskCreated(TaskCreatedEvent event) { - Task task = event.getTask(); - - // 异步发送通知 - notificationService.sendTaskAssignmentNotification(task); - - // 异步记录审计日志 - auditService.logTaskCreation(task); - - // 异步更新统计数据 - statisticsService.updateTaskStatistics(task); - } -} -``` - -**技术优势**: - -1. **解耦合**:事件发布者和监听者之间没有直接依赖关系 - -2. **异步处理**:`@Async`注解实现异步执行,提高系统响应性 - -3. **可扩展性**:可以轻松添加新的事件监听器,不影响现有代码 - -4. **容错性**:某个监听器失败不会影响其他监听器的执行 - ---- - -## 第九部分:性能优化的技术手段 - -### 数据库查询优化策略 - -**设问**:面对大量的数据查询需求,我们如何确保系统的高性能? - -#### 1. EntityGraph解决N+1查询问题 - -```java -@Repository -public interface FeedbackRepository extends JpaRepository { - - @EntityGraph(attributePaths = {"submitter", "attachments", "relatedTask"}) - @Query("SELECT f FROM Feedback f WHERE f.status = :status") - List findByStatusWithDetails(@Param("status") FeedbackStatus status); -} -``` - -**技术原理**:EntityGraph告诉JPA在执行查询时一次性加载相关联的实体,避免了懒加载导致的N+1查询问题。 - -#### 2. 分页查询优化 - -```java -@Service -public class FeedbackService { - - public Page getFeedbacks(FeedbackSearchCriteria criteria, Pageable pageable) { - Specification spec = FeedbackSpecification.buildSpecification(criteria); - Page feedbacks = feedbackRepository.findAll(spec, pageable); - - return feedbacks.map(feedback -> modelMapper.map(feedback, FeedbackDTO.class)); - } -} -``` - -**优化策略**: -- 使用Specification动态构建查询条件 -- 利用数据库索引优化排序和过滤 -- 在DTO转换时避免加载不必要的关联数据 - -#### 3. 缓存策略 - -```java -@Service -public class UserService { - - @Cacheable(value = "users", key = "#email") - public UserAccount findByEmail(String email) { - return userRepository.findByEmail(email) - .orElseThrow(() -> new UserNotFoundException("User not found")); - } - - @CacheEvict(value = "users", key = "#user.email") - public UserAccount updateUser(UserAccount user) { - return userRepository.save(user); - } -} -``` - -**缓存优势**: -- 减少数据库访问次数 -- 提高频繁查询的响应速度 -- 支持分布式缓存扩展 - ---- - -## 第十部分:系统的技术创新点 - -### 1. 智能AI预审核系统 - -**设问**:如何将人工智能技术融入到传统的政务系统中? - -```java -@Service -public class AiReviewService { - - @Autowired - private RestTemplate restTemplate; - - public AiReviewResult reviewFeedback(Feedback feedback) { - // 构建AI分析请求 - AiAnalysisRequest request = AiAnalysisRequest.builder() - .text(feedback.getDescription()) - .imageUrls(feedback.getAttachments().stream() - .map(Attachment::getFileUrl) - .collect(Collectors.toList())) - .location(feedback.getLocation()) - .build(); - - // 调用AI服务 - ResponseEntity response = restTemplate.postForEntity( - aiServiceUrl + "/analyze", request, AiAnalysisResponse.class); - - AiAnalysisResponse analysisResult = response.getBody(); - - // 构建审核结果 - return AiReviewResult.builder() - .isValid(analysisResult.getConfidence() > 0.8) - .confidence(analysisResult.getConfidence()) - .category(analysisResult.getCategory()) - .urgencyLevel(analysisResult.getUrgencyLevel()) - .suggestedActions(analysisResult.getSuggestedActions()) - .build(); - } -} -``` - -**创新点**: -- **多模态分析**:同时分析文本和图像内容 -- **智能分类**:自动识别问题类型和紧急程度 -- **置信度评估**:提供AI判断的可信度指标 - -### 2. 实时数据大屏技术 - -**设问**:如何为决策者提供实时、直观的数据洞察? - -```java -@RestController -@RequestMapping("/api/dashboard") -public class DashboardController { - - @GetMapping("/realtime-stats") - @PreAuthorize("hasRole('DECISION_MAKER')") - public SseEmitter getRealtimeStats() { - SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); - - // 异步推送实时数据 - CompletableFuture.runAsync(() -> { - try { - while (true) { - DashboardStatsDTO stats = dashboardService.getCurrentStats(); - emitter.send(SseEmitter.event() - .name("stats-update") - .data(stats)); - - Thread.sleep(5000); // 每5秒推送一次 - } - } catch (Exception e) { - emitter.completeWithError(e); - } - }); - - return emitter; - } -} -``` - -**技术特点**: -- **Server-Sent Events (SSE)**:实现服务器主动推送数据 -- **异步处理**:使用CompletableFuture避免阻塞主线程 -- **实时性**:数据更新延迟控制在秒级 - -### 3. 地理信息系统集成 - -```java -@Service -public class GeoService { - - public List generateHeatmapData(String dataType, LocalDateTime startTime) { - switch (dataType) { - case "feedback": - return feedbackRepository.getHeatmapData(); - case "aqi": - return aqiDataRepository.getAqiHeatmapData(); - case "task": - return taskRepository.getTaskHeatmapData(startTime); - default: - throw new IllegalArgumentException("Unsupported data type: " + dataType); - } - } - - public GridCoverageDTO calculateGridCoverage() { - long totalGrids = gridRepository.count(); - long coveredGrids = gridRepository.countByIsObstacleFalse(); - - return GridCoverageDTO.builder() - .totalGrids(totalGrids) - .coveredGrids(coveredGrids) - .coverageRate((double) coveredGrids / totalGrids * 100) - .build(); - } -} -``` - -**GIS特性**: -- **热力图生成**:基于地理坐标的数据可视化 -- **网格覆盖分析**:计算监测网络的覆盖率 -- **空间查询**:支持基于地理位置的数据查询 - ---- - ---- - -## 第十一部分:系统的技术特色 - -### 设问:我们的系统有哪些值得关注的技术特点? - -除了基础功能实现,我们的EMS系统还融入了一些实用的技术特色,让系统更加稳定和高效。 - -#### 1. 多任务处理能力 - -**应用场景**:当系统需要同时处理多个用户的请求时,比如多个网格员同时上报数据。 - -**解决方案**:我们使用了异步处理技术,让系统能够同时处理多个任务,提高响应速度。 - -```java -// 异步任务配置 -@Configuration -@EnableAsync -public class AsyncConfig { - - @Bean - public ThreadPoolTaskExecutor taskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - - // 设置合适的线程数量 - executor.setCorePoolSize(4); - executor.setMaxPoolSize(8); - executor.setQueueCapacity(100); - - // 设置线程名称,方便调试 - executor.setThreadNamePrefix("EMS-Task-"); - - executor.initialize(); - return executor; - } -} - -// 异步任务示例 -@Service -public class NotificationService { - - @Async - public void sendNotification(String message) { - // 发送通知的逻辑 - System.out.println("发送通知: " + message); - } -} -``` - -**技术优势**: -- **提高响应速度**:用户操作不会被耗时任务阻塞 -- **合理利用资源**:根据服务器性能配置线程数量 -- **便于问题排查**:通过线程名称快速定位问题 - -#### 2. 任务分配的安全机制 - -**应用场景**:当多个管理员同时分配同一个任务时,需要确保任务不会被重复分配。 - -**解决方案**:使用Redis实现任务锁定机制,确保同一时间只有一个操作能够分配特定任务。 - -```java -@Component -public class TaskLockService { - - @Autowired - private RedisTemplate redisTemplate; - - /** - * 尝试锁定任务 - */ - public boolean lockTask(String taskId) { - String lockKey = "task:lock:" + taskId; - String lockValue = "locked"; - - // 设置锁,30秒后自动过期 - Boolean success = redisTemplate.opsForValue() - .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(30)); - - return Boolean.TRUE.equals(success); - } - - /** - * 释放任务锁 - */ - public void unlockTask(String taskId) { - String lockKey = "task:lock:" + taskId; - redisTemplate.delete(lockKey); - } -} - -// 任务分配服务 -@Service -public class TaskAssignmentService { - - @Autowired - private TaskLockService lockService; - - public boolean assignTask(Long taskId, Long workerId) { - String taskIdStr = taskId.toString(); - - // 尝试锁定任务 - if (!lockService.lockTask(taskIdStr)) { - throw new BusinessException("任务正在被处理,请稍后重试"); - } - - try { - // 检查任务状态 - Task task = taskRepository.findById(taskId) - .orElseThrow(() -> new RuntimeException("任务不存在")); - - if (task.getStatus() != TaskStatus.PENDING) { - throw new BusinessException("任务已被分配"); - } - - // 分配任务 - task.setAssigneeId(workerId); - task.setStatus(TaskStatus.ASSIGNED); - task.setAssignedAt(LocalDateTime.now()); - - taskRepository.save(task); - - return true; - - } finally { - // 释放锁 - lockService.unlockTask(taskIdStr); - } - } -} -``` - -**技术优势**: -- **防止重复分配**:确保任务分配的唯一性 -- **自动释放机制**:避免系统卡死 -- **简单可靠**:使用成熟稳定的技术方案 - -#### 3. 智能任务推荐功能 - -**应用场景**:当有新任务需要分配时,系统能够自动推荐最合适的网格员。 - -**解决方案**:根据不同的任务类型,使用不同的推荐规则来选择最合适的人员。 - -```java -// 任务推荐服务 -@Service -public class TaskRecommendationService { - - /** - * 推荐合适的网格员 - */ - public List recommendWorkers(Task task) { - List availableWorkers = getAvailableWorkers(); - - // 根据任务类型选择推荐方式 - if (task.isUrgent()) { - return recommendByDistance(task, availableWorkers); - } else { - return recommendByExperience(task, availableWorkers); - } - } - - /** - * 基于距离推荐(紧急任务) - */ - private List recommendByDistance(Task task, List workers) { - return workers.stream() - .map(worker -> { - double distance = calculateDistance(worker.getLocation(), task.getLocation()); - int score = (int) (100 - distance * 10); // 距离越近分数越高 - - return new WorkerRecommendation( - worker.getId(), - worker.getName(), - score, - "距离任务地点 " + String.format("%.1f", distance) + " 公里" - ); - }) - .sorted((a, b) -> Integer.compare(b.getScore(), a.getScore())) - .limit(5) - .collect(Collectors.toList()); - } - - /** - * 基于经验推荐(普通任务) - */ - private List recommendByExperience(Task task, List workers) { - return workers.stream() - .map(worker -> { - int completedTasks = getCompletedTaskCount(worker.getId(), task.getType()); - int score = Math.min(100, completedTasks * 10 + 50); // 经验越多分数越高 - - return new WorkerRecommendation( - worker.getId(), - worker.getName(), - score, - "已完成类似任务 " + completedTasks + " 次" - ); - }) - .sorted((a, b) -> Integer.compare(b.getScore(), a.getScore())) - .limit(5) - .collect(Collectors.toList()); - } - - /** - * 计算两点间距离(简化版) - */ - private double calculateDistance(String location1, String location2) { - // 这里可以接入地图API计算实际距离 - // 现在返回模拟距离 - return Math.random() * 10; // 0-10公里 - } - - /** - * 获取已完成任务数量 - */ - private int getCompletedTaskCount(Long workerId, String taskType) { - return taskRepository.countCompletedTasksByWorkerAndType(workerId, taskType); - } -} - -// 推荐结果类 -public class WorkerRecommendation { - private Long workerId; - private String workerName; - private int score; - private String reason; - - // 构造函数和getter/setter - public WorkerRecommendation(Long workerId, String workerName, int score, String reason) { - this.workerId = workerId; - this.workerName = workerName; - this.score = score; - this.reason = reason; - } - - // getter方法... -} -``` - -**技术优势**: -- **智能推荐**:根据任务特点自动推荐合适人员 -- **多种策略**:紧急任务看距离,普通任务看经验 -- **简单易懂**:推荐逻辑清晰,便于维护和扩展 - -#### 4. 操作记录和审计功能 - -**应用场景**:政务系统需要记录所有重要操作,便于后续查询和审计。 - -**解决方案**:建立完整的操作日志系统,记录用户的每一个重要操作。 - -```java -// 操作日志实体 -@Entity -@Table(name = "operation_log") -public class OperationLog { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String userId; // 操作用户 - private String userName; // 用户姓名 - private String operation; // 操作类型 - private String description; // 操作描述 - private String targetId; // 操作对象ID - private String targetType; // 操作对象类型 - private String ipAddress; // IP地址 - private LocalDateTime createTime; // 操作时间 - - // 构造函数和getter/setter... -} - -// 操作日志服务 -@Service -public class OperationLogService { - - @Autowired - private OperationLogRepository logRepository; - - /** - * 记录操作日志 - */ - public void recordOperation(String operation, String description, - String targetId, String targetType) { - // 获取当前用户信息 - UserAccount currentUser = getCurrentUser(); - - OperationLog log = new OperationLog(); - log.setUserId(currentUser.getId().toString()); - log.setUserName(currentUser.getName()); - log.setOperation(operation); - log.setDescription(description); - log.setTargetId(targetId); - log.setTargetType(targetType); - log.setIpAddress(getClientIpAddress()); - log.setCreateTime(LocalDateTime.now()); - - logRepository.save(log); - } - - /** - * 查询操作日志 - */ - public List queryLogs(String userId, String operation, - LocalDateTime startTime, LocalDateTime endTime) { - return logRepository.findByConditions(userId, operation, startTime, endTime); - } - - /** - * 获取客户端IP地址 - */ - private String getClientIpAddress() { - // 从请求中获取IP地址 - HttpServletRequest request = getCurrentRequest(); - String ip = request.getHeader("X-Forwarded-For"); - if (ip == null || ip.isEmpty()) { - ip = request.getRemoteAddr(); - } - return ip; - } -} - -// 使用示例:在任务分配时记录日志 -@Service -public class TaskService { - - @Autowired - private OperationLogService logService; - - public void assignTask(Long taskId, Long workerId) { - // 执行任务分配逻辑 - // ... - - // 记录操作日志 - logService.recordOperation( - "TASK_ASSIGN", - "将任务分配给网格员", - taskId.toString(), - "TASK" - ); - } -} -``` - -**技术优势**: -- **完整记录**:记录所有重要操作,便于追溯 -- **详细信息**:包含操作人、时间、IP等关键信息 -- **便于查询**:支持按条件查询历史操作 -- **安全可靠**:确保操作的可追溯性和透明度 ---- - -## 第十二部分:系统总结与展望 - -### 设问:我们的EMS系统实现了什么目标? - -通过以上的技术实现和功能展示,我们的环境监测系统(EMS)成功地解决了环境管理中的核心问题: - -#### 1. 主要成果 - -**功能完整性**: -- ✅ **数据采集与监测**:实时收集环境数据,支持多种数据源 -- ✅ **任务管理**:完整的任务分配、跟踪、完成流程 -- ✅ **用户权限管理**:多角色权限控制,确保系统安全 -- ✅ **数据可视化**:直观的图表展示,便于决策分析 -- ✅ **移动端支持**:网格员可通过手机APP进行现场操作 - -**技术特色**: -- 🚀 **高性能**:异步处理技术提升系统响应速度 -- 🔒 **高安全性**:完善的权限控制和操作审计 -- 🎯 **智能推荐**:自动推荐最合适的处理人员 -- 📱 **用户友好**:简洁直观的界面设计 - -#### 2. 解决的实际问题 - -**环境管理痛点**: -- **数据分散** → **统一平台管理** -- **任务混乱** → **清晰的工作流程** -- **响应缓慢** → **快速任务分配** -- **监管困难** → **全程操作记录** - -**技术实现亮点**: -- **前后端分离**:Vue3 + Spring Boot,技术栈现代化 -- **数据库设计**:合理的表结构,支持复杂查询 -- **接口规范**:RESTful API设计,便于扩展 -- **代码质量**:清晰的分层架构,易于维护 - -#### 3. 未来发展方向 - -**功能扩展**: -- 📊 **数据分析增强**:加入更多统计分析功能 -- 🤖 **智能化升级**:引入AI辅助决策 -- 📱 **移动端完善**:增加更多移动端功能 -- 🌐 **系统集成**:与其他政务系统对接 - -**技术优化**: -- ⚡ **性能提升**:优化数据库查询,提升响应速度 -- 🔐 **安全加强**:增加更多安全防护措施 -- 📈 **监控完善**:加入系统监控和告警功能 -- 🔄 **自动化**:增加更多自动化处理流程 - -### 总结 - -我们的EMS系统不仅实现了环境监测的基本功能,更在技术实现上体现了现代软件开发的最佳实践。通过合理的架构设计、清晰的代码组织和用户友好的界面,为环境管理工作提供了有力的技术支撑。 - -这个项目展示了我们在**全栈开发**、**系统设计**、**数据库管理**等方面的综合能力,是一个真正能够投入实际使用的完整系统。 - -### 数据异常检测功能 - -为了保证环境数据的准确性,系统提供了简单有效的异常检测功能: - -```java -@Service -public class DataValidationService { - - /** - * 检测异常数据 - */ - public List validateAqiData(List dataList) { - List warnings = new ArrayList<>(); - - for (AqiData data : dataList) { - // 检查数值范围 - if (data.getAqiValue() < 0 || data.getAqiValue() > 500) { - warnings.add("AQI数值异常: " + data.getAqiValue()); - } - - // 检查PM2.5范围 - if (data.getPm25() < 0 || data.getPm25() > 1000) { - warnings.add("PM2.5数值异常: " + data.getPm25()); - } - } - - return warnings; - } - - /** - * 数据趋势分析 - */ - public String analyzeTrend(List recentData) { - if (recentData.size() < 2) { - return "数据不足"; - } - - double firstValue = recentData.get(0).getAqiValue(); - double lastValue = recentData.get(recentData.size() - 1).getAqiValue(); - - if (lastValue > firstValue * 1.2) { - return "空气质量呈恶化趋势"; - } else if (lastValue < firstValue * 0.8) { - return "空气质量呈改善趋势"; - } else { - return "空气质量相对稳定"; - } - } -} -``` - -**功能特点**: -- **数据校验**:检查环境数据是否在合理范围内 -- **趋势分析**:分析空气质量变化趋势 -- **预警提醒**:发现异常数据时及时提醒 ---- - -## 项目总结 - -### 🎯 主要成果 - -1. **功能完整性** - - 实现了环境监测数据的全生命周期管理 - - 提供了直观的数据可视化界面 - - 支持多种数据导入导出格式 - -2. **技术特色** - - 采用前后端分离架构,便于维护和扩展 - - 使用Spring Boot框架,开发效率高 - - 集成了地图展示功能,用户体验良好 - -3. **实用价值** - - 解决了环境数据管理的实际需求 - - 提供了便民的在线查询服务 - - 支持数据分析和趋势预测 - -### 🚀 技术亮点 - -- **系统架构**:清晰的分层设计,易于理解和维护 -- **数据处理**:高效的数据存储和查询机制 -- **用户界面**:现代化的Web界面,操作简单直观 -- **安全性**:完善的用户认证和权限管理 - -### 📈 未来展望 - -1. **功能扩展** - - 增加更多环境指标的监测 - - 支持移动端应用 - - 添加数据预警功能 - -2. **技术优化** - - 提升系统性能和响应速度 - - 增强数据分析能力 - - 优化用户体验 -``` - -**谢谢各位老师的指导!** + +# EMS环境监测系统技术答辩稿 + +## 开场白 + +各位评委老师好!今天我将为大家展示一个基于Spring Boot微服务架构的智能环境监测系统(EMS)。这个系统不仅仅是一个简单的CRUD应用,而是一个融合了现代企业级开发最佳实践的复杂分布式系统。让我们一起探索这个系统背后的技术奥秘。 + +--- + +## 第一部分:架构设计的哲学思考 + +### 为什么选择分层架构?解耦的艺术 + +**设问**:在面对复杂的业务需求时,我们如何确保代码的可维护性和可扩展性? + +答案就在于**分层架构**的设计哲学。我们的系统采用了经典的四层架构模式: + +``` +Controller Layer (控制层) → Service Layer (业务层) → Repository Layer (数据访问层) → Model Layer (数据模型层) +``` + +这种设计遵循了**单一职责原则**和**依赖倒置原则**,实现了真正的**高内聚、低耦合**。每一层都有明确的职责边界,层与层之间通过接口进行通信,这样的设计使得我们可以独立地修改某一层的实现,而不会影响到其他层。 + +**技术亮点**:我们使用了Spring的**依赖注入(DI)**容器来管理对象的生命周期,通过`@Autowired`注解实现了**控制反转(IoC)**,这是企业级应用开发的核心设计模式。 + +--- + +## 第二部分:Controller层的RESTful设计艺术 + +### 为什么我们需要14个专业化的Controller? + +**设问**:传统的单体应用往往将所有功能堆砌在一个Controller中,这样做有什么问题? + +我们的系统包含了14个专业化的Controller,每个Controller都专注于特定的业务领域: + +#### AuthController - 认证控制器的安全哲学 + +```java +@RestController +@RequestMapping("/api/auth") +@Validated +public class AuthController { + + @PostMapping("/login") + public ResponseEntity> authenticateUser( + @Valid @RequestBody LoginRequest loginRequest) { + // JWT令牌生成逻辑 + } +} +``` + +**技术深度解析**: +- **@RestController**:这是Spring Boot的组合注解,等价于`@Controller + @ResponseBody`,自动将返回值序列化为JSON +- **@Validated**:启用方法级别的参数验证,配合`@Valid`实现数据校验的**AOP切面编程** +- **ResponseEntity**:提供了对HTTP响应的完全控制,包括状态码、头部信息和响应体 + +**设计思考**:为什么不直接返回对象,而要包装在`ApiResponse`中?这体现了**统一响应格式**的设计模式,确保前端能够以一致的方式处理所有API响应。 + +#### DashboardController - 数据可视化的技术挑战 + +**设问**:如何在不影响系统性能的前提下,为决策者提供实时的数据洞察? + +```java +@GetMapping("/stats") +@PreAuthorize("hasRole('DECISION_MAKER')") +public ResponseEntity> getDashboardStats() { + // 复杂的数据聚合逻辑 +} +``` + +**技术亮点**: +- **@PreAuthorize**:这是Spring Security的方法级安全注解,实现了**基于角色的访问控制(RBAC)** +- **数据聚合优化**:我们在Repository层使用了复杂的JPQL查询和原生SQL,将计算下推到数据库层,避免了在应用层进行大量数据处理 + +--- + +## 第三部分:Service层的业务编排艺术 + +### 为什么业务逻辑需要如此复杂的编排? + +**设问**:简单的CRUD操作为什么需要这么多Service类?这不是过度设计吗? + +让我们看看`TaskManagementServiceImpl`的复杂性: + +```java +@Service +@Transactional +public class TaskManagementServiceImpl implements TaskManagementService { + + @Autowired + private TaskRepository taskRepository; + + @Autowired + private FeedbackRepository feedbackRepository; + + @Autowired + private ApplicationEventPublisher eventPublisher; + + @Override + public TaskDTO createTaskFromFeedback(Long feedbackId, TaskFromFeedbackDTO dto) { + // 1. 获取并验证反馈 + Feedback feedback = feedbackRepository.findById(feedbackId) + .orElseThrow(() -> new ResourceNotFoundException("Feedback not found")); + + // 2. 业务规则验证 + if (feedback.getStatus() == FeedbackStatus.PROCESSED) { + throw new BusinessException("Feedback already processed"); + } + + // 3. 创建任务实体 + Task task = Task.builder() + .title(dto.getTitle()) + .description(feedback.getDescription()) + .location(feedback.getLocation()) + .feedback(feedback) + .status(TaskStatus.PENDING) + .build(); + + // 4. 事务性操作 + Task savedTask = taskRepository.save(task); + feedback.setStatus(FeedbackStatus.PROCESSED); + feedbackRepository.save(feedback); + + // 5. 发布领域事件 + eventPublisher.publishEvent(new TaskCreatedEvent(savedTask)); + + return modelMapper.map(savedTask, TaskDTO.class); + } +} +``` + +**技术深度解析**: + +1. **@Transactional**:这是Spring的声明式事务管理,确保方法内的所有数据库操作要么全部成功,要么全部回滚,保证了数据的**ACID特性** + +2. **Builder模式**:使用Lombok的`@Builder`注解生成建造者模式代码,提高了对象创建的可读性和安全性 + +3. **事件驱动架构**:通过`ApplicationEventPublisher`发布领域事件,实现了模块间的**松耦合**通信 + +4. **ModelMapper**:使用对象映射框架避免手动的属性拷贝,减少了样板代码 + +**设计哲学**:这种复杂性是必要的,因为它体现了**领域驱动设计(DDD)**的思想,每个Service都是一个业务能力的封装,确保了业务逻辑的完整性和一致性。 + +### AuthServiceImpl - 安全认证的技术堡垒 + +**设问**:在当今网络安全威胁日益严重的环境下,我们如何构建一个既安全又用户友好的认证系统? + +```java +@Service +public class AuthServiceImpl implements AuthService { + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private JwtTokenProvider tokenProvider; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public JwtAuthenticationResponse authenticateUser(LoginRequest loginRequest) { + // 1. Spring Security认证 + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + loginRequest.getUsername(), + loginRequest.getPassword() + ) + ); + + // 2. 设置安全上下文 + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 3. 生成JWT令牌 + String jwt = tokenProvider.generateToken(authentication); + + return new JwtAuthenticationResponse(jwt); + } +} +``` + +**技术亮点**: + +1. **AuthenticationManager**:Spring Security的核心认证管理器,支持多种认证提供者的**策略模式** + +2. **JWT(JSON Web Token)**:无状态的令牌认证机制,支持分布式系统的**水平扩展** + +3. **BCrypt密码编码**:使用自适应哈希算法,具有**盐值**和**工作因子**,抵御彩虹表攻击 + +--- + +## 第四部分:Repository层的数据访问艺术 + +### 为什么我们需要如此复杂的数据访问层? + +**设问**:直接使用SQL不是更简单吗?为什么要引入这么多抽象层? + +让我们看看`FeedbackRepository`的设计: + +```java +@Repository +public interface FeedbackRepository extends JpaRepository, + JpaSpecificationExecutor { + + // 方法名查询 - Spring Data JPA的魔法 + List findByStatus(FeedbackStatus status); + Page findBySubmitterId(Long submitterId, Pageable pageable); + + // 自定义JPQL查询 - 面向对象的查询语言 + @Query("SELECT new com.dne.ems.dto.HeatmapPointDTO(f.gridX, f.gridY, COUNT(f.id)) " + + "FROM Feedback f WHERE f.status = 'CONFIRMED' " + + "AND f.gridX IS NOT NULL AND f.gridY IS NOT NULL " + + "GROUP BY f.gridX, f.gridY") + List getHeatmapData(); + + // 性能优化 - EntityGraph解决N+1问题 + @Override + @EntityGraph(attributePaths = {"user", "attachments"}) + Page findAll(Specification spec, Pageable pageable); +} +``` + +**技术深度解析**: + +1. **Spring Data JPA**:这是Spring生态系统中的数据访问抽象层,它通过**代理模式**自动生成Repository实现类 + +2. **方法名查询**:基于方法名的**约定优于配置**原则,Spring会自动解析方法名并生成相应的查询 + +3. **JPQL(Java Persistence Query Language)**:面向对象的查询语言,具有数据库无关性 + +4. **EntityGraph**:JPA 2.1引入的性能优化特性,通过**预加载**策略解决N+1查询问题 + +5. **Specification模式**:实现了**动态查询**的构建,支持复杂的条件组合 + +### AqiDataRepository - 大数据处理的技术挑战 + +**设问**:面对海量的环境监测数据,我们如何确保查询性能? + +```java +@Repository +public interface AqiDataRepository extends JpaRepository { + + // 复杂的统计查询 + @Query("SELECT new com.dne.ems.dto.AqiDistributionDTO(" + + "CASE WHEN a.aqiValue <= 50 THEN 'Good' " + + "WHEN a.aqiValue <= 100 THEN 'Moderate' " + + "WHEN a.aqiValue <= 150 THEN 'Unhealthy for Sensitive Groups' " + + "WHEN a.aqiValue <= 200 THEN 'Unhealthy' " + + "WHEN a.aqiValue <= 300 THEN 'Very Unhealthy' " + + "ELSE 'Hazardous' END, COUNT(a.id)) " + + "FROM AqiData a GROUP BY 1") + List getAqiDistribution(); + + // 原生SQL查询 - 性能优化 + @Query(value = "SELECT DATE_FORMAT(record_time, '%Y-%m') as yearMonth, " + + "COUNT(id) as count FROM aqi_data " + + "WHERE aqi_value > 100 AND record_time >= :startDate " + + "GROUP BY DATE_FORMAT(record_time, '%Y-%m') ORDER BY 1", + nativeQuery = true) + List getMonthlyExceedanceTrendRaw(@Param("startDate") LocalDateTime startDate); + + // 默认方法 - 数据转换 + default List getMonthlyExceedanceTrend(LocalDateTime startDate) { + List rawResults = getMonthlyExceedanceTrendRaw(startDate); + return rawResults.stream() + .map(row -> new TrendDataPointDTO((String) row[0], ((Number) row[1]).longValue())) + .toList(); + } +} +``` + +**技术亮点**: + +1. **原生SQL查询**:在需要极致性能时,我们使用原生SQL直接操作数据库 + +2. **默认方法**:Java 8的接口默认方法特性,允许我们在接口中提供数据转换逻辑 + +3. **Stream API**:函数式编程范式,提供了优雅的数据处理方式 + +4. **DTO投影**:直接将查询结果映射到DTO对象,避免了不必要的数据传输 + +--- + +## 第五部分:Security模块的安全防护体系 + +### 为什么安全性需要如此复杂的设计? + +**设问**:在微服务架构中,我们如何构建一个既安全又高效的认证授权体系? + +我们的安全模块包含四个核心组件: + +#### JwtAuthenticationFilter - 无状态认证的守护者 + +```java +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JwtService jwtService; + + @Autowired + private UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + // 1. 提取JWT令牌 + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String jwt = authHeader.substring(7); + String username = jwtService.extractUsername(jwt); + + // 2. 验证令牌并设置安全上下文 + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + if (jwtService.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + + filterChain.doFilter(request, response); + } +} +``` + +**技术深度解析**: + +1. **OncePerRequestFilter**:确保过滤器在每个请求中只执行一次,避免重复处理 + +2. **责任链模式**:FilterChain体现了责任链设计模式,每个过滤器处理特定的关注点 + +3. **ThreadLocal安全上下文**:SecurityContextHolder使用ThreadLocal存储认证信息,确保线程安全 + +4. **无状态设计**:JWT令牌包含了所有必要的用户信息,支持水平扩展 + +#### SecurityConfig - 安全策略的总指挥 + +```java +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // 禁用CSRF(无状态应用) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态会话 + .authorizeHttpRequests(authz -> authz + .requestMatchers("/api/auth/**", "/api/public/**").permitAll() + .requestMatchers("/api/dashboard/**").hasRole("DECISION_MAKER") + .requestMatchers("/api/supervisor/**").hasAnyRole("SUPERVISOR", "ADMIN") + .requestMatchers("/api/worker/**").hasRole("GRID_WORKER") + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); // 工作因子12,平衡安全性和性能 + } +} +``` + +**技术亮点**: + +1. **@EnableMethodSecurity**:启用方法级安全控制,支持`@PreAuthorize`等注解 + +2. **流式API配置**:Spring Security 5.7+的新配置方式,更加直观和类型安全 + +3. **细粒度权限控制**:基于URL模式和角色的访问控制 + +4. **BCrypt算法**:自适应哈希算法,工作因子可调,抵御暴力破解 + +--- + +## 第六部分:Model层的领域建模艺术 + +### 为什么需要如此复杂的实体关系? + +**设问**:简单的数据表不就够了吗?为什么要设计这么复杂的实体关系? + +让我们看看`UserAccount`实体的设计: + +```java +@Entity +@Table(name = "user_accounts") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserAccount { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + @Email + private String email; + + @Column(nullable = false) + @Size(min = 8) + private String password; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + + @Enumerated(EnumType.STRING) + @Builder.Default + private UserStatus status = UserStatus.ACTIVE; + + // 网格坐标(用于网格员) + private Integer gridX; + private Integer gridY; + + // 审计字段 + @CreationTimestamp + @Column(updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updatedAt; + + // 关系映射 + @OneToMany(mappedBy = "submitter", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List submittedFeedbacks = new ArrayList<>(); + + @OneToMany(mappedBy = "assignee", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List assignedTasks = new ArrayList<>(); +} +``` + +**技术深度解析**: + +1. **JPA注解体系**: + - `@Entity`:标识为JPA实体 + - `@Table`:指定数据库表名 + - `@Id`和`@GeneratedValue`:主键策略 + - `@Column`:列约束和属性 + +2. **Lombok注解魔法**: + - `@Data`:自动生成getter/setter/toString/equals/hashCode + - `@Builder`:建造者模式,提高对象创建的可读性 + - `@NoArgsConstructor`和`@AllArgsConstructor`:构造器生成 + +3. **枚举映射**:`@Enumerated(EnumType.STRING)`确保数据库存储的是枚举的字符串值,提高可读性 + +4. **审计功能**:`@CreationTimestamp`和`@UpdateTimestamp`自动管理时间戳 + +5. **关系映射**:`@OneToMany`建立了实体间的关联关系,支持对象导航 + +### Feedback实体 - 复杂业务的数据载体 + +```java +@Entity +@Table(name = "feedbacks") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Feedback { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String eventId; // 业务唯一标识 + + @Column(columnDefinition = "TEXT") + private String description; + + @Enumerated(EnumType.STRING) + @Builder.Default + private FeedbackStatus status = FeedbackStatus.PENDING; + + @Enumerated(EnumType.STRING) + private PollutionType pollutionType; + + // 地理信息 + private String location; + private Integer gridX; + private Integer gridY; + + // 关联关系 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "submitter_id") + private UserAccount submitter; + + @OneToOne(mappedBy = "feedback", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Task relatedTask; + + @OneToMany(mappedBy = "feedback", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List attachments = new ArrayList<>(); + + // 自定义转换器 + @Convert(converter = StringListConverter.class) + private List tags = new ArrayList<>(); +} +``` + +**设计亮点**: + +1. **业务标识符**:`eventId`作为业务唯一标识,与技术主键`id`分离 + +2. **状态机模式**:`FeedbackStatus`枚举定义了反馈的生命周期状态 + +3. **地理信息建模**:支持文本地址和网格坐标两种定位方式 + +4. **自定义转换器**:`StringListConverter`将List转换为数据库的JSON字段 + +--- + +## 第七部分:高级特性与算法实现 + +### A*寻路算法 - 智能路径规划的核心 + +**设问**:在复杂的城市环境中,我们如何为网格员规划最优的任务执行路径? + +```java +@Service +public class AStarService { + + public List findPath(Point start, Point goal, int[][] grid) { + PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingDouble(Node::getF)); + Set closedSet = new HashSet<>(); + Map allNodes = new HashMap<>(); + + Node startNode = new Node(start, 0, heuristic(start, goal), null); + openSet.add(startNode); + allNodes.put(start, startNode); + + while (!openSet.isEmpty()) { + Node current = openSet.poll(); + + if (current.getPoint().equals(goal)) { + return reconstructPath(current); + } + + closedSet.add(current.getPoint()); + + for (Point neighbor : getNeighbors(current.getPoint(), grid)) { + if (closedSet.contains(neighbor) || isObstacle(neighbor, grid)) { + continue; + } + + double tentativeG = current.getG() + distance(current.getPoint(), neighbor); + + Node neighborNode = allNodes.get(neighbor); + if (neighborNode == null) { + neighborNode = new Node(neighbor, tentativeG, heuristic(neighbor, goal), current); + allNodes.put(neighbor, neighborNode); + openSet.add(neighborNode); + } else if (tentativeG < neighborNode.getG()) { + neighborNode.setG(tentativeG); + neighborNode.setF(tentativeG + neighborNode.getH()); + neighborNode.setParent(current); + } + } + } + + return Collections.emptyList(); // 无路径 + } + + private double heuristic(Point a, Point b) { + // 曼哈顿距离启发式函数 + return Math.abs(a.getX() - b.getX()) + Math.abs(a.getY() - b.getY()); + } +} +``` + +**算法亮点**: + +1. **优先队列**:使用`PriorityQueue`实现开放集合,确保总是选择f值最小的节点 + +2. **启发式函数**:曼哈顿距离作为启发式函数,保证算法的最优性 + +3. **路径重构**:通过父节点指针重构最优路径 + +4. **空间复杂度优化**:使用HashMap存储所有节点,避免重复创建 + +### 智能任务推荐算法 + +**设问**:面对众多的网格员,我们如何智能地推荐最合适的任务执行者? + +```java +@Service +public class TaskRecommendationService { + + public List recommendWorkersForTask(Long taskId) { + Task task = taskRepository.findById(taskId) + .orElseThrow(() -> new ResourceNotFoundException("Task not found")); + + List availableWorkers = userRepository + .findByRoleAndStatus(Role.GRID_WORKER, UserStatus.ACTIVE); + + return availableWorkers.stream() + .map(worker -> calculateWorkerScore(worker, task)) + .sorted(Comparator.comparingDouble(WorkerRecommendationDTO::getScore).reversed()) + .collect(Collectors.toList()); + } + + private WorkerRecommendationDTO calculateWorkerScore(UserAccount worker, Task task) { + double distanceScore = calculateDistanceScore(worker, task); // 40%权重 + double workloadScore = calculateWorkloadScore(worker); // 30%权重 + double skillScore = calculateSkillScore(worker, task); // 20%权重 + double performanceScore = calculatePerformanceScore(worker); // 10%权重 + + double totalScore = distanceScore * 0.4 + workloadScore * 0.3 + + skillScore * 0.2 + performanceScore * 0.1; + + return WorkerRecommendationDTO.builder() + .workerId(worker.getId()) + .workerName(worker.getName()) + .score(totalScore) + .distanceScore(distanceScore) + .workloadScore(workloadScore) + .skillScore(skillScore) + .performanceScore(performanceScore) + .build(); + } + + private double calculateDistanceScore(UserAccount worker, Task task) { + if (worker.getGridX() == null || worker.getGridY() == null) { + return 0.0; + } + + double distance = Math.sqrt( + Math.pow(worker.getGridX() - task.getGridX(), 2) + + Math.pow(worker.getGridY() - task.getGridY(), 2) + ); + + double maxDistance = 50.0; // 最大考虑距离 + return Math.max(0, (maxDistance - distance) / maxDistance); + } +} +``` + +**算法特点**: + +1. **多维度评分**:综合考虑距离、工作负载、技能匹配和历史表现 + +2. **加权评分模型**:不同维度采用不同权重,可根据业务需求调整 + +3. **流式处理**:使用Stream API进行函数式编程,代码简洁高效 + +4. **可扩展性**:评分算法可以轻松添加新的评分维度 + +--- + +## 第八部分:事件驱动架构的异步魅力 + +### 为什么选择事件驱动架构? + +**设问**:传统的同步调用方式有什么问题?为什么要引入事件驱动的复杂性? + +让我们看看事件驱动架构的实现: + +```java +// 事件定义 +@Getter +@AllArgsConstructor +public class TaskCreatedEvent extends ApplicationEvent { + private final Task task; + + public TaskCreatedEvent(Object source, Task task) { + super(source); + this.task = task; + } +} + +// 事件发布 +@Service +public class TaskManagementServiceImpl { + + @Autowired + private ApplicationEventPublisher eventPublisher; + + public TaskDTO createTask(TaskCreateDTO dto) { + Task task = // ... 创建任务逻辑 + Task savedTask = taskRepository.save(task); + + // 发布事件 - 异步处理 + eventPublisher.publishEvent(new TaskCreatedEvent(this, savedTask)); + + return modelMapper.map(savedTask, TaskDTO.class); + } +} + +// 事件监听 +@Component +public class TaskEventListener { + + @Autowired + private NotificationService notificationService; + + @Autowired + private AuditService auditService; + + @EventListener + @Async + public void handleTaskCreated(TaskCreatedEvent event) { + Task task = event.getTask(); + + // 异步发送通知 + notificationService.sendTaskAssignmentNotification(task); + + // 异步记录审计日志 + auditService.logTaskCreation(task); + + // 异步更新统计数据 + statisticsService.updateTaskStatistics(task); + } +} +``` + +**技术优势**: + +1. **解耦合**:事件发布者和监听者之间没有直接依赖关系 + +2. **异步处理**:`@Async`注解实现异步执行,提高系统响应性 + +3. **可扩展性**:可以轻松添加新的事件监听器,不影响现有代码 + +4. **容错性**:某个监听器失败不会影响其他监听器的执行 + +--- + +## 第九部分:性能优化的技术手段 + +### 数据库查询优化策略 + +**设问**:面对大量的数据查询需求,我们如何确保系统的高性能? + +#### 1. EntityGraph解决N+1查询问题 + +```java +@Repository +public interface FeedbackRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"submitter", "attachments", "relatedTask"}) + @Query("SELECT f FROM Feedback f WHERE f.status = :status") + List findByStatusWithDetails(@Param("status") FeedbackStatus status); +} +``` + +**技术原理**:EntityGraph告诉JPA在执行查询时一次性加载相关联的实体,避免了懒加载导致的N+1查询问题。 + +#### 2. 分页查询优化 + +```java +@Service +public class FeedbackService { + + public Page getFeedbacks(FeedbackSearchCriteria criteria, Pageable pageable) { + Specification spec = FeedbackSpecification.buildSpecification(criteria); + Page feedbacks = feedbackRepository.findAll(spec, pageable); + + return feedbacks.map(feedback -> modelMapper.map(feedback, FeedbackDTO.class)); + } +} +``` + +**优化策略**: +- 使用Specification动态构建查询条件 +- 利用数据库索引优化排序和过滤 +- 在DTO转换时避免加载不必要的关联数据 + +#### 3. 缓存策略 + +```java +@Service +public class UserService { + + @Cacheable(value = "users", key = "#email") + public UserAccount findByEmail(String email) { + return userRepository.findByEmail(email) + .orElseThrow(() -> new UserNotFoundException("User not found")); + } + + @CacheEvict(value = "users", key = "#user.email") + public UserAccount updateUser(UserAccount user) { + return userRepository.save(user); + } +} +``` + +**缓存优势**: +- 减少数据库访问次数 +- 提高频繁查询的响应速度 +- 支持分布式缓存扩展 + +--- + +## 第十部分:系统的技术创新点 + +### 1. 智能AI预审核系统 + +**设问**:如何将人工智能技术融入到传统的政务系统中? + +```java +@Service +public class AiReviewService { + + @Autowired + private RestTemplate restTemplate; + + public AiReviewResult reviewFeedback(Feedback feedback) { + // 构建AI分析请求 + AiAnalysisRequest request = AiAnalysisRequest.builder() + .text(feedback.getDescription()) + .imageUrls(feedback.getAttachments().stream() + .map(Attachment::getFileUrl) + .collect(Collectors.toList())) + .location(feedback.getLocation()) + .build(); + + // 调用AI服务 + ResponseEntity response = restTemplate.postForEntity( + aiServiceUrl + "/analyze", request, AiAnalysisResponse.class); + + AiAnalysisResponse analysisResult = response.getBody(); + + // 构建审核结果 + return AiReviewResult.builder() + .isValid(analysisResult.getConfidence() > 0.8) + .confidence(analysisResult.getConfidence()) + .category(analysisResult.getCategory()) + .urgencyLevel(analysisResult.getUrgencyLevel()) + .suggestedActions(analysisResult.getSuggestedActions()) + .build(); + } +} +``` + +**创新点**: +- **多模态分析**:同时分析文本和图像内容 +- **智能分类**:自动识别问题类型和紧急程度 +- **置信度评估**:提供AI判断的可信度指标 + +### 2. 实时数据大屏技术 + +**设问**:如何为决策者提供实时、直观的数据洞察? + +```java +@RestController +@RequestMapping("/api/dashboard") +public class DashboardController { + + @GetMapping("/realtime-stats") + @PreAuthorize("hasRole('DECISION_MAKER')") + public SseEmitter getRealtimeStats() { + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); + + // 异步推送实时数据 + CompletableFuture.runAsync(() -> { + try { + while (true) { + DashboardStatsDTO stats = dashboardService.getCurrentStats(); + emitter.send(SseEmitter.event() + .name("stats-update") + .data(stats)); + + Thread.sleep(5000); // 每5秒推送一次 + } + } catch (Exception e) { + emitter.completeWithError(e); + } + }); + + return emitter; + } +} +``` + +**技术特点**: +- **Server-Sent Events (SSE)**:实现服务器主动推送数据 +- **异步处理**:使用CompletableFuture避免阻塞主线程 +- **实时性**:数据更新延迟控制在秒级 + +### 3. 地理信息系统集成 + +```java +@Service +public class GeoService { + + public List generateHeatmapData(String dataType, LocalDateTime startTime) { + switch (dataType) { + case "feedback": + return feedbackRepository.getHeatmapData(); + case "aqi": + return aqiDataRepository.getAqiHeatmapData(); + case "task": + return taskRepository.getTaskHeatmapData(startTime); + default: + throw new IllegalArgumentException("Unsupported data type: " + dataType); + } + } + + public GridCoverageDTO calculateGridCoverage() { + long totalGrids = gridRepository.count(); + long coveredGrids = gridRepository.countByIsObstacleFalse(); + + return GridCoverageDTO.builder() + .totalGrids(totalGrids) + .coveredGrids(coveredGrids) + .coverageRate((double) coveredGrids / totalGrids * 100) + .build(); + } +} +``` + +**GIS特性**: +- **热力图生成**:基于地理坐标的数据可视化 +- **网格覆盖分析**:计算监测网络的覆盖率 +- **空间查询**:支持基于地理位置的数据查询 + +--- + +--- + +## 第十一部分:系统的技术特色 + +### 设问:我们的系统有哪些值得关注的技术特点? + +除了基础功能实现,我们的EMS系统还融入了一些实用的技术特色,让系统更加稳定和高效。 + +#### 1. 多任务处理能力 + +**应用场景**:当系统需要同时处理多个用户的请求时,比如多个网格员同时上报数据。 + +**解决方案**:我们使用了异步处理技术,让系统能够同时处理多个任务,提高响应速度。 + +```java +// 异步任务配置 +@Configuration +@EnableAsync +public class AsyncConfig { + + @Bean + public ThreadPoolTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + + // 设置合适的线程数量 + executor.setCorePoolSize(4); + executor.setMaxPoolSize(8); + executor.setQueueCapacity(100); + + // 设置线程名称,方便调试 + executor.setThreadNamePrefix("EMS-Task-"); + + executor.initialize(); + return executor; + } +} + +// 异步任务示例 +@Service +public class NotificationService { + + @Async + public void sendNotification(String message) { + // 发送通知的逻辑 + System.out.println("发送通知: " + message); + } +} +``` + +**技术优势**: +- **提高响应速度**:用户操作不会被耗时任务阻塞 +- **合理利用资源**:根据服务器性能配置线程数量 +- **便于问题排查**:通过线程名称快速定位问题 + +#### 2. 任务分配的安全机制 + +**应用场景**:当多个管理员同时分配同一个任务时,需要确保任务不会被重复分配。 + +**解决方案**:使用Redis实现任务锁定机制,确保同一时间只有一个操作能够分配特定任务。 + +```java +@Component +public class TaskLockService { + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 尝试锁定任务 + */ + public boolean lockTask(String taskId) { + String lockKey = "task:lock:" + taskId; + String lockValue = "locked"; + + // 设置锁,30秒后自动过期 + Boolean success = redisTemplate.opsForValue() + .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(30)); + + return Boolean.TRUE.equals(success); + } + + /** + * 释放任务锁 + */ + public void unlockTask(String taskId) { + String lockKey = "task:lock:" + taskId; + redisTemplate.delete(lockKey); + } +} + +// 任务分配服务 +@Service +public class TaskAssignmentService { + + @Autowired + private TaskLockService lockService; + + public boolean assignTask(Long taskId, Long workerId) { + String taskIdStr = taskId.toString(); + + // 尝试锁定任务 + if (!lockService.lockTask(taskIdStr)) { + throw new BusinessException("任务正在被处理,请稍后重试"); + } + + try { + // 检查任务状态 + Task task = taskRepository.findById(taskId) + .orElseThrow(() -> new RuntimeException("任务不存在")); + + if (task.getStatus() != TaskStatus.PENDING) { + throw new BusinessException("任务已被分配"); + } + + // 分配任务 + task.setAssigneeId(workerId); + task.setStatus(TaskStatus.ASSIGNED); + task.setAssignedAt(LocalDateTime.now()); + + taskRepository.save(task); + + return true; + + } finally { + // 释放锁 + lockService.unlockTask(taskIdStr); + } + } +} +``` + +**技术优势**: +- **防止重复分配**:确保任务分配的唯一性 +- **自动释放机制**:避免系统卡死 +- **简单可靠**:使用成熟稳定的技术方案 + +#### 3. 智能任务推荐功能 + +**应用场景**:当有新任务需要分配时,系统能够自动推荐最合适的网格员。 + +**解决方案**:根据不同的任务类型,使用不同的推荐规则来选择最合适的人员。 + +```java +// 任务推荐服务 +@Service +public class TaskRecommendationService { + + /** + * 推荐合适的网格员 + */ + public List recommendWorkers(Task task) { + List availableWorkers = getAvailableWorkers(); + + // 根据任务类型选择推荐方式 + if (task.isUrgent()) { + return recommendByDistance(task, availableWorkers); + } else { + return recommendByExperience(task, availableWorkers); + } + } + + /** + * 基于距离推荐(紧急任务) + */ + private List recommendByDistance(Task task, List workers) { + return workers.stream() + .map(worker -> { + double distance = calculateDistance(worker.getLocation(), task.getLocation()); + int score = (int) (100 - distance * 10); // 距离越近分数越高 + + return new WorkerRecommendation( + worker.getId(), + worker.getName(), + score, + "距离任务地点 " + String.format("%.1f", distance) + " 公里" + ); + }) + .sorted((a, b) -> Integer.compare(b.getScore(), a.getScore())) + .limit(5) + .collect(Collectors.toList()); + } + + /** + * 基于经验推荐(普通任务) + */ + private List recommendByExperience(Task task, List workers) { + return workers.stream() + .map(worker -> { + int completedTasks = getCompletedTaskCount(worker.getId(), task.getType()); + int score = Math.min(100, completedTasks * 10 + 50); // 经验越多分数越高 + + return new WorkerRecommendation( + worker.getId(), + worker.getName(), + score, + "已完成类似任务 " + completedTasks + " 次" + ); + }) + .sorted((a, b) -> Integer.compare(b.getScore(), a.getScore())) + .limit(5) + .collect(Collectors.toList()); + } + + /** + * 计算两点间距离(简化版) + */ + private double calculateDistance(String location1, String location2) { + // 这里可以接入地图API计算实际距离 + // 现在返回模拟距离 + return Math.random() * 10; // 0-10公里 + } + + /** + * 获取已完成任务数量 + */ + private int getCompletedTaskCount(Long workerId, String taskType) { + return taskRepository.countCompletedTasksByWorkerAndType(workerId, taskType); + } +} + +// 推荐结果类 +public class WorkerRecommendation { + private Long workerId; + private String workerName; + private int score; + private String reason; + + // 构造函数和getter/setter + public WorkerRecommendation(Long workerId, String workerName, int score, String reason) { + this.workerId = workerId; + this.workerName = workerName; + this.score = score; + this.reason = reason; + } + + // getter方法... +} +``` + +**技术优势**: +- **智能推荐**:根据任务特点自动推荐合适人员 +- **多种策略**:紧急任务看距离,普通任务看经验 +- **简单易懂**:推荐逻辑清晰,便于维护和扩展 + +#### 4. 操作记录和审计功能 + +**应用场景**:政务系统需要记录所有重要操作,便于后续查询和审计。 + +**解决方案**:建立完整的操作日志系统,记录用户的每一个重要操作。 + +```java +// 操作日志实体 +@Entity +@Table(name = "operation_log") +public class OperationLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String userId; // 操作用户 + private String userName; // 用户姓名 + private String operation; // 操作类型 + private String description; // 操作描述 + private String targetId; // 操作对象ID + private String targetType; // 操作对象类型 + private String ipAddress; // IP地址 + private LocalDateTime createTime; // 操作时间 + + // 构造函数和getter/setter... +} + +// 操作日志服务 +@Service +public class OperationLogService { + + @Autowired + private OperationLogRepository logRepository; + + /** + * 记录操作日志 + */ + public void recordOperation(String operation, String description, + String targetId, String targetType) { + // 获取当前用户信息 + UserAccount currentUser = getCurrentUser(); + + OperationLog log = new OperationLog(); + log.setUserId(currentUser.getId().toString()); + log.setUserName(currentUser.getName()); + log.setOperation(operation); + log.setDescription(description); + log.setTargetId(targetId); + log.setTargetType(targetType); + log.setIpAddress(getClientIpAddress()); + log.setCreateTime(LocalDateTime.now()); + + logRepository.save(log); + } + + /** + * 查询操作日志 + */ + public List queryLogs(String userId, String operation, + LocalDateTime startTime, LocalDateTime endTime) { + return logRepository.findByConditions(userId, operation, startTime, endTime); + } + + /** + * 获取客户端IP地址 + */ + private String getClientIpAddress() { + // 从请求中获取IP地址 + HttpServletRequest request = getCurrentRequest(); + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty()) { + ip = request.getRemoteAddr(); + } + return ip; + } +} + +// 使用示例:在任务分配时记录日志 +@Service +public class TaskService { + + @Autowired + private OperationLogService logService; + + public void assignTask(Long taskId, Long workerId) { + // 执行任务分配逻辑 + // ... + + // 记录操作日志 + logService.recordOperation( + "TASK_ASSIGN", + "将任务分配给网格员", + taskId.toString(), + "TASK" + ); + } +} +``` + +**技术优势**: +- **完整记录**:记录所有重要操作,便于追溯 +- **详细信息**:包含操作人、时间、IP等关键信息 +- **便于查询**:支持按条件查询历史操作 +- **安全可靠**:确保操作的可追溯性和透明度 +--- + +## 第十二部分:系统总结与展望 + +### 设问:我们的EMS系统实现了什么目标? + +通过以上的技术实现和功能展示,我们的环境监测系统(EMS)成功地解决了环境管理中的核心问题: + +#### 1. 主要成果 + +**功能完整性**: +- ✅ **数据采集与监测**:实时收集环境数据,支持多种数据源 +- ✅ **任务管理**:完整的任务分配、跟踪、完成流程 +- ✅ **用户权限管理**:多角色权限控制,确保系统安全 +- ✅ **数据可视化**:直观的图表展示,便于决策分析 +- ✅ **移动端支持**:网格员可通过手机APP进行现场操作 + +**技术特色**: +- 🚀 **高性能**:异步处理技术提升系统响应速度 +- 🔒 **高安全性**:完善的权限控制和操作审计 +- 🎯 **智能推荐**:自动推荐最合适的处理人员 +- 📱 **用户友好**:简洁直观的界面设计 + +#### 2. 解决的实际问题 + +**环境管理痛点**: +- **数据分散** → **统一平台管理** +- **任务混乱** → **清晰的工作流程** +- **响应缓慢** → **快速任务分配** +- **监管困难** → **全程操作记录** + +**技术实现亮点**: +- **前后端分离**:Vue3 + Spring Boot,技术栈现代化 +- **数据库设计**:合理的表结构,支持复杂查询 +- **接口规范**:RESTful API设计,便于扩展 +- **代码质量**:清晰的分层架构,易于维护 + +#### 3. 未来发展方向 + +**功能扩展**: +- 📊 **数据分析增强**:加入更多统计分析功能 +- 🤖 **智能化升级**:引入AI辅助决策 +- 📱 **移动端完善**:增加更多移动端功能 +- 🌐 **系统集成**:与其他政务系统对接 + +**技术优化**: +- ⚡ **性能提升**:优化数据库查询,提升响应速度 +- 🔐 **安全加强**:增加更多安全防护措施 +- 📈 **监控完善**:加入系统监控和告警功能 +- 🔄 **自动化**:增加更多自动化处理流程 + +### 总结 + +我们的EMS系统不仅实现了环境监测的基本功能,更在技术实现上体现了现代软件开发的最佳实践。通过合理的架构设计、清晰的代码组织和用户友好的界面,为环境管理工作提供了有力的技术支撑。 + +这个项目展示了我们在**全栈开发**、**系统设计**、**数据库管理**等方面的综合能力,是一个真正能够投入实际使用的完整系统。 + +### 数据异常检测功能 + +为了保证环境数据的准确性,系统提供了简单有效的异常检测功能: + +```java +@Service +public class DataValidationService { + + /** + * 检测异常数据 + */ + public List validateAqiData(List dataList) { + List warnings = new ArrayList<>(); + + for (AqiData data : dataList) { + // 检查数值范围 + if (data.getAqiValue() < 0 || data.getAqiValue() > 500) { + warnings.add("AQI数值异常: " + data.getAqiValue()); + } + + // 检查PM2.5范围 + if (data.getPm25() < 0 || data.getPm25() > 1000) { + warnings.add("PM2.5数值异常: " + data.getPm25()); + } + } + + return warnings; + } + + /** + * 数据趋势分析 + */ + public String analyzeTrend(List recentData) { + if (recentData.size() < 2) { + return "数据不足"; + } + + double firstValue = recentData.get(0).getAqiValue(); + double lastValue = recentData.get(recentData.size() - 1).getAqiValue(); + + if (lastValue > firstValue * 1.2) { + return "空气质量呈恶化趋势"; + } else if (lastValue < firstValue * 0.8) { + return "空气质量呈改善趋势"; + } else { + return "空气质量相对稳定"; + } + } +} +``` + +**功能特点**: +- **数据校验**:检查环境数据是否在合理范围内 +- **趋势分析**:分析空气质量变化趋势 +- **预警提醒**:发现异常数据时及时提醒 +--- + +## 项目总结 + +### 🎯 主要成果 + +1. **功能完整性** + - 实现了环境监测数据的全生命周期管理 + - 提供了直观的数据可视化界面 + - 支持多种数据导入导出格式 + +2. **技术特色** + - 采用前后端分离架构,便于维护和扩展 + - 使用Spring Boot框架,开发效率高 + - 集成了地图展示功能,用户体验良好 + +3. **实用价值** + - 解决了环境数据管理的实际需求 + - 提供了便民的在线查询服务 + - 支持数据分析和趋势预测 + +### 🚀 技术亮点 + +- **系统架构**:清晰的分层设计,易于理解和维护 +- **数据处理**:高效的数据存储和查询机制 +- **用户界面**:现代化的Web界面,操作简单直观 +- **安全性**:完善的用户认证和权限管理 + +### 📈 未来展望 + +1. **功能扩展** + - 增加更多环境指标的监测 + - 支持移动端应用 + - 添加数据预警功能 + +2. **技术优化** + - 提升系统性能和响应速度 + - 增强数据分析能力 + - 优化用户体验 +``` + +**谢谢各位老师的指导!** --- - + ### Design/公众反馈功能设计.md - -# 公众反馈功能 - 设计文档 - -## 1. 功能描述 -本功能是系统面向公众的核心入口(NEPS端),允许公众监督员提交图文并茂的空气质量问题反馈。提交后,系统将**异步调用AI服务**对反馈内容进行审核,最终将有效反馈转化为待处理的任务,送入内部管理流程。 - -## 2. 涉及角色 -- **主要使用者**: `公众监督员 (PUBLIC_SUPERVISOR)` 或任何已认证用户 -- **间接关联者**: `主管 (SUPERVISOR)` (作为反馈的接收和处理者) - -## 3. 业务规则 - -### 3.1 提交规则 -- 用户必须处于**已认证 (Authenticated)** 状态才能提交反馈。 -- 提交时,`标题`、`污染类型`、`严重等级`、`地理位置`为必填项。 -- 地理位置通过前端组件获取,同时生成`文字地址`和`地理坐标(经纬度)`,一并提交给后端。 - -### 3.2 AI处理规则 -- **模型**: 所有AI处理均调用火山引擎的大语言模型API。 -- **触发方式**: 用户提交反馈后,系统立即将反馈状态置为 `AI_REVIEWING`,并发布一个**异步事件**来触发AI审核,不会阻塞用户的提交操作。 -- **审核逻辑**: AI服务会对用户提交的`标题`、`描述`和`图片`进行**单阶段审核**,通过Function Calling能力,**同时判断**以下两点: - - `is_compliant`: 内容是否合规(不含敏感词等)。 - - `is_relevant`: 内容是否与大气污染问题相关。 -- **输出**: - - 若AI判断为**合规且相关**,则反馈状态更新为 `PENDING_REVIEW`。 - - 若AI判断为**不合规或不相关**,则反馈状态更新为 `CLOSED_INVALID`。 - - 若AI服务调用**失败**(如网络异常、API错误),则反馈状态更新为 `AI_REVIEW_FAILED`,以便后续人工介入或重试。 - -### 3.3 事件ID规则 -- 反馈创建时,系统后端自动使用 `UUID.randomUUID().toString()` 生成一个唯一的字符串作为 `eventId`。 - -## 4. 功能实现流程 -```mermaid -graph TD - subgraph "NEPS 端 (用户操作)" - A[填写反馈表单] --> B{点击提交}; - end - - subgraph "后端服务 (自动化流程)" - B --> C[创建Feedback记录
status: AI_REVIEWING]; - C --> D{发布异步事件
触发AI审核}; - D -- AI审核成功
合规且相关 --> F[status: PENDING_REVIEW]; - D -- AI审核成功
不合规或不相关 --> G[status: CLOSED_INVALID]; - D -- AI服务调用失败 --> H[status: AI_REVIEW_FAILED]; - end - - subgraph "NEPM 端 (后续操作)" - F --> I[进入主管审核列表]; - end - - style C fill:#E3F2FD,stroke:#333 - style G fill:#FFCDD2,stroke:#333 - style H fill:#FFE0B2,stroke:#333 - style F fill:#C8E6C9,stroke:#333 -``` - -## 5. API 接口设计 - -### 5.1 提交反馈接口 - -- **URL**: `POST /api/feedback/submit` -- **描述**: 用户提交新的环境问题反馈。 -- **权限**: `isAuthenticated()` (任何已认证用户) -- **请求类型**: `multipart/form-data` -- **请求体**: - - `feedback` (JSON字符串,DTO: `FeedbackSubmissionRequest`): - ```json - { - "title": "长安区工业园附近空气异味严重", - "description": "每天下午都能闻到刺鼻的气味...", - "pollutionType": "OTHER", - "severityLevel": "HIGH", - "location": { - "latitude": 38.04, - "longitude": 114.5 - } - } - ``` - - `files` (File数组): 用户上传的图片文件(可选)。 -- **成功响应** (`201 Created`): - - 返回创建的 `Feedback` 实体JSON对象,其中包含由后端生成的`id`和`eventId`(UUID格式)。 - -- **失败响应**: - - `400 Bad Request`: 表单验证失败。 - - `401 Unauthorized`: 用户未登录。 - - `413 Payload Too Large`: 上传文件过大。 - - `500 Internal Server Error`: 服务器内部错误。 - -## 6. 状态机详解 -```mermaid -stateDiagram-v2 - [*] --> AI_REVIEWING: 用户提交 - - AI_REVIEWING --> PENDING_REVIEW: AI审核通过 - AI_REVIEWING --> CLOSED_INVALID: AI审核拒绝 (内容问题) - AI_REVIEWING --> AI_REVIEW_FAILED: AI审核失败 (技术问题) - - PENDING_REVIEW: 待主管审核 - CLOSED_INVALID: 已关闭/无效 - AI_REVIEW_FAILED: AI审核失败 - - state PENDING_REVIEW { - note right of PENDING_REVIEW - 此状态的反馈将出现在 - 主管的管理界面中, - 等待人工处理。 - end note - } -``` - -## 7. 错误处理与边界情况 -| 场景 | 触发条件 | 系统处理 | 用户提示 | -| :--- | :--- | :--- | :--- | -| **表单验证失败** | 必填项为空或格式不正确 | 后端返回`400`错误,不创建记录 | 在对应表单项下方显示红色错误提示 | -| **文件上传失败** | 文件过大、格式不支持、网络中断 | 后端返回`413`或`500`错误 | 弹出全局错误消息:"图片上传失败,请稍后重试" | -| **AI服务调用异常**| AI接口超时或返回错误码 | ① `AiReviewService`捕获异常
② 将反馈状态设置为`AI_REVIEW_FAILED`并保存
③ 流程终止,等待人工处理 | 对用户无感知,后台自动处理 | -| **用户重复提交** | 用户快速点击提交按钮 | 前端在点击后禁用按钮并显示加载动画;
后端对相同用户的请求在短时间内进行幂等性处理 | "正在提交,请稍候..." | - -## 8. 界面与交互细节 - -### 8.1 反馈提交页面 (NEPS) -- **表单设计**: - - `标题`: 单行文本输入框。 - - `描述`: 多行文本域,允许输入较长内容。 - - `污染类型`: **单选框 (Radio Group)** 或 **下拉选择框 (Select)**,选项为`PM2.5`, `O3`, `NO2`, `SO2`, `OTHER`。 - - `严重等级`: **单选框 (Radio Group)**,选项为`高`, `中`, `低`,并附有简单的解释说明。 - - `地理位置`: 提供一个地图选点组件,用户点击地图即可自动填充文字地址和网格坐标。也应允许用户手动输入地址。 - - `照片上传`: 提供一个图片上传组件,支持多选、预览和删除已上传的图片。**明确提示支持的格式(JPG, PNG)和大小限制**。 -- **交互细节**: - - **加载状态**: 点击"确认提交"后,按钮变为"提交中..."并显示加载动画,同时整个表单区域置为不可编辑状态。 - - **成功提示**: 提交成功后,弹出一个全局成功的消息提示(e.g., "提交成功!感谢您的反馈。"),并自动跳转到反馈历史页面。 - - **失败提示**: 如果提交失败,根据错误类型给出明确的全局错误提示,并保持表单内容,让用户可以修改后重新提交。 - -### 8.2 反馈历史页面 (NEPS) -- 以列表形式展示用户所有提交过的反馈。 -- 每个列表项应以卡片形式展示,包含`标题`, `提交时间`, `事件ID`, 和一个醒目的**`状态标签`**。 -- **状态标签**: - - `待审核`: 蓝色或灰色 - - `待分配`: 橙色 - - `已分配`/`处理中`/`已提交`/`已完成`: 绿色 - - `已作废`/`已取消`: 红色 -- 点击卡片可进入反馈详情页,查看所有提交信息、`事件ID`和处理流程的详细时间线。 -- 对于状态为`PENDING_REVIEW`或`PENDING_ASSIGNMENT`的反馈,详情页提供"撤销提交"按钮。点击后需**二次确认弹窗**,防止误操作。 + +# 公众反馈功能 - 设计文档 + +## 1. 功能描述 +本功能是系统面向公众的核心入口(NEPS端),允许公众监督员提交图文并茂的空气质量问题反馈。提交后,系统将**异步调用AI服务**对反馈内容进行审核,最终将有效反馈转化为待处理的任务,送入内部管理流程。 + +## 2. 涉及角色 +- **主要使用者**: `公众监督员 (PUBLIC_SUPERVISOR)` 或任何已认证用户 +- **间接关联者**: `主管 (SUPERVISOR)` (作为反馈的接收和处理者) + +## 3. 业务规则 + +### 3.1 提交规则 +- 用户必须处于**已认证 (Authenticated)** 状态才能提交反馈。 +- 提交时,`标题`、`污染类型`、`严重等级`、`地理位置`为必填项。 +- 地理位置通过前端组件获取,同时生成`文字地址`和`地理坐标(经纬度)`,一并提交给后端。 + +### 3.2 AI处理规则 +- **模型**: 所有AI处理均调用火山引擎的大语言模型API。 +- **触发方式**: 用户提交反馈后,系统立即将反馈状态置为 `AI_REVIEWING`,并发布一个**异步事件**来触发AI审核,不会阻塞用户的提交操作。 +- **审核逻辑**: AI服务会对用户提交的`标题`、`描述`和`图片`进行**单阶段审核**,通过Function Calling能力,**同时判断**以下两点: + - `is_compliant`: 内容是否合规(不含敏感词等)。 + - `is_relevant`: 内容是否与大气污染问题相关。 +- **输出**: + - 若AI判断为**合规且相关**,则反馈状态更新为 `PENDING_REVIEW`。 + - 若AI判断为**不合规或不相关**,则反馈状态更新为 `CLOSED_INVALID`。 + - 若AI服务调用**失败**(如网络异常、API错误),则反馈状态更新为 `AI_REVIEW_FAILED`,以便后续人工介入或重试。 + +### 3.3 事件ID规则 +- 反馈创建时,系统后端自动使用 `UUID.randomUUID().toString()` 生成一个唯一的字符串作为 `eventId`。 + +## 4. 功能实现流程 +```mermaid +graph TD + subgraph "NEPS 端 (用户操作)" + A[填写反馈表单] --> B{点击提交}; + end + + subgraph "后端服务 (自动化流程)" + B --> C[创建Feedback记录
status: AI_REVIEWING]; + C --> D{发布异步事件
触发AI审核}; + D -- AI审核成功
合规且相关 --> F[status: PENDING_REVIEW]; + D -- AI审核成功
不合规或不相关 --> G[status: CLOSED_INVALID]; + D -- AI服务调用失败 --> H[status: AI_REVIEW_FAILED]; + end + + subgraph "NEPM 端 (后续操作)" + F --> I[进入主管审核列表]; + end + + style C fill:#E3F2FD,stroke:#333 + style G fill:#FFCDD2,stroke:#333 + style H fill:#FFE0B2,stroke:#333 + style F fill:#C8E6C9,stroke:#333 +``` + +## 5. API 接口设计 + +### 5.1 提交反馈接口 + +- **URL**: `POST /api/feedback/submit` +- **描述**: 用户提交新的环境问题反馈。 +- **权限**: `isAuthenticated()` (任何已认证用户) +- **请求类型**: `multipart/form-data` +- **请求体**: + - `feedback` (JSON字符串,DTO: `FeedbackSubmissionRequest`): + ```json + { + "title": "长安区工业园附近空气异味严重", + "description": "每天下午都能闻到刺鼻的气味...", + "pollutionType": "OTHER", + "severityLevel": "HIGH", + "location": { + "latitude": 38.04, + "longitude": 114.5 + } + } + ``` + - `files` (File数组): 用户上传的图片文件(可选)。 +- **成功响应** (`201 Created`): + - 返回创建的 `Feedback` 实体JSON对象,其中包含由后端生成的`id`和`eventId`(UUID格式)。 + +- **失败响应**: + - `400 Bad Request`: 表单验证失败。 + - `401 Unauthorized`: 用户未登录。 + - `413 Payload Too Large`: 上传文件过大。 + - `500 Internal Server Error`: 服务器内部错误。 + +## 6. 状态机详解 +```mermaid +stateDiagram-v2 + [*] --> AI_REVIEWING: 用户提交 + + AI_REVIEWING --> PENDING_REVIEW: AI审核通过 + AI_REVIEWING --> CLOSED_INVALID: AI审核拒绝 (内容问题) + AI_REVIEWING --> AI_REVIEW_FAILED: AI审核失败 (技术问题) + + PENDING_REVIEW: 待主管审核 + CLOSED_INVALID: 已关闭/无效 + AI_REVIEW_FAILED: AI审核失败 + + state PENDING_REVIEW { + note right of PENDING_REVIEW + 此状态的反馈将出现在 + 主管的管理界面中, + 等待人工处理。 + end note + } +``` + +## 7. 错误处理与边界情况 +| 场景 | 触发条件 | 系统处理 | 用户提示 | +| :--- | :--- | :--- | :--- | +| **表单验证失败** | 必填项为空或格式不正确 | 后端返回`400`错误,不创建记录 | 在对应表单项下方显示红色错误提示 | +| **文件上传失败** | 文件过大、格式不支持、网络中断 | 后端返回`413`或`500`错误 | 弹出全局错误消息:"图片上传失败,请稍后重试" | +| **AI服务调用异常**| AI接口超时或返回错误码 | ① `AiReviewService`捕获异常
② 将反馈状态设置为`AI_REVIEW_FAILED`并保存
③ 流程终止,等待人工处理 | 对用户无感知,后台自动处理 | +| **用户重复提交** | 用户快速点击提交按钮 | 前端在点击后禁用按钮并显示加载动画;
后端对相同用户的请求在短时间内进行幂等性处理 | "正在提交,请稍候..." | + +## 8. 界面与交互细节 + +### 8.1 反馈提交页面 (NEPS) +- **表单设计**: + - `标题`: 单行文本输入框。 + - `描述`: 多行文本域,允许输入较长内容。 + - `污染类型`: **单选框 (Radio Group)** 或 **下拉选择框 (Select)**,选项为`PM2.5`, `O3`, `NO2`, `SO2`, `OTHER`。 + - `严重等级`: **单选框 (Radio Group)**,选项为`高`, `中`, `低`,并附有简单的解释说明。 + - `地理位置`: 提供一个地图选点组件,用户点击地图即可自动填充文字地址和网格坐标。也应允许用户手动输入地址。 + - `照片上传`: 提供一个图片上传组件,支持多选、预览和删除已上传的图片。**明确提示支持的格式(JPG, PNG)和大小限制**。 +- **交互细节**: + - **加载状态**: 点击"确认提交"后,按钮变为"提交中..."并显示加载动画,同时整个表单区域置为不可编辑状态。 + - **成功提示**: 提交成功后,弹出一个全局成功的消息提示(e.g., "提交成功!感谢您的反馈。"),并自动跳转到反馈历史页面。 + - **失败提示**: 如果提交失败,根据错误类型给出明确的全局错误提示,并保持表单内容,让用户可以修改后重新提交。 + +### 8.2 反馈历史页面 (NEPS) +- 以列表形式展示用户所有提交过的反馈。 +- 每个列表项应以卡片形式展示,包含`标题`, `提交时间`, `事件ID`, 和一个醒目的**`状态标签`**。 +- **状态标签**: + - `待审核`: 蓝色或灰色 + - `待分配`: 橙色 + - `已分配`/`处理中`/`已提交`/`已完成`: 绿色 + - `已作废`/`已取消`: 红色 +- 点击卡片可进入反馈详情页,查看所有提交信息、`事件ID`和处理流程的详细时间线。 +- 对于状态为`PENDING_REVIEW`或`PENDING_ASSIGNMENT`的反馈,详情页提供"撤销提交"按钮。点击后需**二次确认弹窗**,防止误操作。 --- - + ### Design/角色与晋升体系设计.md - -# 角色与晋升体系 - 功能设计文档 - -## 1. 功能描述 -本功能模块定义了系统内所有用户的角色身份、权限边界、以及角色之间的流转路径。它旨在建立一个清晰、安全、易于管理的权限体系,支撑起"公众参与"和"内部管理"两条核心业务线。 - -## 2. 涉及角色 -- **所有角色**: `公众监督员`, `业务主管`, `网格员`, `系统管理员`, `决策者`。 -- **核心操作者**: `系统管理员 (ADMIN)` 是执行角色晋升和管理的主要操作者。 - -## 3. 业务规则 - -### 3.1 角色定义与权限 -- **公众监督员 (PUBLIC_SUPERVISOR)**: 外部用户,只能使用NEPS端,仅能查看和管理自己提交的反馈。 -- **业务主管 (SUPERVISOR)**: 内部核心**业务管理者**。使用NEPM管理端,负责其管辖区域内的**任务创建、分配、审核、取消**等全生命周期管理。 -- **网格员 (GRID_WORKER)**: 内部核心**任务执行者**。使用NEPG端,负责接收、执行和提交任务。 -- **系统管理员 (ADMIN)**: 内部核心**系统维护者**。使用NEPM管理端,负责用户账户管理(审批、创建、晋升)、系统配置等,**不参与日常业务流程**。 -- **决策者 (DECISION_MAKER)**: 内部高级观察用户,使用NEPV端,拥有对所有统计和可视化数据的**只读权限**。 - -### 3.2 角色晋升规则 -- 晋升是**单向**的、**逐级**的。 -- **禁止跨级晋升**。 -- **`PUBLIC_SUPERVISOR` -> `GRID_WORKER` (入职)**: 由用户在NEPS端主动提交申请,经由**系统管理员(ADMIN)**在NEPM端审批通过,代表用户正式成为内部员工。 -- **`GRID_WORKER` -> `SUPERVISOR` (晋升)**: 由其直属上级或**系统管理员(ADMIN)**在NEPM端为其执行`promote()`操作,代表从一线执行者晋升为业务管理者。 -- **`ADMIN` 与 `DECISION_MAKER`**: 特殊角色,不参与业务晋升流。由系统更高权限的管理员在后台直接配置。 - -### 3.3 数据模型映射 -- **实体关系**: 一个 `User` 实体**拥有一个** `Role` 属性。 -- **关键字段**: 角色和状态的管理主要依赖 `user_account` 表中的以下字段: - - `role` (Enum: `PUBLIC_SUPERVISOR`, `GRID_WORKER`, `SUPERVISOR`, `ADMIN`, `DECISION_MAKER`): 定义用户的角色。 - - `status` (Enum: `PENDING_APPROVAL`, `ACTIVE`, `REJECTED`, `DISABLED`): 定义用户的状态。新申请的用户状态为`PENDING_APPROVAL`,审批通过后为`ACTIVE`。 -- **操作**: 角色晋升或状态变更是对 `user_account` 表中特定记录的 `role` 和 `status` 字段的原子更新。 - -### 3.4 权限矩阵 (Permission Matrix) -| 功能模块 | API 端点 (示例) | `PUBLIC_SUPERVISOR` | `GRID_WORKER` | `SUPERVISOR` | `ADMIN` | `DECISION_MAKER` | -| :--- | :--- | :---: | :---: | :---: | :---: | :---: | -| **公众反馈** | `POST /api/feedback` | C | - | - | - | - | -| | `GET /api/feedback/my` | R | - | - | - | - | -| **任务处理** | `GET /api/worker/tasks` | - | R | - | - | - | -| | `POST /api/worker/tasks/{id}/submit`| - | U | - | - | - | -| **任务管理** | `GET /api/supervisor/tasks` | - | - | R | - | - | -| | `POST /api/supervisor/tasks` | - | - | C | - | - | -| | `PUT /api/supervisor/tasks/{id}` | - | - | U | - | - | -| | `DELETE /api/supervisor/tasks/{id}`| - | - | D | - | - | -| | `POST /api/supervisor/tasks/{id}/approve` | - | - | **Approve** | - | - | -| **用户管理** | `GET /api/admin/users` | - | - | R (下属) | R (全部) | - | -| | `POST /api/admin/users/{id}/promote` | - | - | - | **Promote** | - | -| **入职审批** | `GET /api/admin/approvals` | - | - | - | R | - | -| | `POST /api/admin/approvals/{id}/approve` | - | - | - | **Approve** | - | -| **数据大屏** | `GET /api/data-v/*` | - | - | - | R | R | -| *C=Create, R=Read, U=Update, D=Delete* | - -## 4. 功能实现流程 - -### 4.1 总体晋升流程 -```mermaid -graph TD - subgraph "外部用户区 (NEPS)" - A[公众注册
默认角色: PUBLIC_SUPERVISOR] --> B{个人中心}; - B --> C[提交入职申请
成为网格员]; - end - - subgraph "内部管理区 (NEPM)" - C --> D["管理员(ADMIN)审批列表"]; - D -- 通过 --> E[角色变更为 GRID_WORKER]; - E --> F{用户管理}; - F -- 管理员执行promote() --> G[角色变更为 SUPERVISOR]; - end - - subgraph "后台配置" - H[系统最高管理员] --> I[为特定用户分配
ADMIN/DECISION_MAKER角色]; - end - - style A fill:#E3F2FD,stroke:#333,stroke-width:2px - style E fill:#C8E6C9,stroke:#333,stroke-width:2px - style G fill:#FFD54F,stroke:#333,stroke-width:2px - style I fill:#FFECB3,stroke:#333,stroke-width:2px -``` - -### 4.2 申请入职时序图 -```mermaid -sequenceDiagram - participant User as "公众监督员 (NEPS)" - participant Frontend as "前端 (NEPS)" - participant Backend as "后端 (API)" - participant Admin as "系统管理员 (NEPM)" - - User->>Frontend: 点击"申请成为网格员" - Frontend->>User: 显示申请信息填写表单 - User->>Frontend: 填写并提交申请 - Frontend->>Backend: POST /api/users/apply-for-worker (携带申请信息) - Backend->>Backend: 创建申请记录, 更新用户状态为`PENDING_APPROVAL` - Backend-->>Frontend: 申请已提交 (200 OK) - Frontend->>User: 显示"申请已提交,等待管理员审批" - - Admin->>Backend: (在NEPM端) GET /api/admin/approvals - Backend-->>Admin: 返回待审批用户列表 - Admin->>Backend: POST /api/admin/approvals/{userId}/approve - Backend->>Backend: 校验权限, 更新用户role为`GRID_WORKER`, status为`ACTIVE` - Backend-->>Admin: 审批成功 (200 OK) -``` - -### 4.3 角色晋升时序图 -```mermaid -sequenceDiagram - participant Admin as "系统管理员 (NEPM)" - participant Frontend as "前端 (NEPM)" - participant Backend as "后端 (API)" - participant User as "被晋升的用户 (Grid Worker)" - - Admin->>Frontend: 在用户管理列表, 点击"晋升为主管" - Frontend->>Frontend: 弹出二次确认对话框 - Admin->>Frontend: 确认操作 - Frontend->>Backend: POST /api/admin/users/{userId}/promote - Backend->>Backend: 校验管理员权限 - Backend->>Backend: 校验目标用户当前角色为`GRID_WORKER` - Backend->>Backend: 更新用户role为`SUPERVISOR` - Backend-->>Frontend: 晋升成功 (200 OK) - Frontend->>Admin: 刷新列表, 显示用户新角色 - - User->>Backend: (下次登录或刷新页面时) - Backend-->>User: 返回更新后的角色信息和权限 - Note right of User: 用户界面(菜单、操作按钮)随新角色动态更新 -``` - -## 5. API接口设计 - -### 5.1 用户申请成为网格员 -- **URL**: `POST /api/users/apply-for-worker` -- **权限**: `PUBLIC_SUPERVISOR` -- **请求体**: `applicationReason`, `experience` -- **响应**: `200 OK` - -### 5.2 管理员获取待审批列表 -- **URL**: `GET /api/admin/approvals` -- **权限**: `ADMIN` -- **响应**: `200 OK`, 返回用户申请列表 `[{userId, name, applicationTime, ...}]` - -### 5.3 管理员审批 -- **URL**: `POST /api/admin/approvals/{userId}/approve` 或 `.../reject` -- **权限**: `ADMIN` -- **请求体**: (可选) `rejectionReason` -- **响应**: `200 OK` - -### 5.4 管理员晋升用户 -- **URL**: `POST /api/admin/users/{userId}/promote` -- **权限**: `ADMIN` -- **响应**: `200 OK` - -## 6. 界面设计要求 - -### 6.1 NEPS端 (公众监督员) -- 在"个人中心"或"我的"页面,应有一个醒目的入口,如"申请成为网格员"按钮。 -- 点击后, 弹出一个表单或新页面, 要求用户填写申请理由、相关经验等附加信息, 并提交。 -- 提交后,按钮应变为"审批中..."的不可点击状态。 -- 用户的申请状态(待审批、已通过、已驳回)应在个人中心清晰展示。若被驳回,应显示驳回原因。 - -### 6.2 NEPM端 (管理员 ADMIN) -- 在侧边栏应有"用户管理"和"入职审批"两个菜单项。 -- **入职审批页面**: 以列表形式展示所有待审批的申请,包含申请人姓名、申请时间、申请理由等。管理员可点击"通过"或"驳回"。操作后有明确的成功提示,且该条目从列表中移除。 -- **用户管理页面**: - - 以表格形式展示其管辖范围内的所有内部用户列表(包括网格员、主管),并可通过角色、状态进行筛选。 - - 表格的"操作"列中,应包含对符合条件的`GRID_WORKER`的"晋升为主管"按钮。 - - 点击"晋升"按钮,应有二次确认弹窗,明确告知"用户XXX的角色将从GridWorker变更为Supervisor",防止误操作。操作成功后,该用户的角色信息在表格中应立即更新。 + +# 角色与晋升体系 - 功能设计文档 + +## 1. 功能描述 +本功能模块定义了系统内所有用户的角色身份、权限边界、以及角色之间的流转路径。它旨在建立一个清晰、安全、易于管理的权限体系,支撑起"公众参与"和"内部管理"两条核心业务线。 + +## 2. 涉及角色 +- **所有角色**: `公众监督员`, `业务主管`, `网格员`, `系统管理员`, `决策者`。 +- **核心操作者**: `系统管理员 (ADMIN)` 是执行角色晋升和管理的主要操作者。 + +## 3. 业务规则 + +### 3.1 角色定义与权限 +- **公众监督员 (PUBLIC_SUPERVISOR)**: 外部用户,只能使用NEPS端,仅能查看和管理自己提交的反馈。 +- **业务主管 (SUPERVISOR)**: 内部核心**业务管理者**。使用NEPM管理端,负责其管辖区域内的**任务创建、分配、审核、取消**等全生命周期管理。 +- **网格员 (GRID_WORKER)**: 内部核心**任务执行者**。使用NEPG端,负责接收、执行和提交任务。 +- **系统管理员 (ADMIN)**: 内部核心**系统维护者**。使用NEPM管理端,负责用户账户管理(审批、创建、晋升)、系统配置等,**不参与日常业务流程**。 +- **决策者 (DECISION_MAKER)**: 内部高级观察用户,使用NEPV端,拥有对所有统计和可视化数据的**只读权限**。 + +### 3.2 角色晋升规则 +- 晋升是**单向**的、**逐级**的。 +- **禁止跨级晋升**。 +- **`PUBLIC_SUPERVISOR` -> `GRID_WORKER` (入职)**: 由用户在NEPS端主动提交申请,经由**系统管理员(ADMIN)**在NEPM端审批通过,代表用户正式成为内部员工。 +- **`GRID_WORKER` -> `SUPERVISOR` (晋升)**: 由其直属上级或**系统管理员(ADMIN)**在NEPM端为其执行`promote()`操作,代表从一线执行者晋升为业务管理者。 +- **`ADMIN` 与 `DECISION_MAKER`**: 特殊角色,不参与业务晋升流。由系统更高权限的管理员在后台直接配置。 + +### 3.3 数据模型映射 +- **实体关系**: 一个 `User` 实体**拥有一个** `Role` 属性。 +- **关键字段**: 角色和状态的管理主要依赖 `user_account` 表中的以下字段: + - `role` (Enum: `PUBLIC_SUPERVISOR`, `GRID_WORKER`, `SUPERVISOR`, `ADMIN`, `DECISION_MAKER`): 定义用户的角色。 + - `status` (Enum: `PENDING_APPROVAL`, `ACTIVE`, `REJECTED`, `DISABLED`): 定义用户的状态。新申请的用户状态为`PENDING_APPROVAL`,审批通过后为`ACTIVE`。 +- **操作**: 角色晋升或状态变更是对 `user_account` 表中特定记录的 `role` 和 `status` 字段的原子更新。 + +### 3.4 权限矩阵 (Permission Matrix) +| 功能模块 | API 端点 (示例) | `PUBLIC_SUPERVISOR` | `GRID_WORKER` | `SUPERVISOR` | `ADMIN` | `DECISION_MAKER` | +| :--- | :--- | :---: | :---: | :---: | :---: | :---: | +| **公众反馈** | `POST /api/feedback` | C | - | - | - | - | +| | `GET /api/feedback/my` | R | - | - | - | - | +| **任务处理** | `GET /api/worker/tasks` | - | R | - | - | - | +| | `POST /api/worker/tasks/{id}/submit`| - | U | - | - | - | +| **任务管理** | `GET /api/supervisor/tasks` | - | - | R | - | - | +| | `POST /api/supervisor/tasks` | - | - | C | - | - | +| | `PUT /api/supervisor/tasks/{id}` | - | - | U | - | - | +| | `DELETE /api/supervisor/tasks/{id}`| - | - | D | - | - | +| | `POST /api/supervisor/tasks/{id}/approve` | - | - | **Approve** | - | - | +| **用户管理** | `GET /api/admin/users` | - | - | R (下属) | R (全部) | - | +| | `POST /api/admin/users/{id}/promote` | - | - | - | **Promote** | - | +| **入职审批** | `GET /api/admin/approvals` | - | - | - | R | - | +| | `POST /api/admin/approvals/{id}/approve` | - | - | - | **Approve** | - | +| **数据大屏** | `GET /api/data-v/*` | - | - | - | R | R | +| *C=Create, R=Read, U=Update, D=Delete* | + +## 4. 功能实现流程 + +### 4.1 总体晋升流程 +```mermaid +graph TD + subgraph "外部用户区 (NEPS)" + A[公众注册
默认角色: PUBLIC_SUPERVISOR] --> B{个人中心}; + B --> C[提交入职申请
成为网格员]; + end + + subgraph "内部管理区 (NEPM)" + C --> D["管理员(ADMIN)审批列表"]; + D -- 通过 --> E[角色变更为 GRID_WORKER]; + E --> F{用户管理}; + F -- 管理员执行promote() --> G[角色变更为 SUPERVISOR]; + end + + subgraph "后台配置" + H[系统最高管理员] --> I[为特定用户分配
ADMIN/DECISION_MAKER角色]; + end + + style A fill:#E3F2FD,stroke:#333,stroke-width:2px + style E fill:#C8E6C9,stroke:#333,stroke-width:2px + style G fill:#FFD54F,stroke:#333,stroke-width:2px + style I fill:#FFECB3,stroke:#333,stroke-width:2px +``` + +### 4.2 申请入职时序图 +```mermaid +sequenceDiagram + participant User as "公众监督员 (NEPS)" + participant Frontend as "前端 (NEPS)" + participant Backend as "后端 (API)" + participant Admin as "系统管理员 (NEPM)" + + User->>Frontend: 点击"申请成为网格员" + Frontend->>User: 显示申请信息填写表单 + User->>Frontend: 填写并提交申请 + Frontend->>Backend: POST /api/users/apply-for-worker (携带申请信息) + Backend->>Backend: 创建申请记录, 更新用户状态为`PENDING_APPROVAL` + Backend-->>Frontend: 申请已提交 (200 OK) + Frontend->>User: 显示"申请已提交,等待管理员审批" + + Admin->>Backend: (在NEPM端) GET /api/admin/approvals + Backend-->>Admin: 返回待审批用户列表 + Admin->>Backend: POST /api/admin/approvals/{userId}/approve + Backend->>Backend: 校验权限, 更新用户role为`GRID_WORKER`, status为`ACTIVE` + Backend-->>Admin: 审批成功 (200 OK) +``` + +### 4.3 角色晋升时序图 +```mermaid +sequenceDiagram + participant Admin as "系统管理员 (NEPM)" + participant Frontend as "前端 (NEPM)" + participant Backend as "后端 (API)" + participant User as "被晋升的用户 (Grid Worker)" + + Admin->>Frontend: 在用户管理列表, 点击"晋升为主管" + Frontend->>Frontend: 弹出二次确认对话框 + Admin->>Frontend: 确认操作 + Frontend->>Backend: POST /api/admin/users/{userId}/promote + Backend->>Backend: 校验管理员权限 + Backend->>Backend: 校验目标用户当前角色为`GRID_WORKER` + Backend->>Backend: 更新用户role为`SUPERVISOR` + Backend-->>Frontend: 晋升成功 (200 OK) + Frontend->>Admin: 刷新列表, 显示用户新角色 + + User->>Backend: (下次登录或刷新页面时) + Backend-->>User: 返回更新后的角色信息和权限 + Note right of User: 用户界面(菜单、操作按钮)随新角色动态更新 +``` + +## 5. API接口设计 + +### 5.1 用户申请成为网格员 +- **URL**: `POST /api/users/apply-for-worker` +- **权限**: `PUBLIC_SUPERVISOR` +- **请求体**: `applicationReason`, `experience` +- **响应**: `200 OK` + +### 5.2 管理员获取待审批列表 +- **URL**: `GET /api/admin/approvals` +- **权限**: `ADMIN` +- **响应**: `200 OK`, 返回用户申请列表 `[{userId, name, applicationTime, ...}]` + +### 5.3 管理员审批 +- **URL**: `POST /api/admin/approvals/{userId}/approve` 或 `.../reject` +- **权限**: `ADMIN` +- **请求体**: (可选) `rejectionReason` +- **响应**: `200 OK` + +### 5.4 管理员晋升用户 +- **URL**: `POST /api/admin/users/{userId}/promote` +- **权限**: `ADMIN` +- **响应**: `200 OK` + +## 6. 界面设计要求 + +### 6.1 NEPS端 (公众监督员) +- 在"个人中心"或"我的"页面,应有一个醒目的入口,如"申请成为网格员"按钮。 +- 点击后, 弹出一个表单或新页面, 要求用户填写申请理由、相关经验等附加信息, 并提交。 +- 提交后,按钮应变为"审批中..."的不可点击状态。 +- 用户的申请状态(待审批、已通过、已驳回)应在个人中心清晰展示。若被驳回,应显示驳回原因。 + +### 6.2 NEPM端 (管理员 ADMIN) +- 在侧边栏应有"用户管理"和"入职审批"两个菜单项。 +- **入职审批页面**: 以列表形式展示所有待审批的申请,包含申请人姓名、申请时间、申请理由等。管理员可点击"通过"或"驳回"。操作后有明确的成功提示,且该条目从列表中移除。 +- **用户管理页面**: + - 以表格形式展示其管辖范围内的所有内部用户列表(包括网格员、主管),并可通过角色、状态进行筛选。 + - 表格的"操作"列中,应包含对符合条件的`GRID_WORKER`的"晋升为主管"按钮。 + - 点击"晋升"按钮,应有二次确认弹窗,明确告知"用户XXX的角色将从GridWorker变更为Supervisor",防止误操作。操作成功后,该用户的角色信息在表格中应立即更新。 --- - + ### Design/决策者大屏功能设计.md - -# 决策者大屏功能 - 设计文档 - -## 1. 功能描述 -本功能模块是专为决策者和高级管理员(NEPV端)设计的全局态势感知中心。它通过一系列数据可视化图表、关键绩效指标(KPI)和地图,将系统采集和处理的海量数据,转化为直观、易于理解的宏观洞察,旨在为环保工作的战略规划和决策提供强有力的数据支持。 - -## 2. 涉及角色 -- **主要使用者**: `决策者 (DECISION_MAKER)` -- **次要使用者**: `系统管理员 (ADMIN)` (需要访问以进行日常运营监控) - -## 3. 业务规则 - -### 3.1 权限与数据规则 -- **完全只读**: 大屏上的所有元素均为只读,不提供任何数据修改、删除或提交的入口。 -- **数据聚合性**: 除非特殊说明,大屏展示的数据均为聚合后的统计结果(如区域平均值、月度总数),不暴露任何单个反馈或用户的详细隐私信息。 -- **实时性**: 部分KPI指标(如"今日新增反馈")要求准实时更新,后端需提供高效的查询接口。大部分统计图表数据可通过后台定时任务(如每小时或每日)预计算并缓存,以提升加载性能。 -- **实时性与缓存**: - - **实时数据**: 关键KPI指标(如"今日新增反馈")和实时检测数量图,应通过WebSocket或短轮询实现准实时更新。 - - **缓存策略**: 大部分统计图表(如月度趋势、类型分布)的数据应由后端的定时任务(如每小时)预先计算,并存储在Redis等内存数据库中。前端请求API时,直接从缓存读取,极大提升加载速度。缓存的Key设计应包含日期和区域等维度,例如`dashboard:aqi_trend:2024-07`。 - - **缓存失效**: 定时任务在生成新数据后,会覆盖旧的缓存,实现自动更新。 - -### 3.2 核心组件数据来源 -- **KPI指标卡**: 直接查询数据库,对`feedback`和`user_account`等表进行快速COUNT或AVG聚合。 -- **统计图表 (趋势图、分布图)**: 由后端的聚合服务,基于`aqi_data`和`feedback`表,按指定维度(时间、类型)进行GROUP BY计算后生成。 -- **污染源热力图**: 后端聚合`feedback`表中的`grid_x`, `grid_y`坐标和`severityLevel`,生成一个包含坐标和权重的数据集,供前端渲染。数据同样需要被缓存。 -- **AI年度报告**: - - 由一个年度批处理任务(Cron Job)在每年年底或次年年初自动触发。 - - 该任务负责从数据库抽取全年关键统计数据,形成一份综合材料。 - - 调用`deepseek-v3-250324`大模型API,将结构化的统计数据和分析要点作为Prompt,生成一份图文并茂的分析报告。 - - 生成的报告(如PDF或Markdown格式)存储在文件服务器上,大屏端仅提供一个下载链接。 - -### 3.3 交互设计规则 -- **全局筛选**: 在大屏顶部或侧边提供全局筛选控件,至少包括 **时间范围选择器**(如"近7天", "本月", "本年", 自定义范围)和 **区域选择器**(当下钻到省市级别时)。所有组件都应响应这些筛选条件的变化。 -- **图表联动与下钻**: - - **联动**: 点击饼图的某个扇区(如"工业污染"),其他图表(如AQI趋势图、热力图)应能自动筛选并重新渲染,仅显示与"工业污染"相关的数据。 - - **下钻**: 在地图或区域排行榜上,点击某个省份,可以下钻到该省的市级数据视图。此时,应有明显的面包屑导航提示当前所处的层级(如"全国 > 浙江省")。 - -## 4. 功能实现流程 - -### 4.1 数据流架构 -```mermaid -graph TD - subgraph "数据源 (DB)" - A[aqi_data] - B[feedback] - C[user_account] - end - - subgraph "数据处理层" - D[定时聚合服务 (Cron Job)] - E[实时API服务] - F[

缓存 (Redis)

] - end - - subgraph "前端大屏 (NEPV)" - G[KPI 指标卡] - H[AQI 趋势图] - I[污染类型分布图] - J[污染源热力图] - K[年度报告下载] - L[WebSocket 连接] - end - - A & B & C --> D - D --> F - B & C --> E - - E -- 实时查询 --> G - E -- 建立长连接 --> L - L -- 推送实时数据 --> G - - F -- 读取缓存 --> H - F -- 读取缓存 --> I - F -- 读取缓存 --> J - - subgraph "AI服务" - M[AI报告生成服务 (Annual Cron)] - N[大模型 API] - O[文件服务器] - end - - A & B & C --> M - M --> N - N --> M - M --> O - O --> K -``` - -### 4.2 AI年度报告生成时序图 -```mermaid -sequenceDiagram - participant Cron as "年度定时任务 (Cron Job)" - participant Service as "报告生成服务" - participant DB as "数据库" - participant LLM as "大模型API" - participant Storage as "文件服务器 (S3/MinIO)" - - Cron->>Service: 触发年度报告生成任务 (e.g., for year 2024) - Service->>DB: 查询全年关键数据 (AQI, Feedback, etc.) - DB-->>Service: 返回年度统计数据 - Service->>Service: 根据数据构建Prompt (包含统计表格和分析要点) - Service->>LLM: 发送请求 (POST /api/generate-report) - LLM-->>Service: 返回生成的报告内容 (Markdown/HTML) - Service->>Service: (可选) 将报告内容渲染为PDF - Service->>Storage: 上传报告文件 (e.g., report-2024.pdf) - Storage-->>Service: 返回文件URL - Service->>DB: 将报告URL存入`annual_reports`表 - DB-->>Service: 存储成功 -``` - -## 5. API 接口设计 - -### 5.1 KPI 指标接口 -- **URL**: `GET /api/data-v/kpis` -- **查询参数**: `?timeRange=...&area=...` -- **响应**: `200 OK`, 返回 `[{ "title": "累计反馈总数", "value": 1024, "change": "+5%" }, ...]` - -### 5.2 图表数据接口 (通用) -- **URL**: `GET /api/data-v/chart` -- **查询参数**: `?name=aqiTrend&timeRange=...&area=...` (name用于指定图表类型) -- **响应**: `200 OK`, 返回符合ECharts等图表库格式要求的数据结构。 - -### 5.3 热力图数据接口 -- **URL**: `GET /api/data-v/heatmap` -- **查询参数**: `?timeRange=...&area=...` -- **响应**: `200 OK`, 返回 `[{ "lng": 120.15, "lat": 30.28, "count": 95 }, ...]` - -### 5.4 实时数据推送 -- **WebSocket**: `ws://your-domain/api/data-v/realtime` -- **推送消息格式**: `{ "type": "newFeedback", "data": { ... } }` or `{ "type": "kpiUpdate", "data": { ... } }` - -## 6. 界面设计要求 - -### 6.1 整体布局与风格 -- **主题**: 采用深色科技感主题(如深蓝色、暗灰色背景),以突出数据图表的色彩。 -- **布局**: 基于`1920x1080`分辨率进行设计,采用栅格系统(如24列)进行灵活布局,确保在主流大屏上不变形。 -- **组件**: 所有可视化组件(Widgets)以卡片形式组织,每个卡片都有清晰的标题。 -- **交互性**: 所有图表在鼠标悬浮时应显示详细的Tooltip提示。支持点击事件,用于图表间的联动和下钻。 -- **技术选型**: 推荐使用成熟的图表库,如`ECharts`或`AntV`,来实现高质量的可视化效果。 - -### 6.2 核心可视化组件详解 - -- **顶部 - KPI指标行 & 全局筛选器**: - - 在屏幕顶端横向排列4-6个核心指标卡。 - - 指标行旁边或下方设置全局筛选器,包括时间范围和区域选择。 - - 每个卡片包含一个醒目的图标、一个加粗的巨大数值、和一个清晰的指标名称(如"累计反馈总数"、"当前活跃网格员"、"本月平均AQI")。 - -- **中部 - 可交互的污染源热力图**: - - **需求**: 在地图上查看可视化污染源热力图。 - - **呈现形式**: 占据屏幕最大面积的核心组件。在二维网格地图上,根据各网格内高严重等级反馈的数量和权重,用不同的颜色(如从蓝色到红色)渲染,形成热力效果。地图应支持基本的缩放和拖拽操作。 - -- **图表组件 - 左侧面板**: - - **空气质量超标趋势图 (折线图)**: - - **需求**: 查看过去12个月的空气质量超标趋势。 - - **呈现形式**: X轴为过去12个月份,Y轴为超标天数或超标事件次数。用平滑的曲线展示AQI超标趋势,并可提供与去年同期的对比线。 - - **总AQI及分项污染物超标统计 (柱形图)**: - - **需求**: 查看总AQI及各分项污染物浓度超标的累计统计。 - - **呈现形式**: X轴为污染物类型(总AQI, PM2.5, O3, NO2, SO2等),Y轴为本年度累计超标次数。每个污染物为一根柱子,清晰对比。 - -- **图表组件 - 右侧面板**: - - **AQI级别分布图 (饼图)**: - - **需求**: 查看AQI级别分布。 - - **呈现形式**: 标准饼图,将AQI数据按标准(优、良、轻度污染、中度污染、重度污染、严重污染)划分,展示各级别占比。 - - **各省市网格覆盖率 (饼图)**: - - **需求**: 查看各省市的网格覆盖率。 - - **呈现形式**: 饼图展示已覆盖与未覆盖网格的**总体比例**。为获得更佳体验,可提供下钻功能或辅以排行榜/地图着色展示各省市详情。 - - **实时空气质量检测数量 (实时折线图)**: - - **需求**: 查看实时的空气质量检测数量统计。 - - **呈现形式**: X轴为过去24小时,Y轴为该时段内完成的AQI检测数,线条实时或准实时更新。 - - **AI年度报告下载**: - - 一个简洁的卡片,包含报告年份,和一个醒目的"下载报告"按钮。 + +# 决策者大屏功能 - 设计文档 + +## 1. 功能描述 +本功能模块是专为决策者和高级管理员(NEPV端)设计的全局态势感知中心。它通过一系列数据可视化图表、关键绩效指标(KPI)和地图,将系统采集和处理的海量数据,转化为直观、易于理解的宏观洞察,旨在为环保工作的战略规划和决策提供强有力的数据支持。 + +## 2. 涉及角色 +- **主要使用者**: `决策者 (DECISION_MAKER)` +- **次要使用者**: `系统管理员 (ADMIN)` (需要访问以进行日常运营监控) + +## 3. 业务规则 + +### 3.1 权限与数据规则 +- **完全只读**: 大屏上的所有元素均为只读,不提供任何数据修改、删除或提交的入口。 +- **数据聚合性**: 除非特殊说明,大屏展示的数据均为聚合后的统计结果(如区域平均值、月度总数),不暴露任何单个反馈或用户的详细隐私信息。 +- **实时性**: 部分KPI指标(如"今日新增反馈")要求准实时更新,后端需提供高效的查询接口。大部分统计图表数据可通过后台定时任务(如每小时或每日)预计算并缓存,以提升加载性能。 +- **实时性与缓存**: + - **实时数据**: 关键KPI指标(如"今日新增反馈")和实时检测数量图,应通过WebSocket或短轮询实现准实时更新。 + - **缓存策略**: 大部分统计图表(如月度趋势、类型分布)的数据应由后端的定时任务(如每小时)预先计算,并存储在Redis等内存数据库中。前端请求API时,直接从缓存读取,极大提升加载速度。缓存的Key设计应包含日期和区域等维度,例如`dashboard:aqi_trend:2024-07`。 + - **缓存失效**: 定时任务在生成新数据后,会覆盖旧的缓存,实现自动更新。 + +### 3.2 核心组件数据来源 +- **KPI指标卡**: 直接查询数据库,对`feedback`和`user_account`等表进行快速COUNT或AVG聚合。 +- **统计图表 (趋势图、分布图)**: 由后端的聚合服务,基于`aqi_data`和`feedback`表,按指定维度(时间、类型)进行GROUP BY计算后生成。 +- **污染源热力图**: 后端聚合`feedback`表中的`grid_x`, `grid_y`坐标和`severityLevel`,生成一个包含坐标和权重的数据集,供前端渲染。数据同样需要被缓存。 +- **AI年度报告**: + - 由一个年度批处理任务(Cron Job)在每年年底或次年年初自动触发。 + - 该任务负责从数据库抽取全年关键统计数据,形成一份综合材料。 + - 调用`deepseek-v3-250324`大模型API,将结构化的统计数据和分析要点作为Prompt,生成一份图文并茂的分析报告。 + - 生成的报告(如PDF或Markdown格式)存储在文件服务器上,大屏端仅提供一个下载链接。 + +### 3.3 交互设计规则 +- **全局筛选**: 在大屏顶部或侧边提供全局筛选控件,至少包括 **时间范围选择器**(如"近7天", "本月", "本年", 自定义范围)和 **区域选择器**(当下钻到省市级别时)。所有组件都应响应这些筛选条件的变化。 +- **图表联动与下钻**: + - **联动**: 点击饼图的某个扇区(如"工业污染"),其他图表(如AQI趋势图、热力图)应能自动筛选并重新渲染,仅显示与"工业污染"相关的数据。 + - **下钻**: 在地图或区域排行榜上,点击某个省份,可以下钻到该省的市级数据视图。此时,应有明显的面包屑导航提示当前所处的层级(如"全国 > 浙江省")。 + +## 4. 功能实现流程 + +### 4.1 数据流架构 +```mermaid +graph TD + subgraph "数据源 (DB)" + A[aqi_data] + B[feedback] + C[user_account] + end + + subgraph "数据处理层" + D[定时聚合服务 (Cron Job)] + E[实时API服务] + F[

缓存 (Redis)

] + end + + subgraph "前端大屏 (NEPV)" + G[KPI 指标卡] + H[AQI 趋势图] + I[污染类型分布图] + J[污染源热力图] + K[年度报告下载] + L[WebSocket 连接] + end + + A & B & C --> D + D --> F + B & C --> E + + E -- 实时查询 --> G + E -- 建立长连接 --> L + L -- 推送实时数据 --> G + + F -- 读取缓存 --> H + F -- 读取缓存 --> I + F -- 读取缓存 --> J + + subgraph "AI服务" + M[AI报告生成服务 (Annual Cron)] + N[大模型 API] + O[文件服务器] + end + + A & B & C --> M + M --> N + N --> M + M --> O + O --> K +``` + +### 4.2 AI年度报告生成时序图 +```mermaid +sequenceDiagram + participant Cron as "年度定时任务 (Cron Job)" + participant Service as "报告生成服务" + participant DB as "数据库" + participant LLM as "大模型API" + participant Storage as "文件服务器 (S3/MinIO)" + + Cron->>Service: 触发年度报告生成任务 (e.g., for year 2024) + Service->>DB: 查询全年关键数据 (AQI, Feedback, etc.) + DB-->>Service: 返回年度统计数据 + Service->>Service: 根据数据构建Prompt (包含统计表格和分析要点) + Service->>LLM: 发送请求 (POST /api/generate-report) + LLM-->>Service: 返回生成的报告内容 (Markdown/HTML) + Service->>Service: (可选) 将报告内容渲染为PDF + Service->>Storage: 上传报告文件 (e.g., report-2024.pdf) + Storage-->>Service: 返回文件URL + Service->>DB: 将报告URL存入`annual_reports`表 + DB-->>Service: 存储成功 +``` + +## 5. API 接口设计 + +### 5.1 KPI 指标接口 +- **URL**: `GET /api/data-v/kpis` +- **查询参数**: `?timeRange=...&area=...` +- **响应**: `200 OK`, 返回 `[{ "title": "累计反馈总数", "value": 1024, "change": "+5%" }, ...]` + +### 5.2 图表数据接口 (通用) +- **URL**: `GET /api/data-v/chart` +- **查询参数**: `?name=aqiTrend&timeRange=...&area=...` (name用于指定图表类型) +- **响应**: `200 OK`, 返回符合ECharts等图表库格式要求的数据结构。 + +### 5.3 热力图数据接口 +- **URL**: `GET /api/data-v/heatmap` +- **查询参数**: `?timeRange=...&area=...` +- **响应**: `200 OK`, 返回 `[{ "lng": 120.15, "lat": 30.28, "count": 95 }, ...]` + +### 5.4 实时数据推送 +- **WebSocket**: `ws://your-domain/api/data-v/realtime` +- **推送消息格式**: `{ "type": "newFeedback", "data": { ... } }` or `{ "type": "kpiUpdate", "data": { ... } }` + +## 6. 界面设计要求 + +### 6.1 整体布局与风格 +- **主题**: 采用深色科技感主题(如深蓝色、暗灰色背景),以突出数据图表的色彩。 +- **布局**: 基于`1920x1080`分辨率进行设计,采用栅格系统(如24列)进行灵活布局,确保在主流大屏上不变形。 +- **组件**: 所有可视化组件(Widgets)以卡片形式组织,每个卡片都有清晰的标题。 +- **交互性**: 所有图表在鼠标悬浮时应显示详细的Tooltip提示。支持点击事件,用于图表间的联动和下钻。 +- **技术选型**: 推荐使用成熟的图表库,如`ECharts`或`AntV`,来实现高质量的可视化效果。 + +### 6.2 核心可视化组件详解 + +- **顶部 - KPI指标行 & 全局筛选器**: + - 在屏幕顶端横向排列4-6个核心指标卡。 + - 指标行旁边或下方设置全局筛选器,包括时间范围和区域选择。 + - 每个卡片包含一个醒目的图标、一个加粗的巨大数值、和一个清晰的指标名称(如"累计反馈总数"、"当前活跃网格员"、"本月平均AQI")。 + +- **中部 - 可交互的污染源热力图**: + - **需求**: 在地图上查看可视化污染源热力图。 + - **呈现形式**: 占据屏幕最大面积的核心组件。在二维网格地图上,根据各网格内高严重等级反馈的数量和权重,用不同的颜色(如从蓝色到红色)渲染,形成热力效果。地图应支持基本的缩放和拖拽操作。 + +- **图表组件 - 左侧面板**: + - **空气质量超标趋势图 (折线图)**: + - **需求**: 查看过去12个月的空气质量超标趋势。 + - **呈现形式**: X轴为过去12个月份,Y轴为超标天数或超标事件次数。用平滑的曲线展示AQI超标趋势,并可提供与去年同期的对比线。 + - **总AQI及分项污染物超标统计 (柱形图)**: + - **需求**: 查看总AQI及各分项污染物浓度超标的累计统计。 + - **呈现形式**: X轴为污染物类型(总AQI, PM2.5, O3, NO2, SO2等),Y轴为本年度累计超标次数。每个污染物为一根柱子,清晰对比。 + +- **图表组件 - 右侧面板**: + - **AQI级别分布图 (饼图)**: + - **需求**: 查看AQI级别分布。 + - **呈现形式**: 标准饼图,将AQI数据按标准(优、良、轻度污染、中度污染、重度污染、严重污染)划分,展示各级别占比。 + - **各省市网格覆盖率 (饼图)**: + - **需求**: 查看各省市的网格覆盖率。 + - **呈现形式**: 饼图展示已覆盖与未覆盖网格的**总体比例**。为获得更佳体验,可提供下钻功能或辅以排行榜/地图着色展示各省市详情。 + - **实时空气质量检测数量 (实时折线图)**: + - **需求**: 查看实时的空气质量检测数量统计。 + - **呈现形式**: X轴为过去24小时,Y轴为该时段内完成的AQI检测数,线条实时或准实时更新。 + - **AI年度报告下载**: + - 一个简洁的卡片,包含报告年份,和一个醒目的"下载报告"按钮。 --- - + ### Design/数据库设计.md - -# 东软环保公众监督系统 - 数据库设计文档 - -## 1. 设计概述 -本数据库设计旨在支持一个高效、可扩展的环保监督系统。设计遵循第三范式(3NF),通过主外键关联确保数据的一致性和完整性。所有表均采用`InnoDB`存储引擎,以支持事务和行级锁定。 - -- **命名规范**: 表名采用小写下划线命名法(e.g., `user_account`),字段名采用驼峰命名法(e.g., `createdAt`)。 -- **主键**: 所有表均包含一个`BIGINT`类型的自增主键`id`。 -- **时间戳**: 关键业务表包含`createdAt`和`updatedAt`字段,用于追踪记录的创建和最后修改时间。 - -## 2. 实体关系图 (E-R Diagram) -```mermaid -erDiagram - USER_ACCOUNT }|--o{ FEEDBACK : "submits" - USER_ACCOUNT }|--o{ ASSIGNMENT_RECORD : "assigns (as supervisor)" - USER_ACCOUNT }|--o{ ASSIGNMENT_RECORD : "is assigned (as grid_worker)" - USER_ACCOUNT }|--o{ AQI_DATA : "reports" - USER_ACCOUNT }|--o{ USER_LOGIN_HISTORY : "logs" - - FEEDBACK ||--|{ ASSIGNMENT_RECORD : "is subject of" - FEEDBACK ||--o{ AQI_DATA : "is verified by" - FEEDBACK ||--|{ FEEDBACK_IMAGE : "has" - - GRID ||--o{ AQI_DATA : "is location for" - - USER_ACCOUNT { - BIGINT id PK - VARCHAR name - VARCHAR phone "UK" - VARCHAR email "UK" - VARCHAR password - VARCHAR gender - VARCHAR role - VARCHAR status - VARCHAR region - VARCHAR level - JSON skills - DATETIME createdAt - DATETIME updatedAt - } - - FEEDBACK { - BIGINT id PK - VARCHAR eventId "UK" - VARCHAR title - TEXT description - VARCHAR pollutionType - VARCHAR severityLevel - VARCHAR status - VARCHAR textAddress - INT gridX - INT gridY - BIGINT submitterId FK - DATETIME createdAt - DATETIME updatedAt - } - - FEEDBACK_IMAGE { - BIGINT id PK - BIGINT feedbackId FK - VARCHAR imageUrl "URL of the stored image" - VARCHAR imageName - DATETIME createdAt - } - - ASSIGNMENT_RECORD { - BIGINT id PK - BIGINT feedbackId FK - BIGINT gridWorkerId FK - BIGINT supervisorId FK - VARCHAR assignmentMethod - JSON algorithmDetails - VARCHAR status - DATETIME createdAt - } - - AQI_DATA { - BIGINT id PK - BIGINT feedbackId FK "Nullable" - BIGINT gridId FK - BIGINT reporterId FK - DOUBLE aqiValue - DOUBLE pm25 - DOUBLE pm10 - DOUBLE so2 - DOUBLE no2 - DOUBLE co - DOUBLE o3 - DATETIME recordTime - } - - GRID { - BIGINT id PK - INT gridX - INT gridY "UK" - VARCHAR cityName - VARCHAR districtName - BOOLEAN isObstacle - } - - USER_LOGIN_HISTORY { - BIGINT id PK - BIGINT userId FK - VARCHAR ipAddress - VARCHAR userAgent - VARCHAR status - DATETIME loginTime - } -``` - -## 3. 数据表详细设计 - -### 3.1 用户表 (`user_account`) -存储所有系统用户,包括公众、监督员、网格员、管理员和决策者。 - -| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | -| :--- | :--- | :--- | :--- | :--- | :--- | -| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | -| `name` | `VARCHAR`| 255 | No | | 真实姓名 | -| `phone` | `VARCHAR`| 50 | No | | 手机号 (唯一,用于登录) | -| `email` | `VARCHAR`| 255 | No | | 邮箱 (唯一,用于登录和验证) | -| `password`| `VARCHAR`| 255 | No | | BCrypt加密后的密码 | -| `gender` | `VARCHAR`| 10 | Yes | | 性别 (Enum: `MALE`, `FEMALE`, `UNKNOWN`) | -| `role` | `VARCHAR`| 50 | No | `PUBLIC_SUPERVISOR` | 角色 (Enum: `PUBLIC_SUPERVISOR`, `SUPERVISOR` (业务主管), `GRID_WORKER` (网格员), `ADMIN` (系统管理员), `DECISION_MAKER` (决策者)) | -| `status` | `VARCHAR`| 50 | No | `ACTIVE`| 账户状态 (Enum: `ACTIVE`, `INACTIVE`, `ON_LEAVE`) | -| `region` | `VARCHAR`| 255| Yes | | 所属区域 (格式: "省-市-区") | -| `level` | `VARCHAR`| 50 | Yes | | 行政级别 (Enum: `PROVINCE`, `CITY`) | -| `skills` | `JSON` | | Yes | | 技能标签 (仅对网格员有效, e.g., `["PM2.5", "O3"]`) | -| `createdAt` | `DATETIME` | | No | `CURRENT_TIMESTAMP` | 创建时间 | -| `updatedAt` | `DATETIME` | | No | `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` | 更新时间 | - -**索引设计**: -- `PRIMARY KEY (id)` -- `UNIQUE KEY uk_phone (phone)` -- `UNIQUE KEY uk_email (email)` -- `KEY idx_role_status (role, status)` -- `KEY idx_region_level (region, level)` - -### 3.2 反馈信息表 (`feedback`) -存储由公众监督员提交的污染反馈信息。 - -| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | -| :--- | :--- | :--- | :--- | :--- | :--- | -| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | -| `eventId`| `VARCHAR`| 255 | `UK, NOT NULL` | | 业务事件ID (USR...格式),对用户可见、唯一 | -| `title` | `VARCHAR`| 255 | No | | 反馈标题 | -| `description`| `TEXT` | | Yes | | 详细描述 | -| `pollutionType`|`VARCHAR`| 50 | No | | 污染类型 (Enum: `PM2.5`, `O3`, `NO2`, `SO2`, `OTHER`) | -| `severityLevel`|`VARCHAR`| 50 | No | | 严重等级 (Enum: `LOW`, `MEDIUM`, `HIGH`) | -| `status` | `VARCHAR`| 50 | No | `AI_REVIEWING`| 任务状态 (Enum: `AI_REVIEWING`, `AI_PROCESSING`, `PENDING_ASSIGNMENT`, `ASSIGNED`, `IN_PROGRESS`, `SUBMITTED`, `COMPLETED`, `CANCELLED`, `CLOSED_INVALID`) | -| `textAddress`| `VARCHAR`| 255 | Yes | | 文字地址 (格式: "省份-城市-区") | -| `gridX` | `INT` | | Yes | | 对应的网格X坐标 | -| `gridY` | `INT` | | Yes | | 对应的网格Y坐标 | -| `submitterId`| `BIGINT` | `FK -> user_account.id` | No | | 提交者ID | -| `createdAt` | `DATETIME` | | No | `CURRENT_TIMESTAMP` | 创建时间 | -| `updatedAt` | `DATETIME` | | No | `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` | 更新时间 | - -**索引设计**: -- `PRIMARY KEY (id)` -- `UNIQUE KEY uk_event_id (eventId)` -- `KEY idx_submitter_id (submitterId)` -- `KEY idx_status_created_at (status, createdAt)` -- `KEY idx_grid_coords (gridX, gridY)` - -### 3.3 反馈图片表 (`feedback_image`) -存储与反馈信息关联的图片。 - -| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | -| :--- | :--- | :--- | :--- | :--- | :--- | -| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | -| `feedbackId` | `BIGINT` | `FK -> feedback.id`| No | | 关联的反馈ID | -| `imageUrl` | `VARCHAR`| 512 | No | | 存储的图片URL | -| `imageName` | `VARCHAR`| 255 | Yes | | 图片原始文件名 | -| `createdAt` | `DATETIME`| | No |`CURRENT_TIMESTAMP`| 创建时间 | - -**索引设计**: -- `PRIMARY KEY (id)` -- `KEY idx_feedback_id (feedbackId)` - -### 3.4 任务分配记录表 (`assignment_record`) -记录每一次任务分配的详细信息。 - -| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | -| :--- | :--- | :--- | :--- | :--- | :--- | -| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | -| `feedbackId`| `BIGINT` | `FK -> feedback.id`| No | | 关联的反馈/任务ID | -| `gridWorkerId`|`BIGINT`|`FK -> user_account.id`| No | | 被分配的网格员ID | -| `supervisorId` | `BIGINT`| `FK -> user_account.id`| No | | 执行分配的主管ID | -| `assignmentMethod`|`VARCHAR`|50| No | | 分配方式 (Enum: `MANUAL`, `INTELLIGENT`) | -| `algorithmDetails`|`JSON`| | Yes | | 智能分配算法的快照 (e.g., ETR, 候选人) | -| `status` | `VARCHAR`| 50 | No | `PENDING`| 分配状态 (Enum: `PENDING`, `ACCEPTED`, `REJECTED`) | -| `createdAt` |`DATETIME`| | No |`CURRENT_TIMESTAMP`| 创建时间 | - -**索引设计**: -- `PRIMARY KEY (id)` -- `KEY idx_feedback_id (feedbackId)` -- `KEY idx_grid_worker_id_status (gridWorkerId, status)` -- `KEY idx_supervisor_id (supervisorId)` - -### 3.5 空气质量数据表 (`aqi_data`) -存储网格员现场核查后上报的精确AQI数据。 - -| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | -| :--- | :--- | :--- | :--- | :--- | :--- | -| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | -| `feedbackId`| `BIGINT` | `FK -> feedback.id`| Yes | | 关联的反馈ID (可为空,支持主动上报) | -| `gridId` | `BIGINT` | `FK -> grid.id`| No | | 数据采集所在的网格ID | -| `reporterId`| `BIGINT` | `FK -> user_account.id`| No | | 上报人ID(网格员)| -| `aqiValue` | `DOUBLE` | | Yes | | 综合AQI指数 | -| `pm25` | `DOUBLE` | | Yes | | PM2.5读数 (μg/m³) | -| `pm10` | `DOUBLE` | | Yes | | PM10读数 (μg/m³) | -| `so2` | `DOUBLE` | | Yes | | 二氧化硫读数 (μg/m³) | -| `no2` | `DOUBLE` | | Yes | | 二氧化氮读数 (μg/m³) | -| `co` | `DOUBLE` | | Yes | | 一氧化碳读数 (mg/m³) | -| `o3` | `DOUBLE` | | Yes | | 臭氧读数 (μg/m³) | -| `recordTime`|`DATETIME`| | No |`CURRENT_TIMESTAMP`| 记录时间 | - -**索引设计**: -- `PRIMARY KEY (id)` -- `KEY idx_feedback_id (feedbackId)` -- `KEY idx_grid_id_record_time (gridId, recordTime)` -- `KEY idx_reporter_id (reporterId)` - -### 3.6 网格地图表 (`grid`) -定义抽象的二维网格地图。 - -| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | -| :--- | :--- | :--- | :--- | :--- | :--- | -| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | -| `gridX` | `INT` | | No | | 网格X坐标 | -| `gridY` | `INT` | | No | | 网格Y坐标 | -| `cityName`| `VARCHAR`| 255 | Yes | | 所属城市 | -| `districtName`|`VARCHAR`|255| Yes | | 所属区域 | -| `isObstacle`|`BOOLEAN`| | No | `false`| 是否为障碍物 | - -**索引设计**: -- `PRIMARY KEY (id)` -- `UNIQUE KEY uk_grid_coords (gridX, gridY)` -- `KEY idx_city_district (cityName, districtName)` - -### 3.7 用户登录历史表 (`user_login_history`) -记录用户登录活动,用于安全审计。 - -| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | -| :--- | :--- | :--- | :--- | :--- | :--- | -| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | -| `userId` | `BIGINT` | `FK -> user_account.id`| No | | 登录用户ID | -| `ipAddress`| `VARCHAR`| 100 | Yes | | 登录IP地址 | -| `userAgent`| `VARCHAR`| 512 | Yes | | 浏览器或客户端信息 | -| `status` | `VARCHAR`| 50 | No | | 登录状态 (Enum: `SUCCESS`, `FAILED_PASSWORD`, `FAILED_LOCKED`) | -| `loginTime`| `DATETIME`| | No |`CURRENT_TIMESTAMP`| 登录时间 | - -**索引设计**: -- `PRIMARY KEY (id)` -- `KEY idx_user_id_login_time (userId, loginTime)` -- `KEY idx_ip_address (ipAddress)` - -## 4. 初始化数据 (Seed Data) -系统首次部署时,应向`user_account`表插入以下初始用户,以确保各角色可用。密码均为明文示例,实际存储时需加密。 - -| name | phone | email | password | role | status | region | level | -| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | -| "张建华"| "18800000001" | "zhang.jianguo@example.com"| "Admin@123456" | `ADMIN` | `ACTIVE` | "河北省" | `PROVINCE` | -| "李志强"| "18800000002" | "li.zhiqiang@example.com" | "Admin@123456" | `ADMIN` | `ACTIVE` | "河北省-石家庄市"| `CITY` | -| "王伟" | "18800000003" | "wang.wei@example.com" | "User@123456" | `GRID_WORKER`| `ACTIVE` | "河北省-石家庄市-长安区"| `CITY` | -| "刘丽" | "18800000004" | "li.li@example.com" | "User@123456" | `SUPERVISOR`| `ACTIVE` | "河北省-石家庄市-裕华区"| `CITY` | -| "陈思远"| "18800000005" | "chen.siyuan@example.com" | "User@123456" | `DECISION_MAKER`| `ACTIVE`| "全国" | `PROVINCE` | -| "赵敏" | "18800000006" | "zhao.min@example.com" | "User@123456" | `PUBLIC_SUPERVISOR`|`ACTIVE`| "河北省-石家庄市-桥西区"| `CITY` | + +# 东软环保公众监督系统 - 数据库设计文档 + +## 1. 设计概述 +本数据库设计旨在支持一个高效、可扩展的环保监督系统。设计遵循第三范式(3NF),通过主外键关联确保数据的一致性和完整性。所有表均采用`InnoDB`存储引擎,以支持事务和行级锁定。 + +- **命名规范**: 表名采用小写下划线命名法(e.g., `user_account`),字段名采用驼峰命名法(e.g., `createdAt`)。 +- **主键**: 所有表均包含一个`BIGINT`类型的自增主键`id`。 +- **时间戳**: 关键业务表包含`createdAt`和`updatedAt`字段,用于追踪记录的创建和最后修改时间。 + +## 2. 实体关系图 (E-R Diagram) +```mermaid +erDiagram + USER_ACCOUNT }|--o{ FEEDBACK : "submits" + USER_ACCOUNT }|--o{ ASSIGNMENT_RECORD : "assigns (as supervisor)" + USER_ACCOUNT }|--o{ ASSIGNMENT_RECORD : "is assigned (as grid_worker)" + USER_ACCOUNT }|--o{ AQI_DATA : "reports" + USER_ACCOUNT }|--o{ USER_LOGIN_HISTORY : "logs" + + FEEDBACK ||--|{ ASSIGNMENT_RECORD : "is subject of" + FEEDBACK ||--o{ AQI_DATA : "is verified by" + FEEDBACK ||--|{ FEEDBACK_IMAGE : "has" + + GRID ||--o{ AQI_DATA : "is location for" + + USER_ACCOUNT { + BIGINT id PK + VARCHAR name + VARCHAR phone "UK" + VARCHAR email "UK" + VARCHAR password + VARCHAR gender + VARCHAR role + VARCHAR status + VARCHAR region + VARCHAR level + JSON skills + DATETIME createdAt + DATETIME updatedAt + } + + FEEDBACK { + BIGINT id PK + VARCHAR eventId "UK" + VARCHAR title + TEXT description + VARCHAR pollutionType + VARCHAR severityLevel + VARCHAR status + VARCHAR textAddress + INT gridX + INT gridY + BIGINT submitterId FK + DATETIME createdAt + DATETIME updatedAt + } + + FEEDBACK_IMAGE { + BIGINT id PK + BIGINT feedbackId FK + VARCHAR imageUrl "URL of the stored image" + VARCHAR imageName + DATETIME createdAt + } + + ASSIGNMENT_RECORD { + BIGINT id PK + BIGINT feedbackId FK + BIGINT gridWorkerId FK + BIGINT supervisorId FK + VARCHAR assignmentMethod + JSON algorithmDetails + VARCHAR status + DATETIME createdAt + } + + AQI_DATA { + BIGINT id PK + BIGINT feedbackId FK "Nullable" + BIGINT gridId FK + BIGINT reporterId FK + DOUBLE aqiValue + DOUBLE pm25 + DOUBLE pm10 + DOUBLE so2 + DOUBLE no2 + DOUBLE co + DOUBLE o3 + DATETIME recordTime + } + + GRID { + BIGINT id PK + INT gridX + INT gridY "UK" + VARCHAR cityName + VARCHAR districtName + BOOLEAN isObstacle + } + + USER_LOGIN_HISTORY { + BIGINT id PK + BIGINT userId FK + VARCHAR ipAddress + VARCHAR userAgent + VARCHAR status + DATETIME loginTime + } +``` + +## 3. 数据表详细设计 + +### 3.1 用户表 (`user_account`) +存储所有系统用户,包括公众、监督员、网格员、管理员和决策者。 + +| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | +| `name` | `VARCHAR`| 255 | No | | 真实姓名 | +| `phone` | `VARCHAR`| 50 | No | | 手机号 (唯一,用于登录) | +| `email` | `VARCHAR`| 255 | No | | 邮箱 (唯一,用于登录和验证) | +| `password`| `VARCHAR`| 255 | No | | BCrypt加密后的密码 | +| `gender` | `VARCHAR`| 10 | Yes | | 性别 (Enum: `MALE`, `FEMALE`, `UNKNOWN`) | +| `role` | `VARCHAR`| 50 | No | `PUBLIC_SUPERVISOR` | 角色 (Enum: `PUBLIC_SUPERVISOR`, `SUPERVISOR` (业务主管), `GRID_WORKER` (网格员), `ADMIN` (系统管理员), `DECISION_MAKER` (决策者)) | +| `status` | `VARCHAR`| 50 | No | `ACTIVE`| 账户状态 (Enum: `ACTIVE`, `INACTIVE`, `ON_LEAVE`) | +| `region` | `VARCHAR`| 255| Yes | | 所属区域 (格式: "省-市-区") | +| `level` | `VARCHAR`| 50 | Yes | | 行政级别 (Enum: `PROVINCE`, `CITY`) | +| `skills` | `JSON` | | Yes | | 技能标签 (仅对网格员有效, e.g., `["PM2.5", "O3"]`) | +| `createdAt` | `DATETIME` | | No | `CURRENT_TIMESTAMP` | 创建时间 | +| `updatedAt` | `DATETIME` | | No | `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` | 更新时间 | + +**索引设计**: +- `PRIMARY KEY (id)` +- `UNIQUE KEY uk_phone (phone)` +- `UNIQUE KEY uk_email (email)` +- `KEY idx_role_status (role, status)` +- `KEY idx_region_level (region, level)` + +### 3.2 反馈信息表 (`feedback`) +存储由公众监督员提交的污染反馈信息。 + +| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | +| `eventId`| `VARCHAR`| 255 | `UK, NOT NULL` | | 业务事件ID (USR...格式),对用户可见、唯一 | +| `title` | `VARCHAR`| 255 | No | | 反馈标题 | +| `description`| `TEXT` | | Yes | | 详细描述 | +| `pollutionType`|`VARCHAR`| 50 | No | | 污染类型 (Enum: `PM2.5`, `O3`, `NO2`, `SO2`, `OTHER`) | +| `severityLevel`|`VARCHAR`| 50 | No | | 严重等级 (Enum: `LOW`, `MEDIUM`, `HIGH`) | +| `status` | `VARCHAR`| 50 | No | `AI_REVIEWING`| 任务状态 (Enum: `AI_REVIEWING`, `AI_PROCESSING`, `PENDING_ASSIGNMENT`, `ASSIGNED`, `IN_PROGRESS`, `SUBMITTED`, `COMPLETED`, `CANCELLED`, `CLOSED_INVALID`) | +| `textAddress`| `VARCHAR`| 255 | Yes | | 文字地址 (格式: "省份-城市-区") | +| `gridX` | `INT` | | Yes | | 对应的网格X坐标 | +| `gridY` | `INT` | | Yes | | 对应的网格Y坐标 | +| `submitterId`| `BIGINT` | `FK -> user_account.id` | No | | 提交者ID | +| `createdAt` | `DATETIME` | | No | `CURRENT_TIMESTAMP` | 创建时间 | +| `updatedAt` | `DATETIME` | | No | `CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` | 更新时间 | + +**索引设计**: +- `PRIMARY KEY (id)` +- `UNIQUE KEY uk_event_id (eventId)` +- `KEY idx_submitter_id (submitterId)` +- `KEY idx_status_created_at (status, createdAt)` +- `KEY idx_grid_coords (gridX, gridY)` + +### 3.3 反馈图片表 (`feedback_image`) +存储与反馈信息关联的图片。 + +| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | +| `feedbackId` | `BIGINT` | `FK -> feedback.id`| No | | 关联的反馈ID | +| `imageUrl` | `VARCHAR`| 512 | No | | 存储的图片URL | +| `imageName` | `VARCHAR`| 255 | Yes | | 图片原始文件名 | +| `createdAt` | `DATETIME`| | No |`CURRENT_TIMESTAMP`| 创建时间 | + +**索引设计**: +- `PRIMARY KEY (id)` +- `KEY idx_feedback_id (feedbackId)` + +### 3.4 任务分配记录表 (`assignment_record`) +记录每一次任务分配的详细信息。 + +| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | +| `feedbackId`| `BIGINT` | `FK -> feedback.id`| No | | 关联的反馈/任务ID | +| `gridWorkerId`|`BIGINT`|`FK -> user_account.id`| No | | 被分配的网格员ID | +| `supervisorId` | `BIGINT`| `FK -> user_account.id`| No | | 执行分配的主管ID | +| `assignmentMethod`|`VARCHAR`|50| No | | 分配方式 (Enum: `MANUAL`, `INTELLIGENT`) | +| `algorithmDetails`|`JSON`| | Yes | | 智能分配算法的快照 (e.g., ETR, 候选人) | +| `status` | `VARCHAR`| 50 | No | `PENDING`| 分配状态 (Enum: `PENDING`, `ACCEPTED`, `REJECTED`) | +| `createdAt` |`DATETIME`| | No |`CURRENT_TIMESTAMP`| 创建时间 | + +**索引设计**: +- `PRIMARY KEY (id)` +- `KEY idx_feedback_id (feedbackId)` +- `KEY idx_grid_worker_id_status (gridWorkerId, status)` +- `KEY idx_supervisor_id (supervisorId)` + +### 3.5 空气质量数据表 (`aqi_data`) +存储网格员现场核查后上报的精确AQI数据。 + +| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | +| `feedbackId`| `BIGINT` | `FK -> feedback.id`| Yes | | 关联的反馈ID (可为空,支持主动上报) | +| `gridId` | `BIGINT` | `FK -> grid.id`| No | | 数据采集所在的网格ID | +| `reporterId`| `BIGINT` | `FK -> user_account.id`| No | | 上报人ID(网格员)| +| `aqiValue` | `DOUBLE` | | Yes | | 综合AQI指数 | +| `pm25` | `DOUBLE` | | Yes | | PM2.5读数 (μg/m³) | +| `pm10` | `DOUBLE` | | Yes | | PM10读数 (μg/m³) | +| `so2` | `DOUBLE` | | Yes | | 二氧化硫读数 (μg/m³) | +| `no2` | `DOUBLE` | | Yes | | 二氧化氮读数 (μg/m³) | +| `co` | `DOUBLE` | | Yes | | 一氧化碳读数 (mg/m³) | +| `o3` | `DOUBLE` | | Yes | | 臭氧读数 (μg/m³) | +| `recordTime`|`DATETIME`| | No |`CURRENT_TIMESTAMP`| 记录时间 | + +**索引设计**: +- `PRIMARY KEY (id)` +- `KEY idx_feedback_id (feedbackId)` +- `KEY idx_grid_id_record_time (gridId, recordTime)` +- `KEY idx_reporter_id (reporterId)` + +### 3.6 网格地图表 (`grid`) +定义抽象的二维网格地图。 + +| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | +| `gridX` | `INT` | | No | | 网格X坐标 | +| `gridY` | `INT` | | No | | 网格Y坐标 | +| `cityName`| `VARCHAR`| 255 | Yes | | 所属城市 | +| `districtName`|`VARCHAR`|255| Yes | | 所属区域 | +| `isObstacle`|`BOOLEAN`| | No | `false`| 是否为障碍物 | + +**索引设计**: +- `PRIMARY KEY (id)` +- `UNIQUE KEY uk_grid_coords (gridX, gridY)` +- `KEY idx_city_district (cityName, districtName)` + +### 3.7 用户登录历史表 (`user_login_history`) +记录用户登录活动,用于安全审计。 + +| 字段名 | 数据类型 | 长度/约束 | 是否可空 | 默认值 | 描述 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| `id` | `BIGINT` | `PK, AUTO_INCREMENT`| No | | 唯一主键 | +| `userId` | `BIGINT` | `FK -> user_account.id`| No | | 登录用户ID | +| `ipAddress`| `VARCHAR`| 100 | Yes | | 登录IP地址 | +| `userAgent`| `VARCHAR`| 512 | Yes | | 浏览器或客户端信息 | +| `status` | `VARCHAR`| 50 | No | | 登录状态 (Enum: `SUCCESS`, `FAILED_PASSWORD`, `FAILED_LOCKED`) | +| `loginTime`| `DATETIME`| | No |`CURRENT_TIMESTAMP`| 登录时间 | + +**索引设计**: +- `PRIMARY KEY (id)` +- `KEY idx_user_id_login_time (userId, loginTime)` +- `KEY idx_ip_address (ipAddress)` + +## 4. 初始化数据 (Seed Data) +系统首次部署时,应向`user_account`表插入以下初始用户,以确保各角色可用。密码均为明文示例,实际存储时需加密。 + +| name | phone | email | password | role | status | region | level | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| "张建华"| "18800000001" | "zhang.jianguo@example.com"| "Admin@123456" | `ADMIN` | `ACTIVE` | "河北省" | `PROVINCE` | +| "李志强"| "18800000002" | "li.zhiqiang@example.com" | "Admin@123456" | `ADMIN` | `ACTIVE` | "河北省-石家庄市"| `CITY` | +| "王伟" | "18800000003" | "wang.wei@example.com" | "User@123456" | `GRID_WORKER`| `ACTIVE` | "河北省-石家庄市-长安区"| `CITY` | +| "刘丽" | "18800000004" | "li.li@example.com" | "User@123456" | `SUPERVISOR`| `ACTIVE` | "河北省-石家庄市-裕华区"| `CITY` | +| "陈思远"| "18800000005" | "chen.siyuan@example.com" | "User@123456" | `DECISION_MAKER`| `ACTIVE`| "全国" | `PROVINCE` | +| "赵敏" | "18800000006" | "zhao.min@example.com" | "User@123456" | `PUBLIC_SUPERVISOR`|`ACTIVE`| "河北省-石家庄市-桥西区"| `CITY` | --- - + ### Design/网格员任务处理功能设计.md - -# 网格员任务处理功能 - 设计文档 - -## 1. 功能描述 -本功能模块是为一线网格员(NEPG端)设计的核心操作界面。它使得网格员能够清晰地接收、查看和处理由管理员分配的污染勘查任务,并在简化的网格地图上获得路径指引,最终提交现场的AQI数据,完成任务闭环。 - -## 2. 涉及角色 -- **主要使用者**: `网格员 (GRID_WORKER)` - -## 3. 业务规则 - -### 3.1 任务接收与查看 -- 网格员登录后,首页即为任务列表。 -- 任务列表默认按`任务优先级`降序、`创建时间`升序排列。 -- **任务优先级**由后端动态计算,主要基于反馈的`severityLevel`(严重等级)和任务的等待时长。 -- 每个任务项应清晰展示关键信息:`标题`、`文字地址`、`优先级`、`状态`。 - -### 3.2 任务处理规则 -- **任务接受与拒绝**: - - 对于`severityLevel`为`HIGH`的任务,网格员**不可拒绝**。 - - 对于`severityLevel`为`LOW`或`MEDIUM`的任务,网格员在任务详情页可以点击"拒绝"按钮。 - - 拒绝时需选择一个预设的理由(如"当前任务繁忙"、"超出能力范围"等)。 - - 拒绝后,该任务会从该网格员的列表中移除,并由系统自动进行再分配。 -- **数据提交**: - - 网格员到达现场后,必须在任务详情页填写并提交`AQI数据`表单才能完成任务。 - - `AqiData`记录与`Feedback`记录强关联。 -- **主动上报**: 系统允许网格员在没有关联任务的情况下,主动上报其所在网格的AQI数据。 - -### 3.3 地图与定位规则 -- **实时定位**: 进入任务详情页时,App应请求获取网格员的实时地理位置,并在地图上标记。 -- **路径规划**: A*路径规划仅在首次进入详情页时计算一次。如果网格员偏离了规划路径,可以提供一个"重新规划"按钮。 -- **离线地图**: (未来规划)为节省流量和应对网络不佳的情况,可以考虑支持离线网格地图的缓存。 - -## 4. 功能实现流程 - -### 4.1 任务处理流程 -```mermaid -graph TD - subgraph "NEPG 端 (网格员操作)" - A[登录系统] --> B[查看任务列表]; - B --> C{选择一个任务}; - C --> D[查看任务详情]; - - D --> E{任务是否为HIGH等级?}; - E -- 否 --> F[显示"接受"和"拒绝"按钮]; - F -- 点击拒绝 --> G[任务被退回,流程结束]; - E -- 是 --> H[只显示"接受"按钮]; - - F -- 点击接受 --> I{处理任务}; - H --> I; - - I --> J[在网格地图上
查看路径指引]; - J --> K[到达现场
勘查并记录数据]; - K --> L[填写并提交
AQI数据报告]; - end - - subgraph "后端服务" - L --> M[创建AqiData记录]; - M --> N[更新Feedback状态
为SUBMITTED]; - end - - style D fill:#E3F2FD - style G fill:#FFCDD2 - style N fill:#C8E6C9 -``` - -### 4.2 主动上报流程 -```mermaid -graph TD - A[网格员在主界面
点击"主动上报"] --> B[进入上报页面]; - B --> C[系统自动获取
当前网格位置]; - C --> D[填写AQI数据表单]; - D --> E{点击提交}; - E --> F[后端创建无关联
feedbackId的AqiData记录]; - F --> G[提示"上报成功"]; - - style C fill:#E3F2FD - style F fill:#C8E6C9 -``` - -## 5. API 接口设计 - -### 5.1 获取我的任务列表 -- **URL**: `GET /api/worker/tasks` -- **权限**: `GRID_WORKER` -- **查询参数**: `status` (可选: `ASSIGNED`, `IN_PROGRESS`), `page`, `size` -- **成功响应** (`200 OK`): 返回分页的任务列表。 - -### 5.2 获取任务详情 -- **URL**: `GET /api/worker/tasks/{taskId}` -- **权限**: `GRID_WORKER` -- **成功响应** (`200 OK`): 返回任务的详细信息,包含公众提交的描述、图片,以及A*算法规划出的路径坐标点数组。 - -### 5.3 接受任务 -- **URL**: `POST /api/worker/tasks/{taskId}/accept` -- **权限**: `GRID_WORKER` -- **成功响应** (`200 OK`): `{ "message": "任务已接受" }` - -### 5.4 拒绝任务 -- **URL**: `POST /api/worker/tasks/{taskId}/reject` -- **权限**: `GRID_WORKER` -- **请求体**: `{"reason": "当前任务繁忙"}` -- **成功响应** (`200 OK`): `{ "message": "任务已拒绝" }` - -### 5.5 提交任务报告 -- **URL**: `POST /api/worker/tasks/{taskId}/submit` -- **权限**: `GRID_WORKER` -- **请求体** (application/json): - ```json - { - "notes": "已到现场检查,确认存在异味,已要求整改", - "aqiData": { - "pm25": 75.5, "pm10": 120.3, "so2": 15.8, - "no2": 40.2, "co": 1.2, "o3": 65.2 - } - } - ``` -- **成功响应** (`200 OK`): `{ "message": "任务报告提交成功,等待主管审核" }` - -### 5.6 主动上报AQI数据 -- **URL**: `POST /api/worker/aqi-data/report` -- **权限**: `GRID_WORKER` -- **请求体**: - ```json - { - "gridX": 15, - "gridY": 20, - "aqiData": { ... } - } - ``` -- **成功响应** (`201 Created`): `{ "message": "数据上报成功" }` - -## 6. 错误处理与边界情况 - -| 场景 | 触发条件 | 系统处理 | 用户提示 | -| :--- | :--- | :--- | :--- | -| **定位失败** | App无法获取GPS信号 | 地图模块显示默认位置(如市中心),并提示用户检查定位服务 | "无法获取您的位置,请检查GPS设置" | -| **提交时网络中断**| 点击提交后,网络断开 | ① App应缓存用户填写的数据;
② 网络恢复后,提示用户是否重新提交。| "网络连接已断开,您的报告已保存。网络恢复后将提示您重新提交。"| -| **任务已被取消/重分配**| 网格员处理一个已被主管取消的任务 | 调用任何与该任务相关的API时,后端返回`404 Not Found`或`409 Conflict` | 详情页提示"该任务已被取消或重新分配",并引导用户返回列表页 | - -## 7. 界面设计要求 - -### 7.1 任务列表页面 -- **布局**: 采用卡片列表形式,每个卡片代表一个任务。 -- **卡片内容**: - - 左上角用不同颜色的标签标示优先级(`高`-红色, `中`-橙色, `低`-蓝色)。 - - 显示反馈`标题`和`文字地址`。 - - 右下角显示任务当前状态(如`待接受`、`处理中`)。 -- **交互**: - - 点击卡片进入任务详情页。 - - 列表应支持**下拉刷新**功能。 - - 页面右上角提供一个"主动上报"的浮动按钮。 - -### 7.2 任务详情页面 -- **顶部**: 清晰展示任务的`标题`、`详细描述`、`污染类型`、`严重等级`以及公众提交的`现场照片`(支持点击放大)。 -- **中部 (地图)**: - - 内嵌一个简化的网格地图组件。 - - 在地图上用不同图标标示出**网格员当前位置**和**目标任务位置**。 - - 自动规划并高亮显示从起点到终点的**最优路径**(A*算法结果),路径需能绕开障碍网格。 - - 地图右下角应提供"重新定位"和"重新规划路径"的按钮。 -- **底部 (操作区)**: - - **状态: ASSIGNED**: 根据业务规则显示"接受"和"拒绝"按钮。 - - **状态: IN_PROGRESS**: 显示一个"提交报告"按钮。点击后,弹出一个包含AQI数据表单的对话框。 - - **状态: SUBMITTED / COMPLETED**: 不显示任何操作按钮,仅展示任务信息和已提交的报告。 -- **AQI数据提交表单 (弹窗)**: - - 包含`PM2.5`, `PM10`, `SO2`, `NO2`, `CO`, `O3`等数值输入框。 - - 还有一个可选的`备注`多行文本框。 - - 有一个"确认提交"按钮,点击后显示加载状态,成功后关闭弹窗并自动刷新任务详情页的状态。 - -### 7.3 主动上报页面 -- 页面核心是一个地图,自动定位到网格员当前位置。 -- 地图下方是一个简化的AQI数据提交表单,允许网格员直接填写数据并提交。 -- 提交成功后显示成功提示,并返回到主界面。 + +# 网格员任务处理功能 - 设计文档 + +## 1. 功能描述 +本功能模块是为一线网格员(NEPG端)设计的核心操作界面。它使得网格员能够清晰地接收、查看和处理由管理员分配的污染勘查任务,并在简化的网格地图上获得路径指引,最终提交现场的AQI数据,完成任务闭环。 + +## 2. 涉及角色 +- **主要使用者**: `网格员 (GRID_WORKER)` + +## 3. 业务规则 + +### 3.1 任务接收与查看 +- 网格员登录后,首页即为任务列表。 +- 任务列表默认按`任务优先级`降序、`创建时间`升序排列。 +- **任务优先级**由后端动态计算,主要基于反馈的`severityLevel`(严重等级)和任务的等待时长。 +- 每个任务项应清晰展示关键信息:`标题`、`文字地址`、`优先级`、`状态`。 + +### 3.2 任务处理规则 +- **任务接受与拒绝**: + - 对于`severityLevel`为`HIGH`的任务,网格员**不可拒绝**。 + - 对于`severityLevel`为`LOW`或`MEDIUM`的任务,网格员在任务详情页可以点击"拒绝"按钮。 + - 拒绝时需选择一个预设的理由(如"当前任务繁忙"、"超出能力范围"等)。 + - 拒绝后,该任务会从该网格员的列表中移除,并由系统自动进行再分配。 +- **数据提交**: + - 网格员到达现场后,必须在任务详情页填写并提交`AQI数据`表单才能完成任务。 + - `AqiData`记录与`Feedback`记录强关联。 +- **主动上报**: 系统允许网格员在没有关联任务的情况下,主动上报其所在网格的AQI数据。 + +### 3.3 地图与定位规则 +- **实时定位**: 进入任务详情页时,App应请求获取网格员的实时地理位置,并在地图上标记。 +- **路径规划**: A*路径规划仅在首次进入详情页时计算一次。如果网格员偏离了规划路径,可以提供一个"重新规划"按钮。 +- **离线地图**: (未来规划)为节省流量和应对网络不佳的情况,可以考虑支持离线网格地图的缓存。 + +## 4. 功能实现流程 + +### 4.1 任务处理流程 +```mermaid +graph TD + subgraph "NEPG 端 (网格员操作)" + A[登录系统] --> B[查看任务列表]; + B --> C{选择一个任务}; + C --> D[查看任务详情]; + + D --> E{任务是否为HIGH等级?}; + E -- 否 --> F[显示"接受"和"拒绝"按钮]; + F -- 点击拒绝 --> G[任务被退回,流程结束]; + E -- 是 --> H[只显示"接受"按钮]; + + F -- 点击接受 --> I{处理任务}; + H --> I; + + I --> J[在网格地图上
查看路径指引]; + J --> K[到达现场
勘查并记录数据]; + K --> L[填写并提交
AQI数据报告]; + end + + subgraph "后端服务" + L --> M[创建AqiData记录]; + M --> N[更新Feedback状态
为SUBMITTED]; + end + + style D fill:#E3F2FD + style G fill:#FFCDD2 + style N fill:#C8E6C9 +``` + +### 4.2 主动上报流程 +```mermaid +graph TD + A[网格员在主界面
点击"主动上报"] --> B[进入上报页面]; + B --> C[系统自动获取
当前网格位置]; + C --> D[填写AQI数据表单]; + D --> E{点击提交}; + E --> F[后端创建无关联
feedbackId的AqiData记录]; + F --> G[提示"上报成功"]; + + style C fill:#E3F2FD + style F fill:#C8E6C9 +``` + +## 5. API 接口设计 + +### 5.1 获取我的任务列表 +- **URL**: `GET /api/worker/tasks` +- **权限**: `GRID_WORKER` +- **查询参数**: `status` (可选: `ASSIGNED`, `IN_PROGRESS`), `page`, `size` +- **成功响应** (`200 OK`): 返回分页的任务列表。 + +### 5.2 获取任务详情 +- **URL**: `GET /api/worker/tasks/{taskId}` +- **权限**: `GRID_WORKER` +- **成功响应** (`200 OK`): 返回任务的详细信息,包含公众提交的描述、图片,以及A*算法规划出的路径坐标点数组。 + +### 5.3 接受任务 +- **URL**: `POST /api/worker/tasks/{taskId}/accept` +- **权限**: `GRID_WORKER` +- **成功响应** (`200 OK`): `{ "message": "任务已接受" }` + +### 5.4 拒绝任务 +- **URL**: `POST /api/worker/tasks/{taskId}/reject` +- **权限**: `GRID_WORKER` +- **请求体**: `{"reason": "当前任务繁忙"}` +- **成功响应** (`200 OK`): `{ "message": "任务已拒绝" }` + +### 5.5 提交任务报告 +- **URL**: `POST /api/worker/tasks/{taskId}/submit` +- **权限**: `GRID_WORKER` +- **请求体** (application/json): + ```json + { + "notes": "已到现场检查,确认存在异味,已要求整改", + "aqiData": { + "pm25": 75.5, "pm10": 120.3, "so2": 15.8, + "no2": 40.2, "co": 1.2, "o3": 65.2 + } + } + ``` +- **成功响应** (`200 OK`): `{ "message": "任务报告提交成功,等待主管审核" }` + +### 5.6 主动上报AQI数据 +- **URL**: `POST /api/worker/aqi-data/report` +- **权限**: `GRID_WORKER` +- **请求体**: + ```json + { + "gridX": 15, + "gridY": 20, + "aqiData": { ... } + } + ``` +- **成功响应** (`201 Created`): `{ "message": "数据上报成功" }` + +## 6. 错误处理与边界情况 + +| 场景 | 触发条件 | 系统处理 | 用户提示 | +| :--- | :--- | :--- | :--- | +| **定位失败** | App无法获取GPS信号 | 地图模块显示默认位置(如市中心),并提示用户检查定位服务 | "无法获取您的位置,请检查GPS设置" | +| **提交时网络中断**| 点击提交后,网络断开 | ① App应缓存用户填写的数据;
② 网络恢复后,提示用户是否重新提交。| "网络连接已断开,您的报告已保存。网络恢复后将提示您重新提交。"| +| **任务已被取消/重分配**| 网格员处理一个已被主管取消的任务 | 调用任何与该任务相关的API时,后端返回`404 Not Found`或`409 Conflict` | 详情页提示"该任务已被取消或重新分配",并引导用户返回列表页 | + +## 7. 界面设计要求 + +### 7.1 任务列表页面 +- **布局**: 采用卡片列表形式,每个卡片代表一个任务。 +- **卡片内容**: + - 左上角用不同颜色的标签标示优先级(`高`-红色, `中`-橙色, `低`-蓝色)。 + - 显示反馈`标题`和`文字地址`。 + - 右下角显示任务当前状态(如`待接受`、`处理中`)。 +- **交互**: + - 点击卡片进入任务详情页。 + - 列表应支持**下拉刷新**功能。 + - 页面右上角提供一个"主动上报"的浮动按钮。 + +### 7.2 任务详情页面 +- **顶部**: 清晰展示任务的`标题`、`详细描述`、`污染类型`、`严重等级`以及公众提交的`现场照片`(支持点击放大)。 +- **中部 (地图)**: + - 内嵌一个简化的网格地图组件。 + - 在地图上用不同图标标示出**网格员当前位置**和**目标任务位置**。 + - 自动规划并高亮显示从起点到终点的**最优路径**(A*算法结果),路径需能绕开障碍网格。 + - 地图右下角应提供"重新定位"和"重新规划路径"的按钮。 +- **底部 (操作区)**: + - **状态: ASSIGNED**: 根据业务规则显示"接受"和"拒绝"按钮。 + - **状态: IN_PROGRESS**: 显示一个"提交报告"按钮。点击后,弹出一个包含AQI数据表单的对话框。 + - **状态: SUBMITTED / COMPLETED**: 不显示任何操作按钮,仅展示任务信息和已提交的报告。 +- **AQI数据提交表单 (弹窗)**: + - 包含`PM2.5`, `PM10`, `SO2`, `NO2`, `CO`, `O3`等数值输入框。 + - 还有一个可选的`备注`多行文本框。 + - 有一个"确认提交"按钮,点击后显示加载状态,成功后关闭弹窗并自动刷新任务详情页的状态。 + +### 7.3 主动上报页面 +- 页面核心是一个地图,自动定位到网格员当前位置。 +- 地图下方是一个简化的AQI数据提交表单,允许网格员直接填写数据并提交。 +- 提交成功后显示成功提示,并返回到主界面。 --- - + ### Design/业务流程.md - -# EMS 后端业务流程与架构深度解析 - -本文档旨在深入剖析应急管理系统(EMS)后端的核心业务流程、分层架构以及关键技术机制。通过对典型请求生命周期的分析,揭示系统各组件如何协同工作,以实现高效、可靠的业务功能。 -在您的项目中,主要有以下几类常见的注解: - -### 1. JPA (Java Persistence API) 注解 - 用于数据库交互 -这类注解通常用在 model 或 entity 包下的类中,用于将一个Java对象映射到数据库的一张表。 - - @Entity : 声明这个类是一个实体类,它对应数据库中的一张表。 - @Table(name = "aqi_data") : 正如您在 `AqiData.java` 中看到的,这个注解用来指定该实体类对应数据库中的具体表名。在这里, AqiData 类对应 aqi_data 这张表。 - @Id : 指定这个字段是表的主键。 - @Column(name = "user_id") : 指定字段对应数据库表中的列名。 - @GeneratedValue : 定义主键的生成策略(例如,自增长)。 -### 2. Spring 框架注解 - 用于管理组件和处理请求 -这是项目中最核心的注解,用于实现依赖注入(DI)和面向切面编程(AOP)。 - - @Component : 通用的组件注解,表明这个类由 Spring 容器管理。 - @Service : 通常用在业务逻辑层(Service层),是 @Component 的一种特化。 - @Repository : 通常用在数据访问层(DAO/Repository层),也是 @Component 的一种特化。 - @RestController : 用在控制器(Controller)上,表明这个类处理HTTP请求,并且返回的数据会自动序列化为JSON格式。 - @Autowired : 自动注入依赖。Spring会自动寻找匹配的Bean(由 @Component 等注解标记的类的实例)并注入到该字段。 - @RequestMapping , @GetMapping , @PostMapping : 将HTTP请求的URL路径映射到具体的处理方法上。 -### 3. Lombok 注解 - 用于减少样板代码 -Lombok是一个非常实用的Java库,可以通过注解在编译时自动生成代码。 - - @Data : 一个非常方便的组合注解,会自动为类的所有字段生成 getter 、 setter 方法,以及 toString() , equals() , hashCode() 方法。 - @NoArgsConstructor : 生成一个无参构造函数。 - @AllArgsConstructor : 生成一个包含所有字段的构造函数。 - @Slf4j : 为类自动创建一个 log 静态常量,方便您直接使用 log.info(...) 等方法打印日志。 - -## 一、 系统核心思想与分层架构 - -EMS 后端遵循业界主流的**分层架构**设计,旨在实现**高内聚、低耦合**的目标。这种架构将应用程序划分为不同的逻辑层,每一层都承担明确的职责。各层之间通过定义好的接口进行通信,使得系统更易于开发、测试、维护和扩展。 - - -### 1.1 分层架构概览 - -系统从上至下主要分为以下几个核心层级,对应于项目中的不同包: - -- **Controller (控制层)**: `com.dne.ems.controller` - - **职责**: 作为应用的入口,负责接收来自客户端(前端)的 HTTP 请求,解析请求参数,并调用服务层的相应方法来处理业务逻辑。它不包含任何业务逻辑,仅作为请求的调度和转发中心。 - - **交互**: 将处理结果(通常是 DTO 对象)封装成统一的 JSON 格式(`ApiResponse`)返回给客户端。 - -- **Service (服务层)**: `com.dne.ems.service` - - **职责**: 系统的核心,负责实现所有复杂的业务逻辑。它编排一个或多个 Repository 来完成特定功能,处理数据、执行计算、实施业务规则,并处理事务。 - - **交互**: 被 Controller 层调用,同时调用 Repository 层来访问数据库。 - -- **Repository (数据访问层)**: `com.dne.ems.repository` - - **职责**: 定义数据持久化的接口,负责与数据库进行交互。通过继承 Spring Data JPA 的 `JpaRepository`,开发者无需编写具体的 SQL 语句即可实现对数据的增删改查(CRUD)以及复杂的查询。 - - **交互**: 被 Service 层调用,直接操作数据库。 - -- **Model (模型层)**: `com.dne.ems.model` - - **职责**: 定义系统的领域对象,即与数据库表结构一一对应的实体类(Entity)。它们使用 JPA 注解(如 `@Entity`, `@Table`, `@Id`)来映射数据库表和字段。 - - **交互**: 在 Repository 层和 Service 层之间传递数据。 - -- **DTO (数据传输对象)**: `com.dne.ems.dto` - - **职责**: 作为各层之间,特别是 Controller 层与外部客户端之间的数据载体。DTO 的设计可以根据 API 的需要进行定制,隐藏内部领域模型的复杂性,避免暴露不必要的数据,从而提高系统的安全性与灵活性。 - -### 1.2 辅助模块 - -除了核心分层,系统还包含一系列重要的辅助模块,以支持整体功能的实现: - -- **Security (安全模块)**: `com.dne.ems.security` - - **职责**: 基于 Spring Security 实现用户认证(Authentication)和授权(Authorization)。它包含 JWT 的生成与校验、密码加密、以及精细化的接口访问控制策略。 - -- **Exception (异常处理模块)**: `com.dne.ems.exception` - - **职责**: 提供全局统一的异常处理机制。通过 `@ControllerAdvice`,系统可以捕获在业务流程中抛出的各类异常,并将其转换为对客户端友好的、标准化的错误响应。 - -- **Config (配置模块)**: `com.dne.ems.config` - - **职责**: 存放应用的配置类,如 Spring Security 配置、Web MVC 配置、以及应用启动时的数据初始化逻辑(`DataInitializer`)。该模块为EMS系统提供了完整的基础设施支持,包括数据初始化、文件存储配置、API文档配置、HTTP客户端配置、WebSocket配置以及数据转换器等核心配置组件。 - -- **Controller (控制器模块)**: `com.dne.ems.controller` - - **职责**: 作为系统的API入口层,负责处理所有HTTP请求,包括用户认证、反馈管理、任务分配、数据可视化等核心业务功能。该模块包含14个专业化控制器,每个控制器专注于特定的业务领域,提供RESTful API接口,支持权限控制、参数验证、分页查询等功能。 - -- **Event/Listener (事件驱动模块)**: `com.dne.ems.event`, `com.dne.ems.listener` - - **职责**: 实现应用内的异步处理和解耦。例如,用户认证失败时,可以发布一个 `AuthenticationFailureEvent` 事件,由专门的监听器 `AuthenticationFailureEventListener` 异步处理登录失败次数的记录,而不会阻塞主认证流程。 - -## 二、 项目亮点与创新 - -本项目在传统的城市管理系统基础上,融合了多项现代技术与创新理念,旨在打造一个**智能、高效、协同**的城市环境治理平台。其核心亮点体现在以下几个方面: - -### 2.1 业务全流程智能化 - -- **AI 预审核**: 公众提交的反馈信息不再需要人工进行初步筛选。系统集成的 AI 模型能够自动对反馈内容(包括文本和图像)进行分析,判断其有效性、紧急程度,并进行初步的分类。这极大地减轻了主管人员的工作负担,并将他们的精力解放出来,专注于更复杂的决策和任务分配。 -- **智能任务推荐与调度**: 在任务分配环节,系统能够基于网格员的当前位置、历史任务完成效率、技能特长以及当前负载情况,智能推荐最合适的执行人选。在紧急情况下,系统甚至可以自动进行任务的调度与指派,确保问题得到最快响应。 - -### 2.2 事件驱动的异步架构 - -- **松耦合与高响应**: 项目广泛采用了**事件驱动架构 (EDA)**。例如,当一个反馈提交后,系统会发布一个 `FeedbackSubmittedEvent` 事件,后续的 AI 分析、通知发送等多个操作都由独立的监听器异步执行。这种设计使得核心业务流程(如提交反馈)能够瞬间完成,极大地提升了用户体验和系统的吞吐量。同时,模块之间通过事件进行通信,而不是直接调用,实现了高度解耦,使得系统更容易扩展和维护。 - -### 2.3 全方位的数据驱动决策 - -- **实时数据大屏**: 决策者不再依赖于滞后的纸质报告。系统提供了一个动态、可交互的数据驾驶舱,实时展示城市环境的各项关键指标(KPIs),如 AQI 分布、任务完成率、反馈热力图等。所有数据都以直观的图表形式呈现,帮助管理者快速洞察问题、发现趋势,从而做出更科学的决策。 -- **绩效与资源优化**: 系统通过对历史数据的深度分析,能够评估不同区域的环境治理成效、各个网格员的工作效率,为绩效考核提供客观依据,并为未来的资源调配和政策制定提供数据支持。 - -### 2.4 四端一体的协同工作平台 - -- **角色精准赋能**: 系统为四类核心用户——**公众、网格员、主管、决策者**——分别设计了专属的客户端(Web 或移动应用),精准满足不同角色的工作需求。 - - **公众端**: 简化提交流程,提供实时进度查询。 - - **网格员端**: 移动化接单、处理、上报,并提供导航支持。 - - **主管端**: 集中审核、智能分配、实时监控。 - - **决策者端**: 宏观数据洞察,支持战略决策。 -- **无缝信息流转**: 通过统一的后端平台,信息可以在四个客户端之间无缝流转,形成从问题发现、任务分配、现场处理到决策分析的闭环管理,打破了传统工作模式中的信息孤岛。 - -## 三、 核心业务流程与关键机制深度解析 - -下面,我们将以具体的业务场景和技术实现为例,详细拆解请求在系统分层架构中的流转过程。 - -### 2.1 用户认证与授权 - -此流程是系统的安全基石,涉及用户登录、权限验证和 Token 管理。 - -#### API 接口 - -- **用户注册**: `POST /api/auth/signup` -- **用户登录**: `POST /api/auth/login` -- **发送验证码**: `POST /api/auth/send-verification-code` -- **请求重置密码**: `POST /api/auth/request-password-reset` -- **重置密码**: `POST /api/auth/reset-password` - -**场景:用户使用用户名和密码登录 (POST /api/auth/login)** - -1. **前端 -> Controller (`AuthController`)** - - **动作**: 用户在登录页输入凭据,前端发送 `POST` 请求到 `/api/auth/login`,请求体为包含 `username` 和 `password` 的 `LoginRequest` DTO。 - - **Controller 角色**: `AuthController` 的 `authenticateUser` 方法接收请求。`@Valid` 注解确保了输入的基本格式正确。Controller 将 `LoginRequest` 直接传递给 `AuthService` 进行处理。 - -2. **Controller -> Service (`AuthService`)** - - **动作**: `AuthService` 调用 Spring Security 的 `AuthenticationManager` 的 `authenticate` 方法。`AuthenticationManager` 是认证的核心入口。 - - **Service 角色**: `AuthService` 编排了整个认证流程: - - **凭证封装**: 将用户名和密码封装成 `UsernamePasswordAuthenticationToken`。 - - **认证委托**: 将此 `token` 交给 `AuthenticationManager`。`AuthenticationManager` 会遍历其内部的 `AuthenticationProvider` 列表(在我们的例子中是 `DaoAuthenticationProvider`)来寻找能处理此 `token` 的提供者。 - - **用户查找**: `DaoAuthenticationProvider` 会使用 `UserDetailsService` (我们的实现是 `UserDetailsServiceImpl`) 的 `loadUserByUsername` 方法,根据用户名去数据库查找用户。`UserDetailsServiceImpl` 会调用 `UserRepository`。 - - **密码比对**: `DaoAuthenticationProvider` 使用 `PasswordEncoder` (我们配置的 `BCryptPasswordEncoder`) 比较用户提交的密码和数据库中存储的加密哈希值。 - - **生成 JWT**: 如果认证成功,`AuthService` 调用 `JwtTokenProvider` 的 `generateToken` 方法,为认证通过的 `Authentication` 对象生成一个 JWT。 - -3. **Service -> Repository (`UserRepository`)** - - **动作**: `UserDetailsServiceImpl` 调用 `userRepository.findByUsername(username)`。 - - **Repository 角色**: `UserRepository` 执行 `SELECT` 查询,从 `users` 表中找到对应的用户记录,并将其封装成 `User` 实体对象返回。 - -4. **安全组件交互 (`JwtTokenProvider`, `UserDetailsServiceImpl`)** - - `UserDetailsServiceImpl`: 实现了 Spring Security 的 `UserDetailsService` 接口,是连接应用用户数据和 Spring Security 框架的桥梁。 - - `JwtTokenProvider`: 负责 JWT 的生成和解析。`generateToken` 方法会设置主题(用户名)、签发时间、过期时间,并使用预设的密钥和算法(HS512)进行签名。 - -5. **Controller -> 前端** - - **动作**: `AuthService` 返回生成的 JWT。`AuthController` 将此 JWT 包装在 `JwtAuthenticationResponse` DTO 中,再放入 `ApiResponse.success()`,以 HTTP 200 状态码返回给前端。 - - **后续请求**: 前端在后续所有需要认证的请求的 `Authorization` 头中携带此 JWT (`Bearer `)。Spring Security 的 `JwtAuthenticationFilter` 会在每个请求到达 Controller 前拦截并验证此 Token。 - -### 2.2 公众反馈管理 - -#### API 接口 - -- **提交反馈 (JSON)**: `POST /api/feedback/submit-json` -- **提交反馈 (Multipart)**: `POST /api/feedback/submit` -- **获取所有反馈**: `GET /api/feedback` -- **公众提交反馈**: `POST /api/public/feedback` - -**场景:公众提交带图片的反馈 (POST /api/public/feedback)** - -1. **前端 -> Controller (`PublicController`)** - - **动作**: 公众用户填写反馈表单(描述、位置、图片),前端以 `multipart/form-data` 格式提交到 `/api/public/feedback`。 - - **Controller 角色**: `createFeedback` 方法使用 `@RequestPart` 分别接收反馈内容的 DTO (`FeedbackCreateDTO`) 和文件 (`MultipartFile`)。`@Valid` 确保 DTO 数据合规。Controller 随即调用 `FeedbackService`。 - -2. **Controller -> Service (`FeedbackService` & `FileStorageService`)** - - **动作**: `FeedbackService` 的 `createFeedback` 方法被调用。 - - **Service 角色**: - - **文件处理**: 如果 `file` 存在,`FeedbackService` 首先调用 `FileStorageService` 的 `storeFile` 方法。`FileStorageService` 会生成唯一文件名,将文件存储在服务器的 `uploads` 目录下,并返回可访问的文件路径。 - - **实体创建**: `FeedbackService` 创建一个新的 `Feedback` 实体。 - - **数据映射**: 使用 `ModelMapper` 将 `FeedbackCreateDTO` 的数据(如 `description`, `location`)映射到 `Feedback` 实体上。 - - **关联文件**: 将 `FileStorageService` 返回的文件路径设置到 `Feedback` 实体的 `imageUrl` 字段。 - - **状态设定**: 设置反馈的初始状态为 `PENDING`。 - - **持久化**: 调用 `feedbackRepository.save(newFeedback)`。 - -3. **Service -> Repository (`FeedbackRepository`)** - - **动作**: `feedbackRepository.save()` 执行 `INSERT` SQL 语句,将新的反馈记录存入 `feedbacks` 表。 - -4. **Controller -> 前端** - - **动作**: `FeedbackService` 返回成功创建的 `Feedback` 对象的 DTO。`PublicController` 将其包装在 `ApiResponse.success()` 中,以 HTTP 201 (Created) 状态码返回,表示资源创建成功。 - -### 2.3 任务生命周期管理 - -#### API 接口 - -- **获取待审核的反馈列表**: `GET /api/supervisor/reviews` -- **批准反馈**: `POST /api/supervisor/reviews/{feedbackId}/approve` -- **拒绝反馈**: `POST /api/supervisor/reviews/{feedbackId}/reject` -- **获取未分配的反馈**: `GET /api/tasks/unassigned` -- **获取可用的网格员**: `GET /api/tasks/grid-workers` -- **分配任务**: `POST /api/tasks/assign` -- **获取任务列表**: `GET /api/management/tasks` -- **分配任务**: `POST /api/management/tasks/{taskId}/assign` -- **获取任务详情**: `GET /api/management/tasks/{taskId}` -- **审核任务**: `POST /api/management/tasks/{taskId}/review` -- **取消任务**: `POST /api/management/tasks/{taskId}/cancel` -- **直接创建任务**: `POST /api/management/tasks` -- **获取待处理的反馈**: `GET /api/management/tasks/feedback` -- **从反馈创建任务**: `POST /api/management/tasks/feedback/{feedbackId}/create-task` -- **获取我的任务**: `GET /api/worker` -- **获取任务详情**: `GET /api/worker/{taskId}` -- **接受任务**: `POST /api/worker/{taskId}/accept` -- **提交任务**: `POST /api/worker/{taskId}/submit` - -**场景:主管审核反馈并转换为任务 (POST /api/supervisor/feedback/{feedbackId}/convert)** - -1. **前端 -> Controller (`SupervisorController`)** - - **动作**: 主管在管理界面查看一条反馈,点击“转换为任务”按钮。前端发送 `POST` 请求到 `/api/supervisor/feedback/456/convert`,请求体中可能包含指派信息 `TaskFromFeedbackDTO`。 - - **Controller 角色**: `convertFeedbackToTask` 方法接收请求,解析 `feedbackId` 和 DTO,然后调用 `TaskService`。 - -2. **Controller -> Service (`TaskService` & `FeedbackService`)** - - **动作**: `TaskService` 的 `createTaskFromFeedback` 方法被调用。 - - **Service 角色**: 这是一个跨领域服务的协调过程。 - - **获取反馈**: 调用 `feedbackRepository.findById(feedbackId)` 获取原始反馈信息。如果不存在,抛出 `ResourceNotFoundException`。 - - **状态检查**: 检查反馈是否已经是“已处理”状态,防止重复转换。 - - **创建任务实体**: 创建一个新的 `Task` 实体。 - - **数据迁移**: 将 `Feedback` 的 `description`, `location` 等信息复制到 `Task` 实体中。 - - **设置任务属性**: 根据 `TaskFromFeedbackDTO` 设置任务的标题、截止日期、指派的网格员(如果提供了 `workerId`)。如果未指派,任务状态为 `PENDING`;如果已指派,状态为 `ASSIGNED`。 - - **持久化任务**: 调用 `taskRepository.save(newTask)`。 - - **更新反馈状态**: 更新 `Feedback` 实体的状态为 `PROCESSED`,并调用 `feedbackRepository.save(feedback)`。 - - **事务管理**: 整个方法应该被 `@Transactional` 注解标记,确保创建任务和更新反馈状态这两个操作要么同时成功,要么同时失败。 - -3. **Service -> Repository (`TaskRepository`, `FeedbackRepository`)** - - **动作**: `TaskRepository` 插入一条新任务记录。`FeedbackRepository` 更新对应的反馈记录状态。 - -4. **Controller -> 前端** - - **动作**: `TaskService` 返回新创建的任务的 DTO。`SupervisorController` 将其包装在 `ApiResponse.success()` 中返回,前端可以根据返回的任务信息进行页面跳转或状态更新。 - -### 2.4 数据可视化与决策支持 - -#### API 接口 - -- **获取仪表盘统计数据**: `GET /api/dashboard/stats` -- **获取 AQI 分布**: `GET /api/dashboard/reports/aqi-distribution` -- **获取污染超标月度趋势**: `GET /api/dashboard/reports/pollution-trend` -- **获取网格覆盖率**: `GET /api/dashboard/reports/grid-coverage` -- **获取反馈热力图数据**: `GET /api/dashboard/map/heatmap` -- **获取污染类型统计**: `GET /api/dashboard/reports/pollution-stats` -- **获取任务完成情况统计**: `GET /api/dashboard/reports/task-completion-stats` -- **获取 AQI 热力图数据**: `GET /api/dashboard/map/aqi-heatmap` - -### 2.5 智能分配算法 - -#### API 接口 - -- **获取任务的推荐人选**: `GET /api/tasks/{taskId}/recommendations` - -**场景:主管为新任务寻求系统推荐的执行人 (GET /api/tasks/123/recommendations)** - -1. **前端 -> Controller (`TaskManagementController`)** - - **动作**: 主管在任务分配界面,点击“推荐人选”按钮。前端向 `/api/tasks/123/recommendations` 发送 `GET` 请求。 - - **Controller 角色**: `getTaskRecommendations` 方法接收请求,并调用 `TaskRecommendationService`。 - -2. **Controller -> Service (`TaskRecommendationService`)** - - **动作**: `TaskRecommendationService` 的 `recommendWorkersForTask` 方法被调用。 - - **Service 角色**: 这是智能分配算法的核心实现。 - - **获取任务信息**: 首先,从 `taskRepository` 获取任务详情,特别是任务的地理位置和类型。 - - **筛选候选人**: 从 `userRepository` 获取所有角色为“网格员”且状态为“可用”的用户列表。 - - **为每位候选人评分**: 遍历候选人列表,根据一系列预设标准计算综合得分。这是一个典型的加权评分模型: - - **距离 (40%)**: 计算网格员当前位置与任务位置的直线距离。距离越近,得分越高。`Score = (MaxDistance - WorkerDistance) / MaxDistance`。 - - **当前负载 (30%)**: 查询该网格员当前状态为 `ASSIGNED` 或 `IN_PROGRESS` 的任务数量。任务越少,得分越高。`Score = (MaxLoad - WorkerLoad) / MaxLoad`。 - - **技能匹配度 (20%)**: 如果任务有特定的技能要求(如“管道维修”),检查网格员的技能标签。匹配则得满分,否则得0分。 - - **历史表现 (10%)**: 查询该网格员过去完成类似任务的平均耗时和评价。表现越好,得分越高。 - - **加权总分**: `Total Score = Score_Distance * 0.4 + Score_Load * 0.3 + Score_Skill * 0.2 + Score_Performance * 0.1`。 - - **排序**: 根据总分对所有候选人进行降序排序。 - -3. **Service -> Repository (Multiple Repositories)** - - **动作**: `TaskRecommendationService` 会与 `TaskRepository`, `UserRepository`, `WorkerLocationRepository` (假设有这样一个实时位置库) 等多个 Repository 交互,以获取计算所需的数据。 - -4. **Controller -> 前端** - - **动作**: `TaskRecommendationService` 返回一个包含网格员信息和他们各自推荐分数的 DTO 列表。`TaskManagementController` 将此列表包装在 `ApiResponse.success()` 中返回。前端界面会高亮显示得分最高的几位候选人,并附上推荐理由(如“距离最近”,“负载最低”),辅助主管做出最终决策。 - -**场景:决策者查看仪表盘 (GET /api/dashboard)** - -1. **前端 -> Controller (`DashboardController`)** - - **动作**: 决策者访问仪表盘页面,前端发送 `GET` 请求到 `/api/dashboard`。 - - **Controller 角色**: `getDashboardData` 方法直接调用 `DashboardService`。 - -2. **Controller -> Service (`DashboardService`)** - - **动作**: `DashboardService` 的 `getDashboardData` 方法被调用。 - - **Service 角色**: 这是一个典型的数据聚合服务。 - - **并行/串行查询**: `DashboardService` 会向多个 Repository 发起查询请求,以收集构建仪表盘所需的所有数据。 - - **任务统计**: 调用 `taskRepository` 的自定义聚合查询方法(如 `countByStatus`)来获取不同状态的任务数。 - - **反馈统计**: 调用 `feedbackRepository.countByStatus` 获取不同状态的反馈数。 - - **人员概览**: 调用 `personnelRepository.count()` 获取总人数。 - - **AQI 数据**: 调用 `aqiDataRepository.findLatest()` 获取最新的空气质量数据。 - - **数据组装**: 将所有查询到的零散数据,组装成一个专为前端仪表盘设计的 `DashboardDTO` 对象。 - -3. **Service -> Repository (多个)** - - **动作**: `TaskRepository`, `FeedbackRepository`, `PersonnelRepository` 等分别执行高效的数据库聚合查询(如 `SELECT status, COUNT(*) FROM tasks GROUP BY status`)。 - - **Repository 角色**: Repository 接口中定义了这些自定义查询方法,使用 `@Query` 注解编写 JPQL 或原生 SQL,直接利用数据库的计算能力,避免在应用服务层进行大量数据处理。 - -4. **Controller -> 前端** - - **动作**: `DashboardService` 返回填充了完整数据的 `DashboardDTO`。`DashboardController` 将其包装在 `ApiResponse.success()` 中,以 HTTP 200 状态码返回。前端收到 JSON 数据后,使用图表库(如 ECharts)进行渲染。 - -### 2.5 网格与人员管理 - -#### API 接口 - -- **获取网格列表**: `GET /api/grids` -- **更新网格信息**: `PATCH /api/grids/{gridId}` -- **创建用户**: `POST /api/personnel/users` -- **获取用户列表**: `GET /api/personnel/users` -- **获取单个用户**: `GET /api/personnel/users/{userId}` -- **更新用户信息**: `PATCH /api/personnel/users/{userId}` -- **更新用户角色**: `PUT /api/personnel/users/{userId}/role` -- **删除用户**: `DELETE /api/personnel/users/{userId}` -- **更新个人资料**: `PATCH /api/me` -- **更新我的位置**: `POST /api/me/location` -- **获取我的反馈历史**: `GET /api/me/feedback` - -**场景:管理员分页查询所有人员信息 (GET /api/management/personnel?page=0&size=10)** - -1. **前端 -> Controller (`ManagementController`)** - - **动作**: 管理员在人员管理页面,前端发送带分页参数的 `GET` 请求。 - - **Controller 角色**: `getAllPersonnel` 方法使用 `@RequestParam` 接收分页参数,并将其封装成 `Pageable` 对象,然后调用 `PersonnelService`。 - -2. **Controller -> Service (`PersonnelService`)** - - **动作**: `PersonnelService` 的 `findAll` 方法接收 `Pageable` 对象。 - - **Service 角色**: 直接将 `Pageable` 对象传递给 `PersonnelRepository`。对于更复杂的查询(例如,按姓名或角色搜索),Service 层会接收额外的查询参数,并调用 `Specification` 或 `QueryDSL` 来构建动态查询。 - -3. **Service -> Repository (`PersonnelRepository`)** - - **动作**: `personnelRepository.findAll(pageable)` 被调用。 - - **Repository 角色**: Spring Data JPA 会根据 `Pageable` 参数自动生成带有分页和排序的 SQL 查询(如 `SELECT ... FROM personnel LIMIT ? OFFSET ?`),并执行。它返回一个 `Page` 对象,其中包含了当前页的数据列表、总页数、总元素数量等完整的的分页信息。 - -4. **Controller -> 前端** - - **动作**: `PersonnelService` 返回 `Page` 对象。`ManagementController` 将其直接放入 `ApiResponse.success()` 中返回。前端根据返回的 `Page` 对象渲染数据列表和分页控件。 - -### 2.6 AI 辅助功能 - -**场景:AI 辅助生成任务报告摘要 (POST /api/ai/summarize)** - -1. **前端 -> Controller (`AiController`)** - - **动作**: 用户在任务详情页输入或粘贴长篇报告文本,点击“生成摘要”。前端将文本内容包装在 `SummarizeRequest` DTO 中,发送 `POST` 请求。 - - **Controller 角色**: `summarizeText` 方法接收 DTO,并调用 `AiService`。 - -2. **Controller -> Service (`AiService`)** - - **动作**: `AiService` 的 `getSummary` 方法被调用。 - - **Service 角色**: 这是与外部 AI 服务交互的核心。 - - **构建请求**: 根据所使用的 AI 服务(如 OpenAI, Google Gemini, 或其他私有化部署模型)的 API 规范,构建 HTTP 请求。这通常涉及设置 API Key、请求头、以及包含待摘要文本的 JSON 请求体。 - - **API 调用**: 使用 `RestTemplate` 或 `WebClient` 向 AI 服务的 API 端点发送请求。 - - **解析响应**: 接收 AI 服务返回的 JSON 响应,并从中提取出生成的摘要文本。 - - **异常处理**: 处理网络超时、API 认证失败、或 AI 服务返回错误码等异常情况。 - -3. **Controller -> 前端** - - **动作**: `AiService` 返回提取出的摘要字符串。`AiController` 将其包装在 `ApiResponse.success()` 中返回给前端,前端将摘要显示在页面上。 - -### 2.7 关键技术机制深度解析 - -#### A. 文件上传与管理 (`FileStorageService`) -- **流程**: `Controller` 接收 `MultipartFile` -> `Service` 调用 `FileStorageService.storeFile()` -> `FileStorageService` 生成唯一文件名 (e.g., `UUID.randomUUID().toString()`) 并拼接原始文件扩展名 -> 使用 `StringUtils.cleanPath` 清理文件名防止路径遍历攻击 -> 构建目标存储路径 `Path targetLocation = this.fileStorageLocation.resolve(fileName)` -> 使用 `Files.copy` 将文件输入流写入目标路径,并使用 `StandardCopyOption.REPLACE_EXISTING` 覆盖同名文件 -> 返回通过 `ServletUriComponentsBuilder` 构建的、可从外部访问的文件下载 URI。 -- **关键点**: - - **配置化**: 文件存储路径 (`file.upload-dir`) 在 `application.properties` 中配置,具有灵活性。 - - **安全性**: 明确处理了 `FileNameContainsInvalidCharactersException` 和 `FileStorageException`。 - - **资源服务**: 提供了 `loadFileAsResource` 方法,可以将存储的文件作为 `Resource` 对象加载,以便于下载。 - -### 2.8 地图与路径规划 - -#### A. 地图服务 (`MapService`) -- **获取完整地图网格**: `GET /api/map/grid` -- **创建或更新地图单元格**: `POST /api/map/grid` -- **初始化地图**: `POST /api/map/initialize` - -#### B. 路径规划服务 (`PathfindingService`) -- **寻找路径**: `POST /api/pathfinding/find` - -#### B. 文件服务 (`FileService`) -- **下载文件**: `GET /api/files/download/{fileName}` -- **查看文件**: `GET /api/files/view/{fileName}` - -#### C. 邮件与验证码服务 (`EmailService`) -- **流程**: `AuthService` 调用 `EmailService.sendVerificationCode(email)` -> `EmailService` 使用 `Random` 生成6位数字验证码 -> 将 `email:code` 键值对存入 `verificationCodeCache` (一个 `Caffeine` 缓存实例),并设置5分钟过期 -> 使用 `JavaMailSender` 创建 `SimpleMailMessage`,设置收件人、主题和正文 -> `mailSender.send(message)` 发送邮件。 - -## 三、API 接口文档 - -本部分详细列出了系统提供的所有API接口,包括其功能描述、请求路径、方法、所需权限以及请求/响应示例。 - -### 1. 认证模块 (Auth) -**基础路径**: `/api/auth` - -#### 1.1 用户注册 -- **功能描述**: 注册一个新用户。 -- **请求路径**: `/api/auth/signup` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `SignUpRequest` - -#### 1.2 用户登录 -- **功能描述**: 用户登录并获取 JWT Token。 -- **请求路径**: `/api/auth/login` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `LoginRequest` - -#### 1.3 发送验证码 -- **功能描述**: 请求发送验证码到指定邮箱。 -- **请求路径**: `/api/auth/send-verification-code` -- **请求方法**: `POST` -- **所需权限**: 无 -- **查询参数**: `email` (string, 必需) - -#### 1.4 请求重置密码 -- **功能描述**: 请求发送重置密码的链接或令牌。 -- **请求路径**: `/api/auth/request-password-reset` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `PasswordResetRequest` - -#### 1.5 重置密码 -- **功能描述**: 使用令牌重置密码。 -- **请求路径**: `/api/auth/reset-password` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `PasswordResetDto` - -### 2. 仪表盘模块 (Dashboard) -**基础路径**: `/api/dashboard` -**所需权限**: `DECISION_MAKER` - -#### 2.1 获取仪表盘统计数据 -- **请求路径**: `/api/dashboard/stats` -- **请求方法**: `GET` - -#### 2.2 获取 AQI 分布 -- **请求路径**: `/api/dashboard/reports/aqi-distribution` -- **请求方法**: `GET` - -#### 2.3 获取污染超标月度趋势 -- **请求路径**: `/api/dashboard/reports/pollution-trend` -- **请求方法**: `GET` - -#### 2.4 获取网格覆盖率 -- **请求路径**: `/api/dashboard/reports/grid-coverage` -- **请求方法**: `GET` - -#### 2.5 获取反馈热力图数据 -- **请求路径**: `/api/dashboard/map/heatmap` -- **请求方法**: `GET` - -#### 2.6 获取污染类型统计 -- **请求路径**: `/api/dashboard/reports/pollution-stats` -- **请求方法**: `GET` - -#### 2.7 获取任务完成情况统计 -- **请求路径**: `/api/dashboard/reports/task-completion-stats` -- **请求方法**: `GET` - -#### 2.8 获取 AQI 热力图数据 -- **请求路径**: `/api/dashboard/map/aqi-heatmap` -- **请求方法**: `GET` - -### 3. 反馈模块 (Feedback) -**基础路径**: `/api/feedback` - -#### 3.1 提交反馈 (JSON) -- **功能描述**: 用于测试,使用 JSON 提交反馈。 -- **请求路径**: `/api/feedback/submit-json` -- **请求方法**: `POST` -- **所需权限**: 已认证用户 -- **请求体**: `FeedbackSubmissionRequest` - -#### 3.2 提交反馈 (Multipart) -- **功能描述**: 提交反馈,可包含文件。 -- **请求路径**: `/api/feedback/submit` -- **请求方法**: `POST` -- **所需权限**: 已认证用户 -- **请求体**: `multipart/form-data` - -#### 3.3 获取所有反馈 -- **功能描述**: 获取所有反馈(分页)。 -- **请求路径**: `/api/feedback` -- **请求方法**: `GET` -- **所需权限**: `ADMIN` - -### 4. 文件模块 (File) -**基础路径**: `/api/files` -**所需权限**: 公开访问 - -#### 4.1 下载文件 -- **请求路径**: `/api/files/download/{fileName}` -- **请求方法**: `GET` - -#### 4.2 查看文件 -- **请求路径**: `/api/files/view/{fileName}` -- **请求方法**: `GET` - -### 5. 网格模块 (Grid) -**基础路径**: `/api/grids` -**所需权限**: `ADMIN` 或 `DECISION_MAKER` - -#### 5.1 获取网格列表 -- **请求路径**: `/api/grids` -- **请求方法**: `GET` - -#### 5.2 更新网格信息 -- **请求路径**: `/api/grids/{gridId}` -- **请求方法**: `PATCH` -- **所需权限**: `ADMIN` - -### 6. 网格员任务模块 (Worker) -**基础路径**: `/api/worker` -**所需权限**: `GRID_WORKER` - -#### 6.1 获取我的任务 -- **请求路径**: `/api/worker` -- **请求方法**: `GET` - -#### 6.2 获取任务详情 -- **请求路径**: `/api/worker/{taskId}` -- **请求方法**: `GET` - -#### 6.3 接受任务 -- **请求路径**: `/api/worker/{taskId}/accept` -- **请求方法**: `POST` - -#### 6.4 提交任务 -- **请求路径**: `/api/worker/{taskId}/submit` -- **请求方法**: `POST` - -### 7. 地图模块 (Map) -**基础路径**: `/api/map` -**所需权限**: 公开访问 - -#### 7.1 获取完整地图网格 -- **请求路径**: `/api/map/grid` -- **请求方法**: `GET` - -#### 7.2 创建或更新地图单元格 -- **请求路径**: `/api/map/grid` -- **请求方法**: `POST` - -#### 7.3 初始化地图 -- **请求路径**: `/api/map/initialize` -- **请求方法**: `POST` - -### 8. 路径规划模块 (Pathfinding) -**基础路径**: `/api/pathfinding` -**所需权限**: 公开访问 - -#### 8.1 寻找路径 -- **功能描述**: 使用 A* 算法在两点之间寻找最短路径。 -- **请求路径**: `/api/pathfinding/find` -- **请求方法**: `POST` - -### 9. 人员管理模块 (Personnel) -**基础路径**: `/api/personnel` -**所需权限**: `ADMIN` - -#### 9.1 创建用户 -- **请求路径**: `/api/personnel/users` -- **请求方法**: `POST` - -#### 9.2 获取用户列表 -- **请求路径**: `/api/personnel/users` -- **请求方法**: `GET` - -#### 9.3 获取单个用户 -- **请求路径**: `/api/personnel/users/{userId}` -- **请求方法**: `GET` - -#### 9.4 更新用户信息 -- **请求路径**: `/api/personnel/users/{userId}` -- **请求方法**: `PATCH` - -#### 9.5 更新用户角色 -- **请求路径**: `/api/personnel/users/{userId}/role` -- **请求方法**: `PUT` - -#### 9.6 删除用户 -- **请求路径**: `/api/personnel/users/{userId}` -- **请求方法**: `DELETE` - -### 10. 个人资料模块 (Me) -**基础路径**: `/api/me` -**所需权限**: 已认证用户 - -#### 10.1 更新个人资料 -- **请求路径**: `/api/me` -- **请求方法**: `PATCH` - -#### 10.2 更新我的位置 -- **请求路径**: `/api/me/location` -- **请求方法**: `POST` - -#### 10.3 获取我的反馈历史 -- **请求路径**: `/api/me/feedback` -- **请求方法**: `GET` - -### 11. 公共接口模块 (Public) -**基础路径**: `/api/public` -**所需权限**: 公开访问 - -#### 11.1 公众提交反馈 -- **请求路径**: `/api/public/feedback` -- **请求方法**: `POST` - -### 12. 主管模块 (Supervisor) -**基础路径**: `/api/supervisor` -**所需权限**: `SUPERVISOR` 或 `ADMIN` - -#### 12.1 获取待审核的反馈列表 -- **请求路径**: `/api/supervisor/reviews` -- **请求方法**: `GET` - -#### 12.2 批准反馈 -- **请求路径**: `/api/supervisor/reviews/{feedbackId}/approve` -- **请求方法**: `POST` - -#### 12.3 拒绝反馈 -- **请求路径**: `/api/supervisor/reviews/{feedbackId}/reject` -- **请求方法**: `POST` - -### 13. 任务分配模块 (Tasks) -**基础路径**: `/api/tasks` -**所需权限**: `ADMIN` 或 `SUPERVISOR` - -#### 13.1 获取未分配的反馈 -- **请求路径**: `/api/tasks/unassigned` -- **请求方法**: `GET` - -#### 13.2 获取可用的网格员 -- **请求路径**: `/api/tasks/grid-workers` -- **请求方法**: `GET` - -#### 13.3 分配任务 -- **请求路径**: `/api/tasks/assign` -- **请求方法**: `POST` - -### 14. 任务管理模块 (Management) -**基础路径**: `/api/management/tasks` -**所需权限**: `SUPERVISOR` - -#### 14.1 获取任务列表 -- **请求路径**: `/api/management/tasks` -- **请求方法**: `GET` - -#### 14.2 分配任务 -- **请求路径**: `/api/management/tasks/{taskId}/assign` -- **请求方法**: `POST` - -#### 14.3 获取任务详情 -- **请求路径**: `/api/management/tasks/{taskId}` -- **请求方法**: `GET` - -#### 14.4 审核任务 -- **请求路径**: `/api/management/tasks/{taskId}/review` -- **请求方法**: `POST` - -#### 14.5 取消任务 -- **请求路径**: `/api/management/tasks/{taskId}/cancel` -- **请求方法**: `POST` - -#### 14.6 直接创建任务 -- **请求路径**: `/api/management/tasks` -- **请求方法**: `POST` - -#### 14.7 获取待处理的反馈 -- **请求路径**: `/api/management/tasks/feedback` -- **请求方法**: `GET` - -#### 14.8 从反馈创建任务 -- **请求路径**: `/api/management/tasks/feedback/{feedbackId}/create-task` -- **请求方法**: `POST` -- **验证流程**: 用户提交验证码 -> `AuthService` 调用 `EmailService.verifyCode(email, code)` -> `EmailService` 从缓存中通过 `verificationCodeCache.getIfPresent(email)` 获取验证码,并与用户提交的 `code` 进行比对。如果匹配且存在,则验证成功并从缓存中移除该验证码 (`cache.invalidate(email)`)。 - -#### C. 登录安全与尝试锁定 (`AuthenticationFailureEventListener`) -- **流程**: 用户登录失败 -> Spring Security 的 `ProviderManager` 认证失败后,会发布一个 `AuthenticationFailureBadCredentialsEvent` 事件 -> `AuthenticationFailureEventListener` (一个实现了 `ApplicationListener` 的组件) 的 `onApplicationEvent` 方法被触发 -> 监听器从事件中获取用户名 -> 调用 `LoginAttemptService.loginFailed(username)` -> `LoginAttemptService` 使用 `Caffeine` 缓存 `attemptsCache` 来记录失败次数。每次失败,计数器加一。 -- **锁定逻辑**: `LoginAttemptService` 的 `isBlocked(username)` 方法检查失败次数是否超过最大允许次数(如 `MAX_ATTEMPT = 5`)。在 `UserDetailsServiceImpl` 的 `loadUserByUsername` 方法的开头,会先调用 `loginAttemptService.isBlocked()`。如果用户已被锁定,则直接抛出 `LockedException`,阻止后续的数据库查询和密码比对,实现登录锁定。 - -#### D. A* 路径规划 (`PathfindingService`) -- **流程**: `PathfindingController` 调用 `PathfindingService.findPath()` -> `PathfindingService` 初始化一个 `Grid` 对象,该对象包含地图的宽度、高度和障碍物位置集合 -> A* 算法的核心 `AStar.findPath()` 被调用,它接收 `Grid` 和起止点 `Node` 作为参数 -> 算法内部使用 `PriorityQueue` 作为开放列表(优先处理f值最低的节点),使用 `HashSet` 作为关闭列表 -> 循环开始:从开放列表取出当前节点,若为终点则通过 `retracePath` 回溯构建路径 -> 否则,遍历当前节点的邻居节点,计算其 g值(到起点的代价)和 h值(到终点的预估代价,即启发式函数),若邻居节点是更优路径,则更新其代价和父节点,并加入开放列表。 -- **关键点**: - - **数据结构**: `Node` 对象包含了坐标、g/h/f代价值、父节点引用和是否为障碍物等信息。 - - **启发式函数**: 使用曼哈顿距离 `getDistance(nodeA, nodeB)` 作为启发式函数,适用于网格地图。 - - **可扩展性**: `Grid` 可以从数据库、文件或动态生成,使得路径规划可以适应不同的地图场景。 - -## 四、配置模块详细解析 - -配置模块(`com.dne.ems.config`)是EMS系统的基础设施核心,为整个应用提供了完整的配置支持和初始化功能。本节将详细解析各个配置组件的功能、实现原理和使用方式。 - -### 4.1 数据初始化配置 (`DataInitializer.java`) - -#### 功能概述 -`DataInitializer` 是系统启动时的数据初始化组件,实现了 `CommandLineRunner` 接口,确保在Spring Boot应用完全启动后自动执行数据初始化逻辑。 - -#### 核心特性 -- **自动执行**: 实现 `CommandLineRunner` 接口,在应用启动后自动运行 -- **幂等性**: 检查数据是否已存在,避免重复创建 -- **完整性**: 创建系统运行所需的所有基础数据 -- **日志记录**: 包含详细的初始化过程日志 - -#### 初始化内容详解 - -**1. 用户账户初始化** -- 创建五种角色的测试账户:管理员、网格员、决策者、主管、公众监督员 -- 使用BCrypt加密存储密码 -- 为每个角色分配相应的权限和属性 - -**默认账户信息**: -``` -管理员: admin@aizhangz.top / Admin@123 -网格员: worker@aizhangz.top / Worker@123 -决策者: decision.maker@aizhangz.top / Decision@123 -主管: supervisor@aizhangz.top / Supervisor@123 -公众监督员: public.supervisor@aizhangz.top / Public@123 -``` - -**2. 网格数据初始化** -- 生成40x40的完整城市网格系统 -- 覆盖4个主要城市:常州市、无锡市、苏州市、南京市 -- 每个网格单元包含坐标、城市归属、区域信息 -- 支持网格员分配和区域管理 - -**3. 地图网格同步** -- 将业务网格数据同步到寻路网格 -- 支持A*寻路算法的地图表示 -- 处理障碍物和可通行区域的标记 - -**4. AQI数据初始化** -- 创建空气质量指数的历史数据 -- 生成实时AQI监测数据 -- 为数据可视化和分析提供基础数据 - -**5. 反馈和任务数据** -- 创建示例环境问题反馈 -- 初始化任务分配记录 -- 建立完整的业务流程测试数据 - -#### 技术实现要点 -```java -@Component -@Slf4j -public class DataInitializer implements CommandLineRunner { - - @Override - public void run(String... args) throws Exception { - // 检查数据是否已初始化 - if (isDataAlreadyInitialized()) { - log.info("数据已初始化,跳过初始化过程"); - return; - } - - // 执行各项初始化 - initializeUsers(); - initializeGrids(); - initializeAqiData(); - initializeFeedbacks(); - initializeTasks(); - - log.info("数据初始化完成"); - } -} -``` - -### 4.2 文件存储配置 (`FileStorageProperties.java`) - -#### 功能概述 -`FileStorageProperties` 管理文件上传存储的相关配置,使用Spring Boot的配置属性绑定机制。 - -#### 配置属性 -- `file.upload-dir`: 指定上传文件的存储目录 -- 支持相对路径和绝对路径 -- 可通过环境变量或配置文件动态配置 - -#### 使用方式 -**application.yml配置示例**: -```yaml -file: - upload-dir: ./uploads - # 或使用绝对路径 - # upload-dir: /var/app/uploads -``` - -**Java配置类**: -```java -@ConfigurationProperties(prefix = "file") -@Data -public class FileStorageProperties { - private String uploadDir; -} -``` - -### 4.3 OpenAPI配置 (`OpenApiConfig.java`) - -#### 功能概述 -配置Swagger API文档和JWT认证,为开发和测试提供完整的API文档界面。 - -#### 主要特性 -- **API文档生成**: 自动生成完整的REST API文档 -- **JWT认证集成**: 支持在Swagger UI中进行JWT认证测试 -- **安全配置**: 为所有API端点添加安全要求 -- **在线测试**: 提供交互式API测试界面 - -#### 访问方式 -- Swagger UI: `http://localhost:8080/swagger-ui.html` -- API文档JSON: `http://localhost:8080/v3/api-docs` - -#### 安全配置详解 -```java -@Configuration -public class OpenApiConfig { - - @Bean - public OpenAPI customOpenAPI() { - return new OpenAPI() - .info(new Info() - .title("EMS API") - .version("1.0") - .description("环境监测系统API文档")) - .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) - .components(new Components() - .addSecuritySchemes("Bearer Authentication", - new SecurityScheme() - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT"))); - } -} -``` - -### 4.4 RestTemplate配置 (`RestTemplateConfig.java`) - -#### 功能概述 -提供HTTP客户端功能,用于与外部服务进行同步通信。 - -#### 主要用途 -- **外部API集成**: 调用第三方服务API -- **微服务通信**: 在分布式架构中进行服务间调用 -- **数据同步**: 从外部数据源获取信息 -- **第三方认证**: 集成外部认证服务 - -#### 配置示例 -```java -@Configuration -public class RestTemplateConfig { - - @Bean - public RestTemplate restTemplate() { - RestTemplate restTemplate = new RestTemplate(); - - // 配置连接超时和读取超时 - HttpComponentsClientHttpRequestFactory factory = - new HttpComponentsClientHttpRequestFactory(); - factory.setConnectTimeout(5000); - factory.setReadTimeout(10000); - restTemplate.setRequestFactory(factory); - - return restTemplate; - } -} -``` - -#### 使用示例 -```java -@Service -public class ExternalApiService { - - @Autowired - private RestTemplate restTemplate; - - public String callExternalApi(String url) { - return restTemplate.getForObject(url, String.class); - } -} -``` - -### 4.5 WebClient配置 (`WebClientConfig.java`) - -#### 功能概述 -提供响应式HTTP客户端功能,支持非阻塞I/O操作,适用于高并发场景。 - -#### 主要优势 -- **非阻塞I/O**: 提高系统吞吐量和响应性能 -- **响应式编程**: 支持Reactor响应式编程模型 -- **流式处理**: 支持数据流的实时处理 -- **背压处理**: 自动处理生产者和消费者速度不匹配的情况 - -#### 配置示例 -```java -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient() { - return WebClient.builder() - .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) - .build(); - } - - @Bean - public WebClient.Builder webClientBuilder() { - return WebClient.builder(); - } -} -``` - -#### 使用示例 -```java -@Service -public class ReactiveApiService { - - @Autowired - private WebClient webClient; - - public Mono callApiAsync(String url) { - return webClient.get() - .uri(url) - .retrieve() - .bodyToMono(String.class); - } -} -``` - -### 4.6 WebSocket配置 (`WebSocketConfig.java`) - -#### 功能概述 -WebSocket通信配置,为实时通信功能预留扩展空间。 - -#### 预期用途 -- **实时通知**: 任务状态变更的实时推送 -- **在线协作**: 多用户协同操作 -- **实时监控**: 系统状态和数据的实时更新 -- **即时消息**: 用户间的即时通信 - -#### 扩展示例 -```java -@Configuration -@EnableWebSocket -public class WebSocketConfig implements WebSocketConfigurer { - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(new NotificationWebSocketHandler(), "/ws/notifications") - .setAllowedOrigins("*"); - } -} -``` - -### 4.7 字符串列表转换器 (`StringListConverter.java`) - -#### 功能概述 -JPA实体属性转换器,用于在数据库存储和Java对象之间转换字符串列表。 - -#### 主要特性 -- **类型转换**: `List` ↔ JSON字符串 -- **JSON序列化**: 使用Jackson ObjectMapper -- **空值处理**: 支持null和空列表的正确处理 -- **异常处理**: 包含完整的错误处理和日志记录 - -#### 实现原理 -```java -@Converter -public class StringListConverter implements AttributeConverter, String> { - - private final ObjectMapper objectMapper = new ObjectMapper(); - - @Override - public String convertToDatabaseColumn(List attribute) { - if (attribute == null || attribute.isEmpty()) { - return null; - } - try { - return objectMapper.writeValueAsString(attribute); - } catch (JsonProcessingException e) { - log.error("转换List到JSON失败", e); - return null; - } - } - - @Override - public List convertToEntityAttribute(String dbData) { - if (dbData == null || dbData.trim().isEmpty()) { - return new ArrayList<>(); - } - try { - return objectMapper.readValue(dbData, - new TypeReference>() {}); - } catch (JsonProcessingException e) { - log.error("转换JSON到List失败", e); - return new ArrayList<>(); - } - } -} -``` - -#### 使用示例 -```java -@Entity -public class UserAccount { - - @Convert(converter = StringListConverter.class) - @Column(name = "skills", columnDefinition = "TEXT") - private List skills; - - @Convert(converter = StringListConverter.class) - @Column(name = "tags", columnDefinition = "TEXT") - private List tags; -} -``` - -### 4.8 配置模块最佳实践 - -#### 1. 配置外部化 -- 使用`@ConfigurationProperties`进行类型安全的配置绑定 -- 支持多环境配置(dev、test、prod) -- 使用配置验证确保配置的正确性 - -#### 2. 安全考虑 -- 敏感配置信息使用环境变量或加密存储 -- API密钥和数据库密码不硬编码 -- 文件上传路径的安全验证 - -#### 3. 性能优化 -- HTTP客户端连接池配置 -- 合理设置超时时间 -- 缓存配置的优化 - -#### 4. 监控和日志 -- 配置变更的审计日志 -- 初始化过程的详细记录 -- 配置加载失败的错误处理 - -### 4.9 配置模块总结 - -配置模块为EMS系统提供了完整的基础设施支持: - -1. **数据初始化**: 确保系统启动时具备完整的测试数据和基础配置 -2. **文件管理**: 提供灵活的文件存储配置和管理机制 -3. **API文档**: 支持完整的接口文档生成和在线测试 -4. **HTTP客户端**: 提供同步和异步的外部服务调用能力 -5. **数据转换**: 支持复杂数据类型的数据库存储转换 - -## 五、控制器模块详细解析 - -控制器模块(`com.dne.ems.controller`)是EMS系统的API入口层,负责处理所有HTTP请求并提供RESTful API接口。该模块包含14个专业化控制器,每个控制器专注于特定的业务领域,形成了完整的API生态系统。 - -### 5.1 认证控制器 (`AuthController.java`) - -#### 功能概述 -认证控制器负责用户身份验证和账户管理的核心功能,是系统安全的第一道防线。 - -#### 主要接口 -- **用户注册** (`POST /api/auth/signup`): 新用户账户创建 -- **用户登录** (`POST /api/auth/login`): 用户身份验证,返回JWT令牌 -- **发送验证码** (`POST /api/auth/send-verification-code`): 注册验证码发送 -- **密码重置** (`POST /api/auth/send-password-reset-code`): 密码重置验证码发送 -- **验证码重置密码** (`POST /api/auth/reset-password-with-code`): 使用验证码重置密码 - -#### 核心特性 -- **JWT令牌管理**: 生成和验证JSON Web Token -- **邮箱验证**: 支持邮箱验证码验证机制 -- **密码安全**: 多种密码重置方式,确保账户安全 -- **参数验证**: 使用Bean Validation进行请求参数校验 - -#### 代码示例 -```java -@PostMapping("/login") -public ResponseEntity signIn(@Valid @RequestBody LoginRequest request) { - return ResponseEntity.ok(authService.signIn(request)); -} - -@PostMapping("/send-verification-code") -public ResponseEntity sendVerificationCode(@RequestParam @NotBlank @Email String email) { - verificationCodeService.sendVerificationCode(email); - return ResponseEntity.ok().build(); -} -``` - -### 5.2 仪表盘控制器 (`DashboardController.java`) - -#### 功能概述 -仪表盘控制器为决策者和管理员提供全面的数据可视化和统计分析功能,是数据驱动决策的核心支撑。 - -#### 主要接口 -- **核心统计数据** (`GET /api/dashboard/stats`): 系统关键指标统计 -- **AQI分布数据** (`GET /api/dashboard/reports/aqi-distribution`): 空气质量指数分布 -- **月度超标趋势** (`GET /api/dashboard/reports/monthly-exceedance-trend`): 污染超标趋势分析 -- **网格覆盖率** (`GET /api/dashboard/reports/grid-coverage`): 各城市网格覆盖情况 -- **反馈热力图** (`GET /api/dashboard/map/heatmap`): 反馈分布热力图数据 -- **污染物统计** (`GET /api/dashboard/reports/pollution-stats`): 各类污染物统计 -- **任务完成统计** (`GET /api/dashboard/reports/task-completion-stats`): 任务执行效率统计 -- **AQI热力图** (`GET /api/dashboard/map/aqi-heatmap`): AQI分布热力图 -- **污染物阈值管理** (`GET /api/dashboard/thresholds`): 污染物阈值配置 - -#### 权限控制 -```java -@PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") -public ResponseEntity getDashboardStats() { - // 仅决策者和管理员可访问 -} -``` - -#### 数据可视化支持 -- **多维度统计**: 支持时间、地域、类型等多维度数据分析 -- **实时数据**: 提供实时更新的环境监测数据 -- **热力图数据**: 支持地理信息系统(GIS)的数据可视化 -- **趋势分析**: 历史数据趋势和预测分析 - -### 5.3 反馈控制器 (`FeedbackController.java`) - -#### 功能概述 -反馈控制器处理公众环境问题反馈的全生命周期管理,是公众参与环境治理的重要渠道。 - -#### 主要接口 -- **提交反馈** (`POST /api/feedback/submit`): 支持文件上传的反馈提交 -- **JSON格式提交** (`POST /api/feedback/submit-json`): 测试用的JSON格式提交 -- **获取反馈列表** (`GET /api/feedback`): 支持多条件过滤的分页查询 -- **反馈详情** (`GET /api/feedback/{id}`): 获取特定反馈的详细信息 -- **反馈统计** (`GET /api/feedback/stats`): 反馈数据统计分析 - -#### 高级查询功能 -```java -@GetMapping -public ResponseEntity> getAllFeedback( - @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestParam(required = false) FeedbackStatus status, - @RequestParam(required = false) PollutionType pollutionType, - @RequestParam(required = false) SeverityLevel severityLevel, - @RequestParam(required = false) String cityName, - @RequestParam(required = false) String districtName, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, - @RequestParam(required = false) String keyword, - Pageable pageable) { - // 支持多维度过滤查询 -} -``` - -#### 文件上传支持 -- **多文件上传**: 支持同时上传多个附件文件 -- **文件类型验证**: 限制上传文件的类型和大小 -- **安全存储**: 文件安全存储和访问控制 - -### 5.4 文件控制器 (`FileController.java`) - -#### 功能概述 -文件控制器提供文件下载和在线预览功能,支持系统中所有文件资源的访问管理。 - -#### 主要接口 -- **文件下载** (`GET /api/files/{filename}`): 文件下载功能 -- **文件预览** (`GET /api/view/{fileName}`): 在线文件预览 - -#### 技术特性 -- **MIME类型检测**: 自动检测文件类型并设置正确的Content-Type -- **安全下载**: 防止路径遍历攻击 -- **流式传输**: 支持大文件的流式下载 - -#### 实现示例 -```java -@GetMapping("/files/{filename:.+}") -public ResponseEntity downloadFile(@PathVariable String filename, HttpServletRequest request) { - Resource resource = fileStorageService.loadFileAsResource(filename); - - String contentType = null; - try { - contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath()); - } catch (IOException ex) { - logger.info("Could not determine file type."); - } - - if (contentType == null) { - contentType = "application/octet-stream"; - } - - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType(contentType)) - .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"") - .body(resource); -} -``` - -### 5.5 网格控制器 (`GridController.java`) - -#### 功能概述 -网格控制器负责城市网格化管理,包括网格数据维护和网格员分配管理。 - -#### 主要接口 -- **网格列表** (`GET /api/grids`): 获取所有网格信息 -- **更新网格** (`PATCH /api/grids/{id}`): 更新网格信息 -- **网格覆盖率** (`GET /api/grids/coverage`): 网格覆盖率统计 -- **分配网格员** (`POST /api/grids/{gridId}/assign`): 将网格员分配到指定网格 -- **移除网格员** (`POST /api/grids/{gridId}/unassign`): 从网格中移除网格员 -- **坐标分配** (`POST /api/grids/coordinates/{gridX}/{gridY}/assign`): 通过坐标分配网格员 - -#### 网格员管理 -```java -@PostMapping("/{gridId}/assign") -@PreAuthorize("hasRole('ADMIN')") -public ResponseEntity assignGridWorker( - @PathVariable Long gridId, - @RequestBody Map request) { - Long userId = request.get("userId"); - - // 验证用户角色 - if (user.getRole() != Role.GRID_WORKER) { - return ResponseEntity.badRequest().body("只能分配网格员角色的用户"); - } - - // 分配网格坐标 - user.setGridX(grid.getGridX()); - user.setGridY(grid.getGridY()); - userAccountRepository.save(user); - - return ResponseEntity.ok().build(); -} -``` - -### 5.6 网格员任务控制器 (`GridWorkerTaskController.java`) - -#### 功能概述 -专为网格员设计的任务管理控制器,提供任务接收、处理和提交的完整工作流程。 - -#### 主要接口 -- **我的任务** (`GET /api/worker`): 获取分配给当前网格员的任务列表 -- **接受任务** (`POST /api/worker/{taskId}/accept`): 接受指定任务 -- **提交任务** (`POST /api/worker/{taskId}/submit`): 提交任务完成情况 -- **任务详情** (`GET /api/worker/{taskId}`): 查看任务详细信息 - -#### 权限控制 -```java -@RestController -@RequestMapping("/api/worker") -@RequiredArgsConstructor -@PreAuthorize("hasRole('GRID_WORKER')") -public class GridWorkerTaskController { - // 整个控制器仅限网格员角色访问 -} -``` - -#### 任务提交功能 -```java -@PostMapping(value = "/{taskId}/submit", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) -public ResponseEntity submitTask( - @PathVariable Long taskId, - @RequestPart("comments") @Valid String comments, - @RequestPart(value = "files", required = false) List files, - @AuthenticationPrincipal CustomUserDetails userDetails) { - // 支持文件上传的任务提交 -} -``` - -### 5.7 地图控制器 (`MapController.java`) - -#### 功能概述 -地图控制器管理系统的地图网格数据,为路径规划和地理信息展示提供基础数据支持。 - -#### 主要接口 -- **获取地图** (`GET /api/map/grid`): 获取完整的地图网格数据 -- **更新网格** (`POST /api/map/grid`): 创建或更新地图网格单元 -- **初始化地图** (`POST /api/map/initialize`): 初始化指定大小的地图 - -#### 地图初始化 -```java -@PostMapping("/initialize") -@Transactional -public ResponseEntity initializeMap( - @RequestParam(defaultValue = "20") int width, - @RequestParam(defaultValue = "20") int height) { - - mapGridRepository.deleteAll(); - mapGridRepository.flush(); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - MapGrid cell = MapGrid.builder() - .x(x) - .y(y) - .isObstacle(false) - .terrainType("ground") - .build(); - mapGridRepository.save(cell); - } - } - return ResponseEntity.ok("Initialized a " + width + "x" + height + " map."); -} -``` - -### 5.8 路径规划控制器 (`PathfindingController.java`) - -#### 功能概述 -路径规划控制器提供A*寻路算法功能,为网格员提供最优路径导航服务。 - -#### 主要接口 -- **路径查找** (`POST /api/pathfinding/find`): 计算两点之间的最优路径 - -#### A*算法实现 -```java -@PostMapping("/find") -public ResponseEntity> findPath(@RequestBody PathfindingRequest request) { - if (request == null) { - return ResponseEntity.badRequest().build(); - } - Point start = new Point(request.getStartX(), request.getStartY()); - Point end = new Point(request.getEndX(), request.getEndY()); - - List path = aStarService.findPath(start, end); - return ResponseEntity.ok(path); -} -``` - -#### 算法特性 -- **障碍物避让**: 考虑地图中的障碍物 -- **最优路径**: 计算距离最短的路径 -- **实时计算**: 快速响应路径查询请求 - -### 5.9 人员管理控制器 (`PersonnelController.java`) - -#### 功能概述 -人员管理控制器提供用户账号的完整生命周期管理,包括创建、查询、更新和删除操作。 - -#### 主要接口 -- **创建用户** (`POST /api/personnel/users`): 创建新用户账号 -- **用户列表** (`GET /api/personnel/users`): 分页查询用户列表 -- **用户详情** (`GET /api/personnel/users/{userId}`): 获取用户详细信息 -- **更新用户** (`PATCH /api/personnel/users/{userId}`): 更新用户信息 -- **更新角色** (`PUT /api/personnel/users/{userId}/role`): 更新用户角色 -- **删除用户** (`DELETE /api/personnel/users/{userId}`): 删除用户账号 - -#### 权限控制 -```java -@RestController -@RequestMapping("/api/personnel") -@RequiredArgsConstructor -@PreAuthorize("hasRole('ADMIN')") -public class PersonnelController { - // 仅管理员可访问人员管理功能 -} -``` - -#### 高级查询 -```java -@GetMapping("/users") -@PreAuthorize("hasRole('ADMIN') or hasRole('GRID_WORKER')") -public ResponseEntity> getUsers( - @RequestParam(required = false) Role role, - @RequestParam(required = false) String name, - Pageable pageable) { - // 支持按角色和姓名过滤的分页查询 -} -``` - -### 5.10 个人资料控制器 (`ProfileController.java`) - -#### 功能概述 -个人资料控制器处理认证用户的个人数据管理,提供用户自助服务功能。 - -#### 主要接口 -- **反馈历史** (`GET /api/me/feedback`): 获取当前用户的反馈提交历史 - -#### 用户数据隔离 -```java -@GetMapping("/feedback") -@PreAuthorize("isAuthenticated()") -public ResponseEntity> getMyFeedbackHistory( - @AuthenticationPrincipal CustomUserDetails userDetails, - Pageable pageable) { - // 仅返回当前用户的数据 - Page historyPage = userFeedbackService - .getFeedbackHistoryByUserId(userDetails.getId(), pageable); - return ResponseEntity.ok(historyPage.getContent()); -} -``` - -### 5.11 公共接口控制器 (`PublicController.java`) - -#### 功能概述 -公共接口控制器提供无需认证的公共API,主要服务于匿名用户的反馈提交。 - -#### 主要接口 -- **公共反馈提交** (`POST /api/public/feedback`): 支持匿名用户提交环境问题反馈 - -#### 匿名访问支持 -```java -@PostMapping(value = "/feedback", consumes = "multipart/form-data") -public ResponseEntity submitPublicFeedback( - @Valid @RequestPart("feedback") PublicFeedbackRequest request, - @RequestPart(value = "files", required = false) MultipartFile[] files) { - // 无需认证即可提交反馈 - Feedback createdFeedback = feedbackService.createPublicFeedback(request, files); - return new ResponseEntity<>(createdFeedback, HttpStatus.CREATED); -} -``` - -### 5.12 主管控制器 (`SupervisorController.java`) - -#### 功能概述 -主管控制器专为主管角色设计,提供反馈审核和管理功能。 - -#### 主要接口 -- **待审核反馈** (`GET /api/supervisor/reviews`): 获取待审核的反馈列表 -- **审核通过** (`POST /api/supervisor/reviews/{feedbackId}/approve`): 审核通过反馈 -- **审核拒绝** (`POST /api/supervisor/reviews/{feedbackId}/reject`): 审核拒绝反馈 - -#### 审核流程 -```java -@PostMapping("/reviews/{feedbackId}/approve") -@PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") -public ResponseEntity approveFeedback(@PathVariable Long feedbackId) { - supervisorService.approveFeedback(feedbackId); - return ResponseEntity.ok().build(); -} - -@PostMapping("/reviews/{feedbackId}/reject") -@PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") -public ResponseEntity rejectFeedback( - @PathVariable Long feedbackId, - @RequestBody RejectFeedbackRequest request) { - supervisorService.rejectFeedback(feedbackId, request); - return ResponseEntity.ok().build(); -} -``` - -### 5.13 任务分配控制器 (`TaskAssignmentController.java`) - -#### 功能概述 -任务分配控制器处理任务的智能分配和人员调度功能。 - -#### 主要接口 -- **未分配任务** (`GET /api/tasks/unassigned`): 获取未分配的任务列表 -- **可用网格员** (`GET /api/tasks/grid-workers`): 获取可用的网格工作人员 -- **分配任务** (`POST /api/tasks/assign`): 将任务分配给指定工作人员 - -#### 智能分配 -```java -@PostMapping("/assign") -@PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") -public ResponseEntity assignTask( - @RequestBody AssignmentRequest request, - @AuthenticationPrincipal CustomUserDetails adminDetails) { - - Long assignerId = adminDetails.getId(); - Assignment newAssignment = taskAssignmentService.assignTask( - request.feedbackId(), - request.assigneeId(), - assignerId - ); - return ResponseEntity.ok(newAssignment); -} -``` - -### 5.14 任务管理控制器 (`TaskManagementController.java`) - -#### 功能概述 -任务管理控制器提供任务全生命周期的管理功能,是任务管理的核心控制器。 - -#### 主要接口 -- **任务列表** (`GET /api/management/tasks`): 支持多条件过滤的任务分页查询 -- **分配任务** (`POST /api/management/tasks/{taskId}/assign`): 分配任务给工作人员 -- **任务详情** (`GET /api/management/tasks/{taskId}`): 获取任务详细信息 -- **审核任务** (`POST /api/management/tasks/{taskId}/review`): 审核任务完成情况 -- **取消任务** (`POST /api/management/tasks/{taskId}/cancel`): 取消任务 -- **创建任务** (`POST /api/management/tasks`): 创建新任务 -- **待处理反馈** (`GET /api/management/tasks/feedback`): 获取待处理的反馈 -- **从反馈创建任务** (`POST /api/management/tasks/feedback/{feedbackId}/create-task`): 从反馈创建任务 - -#### 高级过滤查询 -```java -@GetMapping -@PreAuthorize("hasAnyAuthority('ADMIN', 'SUPERVISOR', 'DECISION_MAKER')") -public ResponseEntity> getTasks( - @RequestParam(required = false) TaskStatus status, - @RequestParam(required = false) Long assigneeId, - @RequestParam(required = false) SeverityLevel severity, - @RequestParam(required = false) PollutionType pollutionType, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, - Pageable pageable) { - // 支持多维度过滤的任务查询 -} -``` - -#### 任务状态流转 -```java -@PostMapping("/{taskId}/review") -public ResponseEntity reviewTask( - @PathVariable Long taskId, - @RequestBody @Valid TaskApprovalRequest request) { - TaskDetailDTO updatedTask = taskManagementService.reviewTask(taskId, request); - return ResponseEntity.ok(updatedTask); -} - -@PostMapping("/{taskId}/cancel") -public ResponseEntity cancelTask(@PathVariable Long taskId) { - TaskDetailDTO updatedTask = taskManagementService.cancelTask(taskId); - return ResponseEntity.ok(updatedTask); -} -``` - -### 5.15 控制器模块最佳实践 - -#### 1. RESTful API设计 -- **统一的URL命名规范**: 使用名词复数形式,避免动词 -- **HTTP方法语义化**: GET查询、POST创建、PUT/PATCH更新、DELETE删除 -- **状态码规范**: 正确使用HTTP状态码表达操作结果 -- **版本控制**: 通过URL路径进行API版本管理 - -#### 2. 安全控制 -- **细粒度权限控制**: 使用`@PreAuthorize`进行方法级权限控制 -- **输入验证**: 使用Bean Validation进行参数校验 -- **敏感数据保护**: 避免在响应中暴露敏感信息(如密码) -- **CSRF防护**: 对状态变更操作进行CSRF保护 - -#### 3. 异常处理 -- **全局异常处理**: 使用`@ControllerAdvice`统一处理异常 -- **友好错误信息**: 为客户端提供清晰的错误描述 -- **日志记录**: 记录详细的错误日志便于问题排查 - -#### 4. 性能优化 -- **分页查询**: 对大数据量查询使用分页机制 -- **缓存策略**: 对频繁查询的数据使用缓存 -- **异步处理**: 对耗时操作使用异步处理 -- **数据传输优化**: 使用DTO减少不必要的数据传输 - -#### 5. 文档和测试 -- **API文档**: 使用Swagger/OpenAPI生成完整的API文档 -- **单元测试**: 为每个控制器方法编写单元测试 -- **集成测试**: 测试完整的请求-响应流程 -- **性能测试**: 对关键接口进行性能测试 - -### 5.16 控制器模块总结 - -控制器模块为EMS系统提供了完整的API服务层: - -1. **全面的功能覆盖**: 14个专业化控制器覆盖了系统的所有核心功能 -2. **严格的权限控制**: 基于角色的访问控制确保系统安全 -3. **灵活的查询支持**: 支持多维度过滤、分页、排序等高级查询功能 -4. **完善的文件处理**: 支持文件上传、下载、预览等文件操作 -5. **智能的任务管理**: 提供完整的任务生命周期管理和智能分配功能 -6. **实时的数据可视化**: 为决策支持提供丰富的数据分析接口 -7. **用户友好的接口设计**: 遵循RESTful设计原则,提供直观易用的API - -通过这些控制器的协同工作,EMS系统能够为不同角色的用户提供专业化、个性化的服务,实现了从公众反馈到问题解决的完整业务闭环。 -6. **扩展性**: 为WebSocket等实时通信功能预留扩展空间 - -这些配置组件共同构成了一个健壮、可扩展、易维护的系统基础架构,为EMS系统的稳定运行和功能扩展提供了坚实的技术保障。 - ---- - -# DTO模块详细解析 - -## 概述 - -DTO(Data Transfer Object)模块位于 `com.dne.ems.dto` 包下,包含51个数据传输对象,负责在不同层之间传递数据,确保数据的封装性和安全性。DTO模块按功能分类,涵盖用户管理、任务管理、反馈处理、地图网格、统计分析和AI集成等核心业务领域。 - -## 核心功能分类 - -### 1. 用户管理相关DTO - -#### 用户注册和认证 -- **SignUpRequest**: 用户注册请求,包含姓名、邮箱、手机、密码、角色和验证码 -- **LoginRequest**: 用户登录请求,封装邮箱和密码 -- **UserRegistrationRequest**: 公众用户注册请求,支持密码复杂度验证 -- **JwtAuthenticationResponse**: JWT认证响应,返回访问令牌 - -#### 用户信息管理 -- **UserCreationRequest**: 管理员创建用户请求,支持角色分配和区域设置 -- **UserUpdateRequest**: 用户信息更新请求,支持多字段批量更新 -- **UserRoleUpdateRequest**: 用户角色更新请求,网格员需要位置信息 -- **UserInfoDTO**: 用户基本信息传输对象,隐藏敏感数据 -- **UserSummaryDTO**: 用户摘要信息,用于列表展示 - -#### 密码管理 -- **PasswordResetRequest**: 密码重置邮件请求 -- **PasswordResetDto**: 通过令牌重置密码 -- **PasswordResetWithCodeDto**: 通过验证码重置密码 - -### 2. 任务管理相关DTO - -#### 任务创建和分配 -- **TaskCreationRequest**: 任务创建请求,包含标题、描述、污染类型、严重级别和位置 -- **TaskAssignmentRequest**: 任务分配请求,指定受理人ID -- **TaskFromFeedbackRequest**: 从反馈创建任务请求,设置严重级别 - -#### 任务处理和审核 -- **TaskSubmissionRequest**: 任务提交请求,包含处理意见 -- **TaskApprovalRequest**: 任务审核请求,支持通过或拒绝 -- **TaskRejectionRequest**: 任务拒绝请求,必须提供拒绝原因 - -#### 任务信息展示 -- **TaskDTO**: 任务基本信息,包含ID、标题、描述和状态 -- **TaskSummaryDTO**: 任务摘要信息,用于列表展示 -- **TaskDetailDTO**: 任务详细信息,包含完整的任务数据和历史记录 -- **TaskInfoDTO**: 任务简要信息,包含状态和受理人 -- **TaskHistoryDTO**: 任务历史记录,追踪状态变更 -- **TaskStatsDTO**: 任务统计数据,计算完成率 - -### 3. 反馈处理相关DTO - -#### 反馈提交 -- **PublicFeedbackRequest**: 公众反馈提交请求,支持匿名提交 -- **FeedbackSubmissionRequest**: 反馈提交请求,包含位置和附件信息 -- **AqiDataSubmissionRequest**: AQI数据提交请求,专门处理空气质量数据 - -#### 反馈处理 -- **ProcessFeedbackRequest**: 反馈处理请求,支持批准或采取行动 -- **RejectFeedbackRequest**: 反馈拒绝请求,需要提供拒绝原因 - -#### 反馈信息展示 -- **FeedbackDTO**: 反馈详细信息传输对象 -- **FeedbackResponseDTO**: 反馈响应数据封装 -- **UserFeedbackSummaryDTO**: 用户反馈历史摘要 -- **FeedbackStatsResponse**: 反馈统计数据响应 - -### 4. 地图网格相关DTO - -#### 网格管理 -- **GridUpdateRequest**: 网格属性更新请求 -- **GridAssignmentRequest**: 网格员分配请求 -- **GridCoverageDTO**: 网格覆盖统计数据 -- **CityBlueprint**: 城市网格蓝图生成 - -#### 位置和路径 -- **LocationUpdateRequest**: 用户位置更新请求 -- **PathfindingRequest**: A*路径查找请求,包含起点和终点 -- **Point**: 二维坐标点表示 - -### 5. 统计分析相关DTO - -#### 仪表盘统计 -- **DashboardStatsDTO**: 仪表盘关键统计数据 -- **PollutionStatsDTO**: 污染物统计数据 -- **TrendDataPointDTO**: 趋势数据点,用于时间序列分析 - -#### 热力图数据 -- **HeatmapPointDTO**: 通用热力图数据点 -- **AqiHeatmapPointDTO**: AQI热力图专用数据点 -- **AqiDistributionDTO**: AQI分布统计数据 -- **PollutantThresholdDTO**: 污染物阈值信息 - -### 6. 通用工具DTO - -#### 分页和响应 -- **PageDTO**: 分页查询结果封装,支持泛型 -- **ErrorResponseDTO**: API错误响应标准格式 -- **AttachmentDTO**: 附件信息封装 -- **AssigneeInfoDTO**: 任务受理人信息 - -### 7. AI集成相关DTO - -#### 火山引擎AI聊天 -- **VolcanoChatRequest**: 火山引擎聊天API请求,支持多轮对话和工具调用 -- **VolcanoChatResponse**: 火山引擎聊天API响应,包含AI回复和工具调用结果 - -## 核心代码示例 - -### 任务创建请求DTO -```java -public record TaskCreationRequest( - @NotBlank(message = "Title is mandatory") - String title, - String description, - @NotNull(message = "Pollution type is mandatory") - PollutionType pollutionType, - @NotNull(message = "Severity level is mandatory") - SeverityLevel severityLevel, - @NotNull(message = "Location information is mandatory") - LocationDTO location -) { - public record LocationDTO( - @NotBlank(message = "Address is mandatory") - String address, - @NotNull(message = "Grid X coordinate is mandatory") - Integer gridX, - @NotNull(message = "Grid Y coordinate is mandatory") - Integer gridY - ) {} -} -``` - -### 分页查询结果DTO -```java -public record PageDTO( - List content, - int page, - int size, - long totalElements, - int totalPages, - boolean first, - boolean last -) { - public static PageDTO fromPage(Page page) { - return new PageDTO<>( - page.getContent(), - page.getNumber(), - page.getSize(), - page.getTotalElements(), - page.getTotalPages(), - page.isFirst(), - page.isLast() - ); - } -} -``` - -### 用户信息DTO工厂方法 -```java -@Data -@NoArgsConstructor -@AllArgsConstructor -public class UserInfoDTO { - private Long id; - private String name; - private String phone; - private String email; - - public static UserInfoDTO fromEntity(UserAccount user) { - if (user == null) { - return null; - } - return new UserInfoDTO( - user.getId(), - user.getName(), - user.getPhone(), - user.getEmail() - ); - } -} -``` - -### AI聊天请求DTO -```java -@Data -@Builder -@JsonInclude(JsonInclude.Include.NON_NULL) -public class VolcanoChatRequest { - private String model; - private List messages; - private List tools; - - @Data - @SuperBuilder - public static class Message { - private String role; - private List content; - } - - @Data - @SuperBuilder - public static class ContentPart { - private String type; - private String text; - @JsonProperty("image_url") - private ImageUrl imageUrl; - } -} -``` - -## 设计特点 - -### 1. 数据验证 -- 使用Jakarta Validation注解进行参数校验 -- 自定义验证注解(如@ValidPassword、@GridWorkerLocationRequired) -- 分层验证策略,确保数据完整性 - -### 2. 类型安全 -- 大量使用Record类型,确保不可变性 -- 泛型支持,如PageDTO提供类型安全的分页 -- 枚举类型约束,避免无效值 - -### 3. 序列化优化 -- Jackson注解配置,控制JSON序列化行为 -- @JsonInclude(JsonInclude.Include.NON_NULL)避免空值传输 -- @JsonProperty处理字段名映射 - -### 4. 业务语义 -- 清晰的命名约定,Request/Response/DTO后缀明确用途 -- 丰富的JavaDoc文档,说明业务规则和使用场景 -- 嵌套记录结构,体现数据层次关系 - -## DTO模块最佳实践 - -1. **不可变设计**:优先使用Record类型,确保数据传输对象的不可变性 -2. **验证完整性**:在DTO层进行数据验证,避免无效数据进入业务层 -3. **类型安全**:使用强类型和泛型,减少运行时错误 -4. **文档完善**:提供详细的JavaDoc,说明字段含义和业务规则 -5. **序列化优化**:合理使用Jackson注解,优化网络传输效率 -6. **分层隔离**:DTO与实体类分离,避免业务逻辑泄露到表示层 - -## DTO模块总结 - -DTO模块为EMS系统提供了完整的数据传输层: - -1. **全面的功能覆盖**: 51个DTO类覆盖了系统的所有核心功能领域 -2. **严格的数据验证**: 多层次验证机制确保数据质量和安全性 -3. **灵活的数据结构**: 支持复杂嵌套结构和泛型,满足不同业务需求 -4. **优化的传输效率**: 通过合理的序列化配置减少网络传输开销 -5. **清晰的业务语义**: 命名规范和文档完善,便于开发和维护 -6. **类型安全保障**: Record类型和强类型约束减少运行时错误 -7. **扩展性支持**: 为AI集成等新功能提供了良好的数据结构基础 - -通过这些DTO的协同工作,EMS系统能够在各层之间安全、高效地传递数据,为整个系统的稳定运行和功能扩展提供了坚实的数据传输基础。 - ---- - -# Event模块详细解析 - -## 概述 - -Event(事件)模块位于 `com.dne.ems.event` 包下,包含2个应用事件类,实现了系统的事件驱动架构。通过Spring的事件发布机制,实现了业务模块之间的松耦合通信,支持异步处理和系统扩展。 - -## 核心事件类 - -### 1. FeedbackSubmittedForAiReviewEvent - -#### 功能概述 -- **用途**: 反馈提交AI审核事件 -- **触发时机**: 当公众提交反馈后,系统需要进行AI预审核时发布 -- **业务价值**: 实现反馈提交与AI审核的解耦,支持异步AI分析 - -#### 核心特性 -- **继承关系**: 继承自Spring的`ApplicationEvent`基类 -- **数据载体**: 携带完整的`Feedback`实体对象 -- **事件源**: 支持指定事件发布源,便于追踪和调试 - -#### 代码实现 -```java -package com.dne.ems.event; - -import org.springframework.context.ApplicationEvent; -import com.dne.ems.model.Feedback; - -public class FeedbackSubmittedForAiReviewEvent extends ApplicationEvent { - private final Feedback feedback; - - public FeedbackSubmittedForAiReviewEvent(Object source, Feedback feedback) { - super(source); - this.feedback = feedback; - } - - public Feedback getFeedback() { - return feedback; - } -} -``` - -#### 使用场景 -1. **反馈提交后**: 在`FeedbackService`中提交反馈成功后发布此事件 -2. **AI审核触发**: 事件监听器接收到事件后,调用AI服务进行内容分析 -3. **异步处理**: 避免反馈提交流程被AI分析耗时阻塞 - -### 2. TaskReadyForAssignmentEvent - -#### 功能概述 -- **用途**: 任务准备分配事件 -- **触发时机**: 当任务创建完成或状态变更为可分配时发布 -- **业务价值**: 实现任务创建与分配逻辑的解耦,支持智能分配算法 - -#### 核心特性 -- **继承关系**: 继承自Spring的`ApplicationEvent`基类 -- **数据载体**: 携带完整的`Task`实体对象 -- **Lombok支持**: 使用`@Getter`注解简化代码 -- **不可变性**: 任务对象使用`final`修饰,确保事件数据不被修改 - -#### 代码实现 -```java -package com.dne.ems.event; - -import org.springframework.context.ApplicationEvent; -import com.dne.ems.model.Task; -import lombok.Getter; - -@Getter -public class TaskReadyForAssignmentEvent extends ApplicationEvent { - private final Task task; - - public TaskReadyForAssignmentEvent(Object source, Task task) { - super(source); - this.task = task; - } -} -``` - -#### 使用场景 -1. **任务创建后**: 在`TaskService`中创建新任务后发布此事件 -2. **状态变更**: 任务状态从其他状态变更为待分配时发布 -3. **智能分配**: 事件监听器可以触发智能分配算法,推荐最佳执行人 -4. **通知机制**: 通知相关人员有新任务需要分配 - -## 事件驱动架构优势 - -### 1. 松耦合设计 -- **模块独立**: 事件发布者和监听者之间没有直接依赖关系 -- **易于扩展**: 可以随时添加新的事件监听器而不影响现有代码 -- **职责分离**: 每个模块专注于自己的核心业务逻辑 - -### 2. 异步处理能力 -- **性能提升**: 耗时操作(如AI分析)不会阻塞主业务流程 -- **用户体验**: 用户操作可以立即得到响应 -- **系统吞吐**: 提高系统整体处理能力 - -### 3. 可观测性 -- **事件追踪**: 可以记录事件的发布和处理过程 -- **调试支持**: 便于排查业务流程中的问题 -- **监控集成**: 可以与监控系统集成,实时观察系统状态 - -## 事件处理流程 - -### 反馈AI审核流程 -``` -1. 用户提交反馈 → FeedbackService.createFeedback() -2. 保存反馈到数据库 -3. 发布 FeedbackSubmittedForAiReviewEvent -4. AI审核监听器接收事件 -5. 调用AI服务分析反馈内容 -6. 更新反馈状态和AI分析结果 -``` - -### 任务分配流程 -``` -1. 创建新任务 → TaskService.createTask() -2. 保存任务到数据库 -3. 发布 TaskReadyForAssignmentEvent -4. 任务分配监听器接收事件 -5. 执行智能分配算法 -6. 推荐最佳执行人或自动分配 -``` - -## 技术实现细节 - -### 1. Spring事件机制 -- **ApplicationEvent**: 所有自定义事件的基类 -- **ApplicationEventPublisher**: 用于发布事件的接口 -- **@EventListener**: 标记事件监听方法的注解 -- **@Async**: 支持异步事件处理 - -### 2. 事件发布示例 -```java -@Service -public class FeedbackService { - @Autowired - private ApplicationEventPublisher eventPublisher; - - public Feedback createFeedback(FeedbackRequest request) { - // 保存反馈 - Feedback feedback = feedbackRepository.save(newFeedback); - - // 发布事件 - eventPublisher.publishEvent( - new FeedbackSubmittedForAiReviewEvent(this, feedback) - ); - - return feedback; - } -} -``` - -### 3. 事件监听示例 -```java -@Component -public class FeedbackEventListener { - - @EventListener - @Async - public void handleFeedbackSubmitted(FeedbackSubmittedForAiReviewEvent event) { - Feedback feedback = event.getFeedback(); - // 执行AI审核逻辑 - aiReviewService.reviewFeedback(feedback); - } -} -``` - -## Event模块最佳实践 - -1. **事件命名**: 使用清晰的命名约定,体现事件的业务含义 -2. **数据封装**: 事件中携带的数据应该是完整且不可变的 -3. **异步处理**: 对于耗时操作,使用`@Async`注解实现异步处理 -4. **异常处理**: 事件监听器中要有完善的异常处理机制 -5. **幂等性**: 确保事件处理的幂等性,避免重复处理问题 -6. **监控日志**: 记录事件的发布和处理日志,便于问题排查 - -## Event模块总结 - -Event模块为EMS系统提供了强大的事件驱动能力: - -1. **解耦架构**: 通过事件机制实现模块间的松耦合通信 -2. **异步处理**: 支持耗时操作的异步执行,提升系统性能 -3. **扩展性强**: 易于添加新的业务逻辑而不影响现有代码 -4. **可观测性**: 提供清晰的业务流程追踪和监控能力 -5. **Spring集成**: 充分利用Spring框架的事件机制和异步支持 - -虽然目前只有2个事件类,但它们为系统的核心业务流程(反馈处理和任务分配)提供了关键的解耦和异步处理能力,为系统的可扩展性和性能优化奠定了坚实基础。 - ---- - -# Exception模块详细解析 - -## 概述 - -Exception(异常处理)模块位于 `com.dne.ems.exception` 包下,包含7个异常处理相关的类,构建了完整的异常处理体系。通过自定义异常类和全局异常处理器,实现了统一的错误响应格式和完善的异常处理机制,提升了系统的健壮性和用户体验。 - -## 核心异常类 - -### 1. ErrorResponseDTO - -#### 功能概述 -- **用途**: 标准化API错误响应数据传输对象 -- **作用**: 为所有API端点提供一致的错误结构 -- **业务价值**: 统一错误响应格式,提升前端处理效率 - -#### 核心特性 -- **Lombok支持**: 使用`@Data`、`@AllArgsConstructor`、`@NoArgsConstructor`注解 -- **时间戳**: 记录错误发生的精确时间 -- **状态码**: HTTP状态码,便于前端判断错误类型 -- **错误分类**: 提供错误类型和详细消息 -- **路径追踪**: 记录发生错误的请求路径 - -#### 代码实现 -```java -@Data -@AllArgsConstructor -@NoArgsConstructor -public class ErrorResponseDTO { - private LocalDateTime timestamp; - private int status; - private String error; - private String message; - private String path; -} -``` - -### 2. ResourceNotFoundException - -#### 功能概述 -- **用途**: 资源不存在异常 -- **HTTP状态**: 404 Not Found -- **触发场景**: 查询的资源不存在时抛出 -- **业务价值**: 明确区分资源不存在的情况 - -#### 核心特性 -- **Spring集成**: 使用`@ResponseStatus(HttpStatus.NOT_FOUND)`注解 -- **自动映射**: 异常抛出时自动返回404状态码 -- **继承关系**: 继承自`RuntimeException`,支持非检查异常 - -#### 使用场景 -1. **用户查询**: 查询不存在的用户ID -2. **任务查询**: 查询不存在的任务ID -3. **反馈查询**: 查询不存在的反馈记录 -4. **文件查询**: 查询不存在的附件文件 - -### 3. InvalidOperationException - -#### 功能概述 -- **用途**: 无效操作异常 -- **HTTP状态**: 400 Bad Request -- **触发场景**: 业务规则不允许的操作时抛出 -- **业务价值**: 保护业务逻辑完整性 - -#### 核心特性 -- **业务保护**: 防止违反业务规则的操作 -- **状态映射**: 自动返回400状态码 -- **简洁设计**: 只提供消息构造器,专注业务逻辑 - -#### 使用场景 -1. **状态变更**: 任务状态不允许的变更操作 -2. **权限检查**: 用户权限不足的操作 -3. **数据验证**: 业务数据不符合规则 -4. **流程控制**: 违反业务流程的操作 - -### 4. UserAlreadyExistsException - -#### 功能概述 -- **用途**: 用户已存在异常 -- **HTTP状态**: 409 Conflict -- **触发场景**: 注册时用户名或邮箱已存在 -- **业务价值**: 防止重复用户注册 - -#### 核心特性 -- **冲突处理**: 使用409状态码表示资源冲突 -- **注册保护**: 确保用户唯一性 -- **清晰语义**: 明确表达用户重复的问题 - -#### 使用场景 -1. **用户注册**: 用户名已被占用 -2. **邮箱注册**: 邮箱地址已被使用 -3. **账号创建**: 管理员创建重复账号 - -### 5. FileNotFoundException - -#### 功能概述 -- **用途**: 文件不存在异常 -- **HTTP状态**: 404 Not Found -- **触发场景**: 请求的文件不存在时抛出 -- **业务价值**: 专门处理文件相关的404错误 - -#### 核心特性 -- **文件专用**: 专门处理文件操作异常 -- **双构造器**: 支持消息和原因链的构造 -- **异常链**: 支持异常原因追踪 - -#### 使用场景 -1. **附件下载**: 下载不存在的附件文件 -2. **图片访问**: 访问不存在的图片资源 -3. **文档查看**: 查看已删除的文档 - -### 6. FileStorageException - -#### 功能概述 -- **用途**: 文件存储异常 -- **HTTP状态**: 默认500(通过全局处理器处理) -- **触发场景**: 文件存储操作失败时抛出 -- **业务价值**: 统一处理文件存储相关错误 - -#### 核心特性 -- **存储专用**: 专门处理文件存储异常 -- **异常链支持**: 保留原始异常信息 -- **灵活处理**: 可通过全局处理器自定义响应 - -#### 使用场景 -1. **文件上传**: 上传过程中的存储失败 -2. **文件删除**: 删除文件时的IO错误 -3. **文件移动**: 文件移动或重命名失败 -4. **磁盘空间**: 存储空间不足的情况 - -### 7. GlobalExceptionHandler - -#### 功能概述 -- **用途**: 全局异常处理器 -- **作用**: 统一处理控制器层抛出的异常 -- **业务价值**: 提供一致的错误响应和完善的日志记录 - -#### 核心特性 -- **全局捕获**: 使用`@ControllerAdvice`注解实现全局异常捕获 -- **分类处理**: 针对不同异常类型提供专门的处理方法 -- **日志记录**: 使用`@Slf4j`记录详细的错误日志 -- **标准响应**: 返回统一格式的错误响应 - -#### 处理的异常类型 - -##### ResourceNotFoundException处理 -```java -@ExceptionHandler(ResourceNotFoundException.class) -public ResponseEntity handleResourceNotFoundException( - ResourceNotFoundException ex, WebRequest request) { - ErrorResponseDTO errorResponse = new ErrorResponseDTO( - LocalDateTime.now(), - HttpStatus.NOT_FOUND.value(), - "Not Found", - ex.getMessage(), - request.getDescription(false).replace("uri=", "")); - return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); -} -``` - -##### InvalidOperationException处理 -```java -@ExceptionHandler(InvalidOperationException.class) -public ResponseEntity handleInvalidOperationException( - InvalidOperationException ex, WebRequest request) { - ErrorResponseDTO errorResponse = new ErrorResponseDTO( - LocalDateTime.now(), - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - ex.getMessage(), - request.getDescription(false).replace("uri=", "")); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); -} -``` - -##### 全局异常处理 -```java -@ExceptionHandler(Exception.class) -public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { - log.error("捕获到全局未处理异常! 请求: {}", request.getDescription(false), ex); - - Map errorDetails = Map.of( - "message", "服务器内部发生未知错误,请联系技术支持", - "error", ex.getClass().getName(), - "details", ex.getMessage() - ); - - return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR); -} -``` - -## 异常处理架构优势 - -### 1. 统一错误格式 -- **一致性**: 所有API错误都使用相同的响应格式 -- **可预测**: 前端可以统一处理错误响应 -- **信息完整**: 包含时间戳、状态码、错误类型、消息和路径 - -### 2. 分层异常处理 -- **业务异常**: 自定义异常类处理特定业务场景 -- **系统异常**: 全局处理器兜底处理未预期异常 -- **HTTP映射**: 自动将异常映射为合适的HTTP状态码 - -### 3. 完善的日志记录 -- **错误追踪**: 详细记录异常发生的上下文 -- **问题定位**: 便于开发人员快速定位问题 -- **监控集成**: 支持与监控系统集成 - -### 4. 用户友好 -- **清晰消息**: 提供用户可理解的错误消息 -- **状态码**: 正确的HTTP状态码便于前端处理 -- **安全性**: 不暴露系统内部敏感信息 - -## 异常处理流程 - -### 标准异常处理流程 -``` -1. 业务层抛出自定义异常 -2. 控制器层捕获异常 -3. GlobalExceptionHandler拦截异常 -4. 根据异常类型选择处理方法 -5. 构建ErrorResponseDTO响应 -6. 记录错误日志 -7. 返回标准化错误响应 -``` - -### 异常分类处理 -``` -业务异常: -├── ResourceNotFoundException (404) - 资源不存在 -├── InvalidOperationException (400) - 无效操作 -├── UserAlreadyExistsException (409) - 用户已存在 -├── FileNotFoundException (404) - 文件不存在 -└── FileStorageException (500) - 文件存储错误 - -系统异常: -└── Exception (500) - 未处理的系统异常 -``` - -## Exception模块最佳实践 - -1. **异常分类**: 根据业务场景创建专门的异常类 -2. **状态码映射**: 使用`@ResponseStatus`注解自动映射HTTP状态码 -3. **消息设计**: 提供清晰、用户友好的错误消息 -4. **日志记录**: 在全局处理器中记录详细的错误日志 -5. **安全考虑**: 避免在错误响应中暴露敏感信息 -6. **异常链**: 保留原始异常信息便于问题追踪 -7. **统一格式**: 使用ErrorResponseDTO确保响应格式一致 - -## Exception模块总结 - -Exception模块为EMS系统提供了完善的异常处理能力: - -1. **完整体系**: 涵盖业务异常、系统异常和文件异常的完整处理体系 -2. **统一响应**: 通过ErrorResponseDTO提供一致的错误响应格式 -3. **自动映射**: 利用Spring的@ResponseStatus注解自动映射HTTP状态码 -4. **全局处理**: GlobalExceptionHandler提供兜底的异常处理机制 -5. **日志完善**: 详细的错误日志记录便于问题追踪和系统监控 -6. **用户友好**: 清晰的错误消息和正确的状态码提升用户体验 -7. **安全可靠**: 在提供有用信息的同时保护系统安全 - -通过这套异常处理机制,EMS系统能够优雅地处理各种异常情况,为用户提供清晰的错误反馈,为开发人员提供完善的错误追踪能力,大大提升了系统的健壮性和可维护性。 - ---- - -# Listener模块详细解析 - -## 概述 - -Listener(监听器)模块位于 `com.dne.ems.listener` 包下,包含3个事件监听器类,实现了系统的事件驱动响应机制。通过监听Spring Security认证事件和自定义业务事件,实现了安全控制、用户状态管理和业务流程自动化,为系统的安全性和业务连续性提供了重要保障。 - -## 核心监听器类 - -### 1. AuthenticationFailureEventListener - -#### 功能概述 -- **用途**: 认证失败事件监听器 -- **监听事件**: `AuthenticationFailureBadCredentialsEvent` -- **触发场景**: 用户登录认证失败时 -- **业务价值**: 实现登录失败次数统计和IP锁定机制 - -#### 核心特性 -- **Spring Security集成**: 监听Spring Security的认证失败事件 -- **IP地址获取**: 智能获取真实客户端IP地址 -- **代理支持**: 支持X-Forwarded-For头部处理 -- **安全防护**: 防止暴力破解攻击 - -#### 代码实现 -```java -@Component -public class AuthenticationFailureEventListener - implements ApplicationListener { - - @Autowired - private LoginAttemptService loginAttemptService; - - @Autowired - private HttpServletRequest request; - - @Override - public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) { - final String xfHeader = request.getHeader("X-Forwarded-For"); - String ip; - if (xfHeader == null || xfHeader.isEmpty() || !xfHeader.contains(request.getRemoteAddr())) { - ip = request.getRemoteAddr(); - } else { - ip = xfHeader.split(",")[0]; - } - - // 使用IP地址作为锁定键 - loginAttemptService.loginFailed(ip); - } -} -``` - -#### IP地址获取逻辑 -1. **代理检测**: 检查X-Forwarded-For头部是否存在 -2. **真实IP**: 优先使用代理转发的真实客户端IP -3. **直连IP**: 无代理时使用直接连接的IP地址 -4. **安全验证**: 验证代理IP的合法性 - -#### 使用场景 -1. **登录保护**: 记录失败的登录尝试 -2. **暴力破解防护**: 限制同一IP的登录尝试次数 -3. **安全审计**: 记录可疑的登录行为 -4. **IP黑名单**: 自动封禁恶意IP地址 - -### 2. AuthenticationSuccessEventListener - -#### 功能概述 -- **用途**: 认证成功事件监听器 -- **监听事件**: `AuthenticationSuccessEvent` -- **触发场景**: 用户登录认证成功时 -- **业务价值**: 重置用户登录失败计数和解除账户锁定 - -#### 核心特性 -- **状态重置**: 成功登录后重置失败计数 -- **锁定解除**: 自动解除账户锁定状态 -- **用户管理**: 维护用户账户状态的一致性 -- **安全恢复**: 恢复正常用户的访问权限 - -#### 代码实现 -```java -@Component -public class AuthenticationSuccessEventListener - implements ApplicationListener { - - @Autowired - private UserAccountRepository userAccountRepository; - - @Override - public void onApplicationEvent(@NonNull AuthenticationSuccessEvent event) { - Object principal = event.getAuthentication().getPrincipal(); - - if (principal instanceof UserDetails userDetails) { - String username = userDetails.getUsername(); - UserAccount user = userAccountRepository.findByEmail(username).orElse(null); - - if (user != null && user.getFailedLoginAttempts() > 0) { - user.setFailedLoginAttempts(0); - user.setLockoutEndTime(null); - userAccountRepository.save(user); - } - } - } -} -``` - -#### 处理流程 -1. **身份验证**: 确认认证主体是UserDetails实例 -2. **用户查找**: 根据用户名查找用户账户 -3. **状态检查**: 检查是否存在失败登录记录 -4. **状态重置**: 清零失败次数并解除锁定 -5. **数据持久化**: 保存更新后的用户状态 - -#### 使用场景 -1. **账户恢复**: 成功登录后恢复账户正常状态 -2. **计数重置**: 清除之前的失败登录记录 -3. **锁定解除**: 自动解除临时锁定状态 -4. **状态同步**: 保持用户状态的一致性 - -### 3. FeedbackEventListener - -#### 功能概述 -- **用途**: 反馈事件监听器 -- **监听事件**: `FeedbackSubmittedForAiReviewEvent` -- **触发场景**: 反馈提交需要AI审核时 -- **业务价值**: 实现反馈的异步AI审核处理 - -#### 核心特性 -- **异步处理**: 使用`@Async`注解实现异步执行 -- **事件驱动**: 基于Spring的事件机制 -- **AI集成**: 调用AI服务进行内容审核 -- **日志记录**: 详细记录处理过程 - -#### 代码实现 -```java -@Component -@Slf4j -@RequiredArgsConstructor -public class FeedbackEventListener { - - private final AiReviewService aiReviewService; - - @Async - @EventListener - public void handleFeedbackSubmittedForAiReview(FeedbackSubmittedForAiReviewEvent event) { - log.info("Received feedback submission event for AI review. Feedback ID: {}", - event.getFeedback().getId()); - aiReviewService.reviewFeedback(event.getFeedback()); - } -} -``` - -#### 处理特点 -1. **异步执行**: 不阻塞主业务流程 -2. **事件响应**: 响应反馈提交事件 -3. **AI调用**: 调用AI服务进行内容分析 -4. **日志追踪**: 记录处理过程便于监控 - -#### 使用场景 -1. **内容审核**: 自动审核用户提交的反馈内容 -2. **智能分类**: AI分析反馈的类型和优先级 -3. **质量评估**: 评估反馈的有效性和重要性 -4. **自动处理**: 根据AI分析结果自动处理简单反馈 - -## 监听器架构优势 - -### 1. 事件驱动架构 -- **解耦设计**: 事件发布者和监听者之间松耦合 -- **扩展性强**: 可以轻松添加新的事件监听器 -- **职责分离**: 每个监听器专注于特定的业务逻辑 -- **异步支持**: 支持异步事件处理提升性能 - -### 2. 安全机制增强 -- **登录保护**: 自动记录和处理登录失败 -- **状态管理**: 智能管理用户账户状态 -- **攻击防护**: 防止暴力破解和恶意登录 -- **恢复机制**: 自动恢复正常用户的访问权限 - -### 3. 业务流程自动化 -- **智能审核**: 自动触发AI内容审核 -- **状态同步**: 自动同步用户和业务状态 -- **流程优化**: 减少手动干预提升效率 -- **响应及时**: 实时响应业务事件 - -### 4. 系统可观测性 -- **日志记录**: 详细记录事件处理过程 -- **状态追踪**: 跟踪用户和业务状态变化 -- **性能监控**: 监控事件处理性能 -- **问题定位**: 便于排查和解决问题 - -## 事件处理流程 - -### 认证失败处理流程 -``` -1. 用户登录失败 → Spring Security触发AuthenticationFailureBadCredentialsEvent -2. AuthenticationFailureEventListener接收事件 -3. 获取客户端真实IP地址 -4. 调用LoginAttemptService记录失败尝试 -5. 更新IP失败计数和锁定状态 -``` - -### 认证成功处理流程 -``` -1. 用户登录成功 → Spring Security触发AuthenticationSuccessEvent -2. AuthenticationSuccessEventListener接收事件 -3. 获取用户身份信息 -4. 查找用户账户记录 -5. 重置失败计数和解除锁定 -6. 保存用户状态更新 -``` - -### 反馈AI审核流程 -``` -1. 反馈提交 → 发布FeedbackSubmittedForAiReviewEvent -2. FeedbackEventListener异步接收事件 -3. 记录处理日志 -4. 调用AiReviewService进行内容审核 -5. 更新反馈状态和审核结果 -``` - -## 技术实现细节 - -### 1. Spring事件监听机制 -- **ApplicationListener**: 传统的事件监听接口 -- **@EventListener**: 基于注解的事件监听方法 -- **@Async**: 异步事件处理注解 -- **事件发布**: 通过ApplicationEventPublisher发布事件 - -### 2. 依赖注入和管理 -- **@Component**: 将监听器注册为Spring组件 -- **@Autowired**: 自动注入依赖服务 -- **@RequiredArgsConstructor**: Lombok构造器注入 -- **生命周期管理**: Spring容器管理监听器生命周期 - -### 3. 异步处理配置 -```java -@EnableAsync -@Configuration -public class AsyncConfig { - @Bean - public TaskExecutor taskExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(2); - executor.setMaxPoolSize(10); - executor.setQueueCapacity(500); - executor.setThreadNamePrefix("EventListener-"); - executor.initialize(); - return executor; - } -} -``` - -## Listener模块最佳实践 - -1. **事件监听**: 使用合适的监听方式(接口或注解) -2. **异步处理**: 对于耗时操作使用@Async注解 -3. **异常处理**: 在监听器中添加完善的异常处理 -4. **日志记录**: 记录关键事件的处理过程 -5. **性能考虑**: 避免在监听器中执行阻塞操作 -6. **事务管理**: 注意事件监听器的事务边界 -7. **测试覆盖**: 为监听器编写单元测试和集成测试 - -## Listener模块总结 - -Listener模块为EMS系统提供了强大的事件响应能力: - -1. **安全保障**: 通过认证事件监听实现登录安全控制 -2. **状态管理**: 自动管理用户账户和业务状态 -3. **业务自动化**: 实现反馈AI审核等业务流程自动化 -4. **异步处理**: 支持异步事件处理提升系统性能 -5. **扩展性强**: 易于添加新的事件监听器扩展功能 -6. **可观测性**: 提供详细的事件处理日志和监控 -7. **Spring集成**: 充分利用Spring框架的事件机制 - -通过这套监听器机制,EMS系统能够及时响应各种系统事件和业务事件,实现了安全控制、状态管理和业务流程的自动化,为系统的稳定运行和用户体验提供了重要保障。 - ---- - -# Model模块详细解析 - -## 概述 - -Model(数据模型)模块位于 `com.dne.ems.model` 包下,包含14个核心实体类和10个枚举类,构成了EMS系统的完整数据模型。该模块使用JPA/Hibernate进行对象关系映射,定义了系统中所有业务实体的结构、关系和约束,为整个系统提供了坚实的数据基础。 - -## 核心实体类 - -### 1. 用户管理相关实体 - -#### UserAccount(用户账户) - -**功能概述** -- **用途**: 系统用户账户的核心实体 -- **映射表**: `user_account` -- **业务价值**: 管理所有用户的基本信息、角色权限和状态 - -**核心特性** -- **身份验证**: 存储用户登录凭据(邮箱、手机、密码) -- **角色管理**: 支持多种用户角色(管理员、主管、网格员等) -- **地理关联**: 支持网格坐标和区域信息 -- **技能管理**: JSON格式存储用户技能列表 -- **状态控制**: 支持账户激活、停用、请假等状态 - -**关键字段** -```java -@Entity -@Table(name = "user_account") -public class UserAccount { - @Id - private Long id; - - @NotEmpty - @Column(unique = true) - private String email; - - @NotEmpty - @Column(unique = true) - private String phone; - - @Enumerated(EnumType.STRING) - private Role role; - - @Enumerated(EnumType.STRING) - private UserStatus status; - - @Convert(converter = StringListConverter.class) - private List skills; - - private Integer gridX; - private Integer gridY; -} -``` - -#### PasswordResetToken(密码重置令牌) - -**功能概述** -- **用途**: 管理用户密码重置流程的安全令牌 -- **映射表**: `password_reset_token` -- **业务价值**: 提供安全的密码重置机制 - -**核心特性** -- **安全性**: 唯一令牌字符串,防止伪造 -- **时效性**: 60分钟有效期,自动过期 -- **一对一关联**: 每个令牌关联特定用户 -- **状态检查**: 提供过期检查方法 - -### 2. 反馈管理相关实体 - -#### Feedback(环境问题反馈) - -**功能概述** -- **用途**: 系统核心实体,记录用户提交的环境问题报告 -- **映射表**: `feedback` -- **业务价值**: 收集、管理和跟踪环境问题反馈 - -**核心特性** -- **唯一标识**: 人类可读的事件ID -- **分类管理**: 污染类型和严重程度分类 -- **状态跟踪**: 完整的处理状态生命周期 -- **地理定位**: 支持网格坐标和文本地址 -- **关联管理**: 与用户、任务、附件的关联关系 - -**关键字段** -```java -@Entity -@Table(name = "feedback") -public class Feedback { - @Id - private Long id; - - @Column(unique = true) - private String eventId; - - @Enumerated(EnumType.STRING) - private PollutionType pollutionType; - - @Enumerated(EnumType.STRING) - private SeverityLevel severityLevel; - - @Enumerated(EnumType.STRING) - private FeedbackStatus status; - - private Integer gridX; - private Integer gridY; - - @OneToMany(mappedBy = "feedback") - private List attachments; -} -``` - -#### Attachment(附件) - -**功能概述** -- **用途**: 管理反馈和任务提交的文件附件 -- **映射表**: `attachment` -- **业务价值**: 支持多媒体证据收集和存储 - -**核心特性** -- **文件元数据**: 存储文件名、类型、大小等信息 -- **存储管理**: 生成唯一存储文件名防止冲突 -- **多重关联**: 可关联反馈或任务提交 -- **时间戳**: 自动记录上传时间 - -### 3. 任务管理相关实体 - -#### Task(网格任务) - -**功能概述** -- **用途**: 表示网格工作人员执行的任务 -- **映射表**: `tasks` -- **业务价值**: 管理从反馈到任务的完整生命周期 - -**核心特性** -- **反馈关联**: 可从反馈自动生成或手动创建 -- **分配管理**: 支持任务分配和重新分配 -- **状态跟踪**: 完整的任务状态生命周期 -- **时间管理**: 创建、分配、完成时间戳 -- **地理信息**: 继承反馈的位置信息 - -**关键字段** -```java -@Entity -@Table(name = "tasks") -public class Task { - @Id - private Long id; - - @OneToOne - private Feedback feedback; - - @ManyToOne - private UserAccount assignee; - - @Enumerated(EnumType.STRING) - private TaskStatus status; - - @CreationTimestamp - private LocalDateTime createdAt; - - private LocalDateTime assignedAt; - private LocalDateTime completedAt; -} -``` - -#### TaskSubmission(任务提交) - -**功能概述** -- **用途**: 记录网格员完成任务后的提交信息 -- **映射表**: `task_submissions` -- **业务价值**: 收集任务执行结果和工作记录 - -**核心特性** -- **任务关联**: 与特定任务一对多关系 -- **详细记录**: 大文本字段存储工作笔记 -- **时间戳**: 自动记录提交时间 -- **附件支持**: 可关联多个附件文件 - -#### TaskHistory(任务历史) - -**功能概述** -- **用途**: 记录任务状态变更的审计日志 -- **映射表**: `task_history` -- **业务价值**: 提供完整的任务处理轨迹 - -**核心特性** -- **状态变更**: 记录旧状态和新状态 -- **操作人员**: 记录状态变更的执行者 -- **变更时间**: 自动记录变更时间戳 -- **备注信息**: 支持变更原因说明 - -### 4. 分配管理相关实体 - -#### Assignment(任务分配) - -**功能概述** -- **用途**: 管理反馈任务到网格员的分配关系 -- **映射表**: `assignments` -- **业务价值**: 跟踪任务分配的生命周期 - -**核心特性** -- **一对一关联**: 每个分配对应一个任务 -- **分配者记录**: 记录执行分配的用户 -- **截止日期**: 支持任务截止时间设置 -- **状态管理**: 跟踪分配状态变化 -- **备注信息**: 支持分配说明和要求 - -#### AssignmentRecord(分配记录) - -**功能概述** -- **用途**: 详细记录反馈分配的元数据 -- **映射表**: `assignment_record` -- **业务价值**: 提供丰富的分配分析数据 - -**核心特性** -- **关系映射**: 直接关联反馈、网格员、管理员 -- **分配方法**: 记录手动或智能分配方式 -- **算法详情**: JSON格式存储智能分配算法细节 -- **创建时间**: 自动记录分配创建时间 - -### 5. 空气质量数据相关实体 - -#### AqiData(空气质量指数数据) - -**功能概述** -- **用途**: 存储网格员提交的详细空气质量数据 -- **映射表**: `aqi_data` -- **业务价值**: 收集实时环境监测数据 - -**核心特性** -- **多污染物**: 支持PM2.5、PM10、SO2、NO2、CO、O3等 -- **网格关联**: 与具体网格位置关联 -- **报告者**: 记录数据提交的网格员 -- **反馈关联**: 可选关联相关反馈 - -**关键字段** -```java -@Entity -@Table(name = "aqi_data") -public class AqiData { - @Id - private Long id; - - @ManyToOne - private Grid grid; - - private Long reporterId; - private Integer aqiValue; - private Double pm25; - private Double pm10; - private Double so2; - private Double no2; - private Double co; - private Double o3; -} -``` - -#### AqiRecord(空气质量历史记录) - -**功能概述** -- **用途**: 存储历史空气质量数据用于趋势分析 -- **映射表**: `aqi_records` -- **业务价值**: 支持热力图生成和趋势分析 - -**核心特性** -- **城市级别**: 按城市名称组织数据 -- **完整污染物**: 包含所有主要污染物浓度 -- **地理坐标**: 支持经纬度和网格坐标 -- **时间戳**: 自动记录数据时间 - -### 6. 地理网格相关实体 - -#### Grid(地理网格) - -**功能概述** -- **用途**: 表示地理网格单元用于区域管理 -- **映射表**: `grid` -- **业务价值**: 支持基于网格的数据聚合和管理 - -**核心特性** -- **坐标系统**: X、Y坐标定位 -- **行政区划**: 城市和区县信息 -- **障碍标记**: 支持限制区域标记 -- **描述信息**: 网格详细描述 - -#### MapGrid(地图网格) - -**功能概述** -- **用途**: 专门用于A*寻路算法的网格单元 -- **映射表**: `map_grid` -- **业务价值**: 支持智能路径规划和导航 - -**核心特性** -- **唯一约束**: (x,y)坐标唯一性约束 -- **障碍检测**: 支持障碍物标记 -- **地形类型**: 可选的地形类型信息 -- **城市约束**: 限制寻路范围 - -### 7. 系统配置相关实体 - -#### PollutantThreshold(污染物阈值) - -**功能概述** -- **用途**: 存储不同污染物的超标阈值设置 -- **映射表**: `pollutant_thresholds` -- **业务价值**: 支持污染物超标检测和预警 - -**核心特性** -- **类型关联**: 与污染物类型枚举关联 -- **阈值设置**: 可配置的超标阈值 -- **单位管理**: 支持不同计量单位 -- **描述信息**: 阈值设置说明 - -## 枚举类型系统 - -### 1. 用户相关枚举 - -#### Role(用户角色) -```java -public enum Role { - PUBLIC_SUPERVISOR, // 公众监督员 - SUPERVISOR, // 主管 - GRID_WORKER, // 网格员 - ADMIN, // 系统管理员 - DECISION_MAKER // 决策者 -} -``` - -#### UserStatus(用户状态) -```java -public enum UserStatus { - ACTIVE, // 活跃 - INACTIVE, // 停用 - ON_LEAVE // 请假 -} -``` - -#### Gender(性别) -```java -public enum Gender { - MALE, // 男性 - FEMALE, // 女性 - UNKNOWN // 未知 -} -``` - -#### Level(级别) -```java -public enum Level { - PROVINCE, // 省级 - CITY // 市级 -} -``` - -### 2. 业务流程相关枚举 - -#### FeedbackStatus(反馈状态) -```java -public enum FeedbackStatus { - PENDING_REVIEW, // 待审核 - AI_REVIEWING, // AI审核中 - AI_REVIEW_FAILED, // AI审核失败 - AI_PROCESSING, // AI处理中 - PENDING_ASSIGNMENT, // 待分配 - ASSIGNED, // 已分配 - CONFIRMED, // 已确认 - RESOLVED, // 已解决 - CLOSED // 已关闭 -} -``` - -#### TaskStatus(任务状态) -```java -public enum TaskStatus { - PENDING_ASSIGNMENT, // 待分配 - ASSIGNED, // 已分配 - IN_PROGRESS, // 进行中 - SUBMITTED, // 已提交 - COMPLETED, // 已完成 - CANCELLED // 已取消 -} -``` - -#### AssignmentStatus(分配状态) -```java -public enum AssignmentStatus { - PENDING, // 待处理 - IN_PROGRESS, // 进行中 - COMPLETED, // 已完成 - OVERDUE // 已逾期 -} -``` - -#### AssignmentMethod(分配方法) -```java -public enum AssignmentMethod { - MANUAL, // 手动分配 - INTELLIGENT // 智能分配 -} -``` - -### 3. 环境数据相关枚举 - -#### PollutionType(污染类型) -```java -public enum PollutionType { - PM25, // 细颗粒物 - O3, // 臭氧 - NO2, // 二氧化氮 - SO2, // 二氧化硫 - OTHER // 其他 -} -``` - -#### SeverityLevel(严重程度) -```java -public enum SeverityLevel { - HIGH, // 高 - MEDIUM, // 中 - LOW // 低 -} -``` - -## 数据模型设计特点 - -### 1. 关系设计 -- **一对一关系**: Task-Feedback, Assignment-Task -- **一对多关系**: UserAccount-Task, Feedback-Attachment -- **多对一关系**: AqiData-Grid, Task-UserAccount -- **自引用关系**: TaskHistory-Task(审计日志) - -### 2. 约束和验证 -- **唯一性约束**: 用户邮箱、手机号、反馈事件ID -- **非空约束**: 关键业务字段强制非空 -- **枚举约束**: 使用枚举类型确保数据一致性 -- **长度约束**: 字符串字段长度限制 - -### 3. 时间戳管理 -- **创建时间**: @CreationTimestamp自动设置 -- **更新时间**: @UpdateTimestamp自动更新 -- **业务时间**: 分配时间、完成时间等业务节点 -- **过期检查**: 密码重置令牌过期验证 - -### 4. JSON数据支持 -- **技能列表**: 用户技能JSON存储 -- **算法详情**: 智能分配算法详情JSON存储 -- **大文本**: 使用@Lob注解支持长文本 - -## 数据模型架构优势 - -### 1. 业务完整性 -- **全生命周期**: 覆盖从反馈提交到任务完成的完整流程 -- **状态管理**: 详细的状态枚举和状态转换 -- **审计追踪**: 完整的操作历史记录 -- **数据关联**: 清晰的实体关系和数据流向 - -### 2. 扩展性设计 -- **枚举扩展**: 易于添加新的状态和类型 -- **实体扩展**: 支持新增字段和关系 -- **JSON存储**: 灵活的半结构化数据支持 -- **继承支持**: 预留实体继承扩展空间 - -### 3. 性能优化 -- **懒加载**: 使用FetchType.LAZY优化查询性能 -- **索引设计**: 唯一约束自动创建索引 -- **批量操作**: 支持CascadeType级联操作 -- **查询优化**: 合理的关系设计减少N+1问题 - -### 4. 数据安全 -- **密码加密**: 密码字段加密存储 -- **令牌安全**: 密码重置令牌时效性控制 -- **状态控制**: 用户状态控制访问权限 -- **数据完整性**: 外键约束保证数据一致性 - -## 技术实现细节 - -### 1. JPA注解使用 -```java -// 实体映射 -@Entity -@Table(name = "table_name") - -// 主键生成 -@Id -@GeneratedValue(strategy = GenerationType.IDENTITY) - -// 关系映射 -@OneToOne -@OneToMany -@ManyToOne -@ManyToMany - -// 枚举映射 -@Enumerated(EnumType.STRING) - -// 时间戳管理 -@CreationTimestamp -@UpdateTimestamp -``` - -### 2. Lombok注解 -```java -@Data // getter/setter/toString/equals/hashCode -@Builder // 建造者模式 -@NoArgsConstructor // 无参构造器 -@AllArgsConstructor // 全参构造器 -``` - -### 3. 验证注解 -```java -@NotEmpty // 非空验证 -@NotNull // 非null验证 -@Email // 邮箱格式验证 -@Size(min = 8) // 长度验证 -``` - -### 4. 自定义转换器 -```java -@Convert(converter = StringListConverter.class) -private List skills; -``` - -## Model模块最佳实践 - -1. **实体设计**: 遵循单一职责原则,每个实体专注特定业务领域 -2. **关系映射**: 合理使用懒加载,避免性能问题 -3. **枚举使用**: 优先使用枚举类型确保数据一致性 -4. **时间戳**: 统一使用Hibernate注解管理时间字段 -5. **验证约束**: 在实体层添加基础验证约束 -6. **命名规范**: 遵循Java命名规范和数据库命名规范 -7. **文档注释**: 为每个实体和字段添加详细注释 - -## Model模块总结 - -Model模块为EMS系统提供了完整、规范的数据模型基础: - -1. **业务完整性**: 覆盖用户管理、反馈处理、任务分配、数据收集等全业务流程 -2. **数据一致性**: 通过枚举类型、约束和验证确保数据质量 -3. **关系清晰**: 合理的实体关系设计支持复杂业务场景 -4. **扩展性强**: 灵活的设计支持业务需求变化和功能扩展 -5. **性能优化**: 合理的加载策略和索引设计保证系统性能 -6. **安全可靠**: 完善的约束和验证机制保证数据安全 -7. **标准规范**: 遵循JPA规范和最佳实践,便于维护和扩展 - -通过这套完整的数据模型,EMS系统建立了坚实的数据基础,为上层业务逻辑提供了可靠的数据支撑,确保了系统的稳定性、可扩展性和可维护性。 - ---- - -# Repository模块详细解析 - -## 概述 - -Repository(数据访问层)模块位于 `com.dne.ems.repository` 包下,包含14个Repository接口,构成了EMS系统完整的数据访问层。该模块基于Spring Data JPA框架,提供了标准化的数据访问接口,支持基础CRUD操作、自定义查询方法、复杂查询和分页查询,为业务层提供了高效、可靠的数据访问服务。 - -## 核心Repository接口 - -### 1. 用户管理相关Repository - -#### UserAccountRepository(用户账户数据访问) - -**功能概述** -- **继承接口**: `JpaRepository`, `JpaSpecificationExecutor` -- **业务价值**: 提供用户账户的完整数据访问功能 -- **核心特性**: 支持复杂查询、分页查询、规格查询 - -**核心查询方法** -```java -@Repository -public interface UserAccountRepository extends JpaRepository, JpaSpecificationExecutor { - // 基础查询 - Optional findByEmail(String email); - Optional findByPhone(String phone); - Optional findByGridXAndGridY(Integer gridX, Integer gridY); - - // 角色相关查询 - List findByRole(Role role); - long countByRole(Role role); - Page findByRole(Role role, Pageable pageable); - List findByRoleAndStatus(Role role, UserStatus status); - - // 网格员专用查询 - @Query("SELECT u FROM UserAccount u WHERE u.role = 'GRID_WORKER' AND u.status = 'ACTIVE' AND u.gridX BETWEEN ?1 AND ?2 AND u.gridY BETWEEN ?3 AND ?4") - List findActiveWorkersInArea(int minX, int maxX, int minY, int maxY); - - @Query("SELECT u FROM UserAccount u WHERE u.role = 'GRID_WORKER' AND u.gridX = ?1 AND u.gridY = ?2") - List findGridWorkersByCoordinates(int gridX, int gridY); - - @Query("SELECT u FROM UserAccount u WHERE u.role = 'GRID_WORKER' AND u.gridX IS NOT NULL AND u.gridY IS NOT NULL AND u.gridX >= 0 AND u.gridY >= 0") - List findAllAssignedGridWorkers(); - - @Query("SELECT u FROM UserAccount u WHERE u.role = 'GRID_WORKER' AND (u.gridX IS NULL OR u.gridY IS NULL OR u.gridX < 0 OR u.gridY < 0)") - List findAllUnassignedGridWorkers(); - - @Query("SELECT u FROM UserAccount u WHERE u.role = 'GRID_WORKER' AND u.region LIKE %?1%") - Page findGridWorkersByCity(String cityName, Pageable pageable); -} -``` - -**使用场景** -- **身份验证**: 通过邮箱或手机号查找用户 -- **角色管理**: 按角色查询和统计用户 -- **网格管理**: 查找特定区域或坐标的网格员 -- **用户分配**: 查找已分配和未分配网格的网格员 -- **区域查询**: 按城市查找网格员 - -#### PasswordResetTokenRepository(密码重置令牌数据访问) - -**功能概述** -- **继承接口**: `JpaRepository` -- **业务价值**: 管理密码重置令牌的数据访问 -- **核心特性**: 支持令牌查找和用户关联查询 - -**核心查询方法** -```java -@Repository -public interface PasswordResetTokenRepository extends JpaRepository { - Optional findByToken(String token); - Optional findByUser(UserAccount user); -} -``` - -### 2. 反馈管理相关Repository - -#### FeedbackRepository(反馈数据访问) - -**功能概述** -- **继承接口**: `JpaRepository`, `JpaSpecificationExecutor` -- **业务价值**: 提供反馈数据的完整访问功能 -- **核心特性**: 支持复杂查询、分页查询、统计查询、热力图数据 - -**核心查询方法** -```java -@Repository -public interface FeedbackRepository extends JpaRepository, JpaSpecificationExecutor { - // 基础查询 - Optional findByEventId(String eventId); - List findByStatus(FeedbackStatus status); - long countByStatus(FeedbackStatus status); - - // 用户相关查询 - Page findBySubmitterId(Long submitterId, Pageable pageable); - List findBySubmitterId(Long submitterId); - long countBySubmitterId(Long submitterId); - long countByStatusAndSubmitterId(FeedbackStatus status, Long submitterId); - - // 统计查询 - @Query("SELECT new com.dne.ems.dto.HeatmapPointDTO(f.gridX, f.gridY, COUNT(f.id)) FROM Feedback f WHERE f.status = 'CONFIRMED' AND f.gridX IS NOT NULL AND f.gridY IS NOT NULL GROUP BY f.gridX, f.gridY") - List getHeatmapData(); - - @Query("SELECT new com.dne.ems.dto.PollutionStatsDTO(f.pollutionType, COUNT(f)) FROM Feedback f GROUP BY f.pollutionType") - List countByPollutionType(); - - // 性能优化查询 - @Override - @EntityGraph(attributePaths = {"user", "attachments"}) - Page findAll(Specification spec, Pageable pageable); -} -``` - -**使用场景** -- **反馈管理**: 按状态、用户、事件ID查询反馈 -- **数据统计**: 统计各状态反馈数量和污染类型分布 -- **热力图**: 生成反馈分布热力图数据 -- **性能优化**: 使用EntityGraph避免N+1查询问题 - -#### AttachmentRepository(附件数据访问) - -**功能概述** -- **继承接口**: `JpaRepository` -- **业务价值**: 管理反馈和任务提交的附件数据 -- **核心特性**: 支持文件名查找和关联实体查询 - -**核心查询方法** -```java -@Repository -public interface AttachmentRepository extends JpaRepository { - Optional findByStoredFileName(String storedFileName); - List findByTaskSubmission(TaskSubmission taskSubmission); - List findByFeedback(Feedback feedback); -} -``` - -### 3. 任务管理相关Repository - -#### TaskRepository(任务数据访问) - -**功能概述** -- **继承接口**: `JpaRepository`, `JpaSpecificationExecutor` -- **业务价值**: 提供任务数据的完整访问功能 -- **核心特性**: 支持复杂查询、状态统计、分配查询 - -**核心查询方法** -```java -@Repository -public interface TaskRepository extends JpaRepository, JpaSpecificationExecutor { - Optional findByFeedback(Feedback feedback); - long countByStatus(TaskStatus status); - long countByAssigneeIdAndStatusIn(Long assigneeId, Collection statuses); - List findByAssigneeIdAndStatus(Long assigneeId, TaskStatus status); - Page findByStatusAndAssignee(AssignmentStatus status, UserAccount assignee, Pageable pageable); -} -``` - -#### TaskSubmissionRepository(任务提交数据访问) - -**功能概述** -- **继承接口**: `JpaRepository` -- **业务价值**: 管理任务提交记录的数据访问 -- **核心特性**: 支持按任务查找最新提交 - -**核心查询方法** -```java -@Repository -public interface TaskSubmissionRepository extends JpaRepository { - TaskSubmission findFirstByTaskOrderBySubmittedAtDesc(Task task); -} -``` - -#### TaskHistoryRepository(任务历史数据访问) - -**功能概述** -- **继承接口**: `JpaRepository` -- **业务价值**: 管理任务状态变更历史记录 -- **核心特性**: 支持按任务ID查询历史记录 - -**核心查询方法** -```java -@Repository -public interface TaskHistoryRepository extends JpaRepository { - List findByTaskIdOrderByChangedAtDesc(Long taskId); -} -``` - -### 4. 分配管理相关Repository - -#### AssignmentRepository(任务分配数据访问) - -**功能概述** -- **继承接口**: `JpaRepository` -- **业务价值**: 管理任务分配关系的数据访问 -- **核心特性**: 支持按分配者查询分配记录 - -**核心查询方法** -```java -@Repository -public interface AssignmentRepository extends JpaRepository { - List findByTaskAssigneeId(Long assigneeId); -} -``` - -#### AssignmentRecordRepository(分配记录数据访问) - -**功能概述** -- **继承接口**: `JpaRepository` -- **业务价值**: 管理分配记录的详细数据 -- **核心特性**: 基础CRUD操作,预留扩展空间 - -### 5. 空气质量数据相关Repository - -#### AqiDataRepository(空气质量数据访问) - -**功能概述** -- **继承接口**: `JpaRepository` -- **业务价值**: 管理实时空气质量数据的访问 -- **核心特性**: 支持统计查询、趋势分析、热力图数据 - -**核心查询方法** -```java -@Repository -public interface AqiDataRepository extends JpaRepository { - // AQI分布统计 - @Query("SELECT new com.dne.ems.dto.AqiDistributionDTO(CASE WHEN a.aqiValue <= 50 THEN 'Good' WHEN a.aqiValue <= 100 THEN 'Moderate' WHEN a.aqiValue <= 150 THEN 'Unhealthy for Sensitive Groups' WHEN a.aqiValue <= 200 THEN 'Unhealthy' WHEN a.aqiValue <= 300 THEN 'Very Unhealthy' ELSE 'Hazardous' END, COUNT(a.id)) FROM AqiData a GROUP BY 1") - List getAqiDistribution(); - - // 月度超标趋势(原生SQL) - @Query(value = "SELECT DATE_FORMAT(record_time, '%Y-%m') as yearMonth, COUNT(id) as count FROM aqi_data WHERE aqi_value > 100 AND record_time >= :startDate GROUP BY DATE_FORMAT(record_time, '%Y-%m') ORDER BY 1", nativeQuery = true) - List getMonthlyExceedanceTrendRaw(@Param("startDate") LocalDateTime startDate); - - // 默认方法转换原生查询结果 - default List getMonthlyExceedanceTrend(LocalDateTime startDate) { - List rawResults = getMonthlyExceedanceTrendRaw(startDate); - return rawResults.stream() - .map(row -> new TrendDataPointDTO((String) row[0], ((Number) row[1]).longValue())) - .toList(); - } - - // 热力图数据 - @Query("SELECT new com.dne.ems.dto.AqiHeatmapPointDTO(g.gridX, g.gridY, AVG(d.aqiValue)) FROM AqiData d JOIN d.grid g WHERE g.gridX IS NOT NULL AND g.gridY IS NOT NULL GROUP BY g.gridX, g.gridY") - List getAqiHeatmapData(); -} -``` - -#### AqiRecordRepository(空气质量历史记录数据访问) - -**功能概述** -- **继承接口**: `JpaRepository` -- **业务价值**: 管理历史空气质量数据的访问 -- **核心特性**: 支持复杂阈值查询、时间范围查询、热力图数据 - -**核心查询方法** -```java -@Repository -public interface AqiRecordRepository extends JpaRepository { - // AQI分布统计 - @Query("SELECT new com.dne.ems.dto.AqiDistributionDTO(CASE WHEN ar.aqiValue <= 50 THEN 'Good' WHEN ar.aqiValue <= 100 THEN 'Moderate' WHEN ar.aqiValue <= 150 THEN 'Unhealthy for Sensitive Groups' WHEN ar.aqiValue <= 200 THEN 'Unhealthy' WHEN ar.aqiValue <= 300 THEN 'Very Unhealthy' ELSE 'Hazardous' END, COUNT(ar.id)) FROM AqiRecord ar GROUP BY CASE WHEN ar.aqiValue <= 50 THEN 'Good' WHEN ar.aqiValue <= 100 THEN 'Moderate' WHEN ar.aqiValue <= 150 THEN 'Unhealthy for Sensitive Groups' WHEN ar.aqiValue <= 200 THEN 'Unhealthy' WHEN ar.aqiValue <= 300 THEN 'Very Unhealthy' ELSE 'Hazardous' END") - List getAqiDistribution(); - - // 超标记录查询 - @Query("SELECT ar FROM AqiRecord ar WHERE ar.aqiValue > 100 AND ar.recordTime >= :startDate") - List findExceedanceRecordsSince(@Param("startDate") LocalDateTime startDate); - - // 多阈值超标查询 - @Query("SELECT ar FROM AqiRecord ar WHERE ar.recordTime >= :startDate AND (ar.aqiValue > :aqiThreshold OR ar.pm25 > :pm25Threshold OR ar.o3 > :o3Threshold OR ar.no2 > :no2Threshold OR ar.so2 > :so2Threshold)") - List findExceedanceRecordsWithThresholds(@Param("startDate") LocalDateTime startDate, @Param("aqiThreshold") Double aqiThreshold, @Param("pm25Threshold") Double pm25Threshold, @Param("o3Threshold") Double o3Threshold, @Param("no2Threshold") Double no2Threshold, @Param("so2Threshold") Double so2Threshold); - - // 时间范围查询 - List findByRecordTimeAfter(LocalDateTime startDate); - - // 热力图数据(JPQL) - @Query("SELECT new com.dne.ems.dto.AqiHeatmapPointDTO(ar.gridX, ar.gridY, AVG(ar.aqiValue)) FROM AqiRecord ar WHERE ar.gridX IS NOT NULL AND ar.gridY IS NOT NULL GROUP BY ar.gridX, ar.gridY") - List getAqiHeatmapData(); - - // 热力图数据(原生SQL) - @Query(value = "SELECT grid_x, grid_y, AVG(aqi_value) as avg_aqi FROM aqi_records WHERE grid_x IS NOT NULL AND grid_y IS NOT NULL GROUP BY grid_x, grid_y", nativeQuery = true) - List getAqiHeatmapDataRaw(); -} -``` - -### 6. 地理网格相关Repository - -#### GridRepository(地理网格数据访问) - -**功能概述** -- **继承接口**: `JpaRepository`, `JpaSpecificationExecutor` -- **业务价值**: 管理地理网格数据的访问 -- **核心特性**: 支持坐标查询、城市统计、障碍物查询 - -**核心查询方法** -```java -@Repository -public interface GridRepository extends JpaRepository, JpaSpecificationExecutor { - Optional findByGridXAndGridY(Integer gridX, Integer gridY); - List findByIsObstacle(boolean isObstacle); - - @Query("SELECT new com.dne.ems.dto.GridCoverageDTO(g.cityName, COUNT(g.id)) FROM Grid g WHERE g.cityName IS NOT NULL AND g.cityName != '' GROUP BY g.cityName ORDER BY COUNT(g.id) DESC") - List getGridCoverageByCity(); -} -``` - -#### MapGridRepository(地图网格数据访问) - -**功能概述** -- **继承接口**: `JpaRepository` -- **业务价值**: 管理A*寻路算法的网格数据 -- **核心特性**: 支持坐标查询和有序遍历 - -**核心查询方法** -```java -@Repository -public interface MapGridRepository extends JpaRepository { - Optional findByXAndY(int x, int y); - List findAllByOrderByYAscXAsc(); -} -``` - -### 7. 系统配置相关Repository - -#### PollutantThresholdRepository(污染物阈值数据访问) - -**功能概述** -- **继承接口**: `JpaRepository` -- **业务价值**: 管理污染物阈值配置数据 -- **核心特性**: 支持按污染物名称查询阈值 - -**核心查询方法** -```java -@Repository -public interface PollutantThresholdRepository extends JpaRepository { - Optional findByPollutantName(String pollutantName); -} -``` - -## Repository设计特点 - -### 1. 继承体系设计 -- **JpaRepository**: 提供基础CRUD操作 -- **JpaSpecificationExecutor**: 支持动态查询和复杂条件 -- **双重继承**: 核心实体同时继承两个接口,提供完整功能 - -### 2. 查询方法类型 -- **方法名查询**: 基于Spring Data JPA命名规范的自动查询 -- **@Query注解**: 自定义JPQL查询语句 -- **原生SQL**: 使用nativeQuery进行复杂数据库操作 -- **默认方法**: 在接口中提供数据转换逻辑 - -### 3. 性能优化策略 -- **EntityGraph**: 解决N+1查询问题 -- **分页查询**: 支持大数据量的分页处理 -- **索引利用**: 查询方法充分利用数据库索引 -- **批量操作**: 继承JpaRepository的批量操作能力 - -### 4. 查询复杂度分层 -- **简单查询**: 单字段或少量字段的精确匹配 -- **条件查询**: 多条件组合和范围查询 -- **统计查询**: 聚合函数和分组统计 -- **关联查询**: 跨表JOIN查询和子查询 - -## Repository架构优势 - -### 1. 标准化设计 -- **Spring Data JPA**: 遵循Spring生态标准 -- **命名规范**: 统一的方法命名和参数规范 -- **注解驱动**: 使用标准注解进行配置 -- **接口导向**: 面向接口编程,便于测试和扩展 - -### 2. 功能完整性 -- **CRUD操作**: 完整的增删改查功能 -- **复杂查询**: 支持各种业务查询需求 -- **统计分析**: 提供数据统计和分析功能 -- **性能优化**: 内置性能优化机制 - -### 3. 扩展性强 -- **自定义查询**: 支持添加新的查询方法 -- **规格查询**: 支持动态条件构建 -- **原生SQL**: 支持复杂的数据库操作 -- **默认方法**: 支持在接口中添加业务逻辑 - -### 4. 维护性好 -- **类型安全**: 编译时类型检查 -- **自动实现**: Spring自动生成实现类 -- **测试友好**: 易于进行单元测试和集成测试 -- **文档清晰**: 方法名自解释,注释完整 - -## 技术实现细节 - -### 1. Spring Data JPA注解 -```java -@Repository // 标识为Repository组件 -@Query("JPQL语句") // 自定义JPQL查询 -@Query(value="SQL", nativeQuery=true) // 原生SQL查询 -@Param("参数名") // 命名参数绑定 -@EntityGraph(attributePaths={"关联属性"}) // 性能优化 -``` - -### 2. 查询方法命名规范 -```java -findBy... // 查询方法前缀 -countBy... // 统计方法前缀 -existsBy... // 存在性检查前缀 -deleteBy... // 删除方法前缀 - -// 条件关键字 -And, Or // 逻辑连接 -Between, LessThan, GreaterThan // 范围条件 -Like, NotLike // 模糊匹配 -In, NotIn // 集合条件 -IsNull, IsNotNull // 空值检查 -OrderBy...Asc/Desc // 排序 -``` - -### 3. 分页和排序 -```java -Page findBy...(Pageable pageable); // 分页查询 -List findBy...OrderBy...(); // 排序查询 -Slice findBy...(Pageable pageable); // 切片查询 -``` - -### 4. 规格查询(Specification) -```java -public interface EntityRepository extends JpaRepository, JpaSpecificationExecutor { - // 支持动态条件查询 - Page findAll(Specification spec, Pageable pageable); -} -``` - -## Repository模块最佳实践 - -1. **接口设计**: 保持接口简洁,职责单一 -2. **查询优化**: 合理使用EntityGraph避免N+1问题 -3. **命名规范**: 遵循Spring Data JPA命名约定 -4. **参数验证**: 在Service层进行参数验证,Repository专注数据访问 -5. **事务管理**: 在Service层管理事务,Repository保持无状态 -6. **异常处理**: 让Spring Data JPA的异常向上传播 -7. **测试覆盖**: 为自定义查询方法编写集成测试 -8. **性能监控**: 监控慢查询并进行优化 - -## Repository模块总结 - -Repository模块为EMS系统提供了完整、高效的数据访问层: - -1. **标准化架构**: 基于Spring Data JPA的标准化设计 -2. **功能完整**: 覆盖所有业务实体的数据访问需求 -3. **查询丰富**: 支持简单查询、复杂查询、统计查询、分页查询 -4. **性能优化**: 内置多种性能优化机制 -5. **扩展性强**: 支持自定义查询和动态条件构建 -6. **维护性好**: 类型安全、自动实现、测试友好 -7. **业务适配**: 针对EMS业务特点设计的专用查询方法 - -通过这套完整的Repository层,EMS系统建立了高效、可靠的数据访问基础,为业务层提供了丰富的数据操作能力,确保了系统的数据访问性能和开发效率。 - ---- - -# Service模块详细解析 - -## 概述 - -Service(业务服务层)模块位于 `com.dne.ems.service` 包下,包含17个服务接口和对应的实现类,构成了EMS系统完整的业务逻辑层。该模块基于Spring框架的服务层架构,提供了标准化的业务服务接口,实现了复杂的业务逻辑处理、事务管理、安全控制和系统集成,为控制器层提供了高质量的业务服务。 - -## 核心Service接口 - -### 1. 认证与安全相关Service - -#### AuthService(认证服务) - -**功能概述** -- **接口定义**: `AuthService` -- **实现类**: `AuthServiceImpl` -- **业务价值**: 提供用户认证和安全相关的核心功能 -- **核心特性**: 用户注册、登录认证、密码重置、JWT令牌管理 - -**核心方法** -```java -public interface AuthService { - // 密码重置流程 - void requestPasswordReset(String email); - void resetPasswordWithToken(String token, String newPassword); - void resetPasswordWithCode(String email, String code, String newPassword); - - // 用户注册与登录 - void registerUser(SignUpRequest signUpRequest); - JwtAuthenticationResponse signIn(LoginRequest loginRequest); - - // 验证码相关 - void sendRegistrationCode(String email); - boolean validateRegistrationCode(String email, String code); -} -``` - -**业务流程** -- **用户注册**: 验证码校验 → 用户信息验证 → 创建用户账户 → 返回注册结果 -- **用户登录**: 身份验证 → 生成JWT令牌 → 记录登录状态 → 返回认证响应 -- **密码重置**: 发送验证码 → 验证码校验 → 密码更新 → 清理令牌 - -#### JwtService(JWT令牌服务) - -**功能概述** -- **接口定义**: `JwtService` -- **实现类**: `JwtServiceImpl` -- **业务价值**: 处理JWT令牌的生成、解析和验证 -- **核心特性**: HS256算法签名、24小时有效期、用户角色权限声明 - -**核心方法** -```java -public interface JwtService { - String extractUserName(String token); - Claims extractAllClaims(String token); - String generateToken(UserDetails userDetails); - boolean isTokenValid(String token, UserDetails userDetails); -} -``` - -#### LoginAttemptService(登录尝试服务) - -**功能概述** -- **接口定义**: `LoginAttemptService` -- **实现类**: `LoginAttemptServiceImpl` -- **业务价值**: 防止暴力破解攻击,管理登录失败次数 -- **核心特性**: 失败次数统计、账户锁定机制、自动解锁 - -**核心方法** -```java -public interface LoginAttemptService { - void loginSucceeded(String key); - void loginFailed(String key); - boolean isBlocked(String key); -} -``` - -#### VerificationCodeService(验证码服务) - -**功能概述** -- **接口定义**: `VerificationCodeService` -- **实现类**: `VerificationCodeServiceImpl` -- **业务价值**: 生成、发送和校验验证码 -- **核心特性**: 6位数字验证码、5分钟有效期、60秒发送间隔 - -**核心方法** -```java -public interface VerificationCodeService { - void sendVerificationCode(String email); - boolean verifyCode(String email, String code); -} -``` - -### 2. 用户管理相关Service - -#### UserAccountService(用户账户服务) - -**功能概述** -- **接口定义**: `UserAccountService` -- **实现类**: `UserAccountServiceImpl` -- **业务价值**: 管理用户账户的完整生命周期 -- **核心特性**: 用户注册、角色管理、位置更新、用户查询 - -**核心方法** -```java -public interface UserAccountService { - UserAccount registerUser(UserRegistrationRequest registrationRequest); - UserAccount updateUserRole(Long userId, UserRoleUpdateRequest request); - UserAccount getUserById(Long userId); - Page getUsersByRole(Role role, Pageable pageable); - void updateUserLocation(Long userId, Double latitude, Double longitude); - Page getAllUsers(Role role, UserStatus status, Pageable pageable); -} -``` - -#### PersonnelService(人员管理服务) - -**功能概述** -- **接口定义**: `PersonnelService` -- **实现类**: `PersonnelServiceImpl` -- **业务价值**: 提供人员管理的高级功能 -- **核心特性**: 用户创建、信息更新、删除管理、个人资料更新 - -**核心方法** -```java -public interface PersonnelService { - Page getUsers(Role role, String name, Pageable pageable); - UserAccount createUser(UserCreationRequest request); - UserAccount updateUser(Long userId, UserUpdateRequest request); - void deleteUser(Long userId); - UserAccount updateOwnProfile(String currentUserEmail, UserUpdateRequest request); -} -``` - -### 3. 反馈管理相关Service - -#### FeedbackService(反馈服务) - -**功能概述** -- **接口定义**: `FeedbackService` -- **实现类**: `FeedbackServiceImpl` -- **业务价值**: 处理环境问题反馈的提交和管理 -- **核心特性**: 用户反馈提交、匿名反馈、反馈查询、数据统计 - -**核心方法** -```java -public interface FeedbackService { - // 反馈提交 - Feedback submitFeedback(FeedbackSubmissionRequest request, MultipartFile[] files); - Feedback submitPublicFeedback(PublicFeedbackRequest request, MultipartFile[] files); - - // 反馈查询 - Page getFeedbacks(FeedbackStatus status, PollutionType pollutionType, - SeverityLevel severityLevel, LocalDate startDate, - LocalDate endDate, Pageable pageable); - FeedbackResponseDTO getFeedbackById(Long feedbackId); - - // 反馈处理 - void processFeedback(Long feedbackId, ProcessFeedbackRequest request); - - // 统计分析 - FeedbackStatsResponse getFeedbackStats(); -} -``` - -**业务流程** -- **反馈提交**: 用户信息验证 → 反馈数据创建 → 文件上传处理 → AI审核触发 → 返回反馈实体 -- **反馈处理**: 状态验证 → 业务逻辑处理 → 状态更新 → 事件发布 - -#### UserFeedbackService(用户反馈服务) - -**功能概述** -- **接口定义**: `UserFeedbackService` -- **实现类**: `UserFeedbackServiceImpl` -- **业务价值**: 提供用户特定的反馈操作 -- **核心特性**: 用户反馈历史查询 - -**核心方法** -```java -public interface UserFeedbackService { - Page getFeedbackHistoryByUserId(Long userId, Pageable pageable); -} -``` - -#### AiReviewService(AI审核服务) - -**功能概述** -- **接口定义**: `AiReviewService` -- **实现类**: `AiReviewServiceImpl` -- **业务价值**: 使用AI模型审核反馈内容 -- **核心特性**: 外部AI服务调用、反馈状态更新 - -**核心方法** -```java -public interface AiReviewService { - void reviewFeedback(Feedback feedback); -} -``` - -### 4. 任务管理相关Service - -#### TaskManagementService(任务管理服务) - -**功能概述** -- **接口定义**: `TaskManagementService` -- **实现类**: `TaskManagementServiceImpl` -- **业务价值**: 处理任务全生命周期管理 -- **核心特性**: 任务创建、分配、状态管理、审核审批 - -**核心方法** -```java -public interface TaskManagementService { - // 任务查询 - Page getTasks(TaskStatus status, Long assigneeId, SeverityLevel severity, - PollutionType pollutionType, LocalDate startDate, - LocalDate endDate, Pageable pageable); - TaskDetailDTO getTaskById(Long taskId); - - // 任务创建 - TaskDetailDTO createTask(TaskCreationRequest request); - TaskDetailDTO createTaskFromFeedback(TaskFromFeedbackRequest request); - - // 任务分配 - void assignTask(Long taskId, TaskAssignmentRequest request); - - // 任务审批 - void approveTask(Long taskId, TaskApprovalRequest request); - void rejectTask(Long taskId, TaskApprovalRequest request); - - // 任务历史 - List getTaskHistory(Long taskId); -} -``` - -**业务流程** -- **任务创建**: 请求验证 → 任务实体创建 → 历史记录 → 事件发布 -- **任务分配**: 分配者验证 → 被分配者验证 → 分配关系创建 → 状态更新 -- **任务审批**: 权限验证 → 状态流转 → 审批记录 → 通知发送 - -#### TaskAssignmentService(任务分配服务) - -**功能概述** -- **接口定义**: `TaskAssignmentService` -- **实现类**: `TaskAssignmentServiceImpl` -- **业务价值**: 处理任务分配操作 -- **核心特性**: 未分配任务查询、网格员查询、任务分配 - -**核心方法** -```java -public interface TaskAssignmentService { - List getUnassignedFeedback(); - List getAvailableGridWorkers(); - Assignment assignTask(Long feedbackId, Long assigneeId, Long assignerId); -} -``` - -#### GridWorkerTaskService(网格员任务服务) - -**功能概述** -- **接口定义**: `GridWorkerTaskService` -- **实现类**: `GridWorkerTaskServiceImpl` -- **业务价值**: 处理网格员的任务管理操作 -- **核心特性**: 任务查询、状态流转、任务提交 - -**核心方法** -```java -public interface GridWorkerTaskService { - Page getAssignedTasks(Long workerId, TaskStatus status, Pageable pageable); - TaskDetailDTO getTaskDetail(Long taskId, Long workerId); - void acceptTask(Long taskId, Long workerId); - void startTask(Long taskId, Long workerId); - void submitTask(Long taskId, TaskSubmissionRequest request, MultipartFile[] files); - List getTasksNearLocation(Double latitude, Double longitude, Double radiusKm); -} -``` - -### 5. 监管审核相关Service - -#### SupervisorService(主管服务) - -**功能概述** -- **接口定义**: `SupervisorService` -- **实现类**: `SupervisorServiceImpl` -- **业务价值**: 处理主管的审核操作 -- **核心特性**: 反馈审核、审批决策 - -**核心方法** -```java -public interface SupervisorService { - List getFeedbackForReview(); - void approveFeedback(Long feedbackId); - void rejectFeedback(Long feedbackId, RejectFeedbackRequest request); -} -``` - -### 6. 数据分析相关Service - -#### DashboardService(仪表盘服务) - -**功能概述** -- **接口定义**: `DashboardService` -- **实现类**: `DashboardServiceImpl` -- **业务价值**: 提供各类环境监测数据的统计和可视化数据 -- **核心特性**: 环境质量指标统计、AQI数据分布、热力图数据、趋势分析 - -**核心方法** -```java -public interface DashboardService { - // 统计数据 - List getPollutionStats(); - TaskStatsDTO getTaskStats(); - DashboardStatsDTO getDashboardStats(); - - // AQI数据 - List getAqiDistribution(); - List getAqiHeatmapData(); - List getMonthlyExceedanceTrend(); - - // 热力图数据 - List getFeedbackHeatmapData(); - - // 网格覆盖 - List getGridCoverageByCity(); - - // 阈值管理 - List getAllThresholds(); - PollutantThresholdDTO updateThreshold(String pollutantName, Double threshold); -} -``` - -### 7. 基础设施相关Service - -#### FileStorageService(文件存储服务) - -**功能概述** -- **接口定义**: `FileStorageService` -- **实现类**: `FileStorageServiceImpl` -- **业务价值**: 处理文件上传和下载 -- **核心特性**: 安全文件存储、文件关联管理、下载服务 - -**核心方法** -```java -public interface FileStorageService { - @Deprecated - Attachment storeFileAndCreateAttachment(MultipartFile file, Feedback feedback); - String storeFile(MultipartFile file); - Resource loadFileAsResource(String fileName); - void deleteFile(String fileName); -} -``` - -#### MailService(邮件服务) - -**功能概述** -- **接口定义**: `MailService` -- **实现类**: `MailServiceImpl` -- **业务价值**: 发送各类邮件 -- **核心特性**: 密码重置邮件、注册验证码邮件 - -**核心方法** -```java -public interface MailService { - void sendPasswordResetEmail(String to, String code); - void sendVerificationCodeEmail(String to, String code); -} -``` - -#### GridService(网格服务) - -**功能概述** -- **接口定义**: `GridService` -- **实现类**: `GridServiceImpl` -- **业务价值**: 处理网格数据的查询和更新 -- **核心特性**: 网格查询、网格员分配、网格更新 - -**核心方法** -```java -public interface GridService { - List getAllGrids(); - Optional getGridById(Long gridId); - UserAccount assignWorkerToGrid(Long gridId, Long userId); - void unassignWorkerFromGrid(Long gridId); - Page getGridsWithFilters(String cityName, Boolean isObstacle, Pageable pageable); - Grid updateGrid(Long gridId, GridUpdateRequest request); -} -``` - -### 8. 算法服务 - -#### AStarService(A*寻路服务) - -**功能概述** -- **服务类**: `AStarService`(直接实现,无接口) -- **业务价值**: 提供A*寻路算法实现 -- **核心特性**: 最短路径计算、障碍物避让、网格导航 - -**核心方法** -```java -@Service -public class AStarService { - public List findPath(Point start, Point end); - private int calculateHeuristic(Point a, Point b); - private List getNeighbors(Point point); - private List reconstructPath(Node endNode); -} -``` - -## Service设计特点 - -### 1. 分层架构设计 -- **接口与实现分离**: 所有业务服务都定义了接口,便于测试和扩展 -- **职责单一原则**: 每个服务专注于特定的业务领域 -- **依赖注入**: 使用Spring的依赖注入管理服务间依赖 -- **事务管理**: 在服务层统一管理事务边界 - -### 2. 业务逻辑封装 -- **复杂业务流程**: 将复杂的业务逻辑封装在服务方法中 -- **数据转换**: 在服务层完成实体与DTO之间的转换 -- **业务验证**: 在服务层进行业务规则验证 -- **异常处理**: 统一的业务异常处理机制 - -### 3. 安全控制 -- **权限验证**: 在服务层进行细粒度的权限控制 -- **数据安全**: 敏感数据的加密和脱敏处理 -- **审计日志**: 关键操作的审计日志记录 -- **防护机制**: 防止各种安全攻击的保护措施 - -### 4. 性能优化 -- **缓存策略**: 合理使用缓存提升性能 -- **批量处理**: 支持批量操作减少数据库访问 -- **异步处理**: 使用异步机制处理耗时操作 -- **分页查询**: 大数据量查询的分页处理 - -## Service架构优势 - -### 1. 业务完整性 -- **端到端流程**: 完整的业务流程实现 -- **状态管理**: 复杂的业务状态流转管理 -- **数据一致性**: 确保业务数据的一致性 -- **业务规则**: 完整的业务规则实现 - -### 2. 系统集成 -- **外部服务**: 与外部系统的集成接口 -- **事件驱动**: 基于事件的松耦合架构 -- **消息传递**: 系统间的消息传递机制 -- **API网关**: 统一的API访问入口 - -### 3. 扩展性强 -- **插件化**: 支持插件化的功能扩展 -- **配置驱动**: 基于配置的功能开关 -- **版本兼容**: 向后兼容的版本升级 -- **模块化**: 模块化的服务设计 - -### 4. 维护性好 -- **代码规范**: 统一的代码规范和风格 -- **文档完整**: 完整的接口文档和注释 -- **测试覆盖**: 高覆盖率的单元测试和集成测试 -- **监控告警**: 完善的监控和告警机制 - -## 技术实现细节 - -### 1. Spring注解 -```java -@Service // 标识为服务组件 -@Transactional // 事务管理 -@RequiredArgsConstructor // Lombok构造器注入 -@Slf4j // 日志记录 -@Async // 异步处理 -@EventListener // 事件监听 -@Cacheable // 缓存支持 -``` - -### 2. 事务管理 -```java -@Transactional(readOnly = true) // 只读事务 -@Transactional(rollbackFor = Exception.class) // 异常回滚 -@Transactional(propagation = Propagation.REQUIRES_NEW) // 事务传播 -@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // 事务事件 -``` - -### 3. 安全控制 -```java -@PreAuthorize("hasRole('ADMIN')") // 方法级权限控制 -@PostAuthorize("returnObject.owner == authentication.name") // 返回值权限控制 -SecurityContextHolder.getContext().getAuthentication() // 获取当前用户 -``` - -### 4. 异常处理 -```java -@ControllerAdvice // 全局异常处理 -@ExceptionHandler // 特定异常处理 -try-catch-finally // 局部异常处理 -throw new BusinessException() // 业务异常抛出 -``` - -## Service模块最佳实践 - -1. **接口设计**: 保持接口简洁,方法职责单一 -2. **事务边界**: 合理设置事务边界,避免长事务 -3. **异常处理**: 统一的异常处理策略,提供有意义的错误信息 -4. **参数验证**: 在服务入口进行参数验证 -5. **日志记录**: 记录关键业务操作和异常信息 -6. **性能监控**: 监控服务性能,及时发现瓶颈 -7. **单元测试**: 为每个服务方法编写单元测试 -8. **文档维护**: 保持接口文档和代码注释的同步更新 - -## Service模块总结 - -Service模块为EMS系统提供了完整、高质量的业务服务层: - -1. **业务完整**: 覆盖环境监管系统的所有核心业务功能 -2. **架构清晰**: 分层明确、职责单一、依赖合理 -3. **安全可靠**: 完善的安全控制和异常处理机制 -4. **性能优化**: 多种性能优化策略和监控机制 -5. **扩展性强**: 支持业务功能的灵活扩展和配置 -6. **维护性好**: 代码规范、文档完整、测试覆盖率高 -7. **技术先进**: 采用Spring生态的最佳实践和现代化技术 - -通过这套完整的Service层,EMS系统建立了稳定、高效的业务处理能力,为前端应用提供了可靠的业务支撑,确保了系统的业务完整性和技术先进性。 - ---- - -# Security模块详细解析 - -## 概述 - -Security(安全模块)位于 `com.dne.ems.security` 包下,包含4个核心安全组件,构成了EMS系统完整的安全防护体系。该模块基于Spring Security框架,实现了JWT无状态认证、细粒度权限控制、CORS跨域支持和安全过滤机制,为整个系统提供了企业级的安全保障。 - -## 核心Security组件 - -### 1. CustomUserDetails(自定义用户详情) - -**功能概述** -- **类定义**: `CustomUserDetails` -- **实现接口**: `UserDetails` -- **业务价值**: 封装用户认证和授权信息,桥接业务用户实体与Spring Security -- **核心特性**: 用户信息封装、权限映射、账户状态管理、角色权限控制 - -**核心方法** -```java -public class CustomUserDetails implements UserDetails { - private final UserAccount userAccount; - - // 业务信息获取 - public Long getId(); - public String getName(); - public Role getRole(); - public String getPhone(); - public UserStatus getStatus(); - public String getRegion(); - public List getSkills(); - - // Spring Security标准接口实现 - public Collection getAuthorities(); - public String getPassword(); - public String getUsername(); // 使用邮箱作为用户名 - public boolean isAccountNonExpired(); - public boolean isAccountNonLocked(); // 基于lockoutEndTime实现锁定逻辑 - public boolean isCredentialsNonExpired(); - public boolean isEnabled(); // 基于enabled和status字段 -} -``` - -**设计特点** -- **权限映射**: 自动添加"ROLE_"前缀,确保与Spring Security的hasRole()方法兼容 -- **账户锁定**: 基于lockoutEndTime字段实现动态账户锁定机制 -- **状态检查**: 综合enabled字段和UserStatus枚举进行账户状态验证 -- **信息暴露**: 提供完整的用户业务信息访问接口 - -### 2. JwtAuthenticationFilter(JWT认证过滤器) - -**功能概述** -- **类定义**: `JwtAuthenticationFilter` -- **继承关系**: `OncePerRequestFilter` -- **业务价值**: 处理每个HTTP请求的JWT认证,实现无状态认证机制 -- **核心特性**: JWT令牌提取、令牌验证、用户加载、安全上下文设置 - -**核心方法** -```java -@Component -@RequiredArgsConstructor -public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtService jwtService; - private final UserDetailsService userDetailsService; - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException; -} -``` - -**工作流程** -1. **路径检查**: 跳过认证相关路径(/api/auth) -2. **令牌提取**: 从Authorization头提取Bearer令牌 -3. **令牌验证**: 验证JWT签名和有效期 -4. **用户加载**: 根据令牌中的用户名加载用户详情 -5. **上下文设置**: 创建认证对象并设置到SecurityContext - -**安全特性** -- **一次性过滤**: 继承OncePerRequestFilter确保每个请求只过滤一次 -- **路径跳过**: 智能跳过不需要认证的路径 -- **令牌格式**: 严格验证Bearer令牌格式 -- **上下文管理**: 正确设置Spring Security上下文 - -### 3. SecurityConfig(安全配置) - -**功能概述** -- **类定义**: `SecurityConfig` -- **配置注解**: `@Configuration`, `@EnableWebSecurity`, `@EnableMethodSecurity` -- **业务价值**: 定义系统整体安全策略和访问控制规则 -- **核心特性**: HTTP安全配置、CORS配置、认证管理、密码编码 - -**核心配置方法** -```java -@Configuration -@EnableWebSecurity -@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) -@RequiredArgsConstructor -public class SecurityConfig { - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception; - - @Bean - CorsConfigurationSource corsConfigurationSource(); - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration config); - - @Bean - public PasswordEncoder passwordEncoder(); -} -``` - -**安全规则配置** -```java -// HTTP安全配置 -http - .csrf(AbstractHttpConfigurer::disable) // 禁用CSRF(使用JWT) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) // 启用CORS - .authorizeHttpRequests(authorize -> authorize - // 允许OPTIONS预检请求 - .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - // 认证相关端点 - .requestMatchers("/api/auth/**").permitAll() - // 公共访问端点 - .requestMatchers("/api/public/**", "/api/map/**", "/api/pathfinding/**").permitAll() - // 文件访问端点 - .requestMatchers("/api/files/**").permitAll() - // Dashboard端点权限控制 - .requestMatchers("/api/dashboard/**").hasAnyRole("ADMIN", "DECISION_MAKER") - // 其他请求需要认证 - .anyRequest().authenticated() - ) - .sessionManagement(session -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话 - ) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); -``` - -**CORS配置** -```java -@Bean -CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.addAllowedOrigin("*"); // 允许所有来源 - configuration.addAllowedMethod("*"); // 允许所有HTTP方法 - configuration.addAllowedHeader("*"); // 允许所有HTTP头 - // 注册到所有路径 - source.registerCorsConfiguration("/**", configuration); - return source; -} -``` - -**安全特性** -- **无状态认证**: 基于JWT的无状态会话管理 -- **CSRF防护**: 禁用CSRF(因使用JWT令牌) -- **方法级权限**: 启用@PreAuthorize、@Secured、@RolesAllowed注解 -- **细粒度控制**: 基于URL模式的访问控制 -- **密码安全**: 使用BCrypt强密码编码 - -### 4. UserDetailsServiceImpl(用户详情服务实现) - -**功能概述** -- **类定义**: `UserDetailsServiceImpl` -- **实现接口**: `UserDetailsService` -- **业务价值**: 为Spring Security提供用户加载服务 -- **核心特性**: 用户查找、账户锁定检查、异常处理 - -**核心方法** -```java -@Service -@RequiredArgsConstructor -public class UserDetailsServiceImpl implements UserDetailsService { - private final UserAccountRepository userAccountRepository; - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - // 根据邮箱查找用户 - UserAccount userAccount = userAccountRepository.findByEmail(username) - .orElseThrow(() -> new UsernameNotFoundException("未找到邮箱为 " + username + " 的用户。")); - - // 检查账户锁定状态 - if (userAccount.getLockoutEndTime() != null && - userAccount.getLockoutEndTime().isAfter(LocalDateTime.now())) { - throw new LockedException("该账户已被锁定,请稍后再试。"); - } - - return new CustomUserDetails(userAccount); - } -} -``` - -**业务逻辑** -- **用户查找**: 使用邮箱作为用户名进行查找 -- **锁定检查**: 检查lockoutEndTime字段判断账户是否被锁定 -- **异常处理**: 提供清晰的中文错误信息 -- **对象封装**: 返回CustomUserDetails包装的用户信息 - -## Security模块设计特点 - -### 1. 无状态认证架构 -- **JWT令牌**: 使用JWT实现无状态认证,避免服务器端会话存储 -- **令牌验证**: 每个请求都进行令牌验证,确保安全性 -- **过期管理**: 自动处理令牌过期和刷新 -- **跨服务**: 支持微服务架构下的认证共享 - -### 2. 细粒度权限控制 -- **角色映射**: 自动处理Spring Security角色前缀 -- **方法级权限**: 支持@PreAuthorize等注解进行方法级控制 -- **URL级权限**: 基于URL模式的访问控制 -- **动态权限**: 支持运行时权限检查 - -### 3. 安全防护机制 -- **CSRF防护**: 针对JWT场景禁用CSRF -- **CORS支持**: 完整的跨域资源共享配置 -- **账户锁定**: 基于时间的账户锁定机制 -- **密码安全**: BCrypt强密码编码 - -### 4. 异常处理 -- **用户友好**: 提供中文错误信息 -- **安全考虑**: 不泄露敏感信息 -- **分类处理**: 区分不同类型的认证异常 -- **日志记录**: 记录安全相关事件 - -## Security架构优势 - -### 1. 安全性强 -- **多层防护**: 从过滤器到方法级的多层安全防护 -- **标准实现**: 基于Spring Security标准实现 -- **最佳实践**: 采用业界安全最佳实践 -- **持续更新**: 跟随Spring Security版本更新 - -### 2. 性能优化 -- **无状态设计**: 避免服务器端会话存储开销 -- **一次性过滤**: 每个请求只进行一次安全检查 -- **缓存友好**: 支持用户信息缓存 -- **轻量级**: 最小化安全检查开销 - -### 3. 扩展性好 -- **插件化**: 支持自定义安全组件 -- **配置驱动**: 基于配置的安全策略 -- **接口标准**: 遵循Spring Security接口标准 -- **模块化**: 各安全组件职责清晰 - -### 4. 维护性强 -- **代码清晰**: 结构清晰,职责明确 -- **文档完整**: 详细的注释和文档 -- **测试友好**: 易于进行单元测试和集成测试 -- **监控支持**: 支持安全事件监控 - -## 技术实现细节 - -### 1. Spring Security注解 -```java -@EnableWebSecurity // 启用Web安全 -@EnableMethodSecurity // 启用方法级安全 -@Component // 组件注册 -@Service // 服务注册 -@RequiredArgsConstructor // 构造器注入 -``` - -### 2. JWT处理 -```java -// JWT令牌提取 -String jwt = authHeader.substring(7); // 移除"Bearer "前缀 -String userEmail = jwtService.extractUserName(jwt); - -// JWT令牌验证 -if (jwtService.isTokenValid(jwt, userDetails)) { - // 设置认证上下文 -} -``` - -### 3. 权限控制 -```java -// 角色权限映射 -SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + getRole().name()); - -// URL权限控制 -.requestMatchers("/api/dashboard/**").hasAnyRole("ADMIN", "DECISION_MAKER") - -// 方法级权限控制 -@PreAuthorize("hasRole('ADMIN')") -public void adminOnlyMethod() { } -``` - -### 4. 异常处理 -```java -// 用户不存在异常 -throw new UsernameNotFoundException("未找到邮箱为 " + username + " 的用户。"); - -// 账户锁定异常 -throw new LockedException("该账户已被锁定,请稍后再试。"); -``` - -## Security模块最佳实践 - -1. **令牌管理**: 合理设置JWT过期时间,实现令牌刷新机制 -2. **权限设计**: 采用最小权限原则,细粒度控制访问权限 -3. **异常处理**: 提供用户友好的错误信息,避免泄露敏感信息 -4. **日志记录**: 记录关键安全事件,便于审计和监控 -5. **配置管理**: 将安全配置外部化,支持不同环境配置 -6. **测试覆盖**: 编写完整的安全测试用例 -7. **定期更新**: 及时更新安全依赖,修复安全漏洞 -8. **监控告警**: 建立安全事件监控和告警机制 - -## Security模块总结 - -Security模块为EMS系统提供了完整、可靠的安全防护体系: - -1. **认证完整**: 基于JWT的无状态认证机制,支持多种认证场景 -2. **授权精确**: 细粒度的权限控制,从URL到方法级的全面覆盖 -3. **防护全面**: CSRF、CORS、账户锁定等多重安全防护 -4. **性能优秀**: 无状态设计,最小化安全检查开销 -5. **扩展性强**: 模块化设计,支持自定义安全组件 -6. **维护性好**: 代码清晰,文档完整,易于维护和扩展 -7. **标准兼容**: 完全基于Spring Security标准,确保兼容性 - -通过这套完整的Security模块,EMS系统建立了企业级的安全防护能力,确保了用户数据安全、系统访问控制和业务操作的安全性,为整个系统提供了坚实的安全基础。 + +# EMS 后端业务流程与架构深度解析 + +本文档旨在深入剖析应急管理系统(EMS)后端的核心业务流程、分层架构以及关键技术机制。通过对典型请求生命周期的分析,揭示系统各组件如何协同工作,以实现高效、可靠的业务功能。 +在您的项目中,主要有以下几类常见的注解: + +### 1. JPA (Java Persistence API) 注解 - 用于数据库交互 +这类注解通常用在 model 或 entity 包下的类中,用于将一个Java对象映射到数据库的一张表。 + + @Entity : 声明这个类是一个实体类,它对应数据库中的一张表。 + @Table(name = "aqi_data") : 正如您在 `AqiData.java` 中看到的,这个注解用来指定该实体类对应数据库中的具体表名。在这里, AqiData 类对应 aqi_data 这张表。 + @Id : 指定这个字段是表的主键。 + @Column(name = "user_id") : 指定字段对应数据库表中的列名。 + @GeneratedValue : 定义主键的生成策略(例如,自增长)。 +### 2. Spring 框架注解 - 用于管理组件和处理请求 +这是项目中最核心的注解,用于实现依赖注入(DI)和面向切面编程(AOP)。 + + @Component : 通用的组件注解,表明这个类由 Spring 容器管理。 + @Service : 通常用在业务逻辑层(Service层),是 @Component 的一种特化。 + @Repository : 通常用在数据访问层(DAO/Repository层),也是 @Component 的一种特化。 + @RestController : 用在控制器(Controller)上,表明这个类处理HTTP请求,并且返回的数据会自动序列化为JSON格式。 + @Autowired : 自动注入依赖。Spring会自动寻找匹配的Bean(由 @Component 等注解标记的类的实例)并注入到该字段。 + @RequestMapping , @GetMapping , @PostMapping : 将HTTP请求的URL路径映射到具体的处理方法上。 +### 3. Lombok 注解 - 用于减少样板代码 +Lombok是一个非常实用的Java库,可以通过注解在编译时自动生成代码。 + + @Data : 一个非常方便的组合注解,会自动为类的所有字段生成 getter 、 setter 方法,以及 toString() , equals() , hashCode() 方法。 + @NoArgsConstructor : 生成一个无参构造函数。 + @AllArgsConstructor : 生成一个包含所有字段的构造函数。 + @Slf4j : 为类自动创建一个 log 静态常量,方便您直接使用 log.info(...) 等方法打印日志。 + +## 一、 系统核心思想与分层架构 + +EMS 后端遵循业界主流的**分层架构**设计,旨在实现**高内聚、低耦合**的目标。这种架构将应用程序划分为不同的逻辑层,每一层都承担明确的职责。各层之间通过定义好的接口进行通信,使得系统更易于开发、测试、维护和扩展。 + + +### 1.1 分层架构概览 + +系统从上至下主要分为以下几个核心层级,对应于项目中的不同包: + +- **Controller (控制层)**: `com.dne.ems.controller` + - **职责**: 作为应用的入口,负责接收来自客户端(前端)的 HTTP 请求,解析请求参数,并调用服务层的相应方法来处理业务逻辑。它不包含任何业务逻辑,仅作为请求的调度和转发中心。 + - **交互**: 将处理结果(通常是 DTO 对象)封装成统一的 JSON 格式(`ApiResponse`)返回给客户端。 + +- **Service (服务层)**: `com.dne.ems.service` + - **职责**: 系统的核心,负责实现所有复杂的业务逻辑。它编排一个或多个 Repository 来完成特定功能,处理数据、执行计算、实施业务规则,并处理事务。 + - **交互**: 被 Controller 层调用,同时调用 Repository 层来访问数据库。 + +- **Repository (数据访问层)**: `com.dne.ems.repository` + - **职责**: 定义数据持久化的接口,负责与数据库进行交互。通过继承 Spring Data JPA 的 `JpaRepository`,开发者无需编写具体的 SQL 语句即可实现对数据的增删改查(CRUD)以及复杂的查询。 + - **交互**: 被 Service 层调用,直接操作数据库。 + +- **Model (模型层)**: `com.dne.ems.model` + - **职责**: 定义系统的领域对象,即与数据库表结构一一对应的实体类(Entity)。它们使用 JPA 注解(如 `@Entity`, `@Table`, `@Id`)来映射数据库表和字段。 + - **交互**: 在 Repository 层和 Service 层之间传递数据。 + +- **DTO (数据传输对象)**: `com.dne.ems.dto` + - **职责**: 作为各层之间,特别是 Controller 层与外部客户端之间的数据载体。DTO 的设计可以根据 API 的需要进行定制,隐藏内部领域模型的复杂性,避免暴露不必要的数据,从而提高系统的安全性与灵活性。 + +### 1.2 辅助模块 + +除了核心分层,系统还包含一系列重要的辅助模块,以支持整体功能的实现: + +- **Security (安全模块)**: `com.dne.ems.security` + - **职责**: 基于 Spring Security 实现用户认证(Authentication)和授权(Authorization)。它包含 JWT 的生成与校验、密码加密、以及精细化的接口访问控制策略。 + +- **Exception (异常处理模块)**: `com.dne.ems.exception` + - **职责**: 提供全局统一的异常处理机制。通过 `@ControllerAdvice`,系统可以捕获在业务流程中抛出的各类异常,并将其转换为对客户端友好的、标准化的错误响应。 + +- **Config (配置模块)**: `com.dne.ems.config` + - **职责**: 存放应用的配置类,如 Spring Security 配置、Web MVC 配置、以及应用启动时的数据初始化逻辑(`DataInitializer`)。该模块为EMS系统提供了完整的基础设施支持,包括数据初始化、文件存储配置、API文档配置、HTTP客户端配置、WebSocket配置以及数据转换器等核心配置组件。 + +- **Controller (控制器模块)**: `com.dne.ems.controller` + - **职责**: 作为系统的API入口层,负责处理所有HTTP请求,包括用户认证、反馈管理、任务分配、数据可视化等核心业务功能。该模块包含14个专业化控制器,每个控制器专注于特定的业务领域,提供RESTful API接口,支持权限控制、参数验证、分页查询等功能。 + +- **Event/Listener (事件驱动模块)**: `com.dne.ems.event`, `com.dne.ems.listener` + - **职责**: 实现应用内的异步处理和解耦。例如,用户认证失败时,可以发布一个 `AuthenticationFailureEvent` 事件,由专门的监听器 `AuthenticationFailureEventListener` 异步处理登录失败次数的记录,而不会阻塞主认证流程。 + +## 二、 项目亮点与创新 + +本项目在传统的城市管理系统基础上,融合了多项现代技术与创新理念,旨在打造一个**智能、高效、协同**的城市环境治理平台。其核心亮点体现在以下几个方面: + +### 2.1 业务全流程智能化 + +- **AI 预审核**: 公众提交的反馈信息不再需要人工进行初步筛选。系统集成的 AI 模型能够自动对反馈内容(包括文本和图像)进行分析,判断其有效性、紧急程度,并进行初步的分类。这极大地减轻了主管人员的工作负担,并将他们的精力解放出来,专注于更复杂的决策和任务分配。 +- **智能任务推荐与调度**: 在任务分配环节,系统能够基于网格员的当前位置、历史任务完成效率、技能特长以及当前负载情况,智能推荐最合适的执行人选。在紧急情况下,系统甚至可以自动进行任务的调度与指派,确保问题得到最快响应。 + +### 2.2 事件驱动的异步架构 + +- **松耦合与高响应**: 项目广泛采用了**事件驱动架构 (EDA)**。例如,当一个反馈提交后,系统会发布一个 `FeedbackSubmittedEvent` 事件,后续的 AI 分析、通知发送等多个操作都由独立的监听器异步执行。这种设计使得核心业务流程(如提交反馈)能够瞬间完成,极大地提升了用户体验和系统的吞吐量。同时,模块之间通过事件进行通信,而不是直接调用,实现了高度解耦,使得系统更容易扩展和维护。 + +### 2.3 全方位的数据驱动决策 + +- **实时数据大屏**: 决策者不再依赖于滞后的纸质报告。系统提供了一个动态、可交互的数据驾驶舱,实时展示城市环境的各项关键指标(KPIs),如 AQI 分布、任务完成率、反馈热力图等。所有数据都以直观的图表形式呈现,帮助管理者快速洞察问题、发现趋势,从而做出更科学的决策。 +- **绩效与资源优化**: 系统通过对历史数据的深度分析,能够评估不同区域的环境治理成效、各个网格员的工作效率,为绩效考核提供客观依据,并为未来的资源调配和政策制定提供数据支持。 + +### 2.4 四端一体的协同工作平台 + +- **角色精准赋能**: 系统为四类核心用户——**公众、网格员、主管、决策者**——分别设计了专属的客户端(Web 或移动应用),精准满足不同角色的工作需求。 + - **公众端**: 简化提交流程,提供实时进度查询。 + - **网格员端**: 移动化接单、处理、上报,并提供导航支持。 + - **主管端**: 集中审核、智能分配、实时监控。 + - **决策者端**: 宏观数据洞察,支持战略决策。 +- **无缝信息流转**: 通过统一的后端平台,信息可以在四个客户端之间无缝流转,形成从问题发现、任务分配、现场处理到决策分析的闭环管理,打破了传统工作模式中的信息孤岛。 + +## 三、 核心业务流程与关键机制深度解析 + +下面,我们将以具体的业务场景和技术实现为例,详细拆解请求在系统分层架构中的流转过程。 + +### 2.1 用户认证与授权 + +此流程是系统的安全基石,涉及用户登录、权限验证和 Token 管理。 + +#### API 接口 + +- **用户注册**: `POST /api/auth/signup` +- **用户登录**: `POST /api/auth/login` +- **发送验证码**: `POST /api/auth/send-verification-code` +- **请求重置密码**: `POST /api/auth/request-password-reset` +- **重置密码**: `POST /api/auth/reset-password` + +**场景:用户使用用户名和密码登录 (POST /api/auth/login)** + +1. **前端 -> Controller (`AuthController`)** + - **动作**: 用户在登录页输入凭据,前端发送 `POST` 请求到 `/api/auth/login`,请求体为包含 `username` 和 `password` 的 `LoginRequest` DTO。 + - **Controller 角色**: `AuthController` 的 `authenticateUser` 方法接收请求。`@Valid` 注解确保了输入的基本格式正确。Controller 将 `LoginRequest` 直接传递给 `AuthService` 进行处理。 + +2. **Controller -> Service (`AuthService`)** + - **动作**: `AuthService` 调用 Spring Security 的 `AuthenticationManager` 的 `authenticate` 方法。`AuthenticationManager` 是认证的核心入口。 + - **Service 角色**: `AuthService` 编排了整个认证流程: + - **凭证封装**: 将用户名和密码封装成 `UsernamePasswordAuthenticationToken`。 + - **认证委托**: 将此 `token` 交给 `AuthenticationManager`。`AuthenticationManager` 会遍历其内部的 `AuthenticationProvider` 列表(在我们的例子中是 `DaoAuthenticationProvider`)来寻找能处理此 `token` 的提供者。 + - **用户查找**: `DaoAuthenticationProvider` 会使用 `UserDetailsService` (我们的实现是 `UserDetailsServiceImpl`) 的 `loadUserByUsername` 方法,根据用户名去数据库查找用户。`UserDetailsServiceImpl` 会调用 `UserRepository`。 + - **密码比对**: `DaoAuthenticationProvider` 使用 `PasswordEncoder` (我们配置的 `BCryptPasswordEncoder`) 比较用户提交的密码和数据库中存储的加密哈希值。 + - **生成 JWT**: 如果认证成功,`AuthService` 调用 `JwtTokenProvider` 的 `generateToken` 方法,为认证通过的 `Authentication` 对象生成一个 JWT。 + +3. **Service -> Repository (`UserRepository`)** + - **动作**: `UserDetailsServiceImpl` 调用 `userRepository.findByUsername(username)`。 + - **Repository 角色**: `UserRepository` 执行 `SELECT` 查询,从 `users` 表中找到对应的用户记录,并将其封装成 `User` 实体对象返回。 + +4. **安全组件交互 (`JwtTokenProvider`, `UserDetailsServiceImpl`)** + - `UserDetailsServiceImpl`: 实现了 Spring Security 的 `UserDetailsService` 接口,是连接应用用户数据和 Spring Security 框架的桥梁。 + - `JwtTokenProvider`: 负责 JWT 的生成和解析。`generateToken` 方法会设置主题(用户名)、签发时间、过期时间,并使用预设的密钥和算法(HS512)进行签名。 + +5. **Controller -> 前端** + - **动作**: `AuthService` 返回生成的 JWT。`AuthController` 将此 JWT 包装在 `JwtAuthenticationResponse` DTO 中,再放入 `ApiResponse.success()`,以 HTTP 200 状态码返回给前端。 + - **后续请求**: 前端在后续所有需要认证的请求的 `Authorization` 头中携带此 JWT (`Bearer `)。Spring Security 的 `JwtAuthenticationFilter` 会在每个请求到达 Controller 前拦截并验证此 Token。 + +### 2.2 公众反馈管理 + +#### API 接口 + +- **提交反馈 (JSON)**: `POST /api/feedback/submit-json` +- **提交反馈 (Multipart)**: `POST /api/feedback/submit` +- **获取所有反馈**: `GET /api/feedback` +- **公众提交反馈**: `POST /api/public/feedback` + +**场景:公众提交带图片的反馈 (POST /api/public/feedback)** + +1. **前端 -> Controller (`PublicController`)** + - **动作**: 公众用户填写反馈表单(描述、位置、图片),前端以 `multipart/form-data` 格式提交到 `/api/public/feedback`。 + - **Controller 角色**: `createFeedback` 方法使用 `@RequestPart` 分别接收反馈内容的 DTO (`FeedbackCreateDTO`) 和文件 (`MultipartFile`)。`@Valid` 确保 DTO 数据合规。Controller 随即调用 `FeedbackService`。 + +2. **Controller -> Service (`FeedbackService` & `FileStorageService`)** + - **动作**: `FeedbackService` 的 `createFeedback` 方法被调用。 + - **Service 角色**: + - **文件处理**: 如果 `file` 存在,`FeedbackService` 首先调用 `FileStorageService` 的 `storeFile` 方法。`FileStorageService` 会生成唯一文件名,将文件存储在服务器的 `uploads` 目录下,并返回可访问的文件路径。 + - **实体创建**: `FeedbackService` 创建一个新的 `Feedback` 实体。 + - **数据映射**: 使用 `ModelMapper` 将 `FeedbackCreateDTO` 的数据(如 `description`, `location`)映射到 `Feedback` 实体上。 + - **关联文件**: 将 `FileStorageService` 返回的文件路径设置到 `Feedback` 实体的 `imageUrl` 字段。 + - **状态设定**: 设置反馈的初始状态为 `PENDING`。 + - **持久化**: 调用 `feedbackRepository.save(newFeedback)`。 + +3. **Service -> Repository (`FeedbackRepository`)** + - **动作**: `feedbackRepository.save()` 执行 `INSERT` SQL 语句,将新的反馈记录存入 `feedbacks` 表。 + +4. **Controller -> 前端** + - **动作**: `FeedbackService` 返回成功创建的 `Feedback` 对象的 DTO。`PublicController` 将其包装在 `ApiResponse.success()` 中,以 HTTP 201 (Created) 状态码返回,表示资源创建成功。 + +### 2.3 任务生命周期管理 + +#### API 接口 + +- **获取待审核的反馈列表**: `GET /api/supervisor/reviews` +- **批准反馈**: `POST /api/supervisor/reviews/{feedbackId}/approve` +- **拒绝反馈**: `POST /api/supervisor/reviews/{feedbackId}/reject` +- **获取未分配的反馈**: `GET /api/tasks/unassigned` +- **获取可用的网格员**: `GET /api/tasks/grid-workers` +- **分配任务**: `POST /api/tasks/assign` +- **获取任务列表**: `GET /api/management/tasks` +- **分配任务**: `POST /api/management/tasks/{taskId}/assign` +- **获取任务详情**: `GET /api/management/tasks/{taskId}` +- **审核任务**: `POST /api/management/tasks/{taskId}/review` +- **取消任务**: `POST /api/management/tasks/{taskId}/cancel` +- **直接创建任务**: `POST /api/management/tasks` +- **获取待处理的反馈**: `GET /api/management/tasks/feedback` +- **从反馈创建任务**: `POST /api/management/tasks/feedback/{feedbackId}/create-task` +- **获取我的任务**: `GET /api/worker` +- **获取任务详情**: `GET /api/worker/{taskId}` +- **接受任务**: `POST /api/worker/{taskId}/accept` +- **提交任务**: `POST /api/worker/{taskId}/submit` + +**场景:主管审核反馈并转换为任务 (POST /api/supervisor/feedback/{feedbackId}/convert)** + +1. **前端 -> Controller (`SupervisorController`)** + - **动作**: 主管在管理界面查看一条反馈,点击“转换为任务”按钮。前端发送 `POST` 请求到 `/api/supervisor/feedback/456/convert`,请求体中可能包含指派信息 `TaskFromFeedbackDTO`。 + - **Controller 角色**: `convertFeedbackToTask` 方法接收请求,解析 `feedbackId` 和 DTO,然后调用 `TaskService`。 + +2. **Controller -> Service (`TaskService` & `FeedbackService`)** + - **动作**: `TaskService` 的 `createTaskFromFeedback` 方法被调用。 + - **Service 角色**: 这是一个跨领域服务的协调过程。 + - **获取反馈**: 调用 `feedbackRepository.findById(feedbackId)` 获取原始反馈信息。如果不存在,抛出 `ResourceNotFoundException`。 + - **状态检查**: 检查反馈是否已经是“已处理”状态,防止重复转换。 + - **创建任务实体**: 创建一个新的 `Task` 实体。 + - **数据迁移**: 将 `Feedback` 的 `description`, `location` 等信息复制到 `Task` 实体中。 + - **设置任务属性**: 根据 `TaskFromFeedbackDTO` 设置任务的标题、截止日期、指派的网格员(如果提供了 `workerId`)。如果未指派,任务状态为 `PENDING`;如果已指派,状态为 `ASSIGNED`。 + - **持久化任务**: 调用 `taskRepository.save(newTask)`。 + - **更新反馈状态**: 更新 `Feedback` 实体的状态为 `PROCESSED`,并调用 `feedbackRepository.save(feedback)`。 + - **事务管理**: 整个方法应该被 `@Transactional` 注解标记,确保创建任务和更新反馈状态这两个操作要么同时成功,要么同时失败。 + +3. **Service -> Repository (`TaskRepository`, `FeedbackRepository`)** + - **动作**: `TaskRepository` 插入一条新任务记录。`FeedbackRepository` 更新对应的反馈记录状态。 + +4. **Controller -> 前端** + - **动作**: `TaskService` 返回新创建的任务的 DTO。`SupervisorController` 将其包装在 `ApiResponse.success()` 中返回,前端可以根据返回的任务信息进行页面跳转或状态更新。 + +### 2.4 数据可视化与决策支持 + +#### API 接口 + +- **获取仪表盘统计数据**: `GET /api/dashboard/stats` +- **获取 AQI 分布**: `GET /api/dashboard/reports/aqi-distribution` +- **获取污染超标月度趋势**: `GET /api/dashboard/reports/pollution-trend` +- **获取网格覆盖率**: `GET /api/dashboard/reports/grid-coverage` +- **获取反馈热力图数据**: `GET /api/dashboard/map/heatmap` +- **获取污染类型统计**: `GET /api/dashboard/reports/pollution-stats` +- **获取任务完成情况统计**: `GET /api/dashboard/reports/task-completion-stats` +- **获取 AQI 热力图数据**: `GET /api/dashboard/map/aqi-heatmap` + +### 2.5 智能分配算法 + +#### API 接口 + +- **获取任务的推荐人选**: `GET /api/tasks/{taskId}/recommendations` + +**场景:主管为新任务寻求系统推荐的执行人 (GET /api/tasks/123/recommendations)** + +1. **前端 -> Controller (`TaskManagementController`)** + - **动作**: 主管在任务分配界面,点击“推荐人选”按钮。前端向 `/api/tasks/123/recommendations` 发送 `GET` 请求。 + - **Controller 角色**: `getTaskRecommendations` 方法接收请求,并调用 `TaskRecommendationService`。 + +2. **Controller -> Service (`TaskRecommendationService`)** + - **动作**: `TaskRecommendationService` 的 `recommendWorkersForTask` 方法被调用。 + - **Service 角色**: 这是智能分配算法的核心实现。 + - **获取任务信息**: 首先,从 `taskRepository` 获取任务详情,特别是任务的地理位置和类型。 + - **筛选候选人**: 从 `userRepository` 获取所有角色为“网格员”且状态为“可用”的用户列表。 + - **为每位候选人评分**: 遍历候选人列表,根据一系列预设标准计算综合得分。这是一个典型的加权评分模型: + - **距离 (40%)**: 计算网格员当前位置与任务位置的直线距离。距离越近,得分越高。`Score = (MaxDistance - WorkerDistance) / MaxDistance`。 + - **当前负载 (30%)**: 查询该网格员当前状态为 `ASSIGNED` 或 `IN_PROGRESS` 的任务数量。任务越少,得分越高。`Score = (MaxLoad - WorkerLoad) / MaxLoad`。 + - **技能匹配度 (20%)**: 如果任务有特定的技能要求(如“管道维修”),检查网格员的技能标签。匹配则得满分,否则得0分。 + - **历史表现 (10%)**: 查询该网格员过去完成类似任务的平均耗时和评价。表现越好,得分越高。 + - **加权总分**: `Total Score = Score_Distance * 0.4 + Score_Load * 0.3 + Score_Skill * 0.2 + Score_Performance * 0.1`。 + - **排序**: 根据总分对所有候选人进行降序排序。 + +3. **Service -> Repository (Multiple Repositories)** + - **动作**: `TaskRecommendationService` 会与 `TaskRepository`, `UserRepository`, `WorkerLocationRepository` (假设有这样一个实时位置库) 等多个 Repository 交互,以获取计算所需的数据。 + +4. **Controller -> 前端** + - **动作**: `TaskRecommendationService` 返回一个包含网格员信息和他们各自推荐分数的 DTO 列表。`TaskManagementController` 将此列表包装在 `ApiResponse.success()` 中返回。前端界面会高亮显示得分最高的几位候选人,并附上推荐理由(如“距离最近”,“负载最低”),辅助主管做出最终决策。 + +**场景:决策者查看仪表盘 (GET /api/dashboard)** + +1. **前端 -> Controller (`DashboardController`)** + - **动作**: 决策者访问仪表盘页面,前端发送 `GET` 请求到 `/api/dashboard`。 + - **Controller 角色**: `getDashboardData` 方法直接调用 `DashboardService`。 + +2. **Controller -> Service (`DashboardService`)** + - **动作**: `DashboardService` 的 `getDashboardData` 方法被调用。 + - **Service 角色**: 这是一个典型的数据聚合服务。 + - **并行/串行查询**: `DashboardService` 会向多个 Repository 发起查询请求,以收集构建仪表盘所需的所有数据。 + - **任务统计**: 调用 `taskRepository` 的自定义聚合查询方法(如 `countByStatus`)来获取不同状态的任务数。 + - **反馈统计**: 调用 `feedbackRepository.countByStatus` 获取不同状态的反馈数。 + - **人员概览**: 调用 `personnelRepository.count()` 获取总人数。 + - **AQI 数据**: 调用 `aqiDataRepository.findLatest()` 获取最新的空气质量数据。 + - **数据组装**: 将所有查询到的零散数据,组装成一个专为前端仪表盘设计的 `DashboardDTO` 对象。 + +3. **Service -> Repository (多个)** + - **动作**: `TaskRepository`, `FeedbackRepository`, `PersonnelRepository` 等分别执行高效的数据库聚合查询(如 `SELECT status, COUNT(*) FROM tasks GROUP BY status`)。 + - **Repository 角色**: Repository 接口中定义了这些自定义查询方法,使用 `@Query` 注解编写 JPQL 或原生 SQL,直接利用数据库的计算能力,避免在应用服务层进行大量数据处理。 + +4. **Controller -> 前端** + - **动作**: `DashboardService` 返回填充了完整数据的 `DashboardDTO`。`DashboardController` 将其包装在 `ApiResponse.success()` 中,以 HTTP 200 状态码返回。前端收到 JSON 数据后,使用图表库(如 ECharts)进行渲染。 + +### 2.5 网格与人员管理 + +#### API 接口 + +- **获取网格列表**: `GET /api/grids` +- **更新网格信息**: `PATCH /api/grids/{gridId}` +- **创建用户**: `POST /api/personnel/users` +- **获取用户列表**: `GET /api/personnel/users` +- **获取单个用户**: `GET /api/personnel/users/{userId}` +- **更新用户信息**: `PATCH /api/personnel/users/{userId}` +- **更新用户角色**: `PUT /api/personnel/users/{userId}/role` +- **删除用户**: `DELETE /api/personnel/users/{userId}` +- **更新个人资料**: `PATCH /api/me` +- **更新我的位置**: `POST /api/me/location` +- **获取我的反馈历史**: `GET /api/me/feedback` + +**场景:管理员分页查询所有人员信息 (GET /api/management/personnel?page=0&size=10)** + +1. **前端 -> Controller (`ManagementController`)** + - **动作**: 管理员在人员管理页面,前端发送带分页参数的 `GET` 请求。 + - **Controller 角色**: `getAllPersonnel` 方法使用 `@RequestParam` 接收分页参数,并将其封装成 `Pageable` 对象,然后调用 `PersonnelService`。 + +2. **Controller -> Service (`PersonnelService`)** + - **动作**: `PersonnelService` 的 `findAll` 方法接收 `Pageable` 对象。 + - **Service 角色**: 直接将 `Pageable` 对象传递给 `PersonnelRepository`。对于更复杂的查询(例如,按姓名或角色搜索),Service 层会接收额外的查询参数,并调用 `Specification` 或 `QueryDSL` 来构建动态查询。 + +3. **Service -> Repository (`PersonnelRepository`)** + - **动作**: `personnelRepository.findAll(pageable)` 被调用。 + - **Repository 角色**: Spring Data JPA 会根据 `Pageable` 参数自动生成带有分页和排序的 SQL 查询(如 `SELECT ... FROM personnel LIMIT ? OFFSET ?`),并执行。它返回一个 `Page` 对象,其中包含了当前页的数据列表、总页数、总元素数量等完整的的分页信息。 + +4. **Controller -> 前端** + - **动作**: `PersonnelService` 返回 `Page` 对象。`ManagementController` 将其直接放入 `ApiResponse.success()` 中返回。前端根据返回的 `Page` 对象渲染数据列表和分页控件。 + +### 2.6 AI 辅助功能 + +**场景:AI 辅助生成任务报告摘要 (POST /api/ai/summarize)** + +1. **前端 -> Controller (`AiController`)** + - **动作**: 用户在任务详情页输入或粘贴长篇报告文本,点击“生成摘要”。前端将文本内容包装在 `SummarizeRequest` DTO 中,发送 `POST` 请求。 + - **Controller 角色**: `summarizeText` 方法接收 DTO,并调用 `AiService`。 + +2. **Controller -> Service (`AiService`)** + - **动作**: `AiService` 的 `getSummary` 方法被调用。 + - **Service 角色**: 这是与外部 AI 服务交互的核心。 + - **构建请求**: 根据所使用的 AI 服务(如 OpenAI, Google Gemini, 或其他私有化部署模型)的 API 规范,构建 HTTP 请求。这通常涉及设置 API Key、请求头、以及包含待摘要文本的 JSON 请求体。 + - **API 调用**: 使用 `RestTemplate` 或 `WebClient` 向 AI 服务的 API 端点发送请求。 + - **解析响应**: 接收 AI 服务返回的 JSON 响应,并从中提取出生成的摘要文本。 + - **异常处理**: 处理网络超时、API 认证失败、或 AI 服务返回错误码等异常情况。 + +3. **Controller -> 前端** + - **动作**: `AiService` 返回提取出的摘要字符串。`AiController` 将其包装在 `ApiResponse.success()` 中返回给前端,前端将摘要显示在页面上。 + +### 2.7 关键技术机制深度解析 + +#### A. 文件上传与管理 (`FileStorageService`) +- **流程**: `Controller` 接收 `MultipartFile` -> `Service` 调用 `FileStorageService.storeFile()` -> `FileStorageService` 生成唯一文件名 (e.g., `UUID.randomUUID().toString()`) 并拼接原始文件扩展名 -> 使用 `StringUtils.cleanPath` 清理文件名防止路径遍历攻击 -> 构建目标存储路径 `Path targetLocation = this.fileStorageLocation.resolve(fileName)` -> 使用 `Files.copy` 将文件输入流写入目标路径,并使用 `StandardCopyOption.REPLACE_EXISTING` 覆盖同名文件 -> 返回通过 `ServletUriComponentsBuilder` 构建的、可从外部访问的文件下载 URI。 +- **关键点**: + - **配置化**: 文件存储路径 (`file.upload-dir`) 在 `application.properties` 中配置,具有灵活性。 + - **安全性**: 明确处理了 `FileNameContainsInvalidCharactersException` 和 `FileStorageException`。 + - **资源服务**: 提供了 `loadFileAsResource` 方法,可以将存储的文件作为 `Resource` 对象加载,以便于下载。 + +### 2.8 地图与路径规划 + +#### A. 地图服务 (`MapService`) +- **获取完整地图网格**: `GET /api/map/grid` +- **创建或更新地图单元格**: `POST /api/map/grid` +- **初始化地图**: `POST /api/map/initialize` + +#### B. 路径规划服务 (`PathfindingService`) +- **寻找路径**: `POST /api/pathfinding/find` + +#### B. 文件服务 (`FileService`) +- **下载文件**: `GET /api/files/download/{fileName}` +- **查看文件**: `GET /api/files/view/{fileName}` + +#### C. 邮件与验证码服务 (`EmailService`) +- **流程**: `AuthService` 调用 `EmailService.sendVerificationCode(email)` -> `EmailService` 使用 `Random` 生成6位数字验证码 -> 将 `email:code` 键值对存入 `verificationCodeCache` (一个 `Caffeine` 缓存实例),并设置5分钟过期 -> 使用 `JavaMailSender` 创建 `SimpleMailMessage`,设置收件人、主题和正文 -> `mailSender.send(message)` 发送邮件。 + +## 三、API 接口文档 + +本部分详细列出了系统提供的所有API接口,包括其功能描述、请求路径、方法、所需权限以及请求/响应示例。 + +### 1. 认证模块 (Auth) +**基础路径**: `/api/auth` + +#### 1.1 用户注册 +- **功能描述**: 注册一个新用户。 +- **请求路径**: `/api/auth/signup` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `SignUpRequest` + +#### 1.2 用户登录 +- **功能描述**: 用户登录并获取 JWT Token。 +- **请求路径**: `/api/auth/login` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `LoginRequest` + +#### 1.3 发送验证码 +- **功能描述**: 请求发送验证码到指定邮箱。 +- **请求路径**: `/api/auth/send-verification-code` +- **请求方法**: `POST` +- **所需权限**: 无 +- **查询参数**: `email` (string, 必需) + +#### 1.4 请求重置密码 +- **功能描述**: 请求发送重置密码的链接或令牌。 +- **请求路径**: `/api/auth/request-password-reset` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `PasswordResetRequest` + +#### 1.5 重置密码 +- **功能描述**: 使用令牌重置密码。 +- **请求路径**: `/api/auth/reset-password` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `PasswordResetDto` + +### 2. 仪表盘模块 (Dashboard) +**基础路径**: `/api/dashboard` +**所需权限**: `DECISION_MAKER` + +#### 2.1 获取仪表盘统计数据 +- **请求路径**: `/api/dashboard/stats` +- **请求方法**: `GET` + +#### 2.2 获取 AQI 分布 +- **请求路径**: `/api/dashboard/reports/aqi-distribution` +- **请求方法**: `GET` + +#### 2.3 获取污染超标月度趋势 +- **请求路径**: `/api/dashboard/reports/pollution-trend` +- **请求方法**: `GET` + +#### 2.4 获取网格覆盖率 +- **请求路径**: `/api/dashboard/reports/grid-coverage` +- **请求方法**: `GET` + +#### 2.5 获取反馈热力图数据 +- **请求路径**: `/api/dashboard/map/heatmap` +- **请求方法**: `GET` + +#### 2.6 获取污染类型统计 +- **请求路径**: `/api/dashboard/reports/pollution-stats` +- **请求方法**: `GET` + +#### 2.7 获取任务完成情况统计 +- **请求路径**: `/api/dashboard/reports/task-completion-stats` +- **请求方法**: `GET` + +#### 2.8 获取 AQI 热力图数据 +- **请求路径**: `/api/dashboard/map/aqi-heatmap` +- **请求方法**: `GET` + +### 3. 反馈模块 (Feedback) +**基础路径**: `/api/feedback` + +#### 3.1 提交反馈 (JSON) +- **功能描述**: 用于测试,使用 JSON 提交反馈。 +- **请求路径**: `/api/feedback/submit-json` +- **请求方法**: `POST` +- **所需权限**: 已认证用户 +- **请求体**: `FeedbackSubmissionRequest` + +#### 3.2 提交反馈 (Multipart) +- **功能描述**: 提交反馈,可包含文件。 +- **请求路径**: `/api/feedback/submit` +- **请求方法**: `POST` +- **所需权限**: 已认证用户 +- **请求体**: `multipart/form-data` + +#### 3.3 获取所有反馈 +- **功能描述**: 获取所有反馈(分页)。 +- **请求路径**: `/api/feedback` +- **请求方法**: `GET` +- **所需权限**: `ADMIN` + +### 4. 文件模块 (File) +**基础路径**: `/api/files` +**所需权限**: 公开访问 + +#### 4.1 下载文件 +- **请求路径**: `/api/files/download/{fileName}` +- **请求方法**: `GET` + +#### 4.2 查看文件 +- **请求路径**: `/api/files/view/{fileName}` +- **请求方法**: `GET` + +### 5. 网格模块 (Grid) +**基础路径**: `/api/grids` +**所需权限**: `ADMIN` 或 `DECISION_MAKER` + +#### 5.1 获取网格列表 +- **请求路径**: `/api/grids` +- **请求方法**: `GET` + +#### 5.2 更新网格信息 +- **请求路径**: `/api/grids/{gridId}` +- **请求方法**: `PATCH` +- **所需权限**: `ADMIN` + +### 6. 网格员任务模块 (Worker) +**基础路径**: `/api/worker` +**所需权限**: `GRID_WORKER` + +#### 6.1 获取我的任务 +- **请求路径**: `/api/worker` +- **请求方法**: `GET` + +#### 6.2 获取任务详情 +- **请求路径**: `/api/worker/{taskId}` +- **请求方法**: `GET` + +#### 6.3 接受任务 +- **请求路径**: `/api/worker/{taskId}/accept` +- **请求方法**: `POST` + +#### 6.4 提交任务 +- **请求路径**: `/api/worker/{taskId}/submit` +- **请求方法**: `POST` + +### 7. 地图模块 (Map) +**基础路径**: `/api/map` +**所需权限**: 公开访问 + +#### 7.1 获取完整地图网格 +- **请求路径**: `/api/map/grid` +- **请求方法**: `GET` + +#### 7.2 创建或更新地图单元格 +- **请求路径**: `/api/map/grid` +- **请求方法**: `POST` + +#### 7.3 初始化地图 +- **请求路径**: `/api/map/initialize` +- **请求方法**: `POST` + +### 8. 路径规划模块 (Pathfinding) +**基础路径**: `/api/pathfinding` +**所需权限**: 公开访问 + +#### 8.1 寻找路径 +- **功能描述**: 使用 A* 算法在两点之间寻找最短路径。 +- **请求路径**: `/api/pathfinding/find` +- **请求方法**: `POST` + +### 9. 人员管理模块 (Personnel) +**基础路径**: `/api/personnel` +**所需权限**: `ADMIN` + +#### 9.1 创建用户 +- **请求路径**: `/api/personnel/users` +- **请求方法**: `POST` + +#### 9.2 获取用户列表 +- **请求路径**: `/api/personnel/users` +- **请求方法**: `GET` + +#### 9.3 获取单个用户 +- **请求路径**: `/api/personnel/users/{userId}` +- **请求方法**: `GET` + +#### 9.4 更新用户信息 +- **请求路径**: `/api/personnel/users/{userId}` +- **请求方法**: `PATCH` + +#### 9.5 更新用户角色 +- **请求路径**: `/api/personnel/users/{userId}/role` +- **请求方法**: `PUT` + +#### 9.6 删除用户 +- **请求路径**: `/api/personnel/users/{userId}` +- **请求方法**: `DELETE` + +### 10. 个人资料模块 (Me) +**基础路径**: `/api/me` +**所需权限**: 已认证用户 + +#### 10.1 更新个人资料 +- **请求路径**: `/api/me` +- **请求方法**: `PATCH` + +#### 10.2 更新我的位置 +- **请求路径**: `/api/me/location` +- **请求方法**: `POST` + +#### 10.3 获取我的反馈历史 +- **请求路径**: `/api/me/feedback` +- **请求方法**: `GET` + +### 11. 公共接口模块 (Public) +**基础路径**: `/api/public` +**所需权限**: 公开访问 + +#### 11.1 公众提交反馈 +- **请求路径**: `/api/public/feedback` +- **请求方法**: `POST` + +### 12. 主管模块 (Supervisor) +**基础路径**: `/api/supervisor` +**所需权限**: `SUPERVISOR` 或 `ADMIN` + +#### 12.1 获取待审核的反馈列表 +- **请求路径**: `/api/supervisor/reviews` +- **请求方法**: `GET` + +#### 12.2 批准反馈 +- **请求路径**: `/api/supervisor/reviews/{feedbackId}/approve` +- **请求方法**: `POST` + +#### 12.3 拒绝反馈 +- **请求路径**: `/api/supervisor/reviews/{feedbackId}/reject` +- **请求方法**: `POST` + +### 13. 任务分配模块 (Tasks) +**基础路径**: `/api/tasks` +**所需权限**: `ADMIN` 或 `SUPERVISOR` + +#### 13.1 获取未分配的反馈 +- **请求路径**: `/api/tasks/unassigned` +- **请求方法**: `GET` + +#### 13.2 获取可用的网格员 +- **请求路径**: `/api/tasks/grid-workers` +- **请求方法**: `GET` + +#### 13.3 分配任务 +- **请求路径**: `/api/tasks/assign` +- **请求方法**: `POST` + +### 14. 任务管理模块 (Management) +**基础路径**: `/api/management/tasks` +**所需权限**: `SUPERVISOR` + +#### 14.1 获取任务列表 +- **请求路径**: `/api/management/tasks` +- **请求方法**: `GET` + +#### 14.2 分配任务 +- **请求路径**: `/api/management/tasks/{taskId}/assign` +- **请求方法**: `POST` + +#### 14.3 获取任务详情 +- **请求路径**: `/api/management/tasks/{taskId}` +- **请求方法**: `GET` + +#### 14.4 审核任务 +- **请求路径**: `/api/management/tasks/{taskId}/review` +- **请求方法**: `POST` + +#### 14.5 取消任务 +- **请求路径**: `/api/management/tasks/{taskId}/cancel` +- **请求方法**: `POST` + +#### 14.6 直接创建任务 +- **请求路径**: `/api/management/tasks` +- **请求方法**: `POST` + +#### 14.7 获取待处理的反馈 +- **请求路径**: `/api/management/tasks/feedback` +- **请求方法**: `GET` + +#### 14.8 从反馈创建任务 +- **请求路径**: `/api/management/tasks/feedback/{feedbackId}/create-task` +- **请求方法**: `POST` +- **验证流程**: 用户提交验证码 -> `AuthService` 调用 `EmailService.verifyCode(email, code)` -> `EmailService` 从缓存中通过 `verificationCodeCache.getIfPresent(email)` 获取验证码,并与用户提交的 `code` 进行比对。如果匹配且存在,则验证成功并从缓存中移除该验证码 (`cache.invalidate(email)`)。 + +#### C. 登录安全与尝试锁定 (`AuthenticationFailureEventListener`) +- **流程**: 用户登录失败 -> Spring Security 的 `ProviderManager` 认证失败后,会发布一个 `AuthenticationFailureBadCredentialsEvent` 事件 -> `AuthenticationFailureEventListener` (一个实现了 `ApplicationListener` 的组件) 的 `onApplicationEvent` 方法被触发 -> 监听器从事件中获取用户名 -> 调用 `LoginAttemptService.loginFailed(username)` -> `LoginAttemptService` 使用 `Caffeine` 缓存 `attemptsCache` 来记录失败次数。每次失败,计数器加一。 +- **锁定逻辑**: `LoginAttemptService` 的 `isBlocked(username)` 方法检查失败次数是否超过最大允许次数(如 `MAX_ATTEMPT = 5`)。在 `UserDetailsServiceImpl` 的 `loadUserByUsername` 方法的开头,会先调用 `loginAttemptService.isBlocked()`。如果用户已被锁定,则直接抛出 `LockedException`,阻止后续的数据库查询和密码比对,实现登录锁定。 + +#### D. A* 路径规划 (`PathfindingService`) +- **流程**: `PathfindingController` 调用 `PathfindingService.findPath()` -> `PathfindingService` 初始化一个 `Grid` 对象,该对象包含地图的宽度、高度和障碍物位置集合 -> A* 算法的核心 `AStar.findPath()` 被调用,它接收 `Grid` 和起止点 `Node` 作为参数 -> 算法内部使用 `PriorityQueue` 作为开放列表(优先处理f值最低的节点),使用 `HashSet` 作为关闭列表 -> 循环开始:从开放列表取出当前节点,若为终点则通过 `retracePath` 回溯构建路径 -> 否则,遍历当前节点的邻居节点,计算其 g值(到起点的代价)和 h值(到终点的预估代价,即启发式函数),若邻居节点是更优路径,则更新其代价和父节点,并加入开放列表。 +- **关键点**: + - **数据结构**: `Node` 对象包含了坐标、g/h/f代价值、父节点引用和是否为障碍物等信息。 + - **启发式函数**: 使用曼哈顿距离 `getDistance(nodeA, nodeB)` 作为启发式函数,适用于网格地图。 + - **可扩展性**: `Grid` 可以从数据库、文件或动态生成,使得路径规划可以适应不同的地图场景。 + +## 四、配置模块详细解析 + +配置模块(`com.dne.ems.config`)是EMS系统的基础设施核心,为整个应用提供了完整的配置支持和初始化功能。本节将详细解析各个配置组件的功能、实现原理和使用方式。 + +### 4.1 数据初始化配置 (`DataInitializer.java`) + +#### 功能概述 +`DataInitializer` 是系统启动时的数据初始化组件,实现了 `CommandLineRunner` 接口,确保在Spring Boot应用完全启动后自动执行数据初始化逻辑。 + +#### 核心特性 +- **自动执行**: 实现 `CommandLineRunner` 接口,在应用启动后自动运行 +- **幂等性**: 检查数据是否已存在,避免重复创建 +- **完整性**: 创建系统运行所需的所有基础数据 +- **日志记录**: 包含详细的初始化过程日志 + +#### 初始化内容详解 + +**1. 用户账户初始化** +- 创建五种角色的测试账户:管理员、网格员、决策者、主管、公众监督员 +- 使用BCrypt加密存储密码 +- 为每个角色分配相应的权限和属性 + +**默认账户信息**: +``` +管理员: admin@aizhangz.top / Admin@123 +网格员: worker@aizhangz.top / Worker@123 +决策者: decision.maker@aizhangz.top / Decision@123 +主管: supervisor@aizhangz.top / Supervisor@123 +公众监督员: public.supervisor@aizhangz.top / Public@123 +``` + +**2. 网格数据初始化** +- 生成40x40的完整城市网格系统 +- 覆盖4个主要城市:常州市、无锡市、苏州市、南京市 +- 每个网格单元包含坐标、城市归属、区域信息 +- 支持网格员分配和区域管理 + +**3. 地图网格同步** +- 将业务网格数据同步到寻路网格 +- 支持A*寻路算法的地图表示 +- 处理障碍物和可通行区域的标记 + +**4. AQI数据初始化** +- 创建空气质量指数的历史数据 +- 生成实时AQI监测数据 +- 为数据可视化和分析提供基础数据 + +**5. 反馈和任务数据** +- 创建示例环境问题反馈 +- 初始化任务分配记录 +- 建立完整的业务流程测试数据 + +#### 技术实现要点 +```java +@Component +@Slf4j +public class DataInitializer implements CommandLineRunner { + + @Override + public void run(String... args) throws Exception { + // 检查数据是否已初始化 + if (isDataAlreadyInitialized()) { + log.info("数据已初始化,跳过初始化过程"); + return; + } + + // 执行各项初始化 + initializeUsers(); + initializeGrids(); + initializeAqiData(); + initializeFeedbacks(); + initializeTasks(); + + log.info("数据初始化完成"); + } +} +``` + +### 4.2 文件存储配置 (`FileStorageProperties.java`) + +#### 功能概述 +`FileStorageProperties` 管理文件上传存储的相关配置,使用Spring Boot的配置属性绑定机制。 + +#### 配置属性 +- `file.upload-dir`: 指定上传文件的存储目录 +- 支持相对路径和绝对路径 +- 可通过环境变量或配置文件动态配置 + +#### 使用方式 +**application.yml配置示例**: +```yaml +file: + upload-dir: ./uploads + # 或使用绝对路径 + # upload-dir: /var/app/uploads +``` + +**Java配置类**: +```java +@ConfigurationProperties(prefix = "file") +@Data +public class FileStorageProperties { + private String uploadDir; +} +``` + +### 4.3 OpenAPI配置 (`OpenApiConfig.java`) + +#### 功能概述 +配置Swagger API文档和JWT认证,为开发和测试提供完整的API文档界面。 + +#### 主要特性 +- **API文档生成**: 自动生成完整的REST API文档 +- **JWT认证集成**: 支持在Swagger UI中进行JWT认证测试 +- **安全配置**: 为所有API端点添加安全要求 +- **在线测试**: 提供交互式API测试界面 + +#### 访问方式 +- Swagger UI: `http://localhost:8080/swagger-ui.html` +- API文档JSON: `http://localhost:8080/v3/api-docs` + +#### 安全配置详解 +```java +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("EMS API") + .version("1.0") + .description("环境监测系统API文档")) + .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) + .components(new Components() + .addSecuritySchemes("Bearer Authentication", + new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} +``` + +### 4.4 RestTemplate配置 (`RestTemplateConfig.java`) + +#### 功能概述 +提供HTTP客户端功能,用于与外部服务进行同步通信。 + +#### 主要用途 +- **外部API集成**: 调用第三方服务API +- **微服务通信**: 在分布式架构中进行服务间调用 +- **数据同步**: 从外部数据源获取信息 +- **第三方认证**: 集成外部认证服务 + +#### 配置示例 +```java +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(); + + // 配置连接超时和读取超时 + HttpComponentsClientHttpRequestFactory factory = + new HttpComponentsClientHttpRequestFactory(); + factory.setConnectTimeout(5000); + factory.setReadTimeout(10000); + restTemplate.setRequestFactory(factory); + + return restTemplate; + } +} +``` + +#### 使用示例 +```java +@Service +public class ExternalApiService { + + @Autowired + private RestTemplate restTemplate; + + public String callExternalApi(String url) { + return restTemplate.getForObject(url, String.class); + } +} +``` + +### 4.5 WebClient配置 (`WebClientConfig.java`) + +#### 功能概述 +提供响应式HTTP客户端功能,支持非阻塞I/O操作,适用于高并发场景。 + +#### 主要优势 +- **非阻塞I/O**: 提高系统吞吐量和响应性能 +- **响应式编程**: 支持Reactor响应式编程模型 +- **流式处理**: 支持数据流的实时处理 +- **背压处理**: 自动处理生产者和消费者速度不匹配的情况 + +#### 配置示例 +```java +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder() + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) + .build(); + } + + @Bean + public WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } +} +``` + +#### 使用示例 +```java +@Service +public class ReactiveApiService { + + @Autowired + private WebClient webClient; + + public Mono callApiAsync(String url) { + return webClient.get() + .uri(url) + .retrieve() + .bodyToMono(String.class); + } +} +``` + +### 4.6 WebSocket配置 (`WebSocketConfig.java`) + +#### 功能概述 +WebSocket通信配置,为实时通信功能预留扩展空间。 + +#### 预期用途 +- **实时通知**: 任务状态变更的实时推送 +- **在线协作**: 多用户协同操作 +- **实时监控**: 系统状态和数据的实时更新 +- **即时消息**: 用户间的即时通信 + +#### 扩展示例 +```java +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(new NotificationWebSocketHandler(), "/ws/notifications") + .setAllowedOrigins("*"); + } +} +``` + +### 4.7 字符串列表转换器 (`StringListConverter.java`) + +#### 功能概述 +JPA实体属性转换器,用于在数据库存储和Java对象之间转换字符串列表。 + +#### 主要特性 +- **类型转换**: `List` ↔ JSON字符串 +- **JSON序列化**: 使用Jackson ObjectMapper +- **空值处理**: 支持null和空列表的正确处理 +- **异常处理**: 包含完整的错误处理和日志记录 + +#### 实现原理 +```java +@Converter +public class StringListConverter implements AttributeConverter, String> { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(List attribute) { + if (attribute == null || attribute.isEmpty()) { + return null; + } + try { + return objectMapper.writeValueAsString(attribute); + } catch (JsonProcessingException e) { + log.error("转换List到JSON失败", e); + return null; + } + } + + @Override + public List convertToEntityAttribute(String dbData) { + if (dbData == null || dbData.trim().isEmpty()) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(dbData, + new TypeReference>() {}); + } catch (JsonProcessingException e) { + log.error("转换JSON到List失败", e); + return new ArrayList<>(); + } + } +} +``` + +#### 使用示例 +```java +@Entity +public class UserAccount { + + @Convert(converter = StringListConverter.class) + @Column(name = "skills", columnDefinition = "TEXT") + private List skills; + + @Convert(converter = StringListConverter.class) + @Column(name = "tags", columnDefinition = "TEXT") + private List tags; +} +``` + +### 4.8 配置模块最佳实践 + +#### 1. 配置外部化 +- 使用`@ConfigurationProperties`进行类型安全的配置绑定 +- 支持多环境配置(dev、test、prod) +- 使用配置验证确保配置的正确性 + +#### 2. 安全考虑 +- 敏感配置信息使用环境变量或加密存储 +- API密钥和数据库密码不硬编码 +- 文件上传路径的安全验证 + +#### 3. 性能优化 +- HTTP客户端连接池配置 +- 合理设置超时时间 +- 缓存配置的优化 + +#### 4. 监控和日志 +- 配置变更的审计日志 +- 初始化过程的详细记录 +- 配置加载失败的错误处理 + +### 4.9 配置模块总结 + +配置模块为EMS系统提供了完整的基础设施支持: + +1. **数据初始化**: 确保系统启动时具备完整的测试数据和基础配置 +2. **文件管理**: 提供灵活的文件存储配置和管理机制 +3. **API文档**: 支持完整的接口文档生成和在线测试 +4. **HTTP客户端**: 提供同步和异步的外部服务调用能力 +5. **数据转换**: 支持复杂数据类型的数据库存储转换 + +## 五、控制器模块详细解析 + +控制器模块(`com.dne.ems.controller`)是EMS系统的API入口层,负责处理所有HTTP请求并提供RESTful API接口。该模块包含14个专业化控制器,每个控制器专注于特定的业务领域,形成了完整的API生态系统。 + +### 5.1 认证控制器 (`AuthController.java`) + +#### 功能概述 +认证控制器负责用户身份验证和账户管理的核心功能,是系统安全的第一道防线。 + +#### 主要接口 +- **用户注册** (`POST /api/auth/signup`): 新用户账户创建 +- **用户登录** (`POST /api/auth/login`): 用户身份验证,返回JWT令牌 +- **发送验证码** (`POST /api/auth/send-verification-code`): 注册验证码发送 +- **密码重置** (`POST /api/auth/send-password-reset-code`): 密码重置验证码发送 +- **验证码重置密码** (`POST /api/auth/reset-password-with-code`): 使用验证码重置密码 + +#### 核心特性 +- **JWT令牌管理**: 生成和验证JSON Web Token +- **邮箱验证**: 支持邮箱验证码验证机制 +- **密码安全**: 多种密码重置方式,确保账户安全 +- **参数验证**: 使用Bean Validation进行请求参数校验 + +#### 代码示例 +```java +@PostMapping("/login") +public ResponseEntity signIn(@Valid @RequestBody LoginRequest request) { + return ResponseEntity.ok(authService.signIn(request)); +} + +@PostMapping("/send-verification-code") +public ResponseEntity sendVerificationCode(@RequestParam @NotBlank @Email String email) { + verificationCodeService.sendVerificationCode(email); + return ResponseEntity.ok().build(); +} +``` + +### 5.2 仪表盘控制器 (`DashboardController.java`) + +#### 功能概述 +仪表盘控制器为决策者和管理员提供全面的数据可视化和统计分析功能,是数据驱动决策的核心支撑。 + +#### 主要接口 +- **核心统计数据** (`GET /api/dashboard/stats`): 系统关键指标统计 +- **AQI分布数据** (`GET /api/dashboard/reports/aqi-distribution`): 空气质量指数分布 +- **月度超标趋势** (`GET /api/dashboard/reports/monthly-exceedance-trend`): 污染超标趋势分析 +- **网格覆盖率** (`GET /api/dashboard/reports/grid-coverage`): 各城市网格覆盖情况 +- **反馈热力图** (`GET /api/dashboard/map/heatmap`): 反馈分布热力图数据 +- **污染物统计** (`GET /api/dashboard/reports/pollution-stats`): 各类污染物统计 +- **任务完成统计** (`GET /api/dashboard/reports/task-completion-stats`): 任务执行效率统计 +- **AQI热力图** (`GET /api/dashboard/map/aqi-heatmap`): AQI分布热力图 +- **污染物阈值管理** (`GET /api/dashboard/thresholds`): 污染物阈值配置 + +#### 权限控制 +```java +@PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") +public ResponseEntity getDashboardStats() { + // 仅决策者和管理员可访问 +} +``` + +#### 数据可视化支持 +- **多维度统计**: 支持时间、地域、类型等多维度数据分析 +- **实时数据**: 提供实时更新的环境监测数据 +- **热力图数据**: 支持地理信息系统(GIS)的数据可视化 +- **趋势分析**: 历史数据趋势和预测分析 + +### 5.3 反馈控制器 (`FeedbackController.java`) + +#### 功能概述 +反馈控制器处理公众环境问题反馈的全生命周期管理,是公众参与环境治理的重要渠道。 + +#### 主要接口 +- **提交反馈** (`POST /api/feedback/submit`): 支持文件上传的反馈提交 +- **JSON格式提交** (`POST /api/feedback/submit-json`): 测试用的JSON格式提交 +- **获取反馈列表** (`GET /api/feedback`): 支持多条件过滤的分页查询 +- **反馈详情** (`GET /api/feedback/{id}`): 获取特定反馈的详细信息 +- **反馈统计** (`GET /api/feedback/stats`): 反馈数据统计分析 + +#### 高级查询功能 +```java +@GetMapping +public ResponseEntity> getAllFeedback( + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestParam(required = false) FeedbackStatus status, + @RequestParam(required = false) PollutionType pollutionType, + @RequestParam(required = false) SeverityLevel severityLevel, + @RequestParam(required = false) String cityName, + @RequestParam(required = false) String districtName, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + @RequestParam(required = false) String keyword, + Pageable pageable) { + // 支持多维度过滤查询 +} +``` + +#### 文件上传支持 +- **多文件上传**: 支持同时上传多个附件文件 +- **文件类型验证**: 限制上传文件的类型和大小 +- **安全存储**: 文件安全存储和访问控制 + +### 5.4 文件控制器 (`FileController.java`) + +#### 功能概述 +文件控制器提供文件下载和在线预览功能,支持系统中所有文件资源的访问管理。 + +#### 主要接口 +- **文件下载** (`GET /api/files/{filename}`): 文件下载功能 +- **文件预览** (`GET /api/view/{fileName}`): 在线文件预览 + +#### 技术特性 +- **MIME类型检测**: 自动检测文件类型并设置正确的Content-Type +- **安全下载**: 防止路径遍历攻击 +- **流式传输**: 支持大文件的流式下载 + +#### 实现示例 +```java +@GetMapping("/files/{filename:.+}") +public ResponseEntity downloadFile(@PathVariable String filename, HttpServletRequest request) { + Resource resource = fileStorageService.loadFileAsResource(filename); + + String contentType = null; + try { + contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath()); + } catch (IOException ex) { + logger.info("Could not determine file type."); + } + + if (contentType == null) { + contentType = "application/octet-stream"; + } + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(contentType)) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"") + .body(resource); +} +``` + +### 5.5 网格控制器 (`GridController.java`) + +#### 功能概述 +网格控制器负责城市网格化管理,包括网格数据维护和网格员分配管理。 + +#### 主要接口 +- **网格列表** (`GET /api/grids`): 获取所有网格信息 +- **更新网格** (`PATCH /api/grids/{id}`): 更新网格信息 +- **网格覆盖率** (`GET /api/grids/coverage`): 网格覆盖率统计 +- **分配网格员** (`POST /api/grids/{gridId}/assign`): 将网格员分配到指定网格 +- **移除网格员** (`POST /api/grids/{gridId}/unassign`): 从网格中移除网格员 +- **坐标分配** (`POST /api/grids/coordinates/{gridX}/{gridY}/assign`): 通过坐标分配网格员 + +#### 网格员管理 +```java +@PostMapping("/{gridId}/assign") +@PreAuthorize("hasRole('ADMIN')") +public ResponseEntity assignGridWorker( + @PathVariable Long gridId, + @RequestBody Map request) { + Long userId = request.get("userId"); + + // 验证用户角色 + if (user.getRole() != Role.GRID_WORKER) { + return ResponseEntity.badRequest().body("只能分配网格员角色的用户"); + } + + // 分配网格坐标 + user.setGridX(grid.getGridX()); + user.setGridY(grid.getGridY()); + userAccountRepository.save(user); + + return ResponseEntity.ok().build(); +} +``` + +### 5.6 网格员任务控制器 (`GridWorkerTaskController.java`) + +#### 功能概述 +专为网格员设计的任务管理控制器,提供任务接收、处理和提交的完整工作流程。 + +#### 主要接口 +- **我的任务** (`GET /api/worker`): 获取分配给当前网格员的任务列表 +- **接受任务** (`POST /api/worker/{taskId}/accept`): 接受指定任务 +- **提交任务** (`POST /api/worker/{taskId}/submit`): 提交任务完成情况 +- **任务详情** (`GET /api/worker/{taskId}`): 查看任务详细信息 + +#### 权限控制 +```java +@RestController +@RequestMapping("/api/worker") +@RequiredArgsConstructor +@PreAuthorize("hasRole('GRID_WORKER')") +public class GridWorkerTaskController { + // 整个控制器仅限网格员角色访问 +} +``` + +#### 任务提交功能 +```java +@PostMapping(value = "/{taskId}/submit", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) +public ResponseEntity submitTask( + @PathVariable Long taskId, + @RequestPart("comments") @Valid String comments, + @RequestPart(value = "files", required = false) List files, + @AuthenticationPrincipal CustomUserDetails userDetails) { + // 支持文件上传的任务提交 +} +``` + +### 5.7 地图控制器 (`MapController.java`) + +#### 功能概述 +地图控制器管理系统的地图网格数据,为路径规划和地理信息展示提供基础数据支持。 + +#### 主要接口 +- **获取地图** (`GET /api/map/grid`): 获取完整的地图网格数据 +- **更新网格** (`POST /api/map/grid`): 创建或更新地图网格单元 +- **初始化地图** (`POST /api/map/initialize`): 初始化指定大小的地图 + +#### 地图初始化 +```java +@PostMapping("/initialize") +@Transactional +public ResponseEntity initializeMap( + @RequestParam(defaultValue = "20") int width, + @RequestParam(defaultValue = "20") int height) { + + mapGridRepository.deleteAll(); + mapGridRepository.flush(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + MapGrid cell = MapGrid.builder() + .x(x) + .y(y) + .isObstacle(false) + .terrainType("ground") + .build(); + mapGridRepository.save(cell); + } + } + return ResponseEntity.ok("Initialized a " + width + "x" + height + " map."); +} +``` + +### 5.8 路径规划控制器 (`PathfindingController.java`) + +#### 功能概述 +路径规划控制器提供A*寻路算法功能,为网格员提供最优路径导航服务。 + +#### 主要接口 +- **路径查找** (`POST /api/pathfinding/find`): 计算两点之间的最优路径 + +#### A*算法实现 +```java +@PostMapping("/find") +public ResponseEntity> findPath(@RequestBody PathfindingRequest request) { + if (request == null) { + return ResponseEntity.badRequest().build(); + } + Point start = new Point(request.getStartX(), request.getStartY()); + Point end = new Point(request.getEndX(), request.getEndY()); + + List path = aStarService.findPath(start, end); + return ResponseEntity.ok(path); +} +``` + +#### 算法特性 +- **障碍物避让**: 考虑地图中的障碍物 +- **最优路径**: 计算距离最短的路径 +- **实时计算**: 快速响应路径查询请求 + +### 5.9 人员管理控制器 (`PersonnelController.java`) + +#### 功能概述 +人员管理控制器提供用户账号的完整生命周期管理,包括创建、查询、更新和删除操作。 + +#### 主要接口 +- **创建用户** (`POST /api/personnel/users`): 创建新用户账号 +- **用户列表** (`GET /api/personnel/users`): 分页查询用户列表 +- **用户详情** (`GET /api/personnel/users/{userId}`): 获取用户详细信息 +- **更新用户** (`PATCH /api/personnel/users/{userId}`): 更新用户信息 +- **更新角色** (`PUT /api/personnel/users/{userId}/role`): 更新用户角色 +- **删除用户** (`DELETE /api/personnel/users/{userId}`): 删除用户账号 + +#### 权限控制 +```java +@RestController +@RequestMapping("/api/personnel") +@RequiredArgsConstructor +@PreAuthorize("hasRole('ADMIN')") +public class PersonnelController { + // 仅管理员可访问人员管理功能 +} +``` + +#### 高级查询 +```java +@GetMapping("/users") +@PreAuthorize("hasRole('ADMIN') or hasRole('GRID_WORKER')") +public ResponseEntity> getUsers( + @RequestParam(required = false) Role role, + @RequestParam(required = false) String name, + Pageable pageable) { + // 支持按角色和姓名过滤的分页查询 +} +``` + +### 5.10 个人资料控制器 (`ProfileController.java`) + +#### 功能概述 +个人资料控制器处理认证用户的个人数据管理,提供用户自助服务功能。 + +#### 主要接口 +- **反馈历史** (`GET /api/me/feedback`): 获取当前用户的反馈提交历史 + +#### 用户数据隔离 +```java +@GetMapping("/feedback") +@PreAuthorize("isAuthenticated()") +public ResponseEntity> getMyFeedbackHistory( + @AuthenticationPrincipal CustomUserDetails userDetails, + Pageable pageable) { + // 仅返回当前用户的数据 + Page historyPage = userFeedbackService + .getFeedbackHistoryByUserId(userDetails.getId(), pageable); + return ResponseEntity.ok(historyPage.getContent()); +} +``` + +### 5.11 公共接口控制器 (`PublicController.java`) + +#### 功能概述 +公共接口控制器提供无需认证的公共API,主要服务于匿名用户的反馈提交。 + +#### 主要接口 +- **公共反馈提交** (`POST /api/public/feedback`): 支持匿名用户提交环境问题反馈 + +#### 匿名访问支持 +```java +@PostMapping(value = "/feedback", consumes = "multipart/form-data") +public ResponseEntity submitPublicFeedback( + @Valid @RequestPart("feedback") PublicFeedbackRequest request, + @RequestPart(value = "files", required = false) MultipartFile[] files) { + // 无需认证即可提交反馈 + Feedback createdFeedback = feedbackService.createPublicFeedback(request, files); + return new ResponseEntity<>(createdFeedback, HttpStatus.CREATED); +} +``` + +### 5.12 主管控制器 (`SupervisorController.java`) + +#### 功能概述 +主管控制器专为主管角色设计,提供反馈审核和管理功能。 + +#### 主要接口 +- **待审核反馈** (`GET /api/supervisor/reviews`): 获取待审核的反馈列表 +- **审核通过** (`POST /api/supervisor/reviews/{feedbackId}/approve`): 审核通过反馈 +- **审核拒绝** (`POST /api/supervisor/reviews/{feedbackId}/reject`): 审核拒绝反馈 + +#### 审核流程 +```java +@PostMapping("/reviews/{feedbackId}/approve") +@PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") +public ResponseEntity approveFeedback(@PathVariable Long feedbackId) { + supervisorService.approveFeedback(feedbackId); + return ResponseEntity.ok().build(); +} + +@PostMapping("/reviews/{feedbackId}/reject") +@PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") +public ResponseEntity rejectFeedback( + @PathVariable Long feedbackId, + @RequestBody RejectFeedbackRequest request) { + supervisorService.rejectFeedback(feedbackId, request); + return ResponseEntity.ok().build(); +} +``` + +### 5.13 任务分配控制器 (`TaskAssignmentController.java`) + +#### 功能概述 +任务分配控制器处理任务的智能分配和人员调度功能。 + +#### 主要接口 +- **未分配任务** (`GET /api/tasks/unassigned`): 获取未分配的任务列表 +- **可用网格员** (`GET /api/tasks/grid-workers`): 获取可用的网格工作人员 +- **分配任务** (`POST /api/tasks/assign`): 将任务分配给指定工作人员 + +#### 智能分配 +```java +@PostMapping("/assign") +@PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") +public ResponseEntity assignTask( + @RequestBody AssignmentRequest request, + @AuthenticationPrincipal CustomUserDetails adminDetails) { + + Long assignerId = adminDetails.getId(); + Assignment newAssignment = taskAssignmentService.assignTask( + request.feedbackId(), + request.assigneeId(), + assignerId + ); + return ResponseEntity.ok(newAssignment); +} +``` + +### 5.14 任务管理控制器 (`TaskManagementController.java`) + +#### 功能概述 +任务管理控制器提供任务全生命周期的管理功能,是任务管理的核心控制器。 + +#### 主要接口 +- **任务列表** (`GET /api/management/tasks`): 支持多条件过滤的任务分页查询 +- **分配任务** (`POST /api/management/tasks/{taskId}/assign`): 分配任务给工作人员 +- **任务详情** (`GET /api/management/tasks/{taskId}`): 获取任务详细信息 +- **审核任务** (`POST /api/management/tasks/{taskId}/review`): 审核任务完成情况 +- **取消任务** (`POST /api/management/tasks/{taskId}/cancel`): 取消任务 +- **创建任务** (`POST /api/management/tasks`): 创建新任务 +- **待处理反馈** (`GET /api/management/tasks/feedback`): 获取待处理的反馈 +- **从反馈创建任务** (`POST /api/management/tasks/feedback/{feedbackId}/create-task`): 从反馈创建任务 + +#### 高级过滤查询 +```java +@GetMapping +@PreAuthorize("hasAnyAuthority('ADMIN', 'SUPERVISOR', 'DECISION_MAKER')") +public ResponseEntity> getTasks( + @RequestParam(required = false) TaskStatus status, + @RequestParam(required = false) Long assigneeId, + @RequestParam(required = false) SeverityLevel severity, + @RequestParam(required = false) PollutionType pollutionType, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + Pageable pageable) { + // 支持多维度过滤的任务查询 +} +``` + +#### 任务状态流转 +```java +@PostMapping("/{taskId}/review") +public ResponseEntity reviewTask( + @PathVariable Long taskId, + @RequestBody @Valid TaskApprovalRequest request) { + TaskDetailDTO updatedTask = taskManagementService.reviewTask(taskId, request); + return ResponseEntity.ok(updatedTask); +} + +@PostMapping("/{taskId}/cancel") +public ResponseEntity cancelTask(@PathVariable Long taskId) { + TaskDetailDTO updatedTask = taskManagementService.cancelTask(taskId); + return ResponseEntity.ok(updatedTask); +} +``` + +### 5.15 控制器模块最佳实践 + +#### 1. RESTful API设计 +- **统一的URL命名规范**: 使用名词复数形式,避免动词 +- **HTTP方法语义化**: GET查询、POST创建、PUT/PATCH更新、DELETE删除 +- **状态码规范**: 正确使用HTTP状态码表达操作结果 +- **版本控制**: 通过URL路径进行API版本管理 + +#### 2. 安全控制 +- **细粒度权限控制**: 使用`@PreAuthorize`进行方法级权限控制 +- **输入验证**: 使用Bean Validation进行参数校验 +- **敏感数据保护**: 避免在响应中暴露敏感信息(如密码) +- **CSRF防护**: 对状态变更操作进行CSRF保护 + +#### 3. 异常处理 +- **全局异常处理**: 使用`@ControllerAdvice`统一处理异常 +- **友好错误信息**: 为客户端提供清晰的错误描述 +- **日志记录**: 记录详细的错误日志便于问题排查 + +#### 4. 性能优化 +- **分页查询**: 对大数据量查询使用分页机制 +- **缓存策略**: 对频繁查询的数据使用缓存 +- **异步处理**: 对耗时操作使用异步处理 +- **数据传输优化**: 使用DTO减少不必要的数据传输 + +#### 5. 文档和测试 +- **API文档**: 使用Swagger/OpenAPI生成完整的API文档 +- **单元测试**: 为每个控制器方法编写单元测试 +- **集成测试**: 测试完整的请求-响应流程 +- **性能测试**: 对关键接口进行性能测试 + +### 5.16 控制器模块总结 + +控制器模块为EMS系统提供了完整的API服务层: + +1. **全面的功能覆盖**: 14个专业化控制器覆盖了系统的所有核心功能 +2. **严格的权限控制**: 基于角色的访问控制确保系统安全 +3. **灵活的查询支持**: 支持多维度过滤、分页、排序等高级查询功能 +4. **完善的文件处理**: 支持文件上传、下载、预览等文件操作 +5. **智能的任务管理**: 提供完整的任务生命周期管理和智能分配功能 +6. **实时的数据可视化**: 为决策支持提供丰富的数据分析接口 +7. **用户友好的接口设计**: 遵循RESTful设计原则,提供直观易用的API + +通过这些控制器的协同工作,EMS系统能够为不同角色的用户提供专业化、个性化的服务,实现了从公众反馈到问题解决的完整业务闭环。 +6. **扩展性**: 为WebSocket等实时通信功能预留扩展空间 + +这些配置组件共同构成了一个健壮、可扩展、易维护的系统基础架构,为EMS系统的稳定运行和功能扩展提供了坚实的技术保障。 + +--- + +# DTO模块详细解析 + +## 概述 + +DTO(Data Transfer Object)模块位于 `com.dne.ems.dto` 包下,包含51个数据传输对象,负责在不同层之间传递数据,确保数据的封装性和安全性。DTO模块按功能分类,涵盖用户管理、任务管理、反馈处理、地图网格、统计分析和AI集成等核心业务领域。 + +## 核心功能分类 + +### 1. 用户管理相关DTO + +#### 用户注册和认证 +- **SignUpRequest**: 用户注册请求,包含姓名、邮箱、手机、密码、角色和验证码 +- **LoginRequest**: 用户登录请求,封装邮箱和密码 +- **UserRegistrationRequest**: 公众用户注册请求,支持密码复杂度验证 +- **JwtAuthenticationResponse**: JWT认证响应,返回访问令牌 + +#### 用户信息管理 +- **UserCreationRequest**: 管理员创建用户请求,支持角色分配和区域设置 +- **UserUpdateRequest**: 用户信息更新请求,支持多字段批量更新 +- **UserRoleUpdateRequest**: 用户角色更新请求,网格员需要位置信息 +- **UserInfoDTO**: 用户基本信息传输对象,隐藏敏感数据 +- **UserSummaryDTO**: 用户摘要信息,用于列表展示 + +#### 密码管理 +- **PasswordResetRequest**: 密码重置邮件请求 +- **PasswordResetDto**: 通过令牌重置密码 +- **PasswordResetWithCodeDto**: 通过验证码重置密码 + +### 2. 任务管理相关DTO + +#### 任务创建和分配 +- **TaskCreationRequest**: 任务创建请求,包含标题、描述、污染类型、严重级别和位置 +- **TaskAssignmentRequest**: 任务分配请求,指定受理人ID +- **TaskFromFeedbackRequest**: 从反馈创建任务请求,设置严重级别 + +#### 任务处理和审核 +- **TaskSubmissionRequest**: 任务提交请求,包含处理意见 +- **TaskApprovalRequest**: 任务审核请求,支持通过或拒绝 +- **TaskRejectionRequest**: 任务拒绝请求,必须提供拒绝原因 + +#### 任务信息展示 +- **TaskDTO**: 任务基本信息,包含ID、标题、描述和状态 +- **TaskSummaryDTO**: 任务摘要信息,用于列表展示 +- **TaskDetailDTO**: 任务详细信息,包含完整的任务数据和历史记录 +- **TaskInfoDTO**: 任务简要信息,包含状态和受理人 +- **TaskHistoryDTO**: 任务历史记录,追踪状态变更 +- **TaskStatsDTO**: 任务统计数据,计算完成率 + +### 3. 反馈处理相关DTO + +#### 反馈提交 +- **PublicFeedbackRequest**: 公众反馈提交请求,支持匿名提交 +- **FeedbackSubmissionRequest**: 反馈提交请求,包含位置和附件信息 +- **AqiDataSubmissionRequest**: AQI数据提交请求,专门处理空气质量数据 + +#### 反馈处理 +- **ProcessFeedbackRequest**: 反馈处理请求,支持批准或采取行动 +- **RejectFeedbackRequest**: 反馈拒绝请求,需要提供拒绝原因 + +#### 反馈信息展示 +- **FeedbackDTO**: 反馈详细信息传输对象 +- **FeedbackResponseDTO**: 反馈响应数据封装 +- **UserFeedbackSummaryDTO**: 用户反馈历史摘要 +- **FeedbackStatsResponse**: 反馈统计数据响应 + +### 4. 地图网格相关DTO + +#### 网格管理 +- **GridUpdateRequest**: 网格属性更新请求 +- **GridAssignmentRequest**: 网格员分配请求 +- **GridCoverageDTO**: 网格覆盖统计数据 +- **CityBlueprint**: 城市网格蓝图生成 + +#### 位置和路径 +- **LocationUpdateRequest**: 用户位置更新请求 +- **PathfindingRequest**: A*路径查找请求,包含起点和终点 +- **Point**: 二维坐标点表示 + +### 5. 统计分析相关DTO + +#### 仪表盘统计 +- **DashboardStatsDTO**: 仪表盘关键统计数据 +- **PollutionStatsDTO**: 污染物统计数据 +- **TrendDataPointDTO**: 趋势数据点,用于时间序列分析 + +#### 热力图数据 +- **HeatmapPointDTO**: 通用热力图数据点 +- **AqiHeatmapPointDTO**: AQI热力图专用数据点 +- **AqiDistributionDTO**: AQI分布统计数据 +- **PollutantThresholdDTO**: 污染物阈值信息 + +### 6. 通用工具DTO + +#### 分页和响应 +- **PageDTO**: 分页查询结果封装,支持泛型 +- **ErrorResponseDTO**: API错误响应标准格式 +- **AttachmentDTO**: 附件信息封装 +- **AssigneeInfoDTO**: 任务受理人信息 + +### 7. AI集成相关DTO + +#### 火山引擎AI聊天 +- **VolcanoChatRequest**: 火山引擎聊天API请求,支持多轮对话和工具调用 +- **VolcanoChatResponse**: 火山引擎聊天API响应,包含AI回复和工具调用结果 + +## 核心代码示例 + +### 任务创建请求DTO +```java +public record TaskCreationRequest( + @NotBlank(message = "Title is mandatory") + String title, + String description, + @NotNull(message = "Pollution type is mandatory") + PollutionType pollutionType, + @NotNull(message = "Severity level is mandatory") + SeverityLevel severityLevel, + @NotNull(message = "Location information is mandatory") + LocationDTO location +) { + public record LocationDTO( + @NotBlank(message = "Address is mandatory") + String address, + @NotNull(message = "Grid X coordinate is mandatory") + Integer gridX, + @NotNull(message = "Grid Y coordinate is mandatory") + Integer gridY + ) {} +} +``` + +### 分页查询结果DTO +```java +public record PageDTO( + List content, + int page, + int size, + long totalElements, + int totalPages, + boolean first, + boolean last +) { + public static PageDTO fromPage(Page page) { + return new PageDTO<>( + page.getContent(), + page.getNumber(), + page.getSize(), + page.getTotalElements(), + page.getTotalPages(), + page.isFirst(), + page.isLast() + ); + } +} +``` + +### 用户信息DTO工厂方法 +```java +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserInfoDTO { + private Long id; + private String name; + private String phone; + private String email; + + public static UserInfoDTO fromEntity(UserAccount user) { + if (user == null) { + return null; + } + return new UserInfoDTO( + user.getId(), + user.getName(), + user.getPhone(), + user.getEmail() + ); + } +} +``` + +### AI聊天请求DTO +```java +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class VolcanoChatRequest { + private String model; + private List messages; + private List tools; + + @Data + @SuperBuilder + public static class Message { + private String role; + private List content; + } + + @Data + @SuperBuilder + public static class ContentPart { + private String type; + private String text; + @JsonProperty("image_url") + private ImageUrl imageUrl; + } +} +``` + +## 设计特点 + +### 1. 数据验证 +- 使用Jakarta Validation注解进行参数校验 +- 自定义验证注解(如@ValidPassword、@GridWorkerLocationRequired) +- 分层验证策略,确保数据完整性 + +### 2. 类型安全 +- 大量使用Record类型,确保不可变性 +- 泛型支持,如PageDTO提供类型安全的分页 +- 枚举类型约束,避免无效值 + +### 3. 序列化优化 +- Jackson注解配置,控制JSON序列化行为 +- @JsonInclude(JsonInclude.Include.NON_NULL)避免空值传输 +- @JsonProperty处理字段名映射 + +### 4. 业务语义 +- 清晰的命名约定,Request/Response/DTO后缀明确用途 +- 丰富的JavaDoc文档,说明业务规则和使用场景 +- 嵌套记录结构,体现数据层次关系 + +## DTO模块最佳实践 + +1. **不可变设计**:优先使用Record类型,确保数据传输对象的不可变性 +2. **验证完整性**:在DTO层进行数据验证,避免无效数据进入业务层 +3. **类型安全**:使用强类型和泛型,减少运行时错误 +4. **文档完善**:提供详细的JavaDoc,说明字段含义和业务规则 +5. **序列化优化**:合理使用Jackson注解,优化网络传输效率 +6. **分层隔离**:DTO与实体类分离,避免业务逻辑泄露到表示层 + +## DTO模块总结 + +DTO模块为EMS系统提供了完整的数据传输层: + +1. **全面的功能覆盖**: 51个DTO类覆盖了系统的所有核心功能领域 +2. **严格的数据验证**: 多层次验证机制确保数据质量和安全性 +3. **灵活的数据结构**: 支持复杂嵌套结构和泛型,满足不同业务需求 +4. **优化的传输效率**: 通过合理的序列化配置减少网络传输开销 +5. **清晰的业务语义**: 命名规范和文档完善,便于开发和维护 +6. **类型安全保障**: Record类型和强类型约束减少运行时错误 +7. **扩展性支持**: 为AI集成等新功能提供了良好的数据结构基础 + +通过这些DTO的协同工作,EMS系统能够在各层之间安全、高效地传递数据,为整个系统的稳定运行和功能扩展提供了坚实的数据传输基础。 + +--- + +# Event模块详细解析 + +## 概述 + +Event(事件)模块位于 `com.dne.ems.event` 包下,包含2个应用事件类,实现了系统的事件驱动架构。通过Spring的事件发布机制,实现了业务模块之间的松耦合通信,支持异步处理和系统扩展。 + +## 核心事件类 + +### 1. FeedbackSubmittedForAiReviewEvent + +#### 功能概述 +- **用途**: 反馈提交AI审核事件 +- **触发时机**: 当公众提交反馈后,系统需要进行AI预审核时发布 +- **业务价值**: 实现反馈提交与AI审核的解耦,支持异步AI分析 + +#### 核心特性 +- **继承关系**: 继承自Spring的`ApplicationEvent`基类 +- **数据载体**: 携带完整的`Feedback`实体对象 +- **事件源**: 支持指定事件发布源,便于追踪和调试 + +#### 代码实现 +```java +package com.dne.ems.event; + +import org.springframework.context.ApplicationEvent; +import com.dne.ems.model.Feedback; + +public class FeedbackSubmittedForAiReviewEvent extends ApplicationEvent { + private final Feedback feedback; + + public FeedbackSubmittedForAiReviewEvent(Object source, Feedback feedback) { + super(source); + this.feedback = feedback; + } + + public Feedback getFeedback() { + return feedback; + } +} +``` + +#### 使用场景 +1. **反馈提交后**: 在`FeedbackService`中提交反馈成功后发布此事件 +2. **AI审核触发**: 事件监听器接收到事件后,调用AI服务进行内容分析 +3. **异步处理**: 避免反馈提交流程被AI分析耗时阻塞 + +### 2. TaskReadyForAssignmentEvent + +#### 功能概述 +- **用途**: 任务准备分配事件 +- **触发时机**: 当任务创建完成或状态变更为可分配时发布 +- **业务价值**: 实现任务创建与分配逻辑的解耦,支持智能分配算法 + +#### 核心特性 +- **继承关系**: 继承自Spring的`ApplicationEvent`基类 +- **数据载体**: 携带完整的`Task`实体对象 +- **Lombok支持**: 使用`@Getter`注解简化代码 +- **不可变性**: 任务对象使用`final`修饰,确保事件数据不被修改 + +#### 代码实现 +```java +package com.dne.ems.event; + +import org.springframework.context.ApplicationEvent; +import com.dne.ems.model.Task; +import lombok.Getter; + +@Getter +public class TaskReadyForAssignmentEvent extends ApplicationEvent { + private final Task task; + + public TaskReadyForAssignmentEvent(Object source, Task task) { + super(source); + this.task = task; + } +} +``` + +#### 使用场景 +1. **任务创建后**: 在`TaskService`中创建新任务后发布此事件 +2. **状态变更**: 任务状态从其他状态变更为待分配时发布 +3. **智能分配**: 事件监听器可以触发智能分配算法,推荐最佳执行人 +4. **通知机制**: 通知相关人员有新任务需要分配 + +## 事件驱动架构优势 + +### 1. 松耦合设计 +- **模块独立**: 事件发布者和监听者之间没有直接依赖关系 +- **易于扩展**: 可以随时添加新的事件监听器而不影响现有代码 +- **职责分离**: 每个模块专注于自己的核心业务逻辑 + +### 2. 异步处理能力 +- **性能提升**: 耗时操作(如AI分析)不会阻塞主业务流程 +- **用户体验**: 用户操作可以立即得到响应 +- **系统吞吐**: 提高系统整体处理能力 + +### 3. 可观测性 +- **事件追踪**: 可以记录事件的发布和处理过程 +- **调试支持**: 便于排查业务流程中的问题 +- **监控集成**: 可以与监控系统集成,实时观察系统状态 + +## 事件处理流程 + +### 反馈AI审核流程 +``` +1. 用户提交反馈 → FeedbackService.createFeedback() +2. 保存反馈到数据库 +3. 发布 FeedbackSubmittedForAiReviewEvent +4. AI审核监听器接收事件 +5. 调用AI服务分析反馈内容 +6. 更新反馈状态和AI分析结果 +``` + +### 任务分配流程 +``` +1. 创建新任务 → TaskService.createTask() +2. 保存任务到数据库 +3. 发布 TaskReadyForAssignmentEvent +4. 任务分配监听器接收事件 +5. 执行智能分配算法 +6. 推荐最佳执行人或自动分配 +``` + +## 技术实现细节 + +### 1. Spring事件机制 +- **ApplicationEvent**: 所有自定义事件的基类 +- **ApplicationEventPublisher**: 用于发布事件的接口 +- **@EventListener**: 标记事件监听方法的注解 +- **@Async**: 支持异步事件处理 + +### 2. 事件发布示例 +```java +@Service +public class FeedbackService { + @Autowired + private ApplicationEventPublisher eventPublisher; + + public Feedback createFeedback(FeedbackRequest request) { + // 保存反馈 + Feedback feedback = feedbackRepository.save(newFeedback); + + // 发布事件 + eventPublisher.publishEvent( + new FeedbackSubmittedForAiReviewEvent(this, feedback) + ); + + return feedback; + } +} +``` + +### 3. 事件监听示例 +```java +@Component +public class FeedbackEventListener { + + @EventListener + @Async + public void handleFeedbackSubmitted(FeedbackSubmittedForAiReviewEvent event) { + Feedback feedback = event.getFeedback(); + // 执行AI审核逻辑 + aiReviewService.reviewFeedback(feedback); + } +} +``` + +## Event模块最佳实践 + +1. **事件命名**: 使用清晰的命名约定,体现事件的业务含义 +2. **数据封装**: 事件中携带的数据应该是完整且不可变的 +3. **异步处理**: 对于耗时操作,使用`@Async`注解实现异步处理 +4. **异常处理**: 事件监听器中要有完善的异常处理机制 +5. **幂等性**: 确保事件处理的幂等性,避免重复处理问题 +6. **监控日志**: 记录事件的发布和处理日志,便于问题排查 + +## Event模块总结 + +Event模块为EMS系统提供了强大的事件驱动能力: + +1. **解耦架构**: 通过事件机制实现模块间的松耦合通信 +2. **异步处理**: 支持耗时操作的异步执行,提升系统性能 +3. **扩展性强**: 易于添加新的业务逻辑而不影响现有代码 +4. **可观测性**: 提供清晰的业务流程追踪和监控能力 +5. **Spring集成**: 充分利用Spring框架的事件机制和异步支持 + +虽然目前只有2个事件类,但它们为系统的核心业务流程(反馈处理和任务分配)提供了关键的解耦和异步处理能力,为系统的可扩展性和性能优化奠定了坚实基础。 + +--- + +# Exception模块详细解析 + +## 概述 + +Exception(异常处理)模块位于 `com.dne.ems.exception` 包下,包含7个异常处理相关的类,构建了完整的异常处理体系。通过自定义异常类和全局异常处理器,实现了统一的错误响应格式和完善的异常处理机制,提升了系统的健壮性和用户体验。 + +## 核心异常类 + +### 1. ErrorResponseDTO + +#### 功能概述 +- **用途**: 标准化API错误响应数据传输对象 +- **作用**: 为所有API端点提供一致的错误结构 +- **业务价值**: 统一错误响应格式,提升前端处理效率 + +#### 核心特性 +- **Lombok支持**: 使用`@Data`、`@AllArgsConstructor`、`@NoArgsConstructor`注解 +- **时间戳**: 记录错误发生的精确时间 +- **状态码**: HTTP状态码,便于前端判断错误类型 +- **错误分类**: 提供错误类型和详细消息 +- **路径追踪**: 记录发生错误的请求路径 + +#### 代码实现 +```java +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ErrorResponseDTO { + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; +} +``` + +### 2. ResourceNotFoundException + +#### 功能概述 +- **用途**: 资源不存在异常 +- **HTTP状态**: 404 Not Found +- **触发场景**: 查询的资源不存在时抛出 +- **业务价值**: 明确区分资源不存在的情况 + +#### 核心特性 +- **Spring集成**: 使用`@ResponseStatus(HttpStatus.NOT_FOUND)`注解 +- **自动映射**: 异常抛出时自动返回404状态码 +- **继承关系**: 继承自`RuntimeException`,支持非检查异常 + +#### 使用场景 +1. **用户查询**: 查询不存在的用户ID +2. **任务查询**: 查询不存在的任务ID +3. **反馈查询**: 查询不存在的反馈记录 +4. **文件查询**: 查询不存在的附件文件 + +### 3. InvalidOperationException + +#### 功能概述 +- **用途**: 无效操作异常 +- **HTTP状态**: 400 Bad Request +- **触发场景**: 业务规则不允许的操作时抛出 +- **业务价值**: 保护业务逻辑完整性 + +#### 核心特性 +- **业务保护**: 防止违反业务规则的操作 +- **状态映射**: 自动返回400状态码 +- **简洁设计**: 只提供消息构造器,专注业务逻辑 + +#### 使用场景 +1. **状态变更**: 任务状态不允许的变更操作 +2. **权限检查**: 用户权限不足的操作 +3. **数据验证**: 业务数据不符合规则 +4. **流程控制**: 违反业务流程的操作 + +### 4. UserAlreadyExistsException + +#### 功能概述 +- **用途**: 用户已存在异常 +- **HTTP状态**: 409 Conflict +- **触发场景**: 注册时用户名或邮箱已存在 +- **业务价值**: 防止重复用户注册 + +#### 核心特性 +- **冲突处理**: 使用409状态码表示资源冲突 +- **注册保护**: 确保用户唯一性 +- **清晰语义**: 明确表达用户重复的问题 + +#### 使用场景 +1. **用户注册**: 用户名已被占用 +2. **邮箱注册**: 邮箱地址已被使用 +3. **账号创建**: 管理员创建重复账号 + +### 5. FileNotFoundException + +#### 功能概述 +- **用途**: 文件不存在异常 +- **HTTP状态**: 404 Not Found +- **触发场景**: 请求的文件不存在时抛出 +- **业务价值**: 专门处理文件相关的404错误 + +#### 核心特性 +- **文件专用**: 专门处理文件操作异常 +- **双构造器**: 支持消息和原因链的构造 +- **异常链**: 支持异常原因追踪 + +#### 使用场景 +1. **附件下载**: 下载不存在的附件文件 +2. **图片访问**: 访问不存在的图片资源 +3. **文档查看**: 查看已删除的文档 + +### 6. FileStorageException + +#### 功能概述 +- **用途**: 文件存储异常 +- **HTTP状态**: 默认500(通过全局处理器处理) +- **触发场景**: 文件存储操作失败时抛出 +- **业务价值**: 统一处理文件存储相关错误 + +#### 核心特性 +- **存储专用**: 专门处理文件存储异常 +- **异常链支持**: 保留原始异常信息 +- **灵活处理**: 可通过全局处理器自定义响应 + +#### 使用场景 +1. **文件上传**: 上传过程中的存储失败 +2. **文件删除**: 删除文件时的IO错误 +3. **文件移动**: 文件移动或重命名失败 +4. **磁盘空间**: 存储空间不足的情况 + +### 7. GlobalExceptionHandler + +#### 功能概述 +- **用途**: 全局异常处理器 +- **作用**: 统一处理控制器层抛出的异常 +- **业务价值**: 提供一致的错误响应和完善的日志记录 + +#### 核心特性 +- **全局捕获**: 使用`@ControllerAdvice`注解实现全局异常捕获 +- **分类处理**: 针对不同异常类型提供专门的处理方法 +- **日志记录**: 使用`@Slf4j`记录详细的错误日志 +- **标准响应**: 返回统一格式的错误响应 + +#### 处理的异常类型 + +##### ResourceNotFoundException处理 +```java +@ExceptionHandler(ResourceNotFoundException.class) +public ResponseEntity handleResourceNotFoundException( + ResourceNotFoundException ex, WebRequest request) { + ErrorResponseDTO errorResponse = new ErrorResponseDTO( + LocalDateTime.now(), + HttpStatus.NOT_FOUND.value(), + "Not Found", + ex.getMessage(), + request.getDescription(false).replace("uri=", "")); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); +} +``` + +##### InvalidOperationException处理 +```java +@ExceptionHandler(InvalidOperationException.class) +public ResponseEntity handleInvalidOperationException( + InvalidOperationException ex, WebRequest request) { + ErrorResponseDTO errorResponse = new ErrorResponseDTO( + LocalDateTime.now(), + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + ex.getMessage(), + request.getDescription(false).replace("uri=", "")); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); +} +``` + +##### 全局异常处理 +```java +@ExceptionHandler(Exception.class) +public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + log.error("捕获到全局未处理异常! 请求: {}", request.getDescription(false), ex); + + Map errorDetails = Map.of( + "message", "服务器内部发生未知错误,请联系技术支持", + "error", ex.getClass().getName(), + "details", ex.getMessage() + ); + + return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR); +} +``` + +## 异常处理架构优势 + +### 1. 统一错误格式 +- **一致性**: 所有API错误都使用相同的响应格式 +- **可预测**: 前端可以统一处理错误响应 +- **信息完整**: 包含时间戳、状态码、错误类型、消息和路径 + +### 2. 分层异常处理 +- **业务异常**: 自定义异常类处理特定业务场景 +- **系统异常**: 全局处理器兜底处理未预期异常 +- **HTTP映射**: 自动将异常映射为合适的HTTP状态码 + +### 3. 完善的日志记录 +- **错误追踪**: 详细记录异常发生的上下文 +- **问题定位**: 便于开发人员快速定位问题 +- **监控集成**: 支持与监控系统集成 + +### 4. 用户友好 +- **清晰消息**: 提供用户可理解的错误消息 +- **状态码**: 正确的HTTP状态码便于前端处理 +- **安全性**: 不暴露系统内部敏感信息 + +## 异常处理流程 + +### 标准异常处理流程 +``` +1. 业务层抛出自定义异常 +2. 控制器层捕获异常 +3. GlobalExceptionHandler拦截异常 +4. 根据异常类型选择处理方法 +5. 构建ErrorResponseDTO响应 +6. 记录错误日志 +7. 返回标准化错误响应 +``` + +### 异常分类处理 +``` +业务异常: +├── ResourceNotFoundException (404) - 资源不存在 +├── InvalidOperationException (400) - 无效操作 +├── UserAlreadyExistsException (409) - 用户已存在 +├── FileNotFoundException (404) - 文件不存在 +└── FileStorageException (500) - 文件存储错误 + +系统异常: +└── Exception (500) - 未处理的系统异常 +``` + +## Exception模块最佳实践 + +1. **异常分类**: 根据业务场景创建专门的异常类 +2. **状态码映射**: 使用`@ResponseStatus`注解自动映射HTTP状态码 +3. **消息设计**: 提供清晰、用户友好的错误消息 +4. **日志记录**: 在全局处理器中记录详细的错误日志 +5. **安全考虑**: 避免在错误响应中暴露敏感信息 +6. **异常链**: 保留原始异常信息便于问题追踪 +7. **统一格式**: 使用ErrorResponseDTO确保响应格式一致 + +## Exception模块总结 + +Exception模块为EMS系统提供了完善的异常处理能力: + +1. **完整体系**: 涵盖业务异常、系统异常和文件异常的完整处理体系 +2. **统一响应**: 通过ErrorResponseDTO提供一致的错误响应格式 +3. **自动映射**: 利用Spring的@ResponseStatus注解自动映射HTTP状态码 +4. **全局处理**: GlobalExceptionHandler提供兜底的异常处理机制 +5. **日志完善**: 详细的错误日志记录便于问题追踪和系统监控 +6. **用户友好**: 清晰的错误消息和正确的状态码提升用户体验 +7. **安全可靠**: 在提供有用信息的同时保护系统安全 + +通过这套异常处理机制,EMS系统能够优雅地处理各种异常情况,为用户提供清晰的错误反馈,为开发人员提供完善的错误追踪能力,大大提升了系统的健壮性和可维护性。 + +--- + +# Listener模块详细解析 + +## 概述 + +Listener(监听器)模块位于 `com.dne.ems.listener` 包下,包含3个事件监听器类,实现了系统的事件驱动响应机制。通过监听Spring Security认证事件和自定义业务事件,实现了安全控制、用户状态管理和业务流程自动化,为系统的安全性和业务连续性提供了重要保障。 + +## 核心监听器类 + +### 1. AuthenticationFailureEventListener + +#### 功能概述 +- **用途**: 认证失败事件监听器 +- **监听事件**: `AuthenticationFailureBadCredentialsEvent` +- **触发场景**: 用户登录认证失败时 +- **业务价值**: 实现登录失败次数统计和IP锁定机制 + +#### 核心特性 +- **Spring Security集成**: 监听Spring Security的认证失败事件 +- **IP地址获取**: 智能获取真实客户端IP地址 +- **代理支持**: 支持X-Forwarded-For头部处理 +- **安全防护**: 防止暴力破解攻击 + +#### 代码实现 +```java +@Component +public class AuthenticationFailureEventListener + implements ApplicationListener { + + @Autowired + private LoginAttemptService loginAttemptService; + + @Autowired + private HttpServletRequest request; + + @Override + public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) { + final String xfHeader = request.getHeader("X-Forwarded-For"); + String ip; + if (xfHeader == null || xfHeader.isEmpty() || !xfHeader.contains(request.getRemoteAddr())) { + ip = request.getRemoteAddr(); + } else { + ip = xfHeader.split(",")[0]; + } + + // 使用IP地址作为锁定键 + loginAttemptService.loginFailed(ip); + } +} +``` + +#### IP地址获取逻辑 +1. **代理检测**: 检查X-Forwarded-For头部是否存在 +2. **真实IP**: 优先使用代理转发的真实客户端IP +3. **直连IP**: 无代理时使用直接连接的IP地址 +4. **安全验证**: 验证代理IP的合法性 + +#### 使用场景 +1. **登录保护**: 记录失败的登录尝试 +2. **暴力破解防护**: 限制同一IP的登录尝试次数 +3. **安全审计**: 记录可疑的登录行为 +4. **IP黑名单**: 自动封禁恶意IP地址 + +### 2. AuthenticationSuccessEventListener + +#### 功能概述 +- **用途**: 认证成功事件监听器 +- **监听事件**: `AuthenticationSuccessEvent` +- **触发场景**: 用户登录认证成功时 +- **业务价值**: 重置用户登录失败计数和解除账户锁定 + +#### 核心特性 +- **状态重置**: 成功登录后重置失败计数 +- **锁定解除**: 自动解除账户锁定状态 +- **用户管理**: 维护用户账户状态的一致性 +- **安全恢复**: 恢复正常用户的访问权限 + +#### 代码实现 +```java +@Component +public class AuthenticationSuccessEventListener + implements ApplicationListener { + + @Autowired + private UserAccountRepository userAccountRepository; + + @Override + public void onApplicationEvent(@NonNull AuthenticationSuccessEvent event) { + Object principal = event.getAuthentication().getPrincipal(); + + if (principal instanceof UserDetails userDetails) { + String username = userDetails.getUsername(); + UserAccount user = userAccountRepository.findByEmail(username).orElse(null); + + if (user != null && user.getFailedLoginAttempts() > 0) { + user.setFailedLoginAttempts(0); + user.setLockoutEndTime(null); + userAccountRepository.save(user); + } + } + } +} +``` + +#### 处理流程 +1. **身份验证**: 确认认证主体是UserDetails实例 +2. **用户查找**: 根据用户名查找用户账户 +3. **状态检查**: 检查是否存在失败登录记录 +4. **状态重置**: 清零失败次数并解除锁定 +5. **数据持久化**: 保存更新后的用户状态 + +#### 使用场景 +1. **账户恢复**: 成功登录后恢复账户正常状态 +2. **计数重置**: 清除之前的失败登录记录 +3. **锁定解除**: 自动解除临时锁定状态 +4. **状态同步**: 保持用户状态的一致性 + +### 3. FeedbackEventListener + +#### 功能概述 +- **用途**: 反馈事件监听器 +- **监听事件**: `FeedbackSubmittedForAiReviewEvent` +- **触发场景**: 反馈提交需要AI审核时 +- **业务价值**: 实现反馈的异步AI审核处理 + +#### 核心特性 +- **异步处理**: 使用`@Async`注解实现异步执行 +- **事件驱动**: 基于Spring的事件机制 +- **AI集成**: 调用AI服务进行内容审核 +- **日志记录**: 详细记录处理过程 + +#### 代码实现 +```java +@Component +@Slf4j +@RequiredArgsConstructor +public class FeedbackEventListener { + + private final AiReviewService aiReviewService; + + @Async + @EventListener + public void handleFeedbackSubmittedForAiReview(FeedbackSubmittedForAiReviewEvent event) { + log.info("Received feedback submission event for AI review. Feedback ID: {}", + event.getFeedback().getId()); + aiReviewService.reviewFeedback(event.getFeedback()); + } +} +``` + +#### 处理特点 +1. **异步执行**: 不阻塞主业务流程 +2. **事件响应**: 响应反馈提交事件 +3. **AI调用**: 调用AI服务进行内容分析 +4. **日志追踪**: 记录处理过程便于监控 + +#### 使用场景 +1. **内容审核**: 自动审核用户提交的反馈内容 +2. **智能分类**: AI分析反馈的类型和优先级 +3. **质量评估**: 评估反馈的有效性和重要性 +4. **自动处理**: 根据AI分析结果自动处理简单反馈 + +## 监听器架构优势 + +### 1. 事件驱动架构 +- **解耦设计**: 事件发布者和监听者之间松耦合 +- **扩展性强**: 可以轻松添加新的事件监听器 +- **职责分离**: 每个监听器专注于特定的业务逻辑 +- **异步支持**: 支持异步事件处理提升性能 + +### 2. 安全机制增强 +- **登录保护**: 自动记录和处理登录失败 +- **状态管理**: 智能管理用户账户状态 +- **攻击防护**: 防止暴力破解和恶意登录 +- **恢复机制**: 自动恢复正常用户的访问权限 + +### 3. 业务流程自动化 +- **智能审核**: 自动触发AI内容审核 +- **状态同步**: 自动同步用户和业务状态 +- **流程优化**: 减少手动干预提升效率 +- **响应及时**: 实时响应业务事件 + +### 4. 系统可观测性 +- **日志记录**: 详细记录事件处理过程 +- **状态追踪**: 跟踪用户和业务状态变化 +- **性能监控**: 监控事件处理性能 +- **问题定位**: 便于排查和解决问题 + +## 事件处理流程 + +### 认证失败处理流程 +``` +1. 用户登录失败 → Spring Security触发AuthenticationFailureBadCredentialsEvent +2. AuthenticationFailureEventListener接收事件 +3. 获取客户端真实IP地址 +4. 调用LoginAttemptService记录失败尝试 +5. 更新IP失败计数和锁定状态 +``` + +### 认证成功处理流程 +``` +1. 用户登录成功 → Spring Security触发AuthenticationSuccessEvent +2. AuthenticationSuccessEventListener接收事件 +3. 获取用户身份信息 +4. 查找用户账户记录 +5. 重置失败计数和解除锁定 +6. 保存用户状态更新 +``` + +### 反馈AI审核流程 +``` +1. 反馈提交 → 发布FeedbackSubmittedForAiReviewEvent +2. FeedbackEventListener异步接收事件 +3. 记录处理日志 +4. 调用AiReviewService进行内容审核 +5. 更新反馈状态和审核结果 +``` + +## 技术实现细节 + +### 1. Spring事件监听机制 +- **ApplicationListener**: 传统的事件监听接口 +- **@EventListener**: 基于注解的事件监听方法 +- **@Async**: 异步事件处理注解 +- **事件发布**: 通过ApplicationEventPublisher发布事件 + +### 2. 依赖注入和管理 +- **@Component**: 将监听器注册为Spring组件 +- **@Autowired**: 自动注入依赖服务 +- **@RequiredArgsConstructor**: Lombok构造器注入 +- **生命周期管理**: Spring容器管理监听器生命周期 + +### 3. 异步处理配置 +```java +@EnableAsync +@Configuration +public class AsyncConfig { + @Bean + public TaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(500); + executor.setThreadNamePrefix("EventListener-"); + executor.initialize(); + return executor; + } +} +``` + +## Listener模块最佳实践 + +1. **事件监听**: 使用合适的监听方式(接口或注解) +2. **异步处理**: 对于耗时操作使用@Async注解 +3. **异常处理**: 在监听器中添加完善的异常处理 +4. **日志记录**: 记录关键事件的处理过程 +5. **性能考虑**: 避免在监听器中执行阻塞操作 +6. **事务管理**: 注意事件监听器的事务边界 +7. **测试覆盖**: 为监听器编写单元测试和集成测试 + +## Listener模块总结 + +Listener模块为EMS系统提供了强大的事件响应能力: + +1. **安全保障**: 通过认证事件监听实现登录安全控制 +2. **状态管理**: 自动管理用户账户和业务状态 +3. **业务自动化**: 实现反馈AI审核等业务流程自动化 +4. **异步处理**: 支持异步事件处理提升系统性能 +5. **扩展性强**: 易于添加新的事件监听器扩展功能 +6. **可观测性**: 提供详细的事件处理日志和监控 +7. **Spring集成**: 充分利用Spring框架的事件机制 + +通过这套监听器机制,EMS系统能够及时响应各种系统事件和业务事件,实现了安全控制、状态管理和业务流程的自动化,为系统的稳定运行和用户体验提供了重要保障。 + +--- + +# Model模块详细解析 + +## 概述 + +Model(数据模型)模块位于 `com.dne.ems.model` 包下,包含14个核心实体类和10个枚举类,构成了EMS系统的完整数据模型。该模块使用JPA/Hibernate进行对象关系映射,定义了系统中所有业务实体的结构、关系和约束,为整个系统提供了坚实的数据基础。 + +## 核心实体类 + +### 1. 用户管理相关实体 + +#### UserAccount(用户账户) + +**功能概述** +- **用途**: 系统用户账户的核心实体 +- **映射表**: `user_account` +- **业务价值**: 管理所有用户的基本信息、角色权限和状态 + +**核心特性** +- **身份验证**: 存储用户登录凭据(邮箱、手机、密码) +- **角色管理**: 支持多种用户角色(管理员、主管、网格员等) +- **地理关联**: 支持网格坐标和区域信息 +- **技能管理**: JSON格式存储用户技能列表 +- **状态控制**: 支持账户激活、停用、请假等状态 + +**关键字段** +```java +@Entity +@Table(name = "user_account") +public class UserAccount { + @Id + private Long id; + + @NotEmpty + @Column(unique = true) + private String email; + + @NotEmpty + @Column(unique = true) + private String phone; + + @Enumerated(EnumType.STRING) + private Role role; + + @Enumerated(EnumType.STRING) + private UserStatus status; + + @Convert(converter = StringListConverter.class) + private List skills; + + private Integer gridX; + private Integer gridY; +} +``` + +#### PasswordResetToken(密码重置令牌) + +**功能概述** +- **用途**: 管理用户密码重置流程的安全令牌 +- **映射表**: `password_reset_token` +- **业务价值**: 提供安全的密码重置机制 + +**核心特性** +- **安全性**: 唯一令牌字符串,防止伪造 +- **时效性**: 60分钟有效期,自动过期 +- **一对一关联**: 每个令牌关联特定用户 +- **状态检查**: 提供过期检查方法 + +### 2. 反馈管理相关实体 + +#### Feedback(环境问题反馈) + +**功能概述** +- **用途**: 系统核心实体,记录用户提交的环境问题报告 +- **映射表**: `feedback` +- **业务价值**: 收集、管理和跟踪环境问题反馈 + +**核心特性** +- **唯一标识**: 人类可读的事件ID +- **分类管理**: 污染类型和严重程度分类 +- **状态跟踪**: 完整的处理状态生命周期 +- **地理定位**: 支持网格坐标和文本地址 +- **关联管理**: 与用户、任务、附件的关联关系 + +**关键字段** +```java +@Entity +@Table(name = "feedback") +public class Feedback { + @Id + private Long id; + + @Column(unique = true) + private String eventId; + + @Enumerated(EnumType.STRING) + private PollutionType pollutionType; + + @Enumerated(EnumType.STRING) + private SeverityLevel severityLevel; + + @Enumerated(EnumType.STRING) + private FeedbackStatus status; + + private Integer gridX; + private Integer gridY; + + @OneToMany(mappedBy = "feedback") + private List attachments; +} +``` + +#### Attachment(附件) + +**功能概述** +- **用途**: 管理反馈和任务提交的文件附件 +- **映射表**: `attachment` +- **业务价值**: 支持多媒体证据收集和存储 + +**核心特性** +- **文件元数据**: 存储文件名、类型、大小等信息 +- **存储管理**: 生成唯一存储文件名防止冲突 +- **多重关联**: 可关联反馈或任务提交 +- **时间戳**: 自动记录上传时间 + +### 3. 任务管理相关实体 + +#### Task(网格任务) + +**功能概述** +- **用途**: 表示网格工作人员执行的任务 +- **映射表**: `tasks` +- **业务价值**: 管理从反馈到任务的完整生命周期 + +**核心特性** +- **反馈关联**: 可从反馈自动生成或手动创建 +- **分配管理**: 支持任务分配和重新分配 +- **状态跟踪**: 完整的任务状态生命周期 +- **时间管理**: 创建、分配、完成时间戳 +- **地理信息**: 继承反馈的位置信息 + +**关键字段** +```java +@Entity +@Table(name = "tasks") +public class Task { + @Id + private Long id; + + @OneToOne + private Feedback feedback; + + @ManyToOne + private UserAccount assignee; + + @Enumerated(EnumType.STRING) + private TaskStatus status; + + @CreationTimestamp + private LocalDateTime createdAt; + + private LocalDateTime assignedAt; + private LocalDateTime completedAt; +} +``` + +#### TaskSubmission(任务提交) + +**功能概述** +- **用途**: 记录网格员完成任务后的提交信息 +- **映射表**: `task_submissions` +- **业务价值**: 收集任务执行结果和工作记录 + +**核心特性** +- **任务关联**: 与特定任务一对多关系 +- **详细记录**: 大文本字段存储工作笔记 +- **时间戳**: 自动记录提交时间 +- **附件支持**: 可关联多个附件文件 + +#### TaskHistory(任务历史) + +**功能概述** +- **用途**: 记录任务状态变更的审计日志 +- **映射表**: `task_history` +- **业务价值**: 提供完整的任务处理轨迹 + +**核心特性** +- **状态变更**: 记录旧状态和新状态 +- **操作人员**: 记录状态变更的执行者 +- **变更时间**: 自动记录变更时间戳 +- **备注信息**: 支持变更原因说明 + +### 4. 分配管理相关实体 + +#### Assignment(任务分配) + +**功能概述** +- **用途**: 管理反馈任务到网格员的分配关系 +- **映射表**: `assignments` +- **业务价值**: 跟踪任务分配的生命周期 + +**核心特性** +- **一对一关联**: 每个分配对应一个任务 +- **分配者记录**: 记录执行分配的用户 +- **截止日期**: 支持任务截止时间设置 +- **状态管理**: 跟踪分配状态变化 +- **备注信息**: 支持分配说明和要求 + +#### AssignmentRecord(分配记录) + +**功能概述** +- **用途**: 详细记录反馈分配的元数据 +- **映射表**: `assignment_record` +- **业务价值**: 提供丰富的分配分析数据 + +**核心特性** +- **关系映射**: 直接关联反馈、网格员、管理员 +- **分配方法**: 记录手动或智能分配方式 +- **算法详情**: JSON格式存储智能分配算法细节 +- **创建时间**: 自动记录分配创建时间 + +### 5. 空气质量数据相关实体 + +#### AqiData(空气质量指数数据) + +**功能概述** +- **用途**: 存储网格员提交的详细空气质量数据 +- **映射表**: `aqi_data` +- **业务价值**: 收集实时环境监测数据 + +**核心特性** +- **多污染物**: 支持PM2.5、PM10、SO2、NO2、CO、O3等 +- **网格关联**: 与具体网格位置关联 +- **报告者**: 记录数据提交的网格员 +- **反馈关联**: 可选关联相关反馈 + +**关键字段** +```java +@Entity +@Table(name = "aqi_data") +public class AqiData { + @Id + private Long id; + + @ManyToOne + private Grid grid; + + private Long reporterId; + private Integer aqiValue; + private Double pm25; + private Double pm10; + private Double so2; + private Double no2; + private Double co; + private Double o3; +} +``` + +#### AqiRecord(空气质量历史记录) + +**功能概述** +- **用途**: 存储历史空气质量数据用于趋势分析 +- **映射表**: `aqi_records` +- **业务价值**: 支持热力图生成和趋势分析 + +**核心特性** +- **城市级别**: 按城市名称组织数据 +- **完整污染物**: 包含所有主要污染物浓度 +- **地理坐标**: 支持经纬度和网格坐标 +- **时间戳**: 自动记录数据时间 + +### 6. 地理网格相关实体 + +#### Grid(地理网格) + +**功能概述** +- **用途**: 表示地理网格单元用于区域管理 +- **映射表**: `grid` +- **业务价值**: 支持基于网格的数据聚合和管理 + +**核心特性** +- **坐标系统**: X、Y坐标定位 +- **行政区划**: 城市和区县信息 +- **障碍标记**: 支持限制区域标记 +- **描述信息**: 网格详细描述 + +#### MapGrid(地图网格) + +**功能概述** +- **用途**: 专门用于A*寻路算法的网格单元 +- **映射表**: `map_grid` +- **业务价值**: 支持智能路径规划和导航 + +**核心特性** +- **唯一约束**: (x,y)坐标唯一性约束 +- **障碍检测**: 支持障碍物标记 +- **地形类型**: 可选的地形类型信息 +- **城市约束**: 限制寻路范围 + +### 7. 系统配置相关实体 + +#### PollutantThreshold(污染物阈值) + +**功能概述** +- **用途**: 存储不同污染物的超标阈值设置 +- **映射表**: `pollutant_thresholds` +- **业务价值**: 支持污染物超标检测和预警 + +**核心特性** +- **类型关联**: 与污染物类型枚举关联 +- **阈值设置**: 可配置的超标阈值 +- **单位管理**: 支持不同计量单位 +- **描述信息**: 阈值设置说明 + +## 枚举类型系统 + +### 1. 用户相关枚举 + +#### Role(用户角色) +```java +public enum Role { + PUBLIC_SUPERVISOR, // 公众监督员 + SUPERVISOR, // 主管 + GRID_WORKER, // 网格员 + ADMIN, // 系统管理员 + DECISION_MAKER // 决策者 +} +``` + +#### UserStatus(用户状态) +```java +public enum UserStatus { + ACTIVE, // 活跃 + INACTIVE, // 停用 + ON_LEAVE // 请假 +} +``` + +#### Gender(性别) +```java +public enum Gender { + MALE, // 男性 + FEMALE, // 女性 + UNKNOWN // 未知 +} +``` + +#### Level(级别) +```java +public enum Level { + PROVINCE, // 省级 + CITY // 市级 +} +``` + +### 2. 业务流程相关枚举 + +#### FeedbackStatus(反馈状态) +```java +public enum FeedbackStatus { + PENDING_REVIEW, // 待审核 + AI_REVIEWING, // AI审核中 + AI_REVIEW_FAILED, // AI审核失败 + AI_PROCESSING, // AI处理中 + PENDING_ASSIGNMENT, // 待分配 + ASSIGNED, // 已分配 + CONFIRMED, // 已确认 + RESOLVED, // 已解决 + CLOSED // 已关闭 +} +``` + +#### TaskStatus(任务状态) +```java +public enum TaskStatus { + PENDING_ASSIGNMENT, // 待分配 + ASSIGNED, // 已分配 + IN_PROGRESS, // 进行中 + SUBMITTED, // 已提交 + COMPLETED, // 已完成 + CANCELLED // 已取消 +} +``` + +#### AssignmentStatus(分配状态) +```java +public enum AssignmentStatus { + PENDING, // 待处理 + IN_PROGRESS, // 进行中 + COMPLETED, // 已完成 + OVERDUE // 已逾期 +} +``` + +#### AssignmentMethod(分配方法) +```java +public enum AssignmentMethod { + MANUAL, // 手动分配 + INTELLIGENT // 智能分配 +} +``` + +### 3. 环境数据相关枚举 + +#### PollutionType(污染类型) +```java +public enum PollutionType { + PM25, // 细颗粒物 + O3, // 臭氧 + NO2, // 二氧化氮 + SO2, // 二氧化硫 + OTHER // 其他 +} +``` + +#### SeverityLevel(严重程度) +```java +public enum SeverityLevel { + HIGH, // 高 + MEDIUM, // 中 + LOW // 低 +} +``` + +## 数据模型设计特点 + +### 1. 关系设计 +- **一对一关系**: Task-Feedback, Assignment-Task +- **一对多关系**: UserAccount-Task, Feedback-Attachment +- **多对一关系**: AqiData-Grid, Task-UserAccount +- **自引用关系**: TaskHistory-Task(审计日志) + +### 2. 约束和验证 +- **唯一性约束**: 用户邮箱、手机号、反馈事件ID +- **非空约束**: 关键业务字段强制非空 +- **枚举约束**: 使用枚举类型确保数据一致性 +- **长度约束**: 字符串字段长度限制 + +### 3. 时间戳管理 +- **创建时间**: @CreationTimestamp自动设置 +- **更新时间**: @UpdateTimestamp自动更新 +- **业务时间**: 分配时间、完成时间等业务节点 +- **过期检查**: 密码重置令牌过期验证 + +### 4. JSON数据支持 +- **技能列表**: 用户技能JSON存储 +- **算法详情**: 智能分配算法详情JSON存储 +- **大文本**: 使用@Lob注解支持长文本 + +## 数据模型架构优势 + +### 1. 业务完整性 +- **全生命周期**: 覆盖从反馈提交到任务完成的完整流程 +- **状态管理**: 详细的状态枚举和状态转换 +- **审计追踪**: 完整的操作历史记录 +- **数据关联**: 清晰的实体关系和数据流向 + +### 2. 扩展性设计 +- **枚举扩展**: 易于添加新的状态和类型 +- **实体扩展**: 支持新增字段和关系 +- **JSON存储**: 灵活的半结构化数据支持 +- **继承支持**: 预留实体继承扩展空间 + +### 3. 性能优化 +- **懒加载**: 使用FetchType.LAZY优化查询性能 +- **索引设计**: 唯一约束自动创建索引 +- **批量操作**: 支持CascadeType级联操作 +- **查询优化**: 合理的关系设计减少N+1问题 + +### 4. 数据安全 +- **密码加密**: 密码字段加密存储 +- **令牌安全**: 密码重置令牌时效性控制 +- **状态控制**: 用户状态控制访问权限 +- **数据完整性**: 外键约束保证数据一致性 + +## 技术实现细节 + +### 1. JPA注解使用 +```java +// 实体映射 +@Entity +@Table(name = "table_name") + +// 主键生成 +@Id +@GeneratedValue(strategy = GenerationType.IDENTITY) + +// 关系映射 +@OneToOne +@OneToMany +@ManyToOne +@ManyToMany + +// 枚举映射 +@Enumerated(EnumType.STRING) + +// 时间戳管理 +@CreationTimestamp +@UpdateTimestamp +``` + +### 2. Lombok注解 +```java +@Data // getter/setter/toString/equals/hashCode +@Builder // 建造者模式 +@NoArgsConstructor // 无参构造器 +@AllArgsConstructor // 全参构造器 +``` + +### 3. 验证注解 +```java +@NotEmpty // 非空验证 +@NotNull // 非null验证 +@Email // 邮箱格式验证 +@Size(min = 8) // 长度验证 +``` + +### 4. 自定义转换器 +```java +@Convert(converter = StringListConverter.class) +private List skills; +``` + +## Model模块最佳实践 + +1. **实体设计**: 遵循单一职责原则,每个实体专注特定业务领域 +2. **关系映射**: 合理使用懒加载,避免性能问题 +3. **枚举使用**: 优先使用枚举类型确保数据一致性 +4. **时间戳**: 统一使用Hibernate注解管理时间字段 +5. **验证约束**: 在实体层添加基础验证约束 +6. **命名规范**: 遵循Java命名规范和数据库命名规范 +7. **文档注释**: 为每个实体和字段添加详细注释 + +## Model模块总结 + +Model模块为EMS系统提供了完整、规范的数据模型基础: + +1. **业务完整性**: 覆盖用户管理、反馈处理、任务分配、数据收集等全业务流程 +2. **数据一致性**: 通过枚举类型、约束和验证确保数据质量 +3. **关系清晰**: 合理的实体关系设计支持复杂业务场景 +4. **扩展性强**: 灵活的设计支持业务需求变化和功能扩展 +5. **性能优化**: 合理的加载策略和索引设计保证系统性能 +6. **安全可靠**: 完善的约束和验证机制保证数据安全 +7. **标准规范**: 遵循JPA规范和最佳实践,便于维护和扩展 + +通过这套完整的数据模型,EMS系统建立了坚实的数据基础,为上层业务逻辑提供了可靠的数据支撑,确保了系统的稳定性、可扩展性和可维护性。 + +--- + +# Repository模块详细解析 + +## 概述 + +Repository(数据访问层)模块位于 `com.dne.ems.repository` 包下,包含14个Repository接口,构成了EMS系统完整的数据访问层。该模块基于Spring Data JPA框架,提供了标准化的数据访问接口,支持基础CRUD操作、自定义查询方法、复杂查询和分页查询,为业务层提供了高效、可靠的数据访问服务。 + +## 核心Repository接口 + +### 1. 用户管理相关Repository + +#### UserAccountRepository(用户账户数据访问) + +**功能概述** +- **继承接口**: `JpaRepository`, `JpaSpecificationExecutor` +- **业务价值**: 提供用户账户的完整数据访问功能 +- **核心特性**: 支持复杂查询、分页查询、规格查询 + +**核心查询方法** +```java +@Repository +public interface UserAccountRepository extends JpaRepository, JpaSpecificationExecutor { + // 基础查询 + Optional findByEmail(String email); + Optional findByPhone(String phone); + Optional findByGridXAndGridY(Integer gridX, Integer gridY); + + // 角色相关查询 + List findByRole(Role role); + long countByRole(Role role); + Page findByRole(Role role, Pageable pageable); + List findByRoleAndStatus(Role role, UserStatus status); + + // 网格员专用查询 + @Query("SELECT u FROM UserAccount u WHERE u.role = 'GRID_WORKER' AND u.status = 'ACTIVE' AND u.gridX BETWEEN ?1 AND ?2 AND u.gridY BETWEEN ?3 AND ?4") + List findActiveWorkersInArea(int minX, int maxX, int minY, int maxY); + + @Query("SELECT u FROM UserAccount u WHERE u.role = 'GRID_WORKER' AND u.gridX = ?1 AND u.gridY = ?2") + List findGridWorkersByCoordinates(int gridX, int gridY); + + @Query("SELECT u FROM UserAccount u WHERE u.role = 'GRID_WORKER' AND u.gridX IS NOT NULL AND u.gridY IS NOT NULL AND u.gridX >= 0 AND u.gridY >= 0") + List findAllAssignedGridWorkers(); + + @Query("SELECT u FROM UserAccount u WHERE u.role = 'GRID_WORKER' AND (u.gridX IS NULL OR u.gridY IS NULL OR u.gridX < 0 OR u.gridY < 0)") + List findAllUnassignedGridWorkers(); + + @Query("SELECT u FROM UserAccount u WHERE u.role = 'GRID_WORKER' AND u.region LIKE %?1%") + Page findGridWorkersByCity(String cityName, Pageable pageable); +} +``` + +**使用场景** +- **身份验证**: 通过邮箱或手机号查找用户 +- **角色管理**: 按角色查询和统计用户 +- **网格管理**: 查找特定区域或坐标的网格员 +- **用户分配**: 查找已分配和未分配网格的网格员 +- **区域查询**: 按城市查找网格员 + +#### PasswordResetTokenRepository(密码重置令牌数据访问) + +**功能概述** +- **继承接口**: `JpaRepository` +- **业务价值**: 管理密码重置令牌的数据访问 +- **核心特性**: 支持令牌查找和用户关联查询 + +**核心查询方法** +```java +@Repository +public interface PasswordResetTokenRepository extends JpaRepository { + Optional findByToken(String token); + Optional findByUser(UserAccount user); +} +``` + +### 2. 反馈管理相关Repository + +#### FeedbackRepository(反馈数据访问) + +**功能概述** +- **继承接口**: `JpaRepository`, `JpaSpecificationExecutor` +- **业务价值**: 提供反馈数据的完整访问功能 +- **核心特性**: 支持复杂查询、分页查询、统计查询、热力图数据 + +**核心查询方法** +```java +@Repository +public interface FeedbackRepository extends JpaRepository, JpaSpecificationExecutor { + // 基础查询 + Optional findByEventId(String eventId); + List findByStatus(FeedbackStatus status); + long countByStatus(FeedbackStatus status); + + // 用户相关查询 + Page findBySubmitterId(Long submitterId, Pageable pageable); + List findBySubmitterId(Long submitterId); + long countBySubmitterId(Long submitterId); + long countByStatusAndSubmitterId(FeedbackStatus status, Long submitterId); + + // 统计查询 + @Query("SELECT new com.dne.ems.dto.HeatmapPointDTO(f.gridX, f.gridY, COUNT(f.id)) FROM Feedback f WHERE f.status = 'CONFIRMED' AND f.gridX IS NOT NULL AND f.gridY IS NOT NULL GROUP BY f.gridX, f.gridY") + List getHeatmapData(); + + @Query("SELECT new com.dne.ems.dto.PollutionStatsDTO(f.pollutionType, COUNT(f)) FROM Feedback f GROUP BY f.pollutionType") + List countByPollutionType(); + + // 性能优化查询 + @Override + @EntityGraph(attributePaths = {"user", "attachments"}) + Page findAll(Specification spec, Pageable pageable); +} +``` + +**使用场景** +- **反馈管理**: 按状态、用户、事件ID查询反馈 +- **数据统计**: 统计各状态反馈数量和污染类型分布 +- **热力图**: 生成反馈分布热力图数据 +- **性能优化**: 使用EntityGraph避免N+1查询问题 + +#### AttachmentRepository(附件数据访问) + +**功能概述** +- **继承接口**: `JpaRepository` +- **业务价值**: 管理反馈和任务提交的附件数据 +- **核心特性**: 支持文件名查找和关联实体查询 + +**核心查询方法** +```java +@Repository +public interface AttachmentRepository extends JpaRepository { + Optional findByStoredFileName(String storedFileName); + List findByTaskSubmission(TaskSubmission taskSubmission); + List findByFeedback(Feedback feedback); +} +``` + +### 3. 任务管理相关Repository + +#### TaskRepository(任务数据访问) + +**功能概述** +- **继承接口**: `JpaRepository`, `JpaSpecificationExecutor` +- **业务价值**: 提供任务数据的完整访问功能 +- **核心特性**: 支持复杂查询、状态统计、分配查询 + +**核心查询方法** +```java +@Repository +public interface TaskRepository extends JpaRepository, JpaSpecificationExecutor { + Optional findByFeedback(Feedback feedback); + long countByStatus(TaskStatus status); + long countByAssigneeIdAndStatusIn(Long assigneeId, Collection statuses); + List findByAssigneeIdAndStatus(Long assigneeId, TaskStatus status); + Page findByStatusAndAssignee(AssignmentStatus status, UserAccount assignee, Pageable pageable); +} +``` + +#### TaskSubmissionRepository(任务提交数据访问) + +**功能概述** +- **继承接口**: `JpaRepository` +- **业务价值**: 管理任务提交记录的数据访问 +- **核心特性**: 支持按任务查找最新提交 + +**核心查询方法** +```java +@Repository +public interface TaskSubmissionRepository extends JpaRepository { + TaskSubmission findFirstByTaskOrderBySubmittedAtDesc(Task task); +} +``` + +#### TaskHistoryRepository(任务历史数据访问) + +**功能概述** +- **继承接口**: `JpaRepository` +- **业务价值**: 管理任务状态变更历史记录 +- **核心特性**: 支持按任务ID查询历史记录 + +**核心查询方法** +```java +@Repository +public interface TaskHistoryRepository extends JpaRepository { + List findByTaskIdOrderByChangedAtDesc(Long taskId); +} +``` + +### 4. 分配管理相关Repository + +#### AssignmentRepository(任务分配数据访问) + +**功能概述** +- **继承接口**: `JpaRepository` +- **业务价值**: 管理任务分配关系的数据访问 +- **核心特性**: 支持按分配者查询分配记录 + +**核心查询方法** +```java +@Repository +public interface AssignmentRepository extends JpaRepository { + List findByTaskAssigneeId(Long assigneeId); +} +``` + +#### AssignmentRecordRepository(分配记录数据访问) + +**功能概述** +- **继承接口**: `JpaRepository` +- **业务价值**: 管理分配记录的详细数据 +- **核心特性**: 基础CRUD操作,预留扩展空间 + +### 5. 空气质量数据相关Repository + +#### AqiDataRepository(空气质量数据访问) + +**功能概述** +- **继承接口**: `JpaRepository` +- **业务价值**: 管理实时空气质量数据的访问 +- **核心特性**: 支持统计查询、趋势分析、热力图数据 + +**核心查询方法** +```java +@Repository +public interface AqiDataRepository extends JpaRepository { + // AQI分布统计 + @Query("SELECT new com.dne.ems.dto.AqiDistributionDTO(CASE WHEN a.aqiValue <= 50 THEN 'Good' WHEN a.aqiValue <= 100 THEN 'Moderate' WHEN a.aqiValue <= 150 THEN 'Unhealthy for Sensitive Groups' WHEN a.aqiValue <= 200 THEN 'Unhealthy' WHEN a.aqiValue <= 300 THEN 'Very Unhealthy' ELSE 'Hazardous' END, COUNT(a.id)) FROM AqiData a GROUP BY 1") + List getAqiDistribution(); + + // 月度超标趋势(原生SQL) + @Query(value = "SELECT DATE_FORMAT(record_time, '%Y-%m') as yearMonth, COUNT(id) as count FROM aqi_data WHERE aqi_value > 100 AND record_time >= :startDate GROUP BY DATE_FORMAT(record_time, '%Y-%m') ORDER BY 1", nativeQuery = true) + List getMonthlyExceedanceTrendRaw(@Param("startDate") LocalDateTime startDate); + + // 默认方法转换原生查询结果 + default List getMonthlyExceedanceTrend(LocalDateTime startDate) { + List rawResults = getMonthlyExceedanceTrendRaw(startDate); + return rawResults.stream() + .map(row -> new TrendDataPointDTO((String) row[0], ((Number) row[1]).longValue())) + .toList(); + } + + // 热力图数据 + @Query("SELECT new com.dne.ems.dto.AqiHeatmapPointDTO(g.gridX, g.gridY, AVG(d.aqiValue)) FROM AqiData d JOIN d.grid g WHERE g.gridX IS NOT NULL AND g.gridY IS NOT NULL GROUP BY g.gridX, g.gridY") + List getAqiHeatmapData(); +} +``` + +#### AqiRecordRepository(空气质量历史记录数据访问) + +**功能概述** +- **继承接口**: `JpaRepository` +- **业务价值**: 管理历史空气质量数据的访问 +- **核心特性**: 支持复杂阈值查询、时间范围查询、热力图数据 + +**核心查询方法** +```java +@Repository +public interface AqiRecordRepository extends JpaRepository { + // AQI分布统计 + @Query("SELECT new com.dne.ems.dto.AqiDistributionDTO(CASE WHEN ar.aqiValue <= 50 THEN 'Good' WHEN ar.aqiValue <= 100 THEN 'Moderate' WHEN ar.aqiValue <= 150 THEN 'Unhealthy for Sensitive Groups' WHEN ar.aqiValue <= 200 THEN 'Unhealthy' WHEN ar.aqiValue <= 300 THEN 'Very Unhealthy' ELSE 'Hazardous' END, COUNT(ar.id)) FROM AqiRecord ar GROUP BY CASE WHEN ar.aqiValue <= 50 THEN 'Good' WHEN ar.aqiValue <= 100 THEN 'Moderate' WHEN ar.aqiValue <= 150 THEN 'Unhealthy for Sensitive Groups' WHEN ar.aqiValue <= 200 THEN 'Unhealthy' WHEN ar.aqiValue <= 300 THEN 'Very Unhealthy' ELSE 'Hazardous' END") + List getAqiDistribution(); + + // 超标记录查询 + @Query("SELECT ar FROM AqiRecord ar WHERE ar.aqiValue > 100 AND ar.recordTime >= :startDate") + List findExceedanceRecordsSince(@Param("startDate") LocalDateTime startDate); + + // 多阈值超标查询 + @Query("SELECT ar FROM AqiRecord ar WHERE ar.recordTime >= :startDate AND (ar.aqiValue > :aqiThreshold OR ar.pm25 > :pm25Threshold OR ar.o3 > :o3Threshold OR ar.no2 > :no2Threshold OR ar.so2 > :so2Threshold)") + List findExceedanceRecordsWithThresholds(@Param("startDate") LocalDateTime startDate, @Param("aqiThreshold") Double aqiThreshold, @Param("pm25Threshold") Double pm25Threshold, @Param("o3Threshold") Double o3Threshold, @Param("no2Threshold") Double no2Threshold, @Param("so2Threshold") Double so2Threshold); + + // 时间范围查询 + List findByRecordTimeAfter(LocalDateTime startDate); + + // 热力图数据(JPQL) + @Query("SELECT new com.dne.ems.dto.AqiHeatmapPointDTO(ar.gridX, ar.gridY, AVG(ar.aqiValue)) FROM AqiRecord ar WHERE ar.gridX IS NOT NULL AND ar.gridY IS NOT NULL GROUP BY ar.gridX, ar.gridY") + List getAqiHeatmapData(); + + // 热力图数据(原生SQL) + @Query(value = "SELECT grid_x, grid_y, AVG(aqi_value) as avg_aqi FROM aqi_records WHERE grid_x IS NOT NULL AND grid_y IS NOT NULL GROUP BY grid_x, grid_y", nativeQuery = true) + List getAqiHeatmapDataRaw(); +} +``` + +### 6. 地理网格相关Repository + +#### GridRepository(地理网格数据访问) + +**功能概述** +- **继承接口**: `JpaRepository`, `JpaSpecificationExecutor` +- **业务价值**: 管理地理网格数据的访问 +- **核心特性**: 支持坐标查询、城市统计、障碍物查询 + +**核心查询方法** +```java +@Repository +public interface GridRepository extends JpaRepository, JpaSpecificationExecutor { + Optional findByGridXAndGridY(Integer gridX, Integer gridY); + List findByIsObstacle(boolean isObstacle); + + @Query("SELECT new com.dne.ems.dto.GridCoverageDTO(g.cityName, COUNT(g.id)) FROM Grid g WHERE g.cityName IS NOT NULL AND g.cityName != '' GROUP BY g.cityName ORDER BY COUNT(g.id) DESC") + List getGridCoverageByCity(); +} +``` + +#### MapGridRepository(地图网格数据访问) + +**功能概述** +- **继承接口**: `JpaRepository` +- **业务价值**: 管理A*寻路算法的网格数据 +- **核心特性**: 支持坐标查询和有序遍历 + +**核心查询方法** +```java +@Repository +public interface MapGridRepository extends JpaRepository { + Optional findByXAndY(int x, int y); + List findAllByOrderByYAscXAsc(); +} +``` + +### 7. 系统配置相关Repository + +#### PollutantThresholdRepository(污染物阈值数据访问) + +**功能概述** +- **继承接口**: `JpaRepository` +- **业务价值**: 管理污染物阈值配置数据 +- **核心特性**: 支持按污染物名称查询阈值 + +**核心查询方法** +```java +@Repository +public interface PollutantThresholdRepository extends JpaRepository { + Optional findByPollutantName(String pollutantName); +} +``` + +## Repository设计特点 + +### 1. 继承体系设计 +- **JpaRepository**: 提供基础CRUD操作 +- **JpaSpecificationExecutor**: 支持动态查询和复杂条件 +- **双重继承**: 核心实体同时继承两个接口,提供完整功能 + +### 2. 查询方法类型 +- **方法名查询**: 基于Spring Data JPA命名规范的自动查询 +- **@Query注解**: 自定义JPQL查询语句 +- **原生SQL**: 使用nativeQuery进行复杂数据库操作 +- **默认方法**: 在接口中提供数据转换逻辑 + +### 3. 性能优化策略 +- **EntityGraph**: 解决N+1查询问题 +- **分页查询**: 支持大数据量的分页处理 +- **索引利用**: 查询方法充分利用数据库索引 +- **批量操作**: 继承JpaRepository的批量操作能力 + +### 4. 查询复杂度分层 +- **简单查询**: 单字段或少量字段的精确匹配 +- **条件查询**: 多条件组合和范围查询 +- **统计查询**: 聚合函数和分组统计 +- **关联查询**: 跨表JOIN查询和子查询 + +## Repository架构优势 + +### 1. 标准化设计 +- **Spring Data JPA**: 遵循Spring生态标准 +- **命名规范**: 统一的方法命名和参数规范 +- **注解驱动**: 使用标准注解进行配置 +- **接口导向**: 面向接口编程,便于测试和扩展 + +### 2. 功能完整性 +- **CRUD操作**: 完整的增删改查功能 +- **复杂查询**: 支持各种业务查询需求 +- **统计分析**: 提供数据统计和分析功能 +- **性能优化**: 内置性能优化机制 + +### 3. 扩展性强 +- **自定义查询**: 支持添加新的查询方法 +- **规格查询**: 支持动态条件构建 +- **原生SQL**: 支持复杂的数据库操作 +- **默认方法**: 支持在接口中添加业务逻辑 + +### 4. 维护性好 +- **类型安全**: 编译时类型检查 +- **自动实现**: Spring自动生成实现类 +- **测试友好**: 易于进行单元测试和集成测试 +- **文档清晰**: 方法名自解释,注释完整 + +## 技术实现细节 + +### 1. Spring Data JPA注解 +```java +@Repository // 标识为Repository组件 +@Query("JPQL语句") // 自定义JPQL查询 +@Query(value="SQL", nativeQuery=true) // 原生SQL查询 +@Param("参数名") // 命名参数绑定 +@EntityGraph(attributePaths={"关联属性"}) // 性能优化 +``` + +### 2. 查询方法命名规范 +```java +findBy... // 查询方法前缀 +countBy... // 统计方法前缀 +existsBy... // 存在性检查前缀 +deleteBy... // 删除方法前缀 + +// 条件关键字 +And, Or // 逻辑连接 +Between, LessThan, GreaterThan // 范围条件 +Like, NotLike // 模糊匹配 +In, NotIn // 集合条件 +IsNull, IsNotNull // 空值检查 +OrderBy...Asc/Desc // 排序 +``` + +### 3. 分页和排序 +```java +Page findBy...(Pageable pageable); // 分页查询 +List findBy...OrderBy...(); // 排序查询 +Slice findBy...(Pageable pageable); // 切片查询 +``` + +### 4. 规格查询(Specification) +```java +public interface EntityRepository extends JpaRepository, JpaSpecificationExecutor { + // 支持动态条件查询 + Page findAll(Specification spec, Pageable pageable); +} +``` + +## Repository模块最佳实践 + +1. **接口设计**: 保持接口简洁,职责单一 +2. **查询优化**: 合理使用EntityGraph避免N+1问题 +3. **命名规范**: 遵循Spring Data JPA命名约定 +4. **参数验证**: 在Service层进行参数验证,Repository专注数据访问 +5. **事务管理**: 在Service层管理事务,Repository保持无状态 +6. **异常处理**: 让Spring Data JPA的异常向上传播 +7. **测试覆盖**: 为自定义查询方法编写集成测试 +8. **性能监控**: 监控慢查询并进行优化 + +## Repository模块总结 + +Repository模块为EMS系统提供了完整、高效的数据访问层: + +1. **标准化架构**: 基于Spring Data JPA的标准化设计 +2. **功能完整**: 覆盖所有业务实体的数据访问需求 +3. **查询丰富**: 支持简单查询、复杂查询、统计查询、分页查询 +4. **性能优化**: 内置多种性能优化机制 +5. **扩展性强**: 支持自定义查询和动态条件构建 +6. **维护性好**: 类型安全、自动实现、测试友好 +7. **业务适配**: 针对EMS业务特点设计的专用查询方法 + +通过这套完整的Repository层,EMS系统建立了高效、可靠的数据访问基础,为业务层提供了丰富的数据操作能力,确保了系统的数据访问性能和开发效率。 + +--- + +# Service模块详细解析 + +## 概述 + +Service(业务服务层)模块位于 `com.dne.ems.service` 包下,包含17个服务接口和对应的实现类,构成了EMS系统完整的业务逻辑层。该模块基于Spring框架的服务层架构,提供了标准化的业务服务接口,实现了复杂的业务逻辑处理、事务管理、安全控制和系统集成,为控制器层提供了高质量的业务服务。 + +## 核心Service接口 + +### 1. 认证与安全相关Service + +#### AuthService(认证服务) + +**功能概述** +- **接口定义**: `AuthService` +- **实现类**: `AuthServiceImpl` +- **业务价值**: 提供用户认证和安全相关的核心功能 +- **核心特性**: 用户注册、登录认证、密码重置、JWT令牌管理 + +**核心方法** +```java +public interface AuthService { + // 密码重置流程 + void requestPasswordReset(String email); + void resetPasswordWithToken(String token, String newPassword); + void resetPasswordWithCode(String email, String code, String newPassword); + + // 用户注册与登录 + void registerUser(SignUpRequest signUpRequest); + JwtAuthenticationResponse signIn(LoginRequest loginRequest); + + // 验证码相关 + void sendRegistrationCode(String email); + boolean validateRegistrationCode(String email, String code); +} +``` + +**业务流程** +- **用户注册**: 验证码校验 → 用户信息验证 → 创建用户账户 → 返回注册结果 +- **用户登录**: 身份验证 → 生成JWT令牌 → 记录登录状态 → 返回认证响应 +- **密码重置**: 发送验证码 → 验证码校验 → 密码更新 → 清理令牌 + +#### JwtService(JWT令牌服务) + +**功能概述** +- **接口定义**: `JwtService` +- **实现类**: `JwtServiceImpl` +- **业务价值**: 处理JWT令牌的生成、解析和验证 +- **核心特性**: HS256算法签名、24小时有效期、用户角色权限声明 + +**核心方法** +```java +public interface JwtService { + String extractUserName(String token); + Claims extractAllClaims(String token); + String generateToken(UserDetails userDetails); + boolean isTokenValid(String token, UserDetails userDetails); +} +``` + +#### LoginAttemptService(登录尝试服务) + +**功能概述** +- **接口定义**: `LoginAttemptService` +- **实现类**: `LoginAttemptServiceImpl` +- **业务价值**: 防止暴力破解攻击,管理登录失败次数 +- **核心特性**: 失败次数统计、账户锁定机制、自动解锁 + +**核心方法** +```java +public interface LoginAttemptService { + void loginSucceeded(String key); + void loginFailed(String key); + boolean isBlocked(String key); +} +``` + +#### VerificationCodeService(验证码服务) + +**功能概述** +- **接口定义**: `VerificationCodeService` +- **实现类**: `VerificationCodeServiceImpl` +- **业务价值**: 生成、发送和校验验证码 +- **核心特性**: 6位数字验证码、5分钟有效期、60秒发送间隔 + +**核心方法** +```java +public interface VerificationCodeService { + void sendVerificationCode(String email); + boolean verifyCode(String email, String code); +} +``` + +### 2. 用户管理相关Service + +#### UserAccountService(用户账户服务) + +**功能概述** +- **接口定义**: `UserAccountService` +- **实现类**: `UserAccountServiceImpl` +- **业务价值**: 管理用户账户的完整生命周期 +- **核心特性**: 用户注册、角色管理、位置更新、用户查询 + +**核心方法** +```java +public interface UserAccountService { + UserAccount registerUser(UserRegistrationRequest registrationRequest); + UserAccount updateUserRole(Long userId, UserRoleUpdateRequest request); + UserAccount getUserById(Long userId); + Page getUsersByRole(Role role, Pageable pageable); + void updateUserLocation(Long userId, Double latitude, Double longitude); + Page getAllUsers(Role role, UserStatus status, Pageable pageable); +} +``` + +#### PersonnelService(人员管理服务) + +**功能概述** +- **接口定义**: `PersonnelService` +- **实现类**: `PersonnelServiceImpl` +- **业务价值**: 提供人员管理的高级功能 +- **核心特性**: 用户创建、信息更新、删除管理、个人资料更新 + +**核心方法** +```java +public interface PersonnelService { + Page getUsers(Role role, String name, Pageable pageable); + UserAccount createUser(UserCreationRequest request); + UserAccount updateUser(Long userId, UserUpdateRequest request); + void deleteUser(Long userId); + UserAccount updateOwnProfile(String currentUserEmail, UserUpdateRequest request); +} +``` + +### 3. 反馈管理相关Service + +#### FeedbackService(反馈服务) + +**功能概述** +- **接口定义**: `FeedbackService` +- **实现类**: `FeedbackServiceImpl` +- **业务价值**: 处理环境问题反馈的提交和管理 +- **核心特性**: 用户反馈提交、匿名反馈、反馈查询、数据统计 + +**核心方法** +```java +public interface FeedbackService { + // 反馈提交 + Feedback submitFeedback(FeedbackSubmissionRequest request, MultipartFile[] files); + Feedback submitPublicFeedback(PublicFeedbackRequest request, MultipartFile[] files); + + // 反馈查询 + Page getFeedbacks(FeedbackStatus status, PollutionType pollutionType, + SeverityLevel severityLevel, LocalDate startDate, + LocalDate endDate, Pageable pageable); + FeedbackResponseDTO getFeedbackById(Long feedbackId); + + // 反馈处理 + void processFeedback(Long feedbackId, ProcessFeedbackRequest request); + + // 统计分析 + FeedbackStatsResponse getFeedbackStats(); +} +``` + +**业务流程** +- **反馈提交**: 用户信息验证 → 反馈数据创建 → 文件上传处理 → AI审核触发 → 返回反馈实体 +- **反馈处理**: 状态验证 → 业务逻辑处理 → 状态更新 → 事件发布 + +#### UserFeedbackService(用户反馈服务) + +**功能概述** +- **接口定义**: `UserFeedbackService` +- **实现类**: `UserFeedbackServiceImpl` +- **业务价值**: 提供用户特定的反馈操作 +- **核心特性**: 用户反馈历史查询 + +**核心方法** +```java +public interface UserFeedbackService { + Page getFeedbackHistoryByUserId(Long userId, Pageable pageable); +} +``` + +#### AiReviewService(AI审核服务) + +**功能概述** +- **接口定义**: `AiReviewService` +- **实现类**: `AiReviewServiceImpl` +- **业务价值**: 使用AI模型审核反馈内容 +- **核心特性**: 外部AI服务调用、反馈状态更新 + +**核心方法** +```java +public interface AiReviewService { + void reviewFeedback(Feedback feedback); +} +``` + +### 4. 任务管理相关Service + +#### TaskManagementService(任务管理服务) + +**功能概述** +- **接口定义**: `TaskManagementService` +- **实现类**: `TaskManagementServiceImpl` +- **业务价值**: 处理任务全生命周期管理 +- **核心特性**: 任务创建、分配、状态管理、审核审批 + +**核心方法** +```java +public interface TaskManagementService { + // 任务查询 + Page getTasks(TaskStatus status, Long assigneeId, SeverityLevel severity, + PollutionType pollutionType, LocalDate startDate, + LocalDate endDate, Pageable pageable); + TaskDetailDTO getTaskById(Long taskId); + + // 任务创建 + TaskDetailDTO createTask(TaskCreationRequest request); + TaskDetailDTO createTaskFromFeedback(TaskFromFeedbackRequest request); + + // 任务分配 + void assignTask(Long taskId, TaskAssignmentRequest request); + + // 任务审批 + void approveTask(Long taskId, TaskApprovalRequest request); + void rejectTask(Long taskId, TaskApprovalRequest request); + + // 任务历史 + List getTaskHistory(Long taskId); +} +``` + +**业务流程** +- **任务创建**: 请求验证 → 任务实体创建 → 历史记录 → 事件发布 +- **任务分配**: 分配者验证 → 被分配者验证 → 分配关系创建 → 状态更新 +- **任务审批**: 权限验证 → 状态流转 → 审批记录 → 通知发送 + +#### TaskAssignmentService(任务分配服务) + +**功能概述** +- **接口定义**: `TaskAssignmentService` +- **实现类**: `TaskAssignmentServiceImpl` +- **业务价值**: 处理任务分配操作 +- **核心特性**: 未分配任务查询、网格员查询、任务分配 + +**核心方法** +```java +public interface TaskAssignmentService { + List getUnassignedFeedback(); + List getAvailableGridWorkers(); + Assignment assignTask(Long feedbackId, Long assigneeId, Long assignerId); +} +``` + +#### GridWorkerTaskService(网格员任务服务) + +**功能概述** +- **接口定义**: `GridWorkerTaskService` +- **实现类**: `GridWorkerTaskServiceImpl` +- **业务价值**: 处理网格员的任务管理操作 +- **核心特性**: 任务查询、状态流转、任务提交 + +**核心方法** +```java +public interface GridWorkerTaskService { + Page getAssignedTasks(Long workerId, TaskStatus status, Pageable pageable); + TaskDetailDTO getTaskDetail(Long taskId, Long workerId); + void acceptTask(Long taskId, Long workerId); + void startTask(Long taskId, Long workerId); + void submitTask(Long taskId, TaskSubmissionRequest request, MultipartFile[] files); + List getTasksNearLocation(Double latitude, Double longitude, Double radiusKm); +} +``` + +### 5. 监管审核相关Service + +#### SupervisorService(主管服务) + +**功能概述** +- **接口定义**: `SupervisorService` +- **实现类**: `SupervisorServiceImpl` +- **业务价值**: 处理主管的审核操作 +- **核心特性**: 反馈审核、审批决策 + +**核心方法** +```java +public interface SupervisorService { + List getFeedbackForReview(); + void approveFeedback(Long feedbackId); + void rejectFeedback(Long feedbackId, RejectFeedbackRequest request); +} +``` + +### 6. 数据分析相关Service + +#### DashboardService(仪表盘服务) + +**功能概述** +- **接口定义**: `DashboardService` +- **实现类**: `DashboardServiceImpl` +- **业务价值**: 提供各类环境监测数据的统计和可视化数据 +- **核心特性**: 环境质量指标统计、AQI数据分布、热力图数据、趋势分析 + +**核心方法** +```java +public interface DashboardService { + // 统计数据 + List getPollutionStats(); + TaskStatsDTO getTaskStats(); + DashboardStatsDTO getDashboardStats(); + + // AQI数据 + List getAqiDistribution(); + List getAqiHeatmapData(); + List getMonthlyExceedanceTrend(); + + // 热力图数据 + List getFeedbackHeatmapData(); + + // 网格覆盖 + List getGridCoverageByCity(); + + // 阈值管理 + List getAllThresholds(); + PollutantThresholdDTO updateThreshold(String pollutantName, Double threshold); +} +``` + +### 7. 基础设施相关Service + +#### FileStorageService(文件存储服务) + +**功能概述** +- **接口定义**: `FileStorageService` +- **实现类**: `FileStorageServiceImpl` +- **业务价值**: 处理文件上传和下载 +- **核心特性**: 安全文件存储、文件关联管理、下载服务 + +**核心方法** +```java +public interface FileStorageService { + @Deprecated + Attachment storeFileAndCreateAttachment(MultipartFile file, Feedback feedback); + String storeFile(MultipartFile file); + Resource loadFileAsResource(String fileName); + void deleteFile(String fileName); +} +``` + +#### MailService(邮件服务) + +**功能概述** +- **接口定义**: `MailService` +- **实现类**: `MailServiceImpl` +- **业务价值**: 发送各类邮件 +- **核心特性**: 密码重置邮件、注册验证码邮件 + +**核心方法** +```java +public interface MailService { + void sendPasswordResetEmail(String to, String code); + void sendVerificationCodeEmail(String to, String code); +} +``` + +#### GridService(网格服务) + +**功能概述** +- **接口定义**: `GridService` +- **实现类**: `GridServiceImpl` +- **业务价值**: 处理网格数据的查询和更新 +- **核心特性**: 网格查询、网格员分配、网格更新 + +**核心方法** +```java +public interface GridService { + List getAllGrids(); + Optional getGridById(Long gridId); + UserAccount assignWorkerToGrid(Long gridId, Long userId); + void unassignWorkerFromGrid(Long gridId); + Page getGridsWithFilters(String cityName, Boolean isObstacle, Pageable pageable); + Grid updateGrid(Long gridId, GridUpdateRequest request); +} +``` + +### 8. 算法服务 + +#### AStarService(A*寻路服务) + +**功能概述** +- **服务类**: `AStarService`(直接实现,无接口) +- **业务价值**: 提供A*寻路算法实现 +- **核心特性**: 最短路径计算、障碍物避让、网格导航 + +**核心方法** +```java +@Service +public class AStarService { + public List findPath(Point start, Point end); + private int calculateHeuristic(Point a, Point b); + private List getNeighbors(Point point); + private List reconstructPath(Node endNode); +} +``` + +## Service设计特点 + +### 1. 分层架构设计 +- **接口与实现分离**: 所有业务服务都定义了接口,便于测试和扩展 +- **职责单一原则**: 每个服务专注于特定的业务领域 +- **依赖注入**: 使用Spring的依赖注入管理服务间依赖 +- **事务管理**: 在服务层统一管理事务边界 + +### 2. 业务逻辑封装 +- **复杂业务流程**: 将复杂的业务逻辑封装在服务方法中 +- **数据转换**: 在服务层完成实体与DTO之间的转换 +- **业务验证**: 在服务层进行业务规则验证 +- **异常处理**: 统一的业务异常处理机制 + +### 3. 安全控制 +- **权限验证**: 在服务层进行细粒度的权限控制 +- **数据安全**: 敏感数据的加密和脱敏处理 +- **审计日志**: 关键操作的审计日志记录 +- **防护机制**: 防止各种安全攻击的保护措施 + +### 4. 性能优化 +- **缓存策略**: 合理使用缓存提升性能 +- **批量处理**: 支持批量操作减少数据库访问 +- **异步处理**: 使用异步机制处理耗时操作 +- **分页查询**: 大数据量查询的分页处理 + +## Service架构优势 + +### 1. 业务完整性 +- **端到端流程**: 完整的业务流程实现 +- **状态管理**: 复杂的业务状态流转管理 +- **数据一致性**: 确保业务数据的一致性 +- **业务规则**: 完整的业务规则实现 + +### 2. 系统集成 +- **外部服务**: 与外部系统的集成接口 +- **事件驱动**: 基于事件的松耦合架构 +- **消息传递**: 系统间的消息传递机制 +- **API网关**: 统一的API访问入口 + +### 3. 扩展性强 +- **插件化**: 支持插件化的功能扩展 +- **配置驱动**: 基于配置的功能开关 +- **版本兼容**: 向后兼容的版本升级 +- **模块化**: 模块化的服务设计 + +### 4. 维护性好 +- **代码规范**: 统一的代码规范和风格 +- **文档完整**: 完整的接口文档和注释 +- **测试覆盖**: 高覆盖率的单元测试和集成测试 +- **监控告警**: 完善的监控和告警机制 + +## 技术实现细节 + +### 1. Spring注解 +```java +@Service // 标识为服务组件 +@Transactional // 事务管理 +@RequiredArgsConstructor // Lombok构造器注入 +@Slf4j // 日志记录 +@Async // 异步处理 +@EventListener // 事件监听 +@Cacheable // 缓存支持 +``` + +### 2. 事务管理 +```java +@Transactional(readOnly = true) // 只读事务 +@Transactional(rollbackFor = Exception.class) // 异常回滚 +@Transactional(propagation = Propagation.REQUIRES_NEW) // 事务传播 +@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // 事务事件 +``` + +### 3. 安全控制 +```java +@PreAuthorize("hasRole('ADMIN')") // 方法级权限控制 +@PostAuthorize("returnObject.owner == authentication.name") // 返回值权限控制 +SecurityContextHolder.getContext().getAuthentication() // 获取当前用户 +``` + +### 4. 异常处理 +```java +@ControllerAdvice // 全局异常处理 +@ExceptionHandler // 特定异常处理 +try-catch-finally // 局部异常处理 +throw new BusinessException() // 业务异常抛出 +``` + +## Service模块最佳实践 + +1. **接口设计**: 保持接口简洁,方法职责单一 +2. **事务边界**: 合理设置事务边界,避免长事务 +3. **异常处理**: 统一的异常处理策略,提供有意义的错误信息 +4. **参数验证**: 在服务入口进行参数验证 +5. **日志记录**: 记录关键业务操作和异常信息 +6. **性能监控**: 监控服务性能,及时发现瓶颈 +7. **单元测试**: 为每个服务方法编写单元测试 +8. **文档维护**: 保持接口文档和代码注释的同步更新 + +## Service模块总结 + +Service模块为EMS系统提供了完整、高质量的业务服务层: + +1. **业务完整**: 覆盖环境监管系统的所有核心业务功能 +2. **架构清晰**: 分层明确、职责单一、依赖合理 +3. **安全可靠**: 完善的安全控制和异常处理机制 +4. **性能优化**: 多种性能优化策略和监控机制 +5. **扩展性强**: 支持业务功能的灵活扩展和配置 +6. **维护性好**: 代码规范、文档完整、测试覆盖率高 +7. **技术先进**: 采用Spring生态的最佳实践和现代化技术 + +通过这套完整的Service层,EMS系统建立了稳定、高效的业务处理能力,为前端应用提供了可靠的业务支撑,确保了系统的业务完整性和技术先进性。 + +--- + +# Security模块详细解析 + +## 概述 + +Security(安全模块)位于 `com.dne.ems.security` 包下,包含4个核心安全组件,构成了EMS系统完整的安全防护体系。该模块基于Spring Security框架,实现了JWT无状态认证、细粒度权限控制、CORS跨域支持和安全过滤机制,为整个系统提供了企业级的安全保障。 + +## 核心Security组件 + +### 1. CustomUserDetails(自定义用户详情) + +**功能概述** +- **类定义**: `CustomUserDetails` +- **实现接口**: `UserDetails` +- **业务价值**: 封装用户认证和授权信息,桥接业务用户实体与Spring Security +- **核心特性**: 用户信息封装、权限映射、账户状态管理、角色权限控制 + +**核心方法** +```java +public class CustomUserDetails implements UserDetails { + private final UserAccount userAccount; + + // 业务信息获取 + public Long getId(); + public String getName(); + public Role getRole(); + public String getPhone(); + public UserStatus getStatus(); + public String getRegion(); + public List getSkills(); + + // Spring Security标准接口实现 + public Collection getAuthorities(); + public String getPassword(); + public String getUsername(); // 使用邮箱作为用户名 + public boolean isAccountNonExpired(); + public boolean isAccountNonLocked(); // 基于lockoutEndTime实现锁定逻辑 + public boolean isCredentialsNonExpired(); + public boolean isEnabled(); // 基于enabled和status字段 +} +``` + +**设计特点** +- **权限映射**: 自动添加"ROLE_"前缀,确保与Spring Security的hasRole()方法兼容 +- **账户锁定**: 基于lockoutEndTime字段实现动态账户锁定机制 +- **状态检查**: 综合enabled字段和UserStatus枚举进行账户状态验证 +- **信息暴露**: 提供完整的用户业务信息访问接口 + +### 2. JwtAuthenticationFilter(JWT认证过滤器) + +**功能概述** +- **类定义**: `JwtAuthenticationFilter` +- **继承关系**: `OncePerRequestFilter` +- **业务价值**: 处理每个HTTP请求的JWT认证,实现无状态认证机制 +- **核心特性**: JWT令牌提取、令牌验证、用户加载、安全上下文设置 + +**核心方法** +```java +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException; +} +``` + +**工作流程** +1. **路径检查**: 跳过认证相关路径(/api/auth) +2. **令牌提取**: 从Authorization头提取Bearer令牌 +3. **令牌验证**: 验证JWT签名和有效期 +4. **用户加载**: 根据令牌中的用户名加载用户详情 +5. **上下文设置**: 创建认证对象并设置到SecurityContext + +**安全特性** +- **一次性过滤**: 继承OncePerRequestFilter确保每个请求只过滤一次 +- **路径跳过**: 智能跳过不需要认证的路径 +- **令牌格式**: 严格验证Bearer令牌格式 +- **上下文管理**: 正确设置Spring Security上下文 + +### 3. SecurityConfig(安全配置) + +**功能概述** +- **类定义**: `SecurityConfig` +- **配置注解**: `@Configuration`, `@EnableWebSecurity`, `@EnableMethodSecurity` +- **业务价值**: 定义系统整体安全策略和访问控制规则 +- **核心特性**: HTTP安全配置、CORS配置、认证管理、密码编码 + +**核心配置方法** +```java +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) +@RequiredArgsConstructor +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception; + + @Bean + CorsConfigurationSource corsConfigurationSource(); + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config); + + @Bean + public PasswordEncoder passwordEncoder(); +} +``` + +**安全规则配置** +```java +// HTTP安全配置 +http + .csrf(AbstractHttpConfigurer::disable) // 禁用CSRF(使用JWT) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) // 启用CORS + .authorizeHttpRequests(authorize -> authorize + // 允许OPTIONS预检请求 + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + // 认证相关端点 + .requestMatchers("/api/auth/**").permitAll() + // 公共访问端点 + .requestMatchers("/api/public/**", "/api/map/**", "/api/pathfinding/**").permitAll() + // 文件访问端点 + .requestMatchers("/api/files/**").permitAll() + // Dashboard端点权限控制 + .requestMatchers("/api/dashboard/**").hasAnyRole("ADMIN", "DECISION_MAKER") + // 其他请求需要认证 + .anyRequest().authenticated() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话 + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); +``` + +**CORS配置** +```java +@Bean +CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOrigin("*"); // 允许所有来源 + configuration.addAllowedMethod("*"); // 允许所有HTTP方法 + configuration.addAllowedHeader("*"); // 允许所有HTTP头 + // 注册到所有路径 + source.registerCorsConfiguration("/**", configuration); + return source; +} +``` + +**安全特性** +- **无状态认证**: 基于JWT的无状态会话管理 +- **CSRF防护**: 禁用CSRF(因使用JWT令牌) +- **方法级权限**: 启用@PreAuthorize、@Secured、@RolesAllowed注解 +- **细粒度控制**: 基于URL模式的访问控制 +- **密码安全**: 使用BCrypt强密码编码 + +### 4. UserDetailsServiceImpl(用户详情服务实现) + +**功能概述** +- **类定义**: `UserDetailsServiceImpl` +- **实现接口**: `UserDetailsService` +- **业务价值**: 为Spring Security提供用户加载服务 +- **核心特性**: 用户查找、账户锁定检查、异常处理 + +**核心方法** +```java +@Service +@RequiredArgsConstructor +public class UserDetailsServiceImpl implements UserDetailsService { + private final UserAccountRepository userAccountRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 根据邮箱查找用户 + UserAccount userAccount = userAccountRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("未找到邮箱为 " + username + " 的用户。")); + + // 检查账户锁定状态 + if (userAccount.getLockoutEndTime() != null && + userAccount.getLockoutEndTime().isAfter(LocalDateTime.now())) { + throw new LockedException("该账户已被锁定,请稍后再试。"); + } + + return new CustomUserDetails(userAccount); + } +} +``` + +**业务逻辑** +- **用户查找**: 使用邮箱作为用户名进行查找 +- **锁定检查**: 检查lockoutEndTime字段判断账户是否被锁定 +- **异常处理**: 提供清晰的中文错误信息 +- **对象封装**: 返回CustomUserDetails包装的用户信息 + +## Security模块设计特点 + +### 1. 无状态认证架构 +- **JWT令牌**: 使用JWT实现无状态认证,避免服务器端会话存储 +- **令牌验证**: 每个请求都进行令牌验证,确保安全性 +- **过期管理**: 自动处理令牌过期和刷新 +- **跨服务**: 支持微服务架构下的认证共享 + +### 2. 细粒度权限控制 +- **角色映射**: 自动处理Spring Security角色前缀 +- **方法级权限**: 支持@PreAuthorize等注解进行方法级控制 +- **URL级权限**: 基于URL模式的访问控制 +- **动态权限**: 支持运行时权限检查 + +### 3. 安全防护机制 +- **CSRF防护**: 针对JWT场景禁用CSRF +- **CORS支持**: 完整的跨域资源共享配置 +- **账户锁定**: 基于时间的账户锁定机制 +- **密码安全**: BCrypt强密码编码 + +### 4. 异常处理 +- **用户友好**: 提供中文错误信息 +- **安全考虑**: 不泄露敏感信息 +- **分类处理**: 区分不同类型的认证异常 +- **日志记录**: 记录安全相关事件 + +## Security架构优势 + +### 1. 安全性强 +- **多层防护**: 从过滤器到方法级的多层安全防护 +- **标准实现**: 基于Spring Security标准实现 +- **最佳实践**: 采用业界安全最佳实践 +- **持续更新**: 跟随Spring Security版本更新 + +### 2. 性能优化 +- **无状态设计**: 避免服务器端会话存储开销 +- **一次性过滤**: 每个请求只进行一次安全检查 +- **缓存友好**: 支持用户信息缓存 +- **轻量级**: 最小化安全检查开销 + +### 3. 扩展性好 +- **插件化**: 支持自定义安全组件 +- **配置驱动**: 基于配置的安全策略 +- **接口标准**: 遵循Spring Security接口标准 +- **模块化**: 各安全组件职责清晰 + +### 4. 维护性强 +- **代码清晰**: 结构清晰,职责明确 +- **文档完整**: 详细的注释和文档 +- **测试友好**: 易于进行单元测试和集成测试 +- **监控支持**: 支持安全事件监控 + +## 技术实现细节 + +### 1. Spring Security注解 +```java +@EnableWebSecurity // 启用Web安全 +@EnableMethodSecurity // 启用方法级安全 +@Component // 组件注册 +@Service // 服务注册 +@RequiredArgsConstructor // 构造器注入 +``` + +### 2. JWT处理 +```java +// JWT令牌提取 +String jwt = authHeader.substring(7); // 移除"Bearer "前缀 +String userEmail = jwtService.extractUserName(jwt); + +// JWT令牌验证 +if (jwtService.isTokenValid(jwt, userDetails)) { + // 设置认证上下文 +} +``` + +### 3. 权限控制 +```java +// 角色权限映射 +SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + getRole().name()); + +// URL权限控制 +.requestMatchers("/api/dashboard/**").hasAnyRole("ADMIN", "DECISION_MAKER") + +// 方法级权限控制 +@PreAuthorize("hasRole('ADMIN')") +public void adminOnlyMethod() { } +``` + +### 4. 异常处理 +```java +// 用户不存在异常 +throw new UsernameNotFoundException("未找到邮箱为 " + username + " 的用户。"); + +// 账户锁定异常 +throw new LockedException("该账户已被锁定,请稍后再试。"); +``` + +## Security模块最佳实践 + +1. **令牌管理**: 合理设置JWT过期时间,实现令牌刷新机制 +2. **权限设计**: 采用最小权限原则,细粒度控制访问权限 +3. **异常处理**: 提供用户友好的错误信息,避免泄露敏感信息 +4. **日志记录**: 记录关键安全事件,便于审计和监控 +5. **配置管理**: 将安全配置外部化,支持不同环境配置 +6. **测试覆盖**: 编写完整的安全测试用例 +7. **定期更新**: 及时更新安全依赖,修复安全漏洞 +8. **监控告警**: 建立安全事件监控和告警机制 + +## Security模块总结 + +Security模块为EMS系统提供了完整、可靠的安全防护体系: + +1. **认证完整**: 基于JWT的无状态认证机制,支持多种认证场景 +2. **授权精确**: 细粒度的权限控制,从URL到方法级的全面覆盖 +3. **防护全面**: CSRF、CORS、账户锁定等多重安全防护 +4. **性能优秀**: 无状态设计,最小化安全检查开销 +5. **扩展性强**: 模块化设计,支持自定义安全组件 +6. **维护性好**: 代码清晰,文档完整,易于维护和扩展 +7. **标准兼容**: 完全基于Spring Security标准,确保兼容性 + +通过这套完整的Security模块,EMS系统建立了企业级的安全防护能力,确保了用户数据安全、系统访问控制和业务操作的安全性,为整个系统提供了坚实的安全基础。 --- - + ### Design/账号管理功能设计.md - -# 账号管理功能 - 设计文档 - -## 1. 功能描述 -本模块负责处理所有用户的认证与基础账户操作,包括注册、登录和安全设置(如忘记密码、修改密码/手机号)。其核心目标是提供一个安全、可靠且用户体验良好的身份验证入口。 - -## 2. 涉及角色 -- **所有角色**: 该功能是所有需要登录系统的用户(公众监督员、监督员、网格员、管理员、决策者)的通用基础功能。 - -## 3. 业务规则 - -### 3.1 注册规则 -- **唯一性约束**: 注册时使用的`邮箱`和`手机号`必须在`user_account`表中是唯一的。 -- **密码策略**: - - 复杂度: 8-16位,必须同时包含大写字母、小写字母、数字和特殊字符。 - - 确认机制: 注册时密码需要输入两次进行确认。 - - 存储安全: 密码在后端必须使用强哈希算法(如BCrypt)加盐后存储,绝不允许明文存储。 -- **邮箱验证码规则**: - - **时效性**: 验证码生成后5分钟内有效。 - - **冷却期**: "获取验证码"按钮点击后,有60秒的冷却时间,防止恶意请求。 - - **尝试次数**: 同一个验证码最多允许输错2次,第3次输错则该验证码立即失效,需重新获取。 - -### 3.2 登录规则 -- 支持使用`邮箱`或`手机号`作为登录凭据。 -- 登录失败时,应给出统一的、模糊的提示(如"用户名或密码错误"),避免泄露账户是否存在的信息。 -- 可考虑引入登录尝试次数限制机制,例如连续失败5次后,临时锁定账户15分钟。 - -### 3.3 安全验证规则 -- **身份确认**: "忘记密码"或"修改手机号"等敏感操作,必须先通过"邮箱 + 姓名"进行第一步身份确认。 -- **操作授权**: 身份确认通过后,必须再次完成与注册流程完全相同的邮箱动态验证码校验,才能进行下一步操作。 - -### 3.4 JWT (Token) 规则 -- **Payload 结构**: Token的Payload中应至少包含`userId`, `role`, `email`,以便后端进行快速的身份和权限识别。 -- **有效期**: Access Token的有效期应设置为较短的时间(如1小时),以降低泄露风险。 -- **刷新机制**: 提供一个Refresh Token,其有效期更长(如7天)。当Access Token过期后,前端可使用Refresh Token向后端申请新的Access Token,无需用户重新登录。 -- **存储**: 前端应将Access Token存储在内存中,将Refresh Token存储在`HttpOnly`、`Secure`的Cookie中,以提高安全性。 - -### 3.5 系统管理员操作规则 -- **创建内部账户**: 系统管理员(Admin)在NEPM端创建内部用户(如`SUPERVISOR`, `GRID_WORKER`)时,系统应生成一个安全的初始随机密码。 -- **密码重置**: 管理员可以为任何用户重置密码,重置后同样生成一个随机密码。 -- **密码分发**: 生成的初始密码或重置后的密码,应通过安全的、非系统内的方式(如邮件、短信)告知用户,并强制用户在首次登录后立即修改。 - -## 4. 功能实现流程 - -### 4.1 注册流程 -```mermaid -sequenceDiagram - participant User as 用户 - participant Frontend as 前端界面 - participant Backend as 后端服务 - participant Mail as 邮箱服务 - - User->>Frontend: 填写注册信息 (邮箱, 手机, 密码等) - Note right of Frontend: 前端实时校验格式和密码强度 - User->>Frontend: 点击"获取验证码" - Frontend->>Backend: POST /api/auth/send-verification-code - Note over Backend: 生成6位验证码, 存入Redis并设5分钟过期 - Backend->>Mail: 调用163邮箱API发送验证码 - Mail-->>User: 收到验证码邮件 - User->>Frontend: 输入收到的验证码 - User->>Frontend: 点击"注册" - Frontend->>Backend: POST /api/auth/register - Backend->>Backend: 校验验证码 (正确性, 是否过期, 尝试次数) - alt 验证成功 - Backend->>Backend: 校验邮箱/手机唯一性, BCrypt加密密码 - Backend->>Backend: 创建`user_account`记录 - Backend-->>Frontend: 注册成功 (201 Created) - else 验证失败 - Backend-->>Frontend: 返回错误信息 (400 Bad Request) - end -``` - -### 4.2 登录流程 -```mermaid -sequenceDiagram - participant User as 用户 - participant Frontend as 前端 - participant Backend as 后端 - - User->>Frontend: 输入邮箱/手机号和密码, 点击登录 - Frontend->>Backend: POST /api/auth/login - Backend->>Backend: 验证用户信息和密码 - alt 验证成功 - Backend->>Backend: 生成Access Token和Refresh Token - Backend-->>Frontend: 登录成功 (200 OK)
返回Access Token, 并设置Refresh Token到Cookie - Note left of Frontend: 存储Token, 跳转到用户主页 - else 验证失败 - Backend->>Backend: 记录失败次数, 检查是否需要锁定账户 - Backend-->>Frontend: 返回"用户名或密码错误" (401 Unauthorized) - end -``` - -### 4.3 忘记密码流程 -```mermaid -sequenceDiagram - participant User as 用户 - participant Frontend as 前端界面 - participant Backend as 后端服务 - - User->>Frontend: 点击"忘记密码", 输入邮箱和姓名 - Frontend->>Backend: POST /api/auth/request-password-reset - Backend->>Backend: 校验邮箱和姓名是否存在且匹配 - alt 身份校验成功 - Backend-->>Frontend: 跳转至邮箱验证码页面 - Note right of Frontend: (后续流程与注册时的邮箱验证完全相同) - User->>Frontend: 完成邮箱验证, 输入新密码 - Frontend->>Backend: POST /api/auth/reset-password - Backend->>Backend: 更新用户密码 - Backend-->>Frontend: 密码重置成功 (200 OK) - else 身份校验失败 - Backend-->>Frontend: 返回错误信息 (400 Bad Request) - end -``` - -## 5. API 接口设计 - -### 5.1 用户注册 -- **URL**: `POST /api/auth/register` -- **请求体**: `name`, `email`, `phone`, `password`, `verificationCode` -- **响应**: `201 Created` - -### 5.2 用户登录 -- **URL**: `POST /api/auth/login` -- **请求体**: `principal` (邮箱或手机号), `password` -- **响应**: `200 OK`, 返回 `accessToken`,并在Cookie中设置`refreshToken`。 - -### 5.3 发送邮箱验证码 -- **URL**: `POST /api/auth/send-verification-code` -- **请求体**: `email`, `type` (Enum: `REGISTER`, `RESET_PASSWORD`) -- **响应**: `200 OK` - -### 5.4 刷新AccessToken -- **URL**: `POST /api/auth/refresh-token` -- **请求体**: (空, Refresh Token从Cookie中获取) -- **响应**: `200 OK`, 返回新的`accessToken`。 - -### 5.5 请求密码重置 -- **URL**: `POST /api/auth/request-password-reset` -- **请求体**: `email`, `name` -- **响应**: `200 OK` (无论用户是否存在,都返回成功,防止信息泄露) - -### 5.6 重置密码 -- **URL**: `POST /api/auth/reset-password` -- **请求体**: `email`, `newPassword`, `verificationCode` -- **响应**: `200 OK` - -### 5.7 管理员创建用户 -- **URL**: `POST /api/admin/users` -- **权限**: `ADMIN` -- **请求体**: `name`, `email`, `phone`, `role`, `region`, `level` -- **响应**: `201 Created`, 返回包含初始随机密码的用户信息。 - -## 6. 界面设计要求 - -### 6.1 注册页面 -- 表单布局清晰,各输入项有明确的标签和占位提示。 -- "获取验证码"按钮在点击后应变为不可用状态,并显示倒计时。 -- 密码输入框应为密码类型,并提供一个可切换"显示/隐藏"密码的图标。 -- 密码强度提示:实时根据用户输入,以进度条或文字形式提示密码强度(弱、中、强)。 -- 所有输入项的错误提示应在输入框下方实时显示,内容明确。 - -### 6.2 登录页面 -- 界面简洁,突出登录表单。 -- 提供"忘记密码"的链接。 -- 可选提供"记住我"的复选框。 -- 登录成功后,应有平滑的过渡效果,并根据用户角色跳转到对应的系统主页(NEPS, NEPG, NEPM, NEPV)。 - -### 6.3 忘记密码/安全验证页面 -- 流程应分步进行,保持每一步操作的单一和清晰。 -- 第一步:身份验证(输入邮箱、姓名)。 -- 第二步:安全验证(输入邮箱收到的验证码)。 -- 第三步:重置密码(输入新密码并确认)。 -- 每个步骤都应有清晰的标题和进度指示。 -- 操作成功后,应明确提示用户"密码已重置,请使用新密码登录",并引导至登录页面。 + +# 账号管理功能 - 设计文档 + +## 1. 功能描述 +本模块负责处理所有用户的认证与基础账户操作,包括注册、登录和安全设置(如忘记密码、修改密码/手机号)。其核心目标是提供一个安全、可靠且用户体验良好的身份验证入口。 + +## 2. 涉及角色 +- **所有角色**: 该功能是所有需要登录系统的用户(公众监督员、监督员、网格员、管理员、决策者)的通用基础功能。 + +## 3. 业务规则 + +### 3.1 注册规则 +- **唯一性约束**: 注册时使用的`邮箱`和`手机号`必须在`user_account`表中是唯一的。 +- **密码策略**: + - 复杂度: 8-16位,必须同时包含大写字母、小写字母、数字和特殊字符。 + - 确认机制: 注册时密码需要输入两次进行确认。 + - 存储安全: 密码在后端必须使用强哈希算法(如BCrypt)加盐后存储,绝不允许明文存储。 +- **邮箱验证码规则**: + - **时效性**: 验证码生成后5分钟内有效。 + - **冷却期**: "获取验证码"按钮点击后,有60秒的冷却时间,防止恶意请求。 + - **尝试次数**: 同一个验证码最多允许输错2次,第3次输错则该验证码立即失效,需重新获取。 + +### 3.2 登录规则 +- 支持使用`邮箱`或`手机号`作为登录凭据。 +- 登录失败时,应给出统一的、模糊的提示(如"用户名或密码错误"),避免泄露账户是否存在的信息。 +- 可考虑引入登录尝试次数限制机制,例如连续失败5次后,临时锁定账户15分钟。 + +### 3.3 安全验证规则 +- **身份确认**: "忘记密码"或"修改手机号"等敏感操作,必须先通过"邮箱 + 姓名"进行第一步身份确认。 +- **操作授权**: 身份确认通过后,必须再次完成与注册流程完全相同的邮箱动态验证码校验,才能进行下一步操作。 + +### 3.4 JWT (Token) 规则 +- **Payload 结构**: Token的Payload中应至少包含`userId`, `role`, `email`,以便后端进行快速的身份和权限识别。 +- **有效期**: Access Token的有效期应设置为较短的时间(如1小时),以降低泄露风险。 +- **刷新机制**: 提供一个Refresh Token,其有效期更长(如7天)。当Access Token过期后,前端可使用Refresh Token向后端申请新的Access Token,无需用户重新登录。 +- **存储**: 前端应将Access Token存储在内存中,将Refresh Token存储在`HttpOnly`、`Secure`的Cookie中,以提高安全性。 + +### 3.5 系统管理员操作规则 +- **创建内部账户**: 系统管理员(Admin)在NEPM端创建内部用户(如`SUPERVISOR`, `GRID_WORKER`)时,系统应生成一个安全的初始随机密码。 +- **密码重置**: 管理员可以为任何用户重置密码,重置后同样生成一个随机密码。 +- **密码分发**: 生成的初始密码或重置后的密码,应通过安全的、非系统内的方式(如邮件、短信)告知用户,并强制用户在首次登录后立即修改。 + +## 4. 功能实现流程 + +### 4.1 注册流程 +```mermaid +sequenceDiagram + participant User as 用户 + participant Frontend as 前端界面 + participant Backend as 后端服务 + participant Mail as 邮箱服务 + + User->>Frontend: 填写注册信息 (邮箱, 手机, 密码等) + Note right of Frontend: 前端实时校验格式和密码强度 + User->>Frontend: 点击"获取验证码" + Frontend->>Backend: POST /api/auth/send-verification-code + Note over Backend: 生成6位验证码, 存入Redis并设5分钟过期 + Backend->>Mail: 调用163邮箱API发送验证码 + Mail-->>User: 收到验证码邮件 + User->>Frontend: 输入收到的验证码 + User->>Frontend: 点击"注册" + Frontend->>Backend: POST /api/auth/register + Backend->>Backend: 校验验证码 (正确性, 是否过期, 尝试次数) + alt 验证成功 + Backend->>Backend: 校验邮箱/手机唯一性, BCrypt加密密码 + Backend->>Backend: 创建`user_account`记录 + Backend-->>Frontend: 注册成功 (201 Created) + else 验证失败 + Backend-->>Frontend: 返回错误信息 (400 Bad Request) + end +``` + +### 4.2 登录流程 +```mermaid +sequenceDiagram + participant User as 用户 + participant Frontend as 前端 + participant Backend as 后端 + + User->>Frontend: 输入邮箱/手机号和密码, 点击登录 + Frontend->>Backend: POST /api/auth/login + Backend->>Backend: 验证用户信息和密码 + alt 验证成功 + Backend->>Backend: 生成Access Token和Refresh Token + Backend-->>Frontend: 登录成功 (200 OK)
返回Access Token, 并设置Refresh Token到Cookie + Note left of Frontend: 存储Token, 跳转到用户主页 + else 验证失败 + Backend->>Backend: 记录失败次数, 检查是否需要锁定账户 + Backend-->>Frontend: 返回"用户名或密码错误" (401 Unauthorized) + end +``` + +### 4.3 忘记密码流程 +```mermaid +sequenceDiagram + participant User as 用户 + participant Frontend as 前端界面 + participant Backend as 后端服务 + + User->>Frontend: 点击"忘记密码", 输入邮箱和姓名 + Frontend->>Backend: POST /api/auth/request-password-reset + Backend->>Backend: 校验邮箱和姓名是否存在且匹配 + alt 身份校验成功 + Backend-->>Frontend: 跳转至邮箱验证码页面 + Note right of Frontend: (后续流程与注册时的邮箱验证完全相同) + User->>Frontend: 完成邮箱验证, 输入新密码 + Frontend->>Backend: POST /api/auth/reset-password + Backend->>Backend: 更新用户密码 + Backend-->>Frontend: 密码重置成功 (200 OK) + else 身份校验失败 + Backend-->>Frontend: 返回错误信息 (400 Bad Request) + end +``` + +## 5. API 接口设计 + +### 5.1 用户注册 +- **URL**: `POST /api/auth/register` +- **请求体**: `name`, `email`, `phone`, `password`, `verificationCode` +- **响应**: `201 Created` + +### 5.2 用户登录 +- **URL**: `POST /api/auth/login` +- **请求体**: `principal` (邮箱或手机号), `password` +- **响应**: `200 OK`, 返回 `accessToken`,并在Cookie中设置`refreshToken`。 + +### 5.3 发送邮箱验证码 +- **URL**: `POST /api/auth/send-verification-code` +- **请求体**: `email`, `type` (Enum: `REGISTER`, `RESET_PASSWORD`) +- **响应**: `200 OK` + +### 5.4 刷新AccessToken +- **URL**: `POST /api/auth/refresh-token` +- **请求体**: (空, Refresh Token从Cookie中获取) +- **响应**: `200 OK`, 返回新的`accessToken`。 + +### 5.5 请求密码重置 +- **URL**: `POST /api/auth/request-password-reset` +- **请求体**: `email`, `name` +- **响应**: `200 OK` (无论用户是否存在,都返回成功,防止信息泄露) + +### 5.6 重置密码 +- **URL**: `POST /api/auth/reset-password` +- **请求体**: `email`, `newPassword`, `verificationCode` +- **响应**: `200 OK` + +### 5.7 管理员创建用户 +- **URL**: `POST /api/admin/users` +- **权限**: `ADMIN` +- **请求体**: `name`, `email`, `phone`, `role`, `region`, `level` +- **响应**: `201 Created`, 返回包含初始随机密码的用户信息。 + +## 6. 界面设计要求 + +### 6.1 注册页面 +- 表单布局清晰,各输入项有明确的标签和占位提示。 +- "获取验证码"按钮在点击后应变为不可用状态,并显示倒计时。 +- 密码输入框应为密码类型,并提供一个可切换"显示/隐藏"密码的图标。 +- 密码强度提示:实时根据用户输入,以进度条或文字形式提示密码强度(弱、中、强)。 +- 所有输入项的错误提示应在输入框下方实时显示,内容明确。 + +### 6.2 登录页面 +- 界面简洁,突出登录表单。 +- 提供"忘记密码"的链接。 +- 可选提供"记住我"的复选框。 +- 登录成功后,应有平滑的过渡效果,并根据用户角色跳转到对应的系统主页(NEPS, NEPG, NEPM, NEPV)。 + +### 6.3 忘记密码/安全验证页面 +- 流程应分步进行,保持每一步操作的单一和清晰。 +- 第一步:身份验证(输入邮箱、姓名)。 +- 第二步:安全验证(输入邮箱收到的验证码)。 +- 第三步:重置密码(输入新密码并确认)。 +- 每个步骤都应有清晰的标题和进度指示。 +- 操作成功后,应明确提示用户"密码已重置,请使用新密码登录",并引导至登录页面。 --- - + ### Design/主管任务管理功能设计.md - -# 主管任务管理功能 - 设计文档 - -## 1. 功能描述 -本功能模块是系统管理(NEPM)端的核心,赋予**主管**监控所有待处理反馈,并通过手动或智能两种模式,将勘查任务高效、合理地分配给一线网格员的能力。智能分配是本模块的亮点,旨在优化资源配置,提升响应效率。 - -## 2. 涉及角色 -- **主要使用者**: `业务主管 (SUPERVISOR)` -- **被分配者**: `网格员 (GRID_WORKER)` - -## 3. 业务规则 - -### 3.1 任务池规则 -- 所有通过AI审核,状态为`PENDING_ASSIGNMENT`的反馈,都会进入任务分配池,展示在**主管**的工作台。 -- 任务池中的任务应支持多维度筛选(如`区域`、`污染类型`、`严重等级`)和排序。 - -### 3.2 手动分配规则 -- **主管**可以勾选一个或多个任务。 -- 然后,**主管**可以从其管辖范围内的网格员列表中,选择一个或多个来执行任务。 -- 分配后,系统会为每一个"任务-网格员"配对创建一条`assignment_record`记录。 - -### 3.3 智能分配规则 -- **触发**: **主管**选择一个任务,点击"智能分配"按钮。 -- **地图模型**: 分配算法基于简化的二维网格地图。 -- **第一步:候选集生成**: - - 系统获取任务所在的网格坐标 (Tx, Ty)。 - - 以 (Tx, Ty) 为中心,搜索**半径10个网格单位**内的所有网格。 - - 从中筛选出状态为`ACTIVE`且有对应`skills`的网格员,形成候选集。 -- **第二步:最优人选计算**: - - 对候选集中的每个网格员 (Gx, Gy),使用**A\*算法**计算其到目标网格的**最短路径距离 (D)**,算法需能识别并绕开`is_obstacle`为`true`的障碍网格。 - - **ETR (预计任务响应时间)** 公式: `ETR = D + (该网格员当前待处理的任务量)`。 - - ETR值**最小**的网格员即为最优人选。 -- **第三步:推荐与确认**: - - 系统在界面上高亮推荐最优人选。 - - **主管**可以接受推荐,一键分配;也可以忽略推荐,手动选择其他候选人。 -- **算法细节记录**: 每次智能分配成功后,应将计算出的ETR、候选人列表等关键信息,以JSON格式快照存入`assignment_record`表的`algorithmDetails`字段,便于审计和追溯。 - -### 3.4 任务创建与审核规则 -- **创建任务**: 主管可不依赖公众反馈,直接在系统中创建新任务,指派给特定网格员。创建的任务直接进入`ASSIGNED`状态。 -- **审核任务**: 主管负责审核网格员提交的状态为`SUBMITTED`的任务。 - - **批准**: 任务状态变为 `COMPLETED`。 - - **拒绝**: 任务状态退回 `ASSIGNED`,并附上拒绝理由,网格员需重新处理。 - -### 3.5 ETR算法优化 -为了使智能分配更精准,ETR(预计任务响应时间)的计算公式将引入更多权重因子。 -- **基础公式**: `ETR = (A*路径距离) + (当前任务数)` -- **优化后公式**: - `Score = (Wd * D) + (Wn * N) - (Ws * S) + (Wp * P)` - - `D`: A*算法计算出的路径距离。 - - `N`: 网格员当前待办任务数量。 - - `S`: 技能匹配度。如果网格员的`skills`与任务的`pollutionType`匹配,则S=1,否则S=0。 - - `P`: 任务优先级。高优先级任务P=10,中=5,低=0。用于打破僵局。 - - `Wd, Wn, Ws, Wp`: 分别是距离、任务数、技能、优先级的权重系数,可在系统配置中调整。 - - **最终选择Score最小的网格员**。 - -### 3.6 边界情况处理 -- **无可用网格员**: 在指定的搜索半径内,若没有符合条件的`ACTIVE`状态的网格员,系统应提示主管"当前区域无可用网格员,请扩大搜索范围或手动从全局列表选择"。 -- **智能分配失败**: 若因地图数据不完整等原因导致A*算法无法找到路径,系统应提示"路径计算失败,请手动分配"。 - -## 4. 功能实现流程 - -### 4.1 任务分配流程 -```mermaid -graph TD - A[登录NEPM端] --> B[进入任务分配页面]; - B --> C[查看"待处理"反馈列表]; - - C --> D{选择一个反馈}; - D --> E{选择分配模式}; - - E -- 手动分配 --> F[手动勾选网格员]; - E -- 智能分配 --> G[系统执行A*算法
计算ETR并推荐人选]; - - G --> H{主管是否
接受推荐?}; - H -- 是 --> I[确认分配给
推荐人选]; - H -- 否 --> F; - - F --> J[确认分配]; - I --> J; - - J --> K[创建AssignmentRecord]; - K --> L[更新Feedback状态
为ASSIGNED]; - L --> M[通过NEPG端
通知网格员]; - - style G fill:#E3F2FD - style L fill:#C8E6C9 -``` - -### 4.2 任务审核流程 -```mermaid -graph TD - A[主管进入"待审核"任务列表] --> B{选择一个任务}; - B --> C[查看网格员提交的报告和图片]; - C --> D{审核是否通过?}; - D -- 是 --> E[更新任务状态为 COMPLETED]; - D -- 否 --> F[填写拒绝理由]; - F --> G[更新任务状态为 ASSIGNED]; - G --> H[通知网格员重新处理]; - - style E fill:#C8E6C9; - style G fill:#FFCDD2; -``` - -## 5. API 接口设计 - -### 5.1 查询待处理反馈列表 -- **URL**: `GET /api/management/tasks/pending` -- **权限**: `SUPERVISOR` -- **查询参数**: `region`, `pollutionType`, `severity`, `page`, `size` -- **成功响应** (`200 OK`): 返回分页的反馈列表。 - -### 5.2 查询可用网格员列表 -- **URL**: `GET /api/management/grid-workers/available` -- **权限**: `SUPERVISOR` -- **查询参数**: `region` -- **成功响应** (`200 OK`): 返回指定区域内所有状态为`ACTIVE`的网格员列表,包含其当前位置和任务负载。 - -### 5.3 手动分配任务 -- **URL**: `POST /api/management/tasks/assign/manual` -- **权限**: `SUPERVISOR` -- **请求体**: - ```json - { - "feedbackId": 123, - "gridWorkerIds": [10, 15], - "remarks": "请尽快处理" - } - ``` -- **成功响应** (`200 OK`): `{ "message": "任务分配成功" }` - -### 5.4 智能分配任务 -- **URL**: `POST /api/management/tasks/assign/intelligent` -- **权限**: `SUPERVISOR` -- **请求体**: `{"feedbackId": 123}` -- **成功响应** (`200 OK`): 返回包含最优人选和候选集信息的JSON,供前端渲染。 - ```json - { - "recommended": { "userId": 10, "name": "王伟", "score": 15.8, ... }, - "candidates": [ ... ] - } - ``` - -### 5.5 审核任务 -- **URL**: `POST /api/management/tasks/{taskId}/review` -- **权限**: `SUPERVISOR` -- **请求体**: - ```json - { - "approved": false, - "comments": "图片不清晰,请重新拍摄现场照片。" - } - ``` -- **成功响应** (`200 OK`): `{ "message": "审核操作已完成" }` - -## 6. 界面设计要求 - -### 6.1 任务分配主页面 -- **布局**: 采用左右分栏布局。 - - **左侧**: 待处理反馈列表。以紧凑的列表或表格形式展示,包含`标题`、`区域`、`严重等级`。提供筛选和搜索功能。 - - **右侧**: 网格员地图视图。实时展示所选反馈位置,以及管辖区域内所有网格员的分布。**每个网格员的图标应能通过大小或颜色深浅,直观反映其当前的负载(待处理任务数)**。 -- **交互**: - - 点击左侧列表中的一个反馈项,右侧地图应立即定位到该反馈所在的网格,并高亮显示。 - - 同时,地图上应以不同颜色或图标区分网格员的实时状态(如`空闲`-绿色,`繁忙`-橙色,`休假`-灰色)。 - -### 6.2 分配操作界面 -- **触发**: **主管**在左侧列表中选中一个反馈后,列表项下方出现"手动分配"和"智能分配"两个按钮。 -- **手动分配**: - - 点击后,弹出一个包含所有可选网格员列表的对话框。 - - 列表支持搜索和多选。 - - **主管**勾选后点击确认即可分配。 -- **智能分配**: - - 点击后,右侧地图视图进入"推荐模式"。 - - 系统计算后,在地图上用醒目的高亮效果(如闪烁的星星或光圈)标记出最优人选。 - - 同时,在地图下方或侧边栏显示一个信息卡片,包含推荐人选的姓名、当前任务量、**技能匹配度**、**距离**和最终的**推荐分数**,并提供一个"一键分配"按钮。 - - 其他候选人以普通方式显示,**主管**仍可点击查看其分数详情并选择。 - -### 6.3 任务创建页面 -- 提供一个独立的表单页面,允许主管填写`标题`、`描述`、`污染类型`、`严重等级`、`地理位置`等所有任务相关信息。 -- 表单下方直接集成一个网格员选择器,方便主管创建后立即指派。 - -### 6.4 任务审核页面 -- 这是一个任务详情页面,顶部清晰展示任务基础信息。 -- 页面核心区域展示网格员提交的报告,包括文字说明和图片(图片支持点击放大预览)。 -- 页面底部提供"批准"和"拒绝"两个操作按钮。点击"拒绝"时,必须弹出一个对话框,要求主管填写拒绝理由。 + +# 主管任务管理功能 - 设计文档 + +## 1. 功能描述 +本功能模块是系统管理(NEPM)端的核心,赋予**主管**监控所有待处理反馈,并通过手动或智能两种模式,将勘查任务高效、合理地分配给一线网格员的能力。智能分配是本模块的亮点,旨在优化资源配置,提升响应效率。 + +## 2. 涉及角色 +- **主要使用者**: `业务主管 (SUPERVISOR)` +- **被分配者**: `网格员 (GRID_WORKER)` + +## 3. 业务规则 + +### 3.1 任务池规则 +- 所有通过AI审核,状态为`PENDING_ASSIGNMENT`的反馈,都会进入任务分配池,展示在**主管**的工作台。 +- 任务池中的任务应支持多维度筛选(如`区域`、`污染类型`、`严重等级`)和排序。 + +### 3.2 手动分配规则 +- **主管**可以勾选一个或多个任务。 +- 然后,**主管**可以从其管辖范围内的网格员列表中,选择一个或多个来执行任务。 +- 分配后,系统会为每一个"任务-网格员"配对创建一条`assignment_record`记录。 + +### 3.3 智能分配规则 +- **触发**: **主管**选择一个任务,点击"智能分配"按钮。 +- **地图模型**: 分配算法基于简化的二维网格地图。 +- **第一步:候选集生成**: + - 系统获取任务所在的网格坐标 (Tx, Ty)。 + - 以 (Tx, Ty) 为中心,搜索**半径10个网格单位**内的所有网格。 + - 从中筛选出状态为`ACTIVE`且有对应`skills`的网格员,形成候选集。 +- **第二步:最优人选计算**: + - 对候选集中的每个网格员 (Gx, Gy),使用**A\*算法**计算其到目标网格的**最短路径距离 (D)**,算法需能识别并绕开`is_obstacle`为`true`的障碍网格。 + - **ETR (预计任务响应时间)** 公式: `ETR = D + (该网格员当前待处理的任务量)`。 + - ETR值**最小**的网格员即为最优人选。 +- **第三步:推荐与确认**: + - 系统在界面上高亮推荐最优人选。 + - **主管**可以接受推荐,一键分配;也可以忽略推荐,手动选择其他候选人。 +- **算法细节记录**: 每次智能分配成功后,应将计算出的ETR、候选人列表等关键信息,以JSON格式快照存入`assignment_record`表的`algorithmDetails`字段,便于审计和追溯。 + +### 3.4 任务创建与审核规则 +- **创建任务**: 主管可不依赖公众反馈,直接在系统中创建新任务,指派给特定网格员。创建的任务直接进入`ASSIGNED`状态。 +- **审核任务**: 主管负责审核网格员提交的状态为`SUBMITTED`的任务。 + - **批准**: 任务状态变为 `COMPLETED`。 + - **拒绝**: 任务状态退回 `ASSIGNED`,并附上拒绝理由,网格员需重新处理。 + +### 3.5 ETR算法优化 +为了使智能分配更精准,ETR(预计任务响应时间)的计算公式将引入更多权重因子。 +- **基础公式**: `ETR = (A*路径距离) + (当前任务数)` +- **优化后公式**: + `Score = (Wd * D) + (Wn * N) - (Ws * S) + (Wp * P)` + - `D`: A*算法计算出的路径距离。 + - `N`: 网格员当前待办任务数量。 + - `S`: 技能匹配度。如果网格员的`skills`与任务的`pollutionType`匹配,则S=1,否则S=0。 + - `P`: 任务优先级。高优先级任务P=10,中=5,低=0。用于打破僵局。 + - `Wd, Wn, Ws, Wp`: 分别是距离、任务数、技能、优先级的权重系数,可在系统配置中调整。 + - **最终选择Score最小的网格员**。 + +### 3.6 边界情况处理 +- **无可用网格员**: 在指定的搜索半径内,若没有符合条件的`ACTIVE`状态的网格员,系统应提示主管"当前区域无可用网格员,请扩大搜索范围或手动从全局列表选择"。 +- **智能分配失败**: 若因地图数据不完整等原因导致A*算法无法找到路径,系统应提示"路径计算失败,请手动分配"。 + +## 4. 功能实现流程 + +### 4.1 任务分配流程 +```mermaid +graph TD + A[登录NEPM端] --> B[进入任务分配页面]; + B --> C[查看"待处理"反馈列表]; + + C --> D{选择一个反馈}; + D --> E{选择分配模式}; + + E -- 手动分配 --> F[手动勾选网格员]; + E -- 智能分配 --> G[系统执行A*算法
计算ETR并推荐人选]; + + G --> H{主管是否
接受推荐?}; + H -- 是 --> I[确认分配给
推荐人选]; + H -- 否 --> F; + + F --> J[确认分配]; + I --> J; + + J --> K[创建AssignmentRecord]; + K --> L[更新Feedback状态
为ASSIGNED]; + L --> M[通过NEPG端
通知网格员]; + + style G fill:#E3F2FD + style L fill:#C8E6C9 +``` + +### 4.2 任务审核流程 +```mermaid +graph TD + A[主管进入"待审核"任务列表] --> B{选择一个任务}; + B --> C[查看网格员提交的报告和图片]; + C --> D{审核是否通过?}; + D -- 是 --> E[更新任务状态为 COMPLETED]; + D -- 否 --> F[填写拒绝理由]; + F --> G[更新任务状态为 ASSIGNED]; + G --> H[通知网格员重新处理]; + + style E fill:#C8E6C9; + style G fill:#FFCDD2; +``` + +## 5. API 接口设计 + +### 5.1 查询待处理反馈列表 +- **URL**: `GET /api/management/tasks/pending` +- **权限**: `SUPERVISOR` +- **查询参数**: `region`, `pollutionType`, `severity`, `page`, `size` +- **成功响应** (`200 OK`): 返回分页的反馈列表。 + +### 5.2 查询可用网格员列表 +- **URL**: `GET /api/management/grid-workers/available` +- **权限**: `SUPERVISOR` +- **查询参数**: `region` +- **成功响应** (`200 OK`): 返回指定区域内所有状态为`ACTIVE`的网格员列表,包含其当前位置和任务负载。 + +### 5.3 手动分配任务 +- **URL**: `POST /api/management/tasks/assign/manual` +- **权限**: `SUPERVISOR` +- **请求体**: + ```json + { + "feedbackId": 123, + "gridWorkerIds": [10, 15], + "remarks": "请尽快处理" + } + ``` +- **成功响应** (`200 OK`): `{ "message": "任务分配成功" }` + +### 5.4 智能分配任务 +- **URL**: `POST /api/management/tasks/assign/intelligent` +- **权限**: `SUPERVISOR` +- **请求体**: `{"feedbackId": 123}` +- **成功响应** (`200 OK`): 返回包含最优人选和候选集信息的JSON,供前端渲染。 + ```json + { + "recommended": { "userId": 10, "name": "王伟", "score": 15.8, ... }, + "candidates": [ ... ] + } + ``` + +### 5.5 审核任务 +- **URL**: `POST /api/management/tasks/{taskId}/review` +- **权限**: `SUPERVISOR` +- **请求体**: + ```json + { + "approved": false, + "comments": "图片不清晰,请重新拍摄现场照片。" + } + ``` +- **成功响应** (`200 OK`): `{ "message": "审核操作已完成" }` + +## 6. 界面设计要求 + +### 6.1 任务分配主页面 +- **布局**: 采用左右分栏布局。 + - **左侧**: 待处理反馈列表。以紧凑的列表或表格形式展示,包含`标题`、`区域`、`严重等级`。提供筛选和搜索功能。 + - **右侧**: 网格员地图视图。实时展示所选反馈位置,以及管辖区域内所有网格员的分布。**每个网格员的图标应能通过大小或颜色深浅,直观反映其当前的负载(待处理任务数)**。 +- **交互**: + - 点击左侧列表中的一个反馈项,右侧地图应立即定位到该反馈所在的网格,并高亮显示。 + - 同时,地图上应以不同颜色或图标区分网格员的实时状态(如`空闲`-绿色,`繁忙`-橙色,`休假`-灰色)。 + +### 6.2 分配操作界面 +- **触发**: **主管**在左侧列表中选中一个反馈后,列表项下方出现"手动分配"和"智能分配"两个按钮。 +- **手动分配**: + - 点击后,弹出一个包含所有可选网格员列表的对话框。 + - 列表支持搜索和多选。 + - **主管**勾选后点击确认即可分配。 +- **智能分配**: + - 点击后,右侧地图视图进入"推荐模式"。 + - 系统计算后,在地图上用醒目的高亮效果(如闪烁的星星或光圈)标记出最优人选。 + - 同时,在地图下方或侧边栏显示一个信息卡片,包含推荐人选的姓名、当前任务量、**技能匹配度**、**距离**和最终的**推荐分数**,并提供一个"一键分配"按钮。 + - 其他候选人以普通方式显示,**主管**仍可点击查看其分数详情并选择。 + +### 6.3 任务创建页面 +- 提供一个独立的表单页面,允许主管填写`标题`、`描述`、`污染类型`、`严重等级`、`地理位置`等所有任务相关信息。 +- 表单下方直接集成一个网格员选择器,方便主管创建后立即指派。 + +### 6.4 任务审核页面 +- 这是一个任务详情页面,顶部清晰展示任务基础信息。 +- 页面核心区域展示网格员提交的报告,包括文字说明和图片(图片支持点击放大预览)。 +- 页面底部提供"批准"和"拒绝"两个操作按钮。点击"拒绝"时,必须弹出一个对话框,要求主管填写拒绝理由。 --- - + ### Design/主管审核与任务分配功能设计.md - -# 主管审核与任务分配功能 - 设计文档 - -## 1. 功能描述 -本功能模块是NEPM(后台管理端)的核心组成部分,专为`主管 (SUPERVISOR)`和`管理员 (ADMIN)`角色设计。它弥合了AI自动审核与人工任务分配之间的鸿沟,确保了只有经过人工确认的有效反馈才能进入网格员的处理流程。 - -主要职责包括: -- **审核**: 对AI初审通过的反馈 (`PENDING_REVIEW`) 进行人工复核。 -- **决策**: 决定该反馈是应该继续流转 (`PENDING_ASSIGNMENT`) 还是直接关闭 (`CLOSED_INVALID`)。 -- **分配**: 将审核通过的有效反馈手动指派给特定的`网格员 (GRID_WORKER)`。 - -## 2. 业务流程 -```mermaid -graph TD - subgraph "AI自动审核" - A[反馈状态: PENDING_REVIEW] - end - - subgraph "主管人工审核 (Supervisor/Admin 操作)" - A --> B{主管查看待审核列表}; - B --> C{审核决策}; - C -- 审核通过 --> D[调用 Approve API
/api/supervisor/reviews/{id}/approve]; - C -- 审核拒绝 --> E[调用 Reject API
/api/supervisor/reviews/{id}/reject]; - end - - subgraph "后端服务" - D --> F[反馈状态变为
PENDING_ASSIGNMENT]; - E --> G[反馈状态变为
CLOSED_INVALID]; - end - - subgraph "主管手动分配" - F --> H{主管在任务分配界面
查看待分配列表}; - H --> I{选择反馈 + 选择网格员}; - I --> J[调用 Assign API
/api/task-assignment/assign]; - end - - subgraph "后端服务" - J --> K[创建 Assignment 记录
反馈状态变为 ASSIGNED]; - end - - style F fill:#C8E6C9,stroke:#333 - style G fill:#FFCDD2,stroke:#333 - style K fill:#B3E5FC,stroke:#333 -``` - -## 3. API 接口设计 - -### 3.1 主管审核接口 (SupervisorController) - -#### 3.1.1 获取待审核反馈列表 -- **URL**: `GET /api/supervisor/reviews` -- **描述**: 获取所有状态为 `PENDING_REVIEW` 的反馈,供主管进行人工审核。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **成功响应** (`200 OK`): 返回 `Feedback` 对象数组。 - ```json - [ - { - "id": 101, - "eventId": "uuid-...", - "title": "...", - "status": "PENDING_REVIEW", - ... - } - ] - ``` - -#### 3.1.2 批准反馈 -- **URL**: `POST /api/supervisor/reviews/{feedbackId}/approve` -- **描述**: 主管批准一个反馈,使其进入待分配状态。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **路径参数**: `feedbackId` (Long) - 要批准的反馈ID。 -- **成功响应** (`200 OK`): 无内容。 -- **失败响应**: - - `400 Bad Request`: 如果反馈状态不是 `PENDING_REVIEW`。 - - `404 Not Found`: 如果 `feedbackId` 不存在。 - -#### 3.1.3 拒绝反馈 -- **URL**: `POST /api/supervisor/reviews/{feedbackId}/reject` -- **描述**: 主管拒绝一个反馈,将其关闭。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **路径参数**: `feedbackId` (Long) - 要拒绝的反馈ID。 -- **成功响应** (`200 OK`): 无内容。 -- **失败响应**: - - `400 Bad Request`: 如果反馈状态不是 `PENDING_REVIEW`。 - - `404 Not Found`: 如果 `feedbackId` 不存在。 - - -### 3.2 任务分配接口 (TaskAssignmentController) - -#### 3.2.1 获取待分配反馈列表 -- **URL**: `GET /api/task-assignment/unassigned-feedback` -- **描述**: 获取所有状态为 `PENDING_ASSIGNMENT` 的反馈,即已通过主管审核,等待分配。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **成功响应** (`200 OK`): 返回 `Feedback` 对象数组。 - -#### 3.2.2 获取可用网格员列表 -- **URL**: `GET /api/task-assignment/available-workers` -- **描述**: 获取所有角色为 `GRID_WORKER` 的用户列表,用于分配任务时的选择器。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **成功响应** (`200 OK`): 返回 `UserAccount` 对象数组。 - -#### 3.2.3 分配任务 -- **URL**: `POST /api/task-assignment/assign` -- **描述**: 将一个反馈任务指派给一个网格员。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **请求体**: - ```json - { - "feedbackId": 101, - "assigneeId": 205 - } - ``` -- **成功响应** (`201 Created`): 返回新创建的 `Assignment` 对象。 -- **失败响应**: - - `400 Bad Request`: 如果反馈状态不是 `PENDING_ASSIGNMENT` 或用户不是网格员。 - - `404 Not Found`: 如果 `feedbackId` 或 `assigneeId` 不存在。 - -## 4. 状态机详解 -```mermaid -stateDiagram-v2 - state "AI审核通过" as PendingReview - state "待分配" as PendingAssignment - state "已关闭" as ClosedInvalid - state "已分配" as Assigned - - PendingReview --> PendingAssignment: 主管批准 - PendingReview --> ClosedInvalid: 主管拒绝 - - PendingAssignment --> Assigned: 主管分配任务 -``` + +# 主管审核与任务分配功能 - 设计文档 + +## 1. 功能描述 +本功能模块是NEPM(后台管理端)的核心组成部分,专为`主管 (SUPERVISOR)`和`管理员 (ADMIN)`角色设计。它弥合了AI自动审核与人工任务分配之间的鸿沟,确保了只有经过人工确认的有效反馈才能进入网格员的处理流程。 + +主要职责包括: +- **审核**: 对AI初审通过的反馈 (`PENDING_REVIEW`) 进行人工复核。 +- **决策**: 决定该反馈是应该继续流转 (`PENDING_ASSIGNMENT`) 还是直接关闭 (`CLOSED_INVALID`)。 +- **分配**: 将审核通过的有效反馈手动指派给特定的`网格员 (GRID_WORKER)`。 + +## 2. 业务流程 +```mermaid +graph TD + subgraph "AI自动审核" + A[反馈状态: PENDING_REVIEW] + end + + subgraph "主管人工审核 (Supervisor/Admin 操作)" + A --> B{主管查看待审核列表}; + B --> C{审核决策}; + C -- 审核通过 --> D[调用 Approve API
/api/supervisor/reviews/{id}/approve]; + C -- 审核拒绝 --> E[调用 Reject API
/api/supervisor/reviews/{id}/reject]; + end + + subgraph "后端服务" + D --> F[反馈状态变为
PENDING_ASSIGNMENT]; + E --> G[反馈状态变为
CLOSED_INVALID]; + end + + subgraph "主管手动分配" + F --> H{主管在任务分配界面
查看待分配列表}; + H --> I{选择反馈 + 选择网格员}; + I --> J[调用 Assign API
/api/task-assignment/assign]; + end + + subgraph "后端服务" + J --> K[创建 Assignment 记录
反馈状态变为 ASSIGNED]; + end + + style F fill:#C8E6C9,stroke:#333 + style G fill:#FFCDD2,stroke:#333 + style K fill:#B3E5FC,stroke:#333 +``` + +## 3. API 接口设计 + +### 3.1 主管审核接口 (SupervisorController) + +#### 3.1.1 获取待审核反馈列表 +- **URL**: `GET /api/supervisor/reviews` +- **描述**: 获取所有状态为 `PENDING_REVIEW` 的反馈,供主管进行人工审核。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **成功响应** (`200 OK`): 返回 `Feedback` 对象数组。 + ```json + [ + { + "id": 101, + "eventId": "uuid-...", + "title": "...", + "status": "PENDING_REVIEW", + ... + } + ] + ``` + +#### 3.1.2 批准反馈 +- **URL**: `POST /api/supervisor/reviews/{feedbackId}/approve` +- **描述**: 主管批准一个反馈,使其进入待分配状态。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **路径参数**: `feedbackId` (Long) - 要批准的反馈ID。 +- **成功响应** (`200 OK`): 无内容。 +- **失败响应**: + - `400 Bad Request`: 如果反馈状态不是 `PENDING_REVIEW`。 + - `404 Not Found`: 如果 `feedbackId` 不存在。 + +#### 3.1.3 拒绝反馈 +- **URL**: `POST /api/supervisor/reviews/{feedbackId}/reject` +- **描述**: 主管拒绝一个反馈,将其关闭。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **路径参数**: `feedbackId` (Long) - 要拒绝的反馈ID。 +- **成功响应** (`200 OK`): 无内容。 +- **失败响应**: + - `400 Bad Request`: 如果反馈状态不是 `PENDING_REVIEW`。 + - `404 Not Found`: 如果 `feedbackId` 不存在。 + + +### 3.2 任务分配接口 (TaskAssignmentController) + +#### 3.2.1 获取待分配反馈列表 +- **URL**: `GET /api/task-assignment/unassigned-feedback` +- **描述**: 获取所有状态为 `PENDING_ASSIGNMENT` 的反馈,即已通过主管审核,等待分配。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **成功响应** (`200 OK`): 返回 `Feedback` 对象数组。 + +#### 3.2.2 获取可用网格员列表 +- **URL**: `GET /api/task-assignment/available-workers` +- **描述**: 获取所有角色为 `GRID_WORKER` 的用户列表,用于分配任务时的选择器。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **成功响应** (`200 OK`): 返回 `UserAccount` 对象数组。 + +#### 3.2.3 分配任务 +- **URL**: `POST /api/task-assignment/assign` +- **描述**: 将一个反馈任务指派给一个网格员。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **请求体**: + ```json + { + "feedbackId": 101, + "assigneeId": 205 + } + ``` +- **成功响应** (`201 Created`): 返回新创建的 `Assignment` 对象。 +- **失败响应**: + - `400 Bad Request`: 如果反馈状态不是 `PENDING_ASSIGNMENT` 或用户不是网格员。 + - `404 Not Found`: 如果 `feedbackId` 或 `assigneeId` 不存在。 + +## 4. 状态机详解 +```mermaid +stateDiagram-v2 + state "AI审核通过" as PendingReview + state "待分配" as PendingAssignment + state "已关闭" as ClosedInvalid + state "已分配" as Assigned + + PendingReview --> PendingAssignment: 主管批准 + PendingReview --> ClosedInvalid: 主管拒绝 + + PendingAssignment --> Assigned: 主管分配任务 +``` --- - + ### Design/总体设计.md - -# 东软环保公众监督系统 - 总体设计文档 - -## 1. 项目背景与目标 - -### 1.1 项目背景 -随着公众环保意识的增强和环境问题的日益复杂化,传统单一的环境监督模式已难以满足现代社会的需求。为提升环境治理的效率、透明度和公众参与度,我们提出构建一个创新的、多端协同的环保监督平台——"东软环保公众监督系统"。 - -### 1.2 需求背景 -本项目的核心需求源于对现有环保监督流程的优化渴望。当前流程存在信息传递链条长、问题响应慢、数据孤岛效应明显、公众参与感不强等痛点。系统旨在通过整合公众、一线网格员、系统管理者和宏观决策者的力量,打通从问题发现、数据上报、任务分配、现场核实到数据分析和决策支持的全流程闭环。 - -### 1.3 项目目标 -- **提升监督效率**:通过AI预审和智能化任务分配,缩短问题响应时间。 -- **强化公众参与**:提供便捷、透明的反馈渠道,激励公众成为环保监督的"眼睛"。 -- **实现数据驱动**:将零散的反馈数据转化为结构化的、可供分析决策的宝贵资产。 -- **构建管理闭环**:打造一个覆盖"公众-执行-管理-决策"四位一体的协同工作平台。 - -## 2. 功能总体描述 -系统整体上由四个功能明确、相互独立的客户端构成,共同协作完成环保监督任务。 - -- **NEPS端 (公众监督员端)**: 系统的公众入口,允许用户注册、提交污染反馈、追踪反馈处理进度,并查看个人统计数据。 -- **NEPG端 (AQI检测网格员端)**: 一线执行人员的操作平台,用于接收任务、在简化的网格地图上进行路径指引、提交现场勘查的AQI数据报告。 -- **NEPM端 (系统管理端)**: 系统的"大脑",供**主管(Supervisor)**审核反馈、通过手动或智能模式分配任务;同时供**系统管理员(Admin)**管理系统用户、角色及权限。 -- **NEPV端 (可视化大屏端)**: 决策支持中心,以数据可视化的方式向决策者展示区域环境态势、污染热力图、治理成效等宏观指标,并提供AI生成的年度报告。 - -## 3. 技术架构选型 - -为保证系统的稳定性、可扩展性和开发效率,我们选择以下主流且成熟的技术栈: - -### 3.1 开发语言 -- **后端**: Java 21 (充分利用其新特性,如虚拟线程、Record类等) -- **前端**: TypeScript (为JavaScript提供类型安全,提升代码质量) - -### 3.2 开发框架 -- **后端**: Spring Boot 3.x - - **核心优势**: 自动化配置、强大的社区生态、内嵌Web服务器,能快速构建稳定、高效的RESTful API。 - - **关键组件**: - - `Spring Web`: 构建Web应用和API。 - - `Spring Data JPA`: 简化数据库持久化操作。 - - `Spring Security`: 提供强大而灵活的认证和授权功能,保障系统安全。 - - `Spring Validation`: 进行数据校验。 -- **前端**: Vue 3.x - - **核心优势**: 采用组合式API (Composition API),逻辑组织更清晰;基于Vite的构建工具,开发体验极佳;响应式系统性能优越。 - - **关键组件**: - - `Vue Router`: 管理前端路由。 - - `Pinia`: 进行应用的状态管理。 - - `Axios`: 用于与后端API进行HTTP通信。 - - `Element Plus / Ant Design Vue`: 作为基础UI组件库,快速构建美观的界面。 - -### 3.3 数据库 -- **数据库**: MySQL 8.x - - **核心优势**: 作为全球最受欢迎的开源关系型数据库,MySQL具有性能稳定、社区支持广泛、与Java及Spring Boot生态完美集成的优点。其成熟的事务处理和数据一致性保障能力,完全满足本项目对核心业务数据的存储需求。 - -### 3.4 关键第三方服务 -- **邮箱服务**: 调用163邮箱的SMTP API,实现注册、忘记密码等场景的动态验证码发送。 -- **AI大模型服务**: 调用火山引擎的`deepseek-v3-250324`模型API,实现对用户反馈的自动审核与关键信息提取。 - -## 4. 系统架构设计 - -### 4.1 总体架构图 - -系统采用微服务思想指导下的单体架构,前后端分离,四端统一调用后端API。这种设计在项目初期能保证快速迭代,同时为未来向微服务演进预留了空间。 - -```mermaid -graph TD - subgraph 用户端 - A[NEPS 公众监督员端
(Vue3/H5)] - B[NEPG 网格员端
(Vue3/H5)] - C[NEPM 系统管理端
(Vue3/Web)] - D[NEPV 可视化大屏端
(Vue3/Web)] - end - - subgraph "后端服务 (Spring Boot)" - E(API网关
Spring Cloud Gateway) - F[用户认证服务
Spring Security] - G[反馈管理模块] - H[任务管理模块] - I[智能分配模块
A*算法] - J[数据可视化模块] - K[文件存储模块] - end - - subgraph "数据与服务" - L[MySQL 8.x
主数据库] - M[火山引擎
AI大模型] - N[163邮箱
SMTP服务] - end - - A -->|HTTPS/REST API| E - B -->|HTTPS/REST API| E - C -->|HTTPS/REST API| E - D -->|HTTPS/REST API| E - - E --> F - E --> G - E --> H - E --> I - E --> J - E --> K - - F --> L - G --> L - H --> L - I --> L - J --> L - K --> L - - G -->|异步调用| M - F -->|SMTP| N - -``` - -### 4.2 核心数据流(问题处理流程) - -1. **提交**: 公众监督员通过NEPS端提交环境问题反馈,数据进入后端`反馈管理模块`,初始状态为`AI审核中`。 -2. **审核**: `反馈管理模块`异步调用`火山引擎AI服务`进行内容审核与信息提取。审核通过后,状态更新为`待处理`。 -3. **分配**: 主管在NEPM端查看`待处理`的反馈。`任务管理模块`调用`智能分配模块`的A*算法,推荐最优网格员。主管确认后,生成任务并分配。 -4. **处理**: 网格员在NEPG端接收任务,前往现场处理,并提交包含AQI数据的报告。 -5. **审核与闭环**: 主管在NEPM端审核任务报告。审核通过则任务完成,状态更新为`已完成`;不通过则打回给网格员。 -6. **可视化**: 所有处理完成的数据,由`数据可视化模块`进行聚合与分析,最终在NEPV大屏端展示。 - -## 5. 非功能性需求设计 - -### 5.1 性能需求 -- **核心API响应时间**: 95%的核心API请求(如登录、任务查询)应在500ms内响应。 -- **AI处理时间**: AI审核与信息提取的异步处理过程应在30秒内完成。 -- **并发用户数**: 系统应支持至少500个并发用户在线,核心功能稳定运行。 -- **大屏刷新率**: NEPV端数据应支持分钟级刷新。 - -### 5.2 安全性需求 -- **认证与授权**: 所有API均需通过Spring Security进行认证和授权检查,防止未授权访问。 -- **数据传输**: 全站强制使用HTTPS,确保数据在传输过程中的加密。 -- **数据存储**: 用户密码、API密钥等敏感信息必须加密存储。 -- **防SQL注入**: 采用JPA和参数化查询,从根本上防止SQL注入攻击。 -- **防XSS攻击**: 前端对用户输入进行严格过滤和转义,防止跨站脚本攻击。 - -### 5.3 可扩展性需求 -- **模块化设计**: 后端业务逻辑严格按照模块划分,降低耦合度,便于未来将单个模块拆分为微服务。 -- **无状态服务**: 后端核心业务服务设计为无状态,便于水平扩展和负载均衡。 -- **消息队列**: (未来规划)引入RabbitMQ或Kafka,实现服务间的异步解耦,提升系统的削峰填谷和弹性能力。 - -### 5.4 可维护性需求 -- **统一编码规范**: 全项目遵循统一的Java和TypeScript编码规范。 -- **日志记录**: 对关键业务流程和异常情况进行详细的结构化日志记录,便于问题排查。 -- **API文档**: 使用Swagger/OpenAPI自动生成并维护最新的API文档。 -- **模块化前端**: 前端代码按四端和功能模块组织,提高代码的可读性和复用性。 + +# 东软环保公众监督系统 - 总体设计文档 + +## 1. 项目背景与目标 + +### 1.1 项目背景 +随着公众环保意识的增强和环境问题的日益复杂化,传统单一的环境监督模式已难以满足现代社会的需求。为提升环境治理的效率、透明度和公众参与度,我们提出构建一个创新的、多端协同的环保监督平台——"东软环保公众监督系统"。 + +### 1.2 需求背景 +本项目的核心需求源于对现有环保监督流程的优化渴望。当前流程存在信息传递链条长、问题响应慢、数据孤岛效应明显、公众参与感不强等痛点。系统旨在通过整合公众、一线网格员、系统管理者和宏观决策者的力量,打通从问题发现、数据上报、任务分配、现场核实到数据分析和决策支持的全流程闭环。 + +### 1.3 项目目标 +- **提升监督效率**:通过AI预审和智能化任务分配,缩短问题响应时间。 +- **强化公众参与**:提供便捷、透明的反馈渠道,激励公众成为环保监督的"眼睛"。 +- **实现数据驱动**:将零散的反馈数据转化为结构化的、可供分析决策的宝贵资产。 +- **构建管理闭环**:打造一个覆盖"公众-执行-管理-决策"四位一体的协同工作平台。 + +## 2. 功能总体描述 +系统整体上由四个功能明确、相互独立的客户端构成,共同协作完成环保监督任务。 + +- **NEPS端 (公众监督员端)**: 系统的公众入口,允许用户注册、提交污染反馈、追踪反馈处理进度,并查看个人统计数据。 +- **NEPG端 (AQI检测网格员端)**: 一线执行人员的操作平台,用于接收任务、在简化的网格地图上进行路径指引、提交现场勘查的AQI数据报告。 +- **NEPM端 (系统管理端)**: 系统的"大脑",供**主管(Supervisor)**审核反馈、通过手动或智能模式分配任务;同时供**系统管理员(Admin)**管理系统用户、角色及权限。 +- **NEPV端 (可视化大屏端)**: 决策支持中心,以数据可视化的方式向决策者展示区域环境态势、污染热力图、治理成效等宏观指标,并提供AI生成的年度报告。 + +## 3. 技术架构选型 + +为保证系统的稳定性、可扩展性和开发效率,我们选择以下主流且成熟的技术栈: + +### 3.1 开发语言 +- **后端**: Java 21 (充分利用其新特性,如虚拟线程、Record类等) +- **前端**: TypeScript (为JavaScript提供类型安全,提升代码质量) + +### 3.2 开发框架 +- **后端**: Spring Boot 3.x + - **核心优势**: 自动化配置、强大的社区生态、内嵌Web服务器,能快速构建稳定、高效的RESTful API。 + - **关键组件**: + - `Spring Web`: 构建Web应用和API。 + - `Spring Data JPA`: 简化数据库持久化操作。 + - `Spring Security`: 提供强大而灵活的认证和授权功能,保障系统安全。 + - `Spring Validation`: 进行数据校验。 +- **前端**: Vue 3.x + - **核心优势**: 采用组合式API (Composition API),逻辑组织更清晰;基于Vite的构建工具,开发体验极佳;响应式系统性能优越。 + - **关键组件**: + - `Vue Router`: 管理前端路由。 + - `Pinia`: 进行应用的状态管理。 + - `Axios`: 用于与后端API进行HTTP通信。 + - `Element Plus / Ant Design Vue`: 作为基础UI组件库,快速构建美观的界面。 + +### 3.3 数据库 +- **数据库**: MySQL 8.x + - **核心优势**: 作为全球最受欢迎的开源关系型数据库,MySQL具有性能稳定、社区支持广泛、与Java及Spring Boot生态完美集成的优点。其成熟的事务处理和数据一致性保障能力,完全满足本项目对核心业务数据的存储需求。 + +### 3.4 关键第三方服务 +- **邮箱服务**: 调用163邮箱的SMTP API,实现注册、忘记密码等场景的动态验证码发送。 +- **AI大模型服务**: 调用火山引擎的`deepseek-v3-250324`模型API,实现对用户反馈的自动审核与关键信息提取。 + +## 4. 系统架构设计 + +### 4.1 总体架构图 + +系统采用微服务思想指导下的单体架构,前后端分离,四端统一调用后端API。这种设计在项目初期能保证快速迭代,同时为未来向微服务演进预留了空间。 + +```mermaid +graph TD + subgraph 用户端 + A[NEPS 公众监督员端
(Vue3/H5)] + B[NEPG 网格员端
(Vue3/H5)] + C[NEPM 系统管理端
(Vue3/Web)] + D[NEPV 可视化大屏端
(Vue3/Web)] + end + + subgraph "后端服务 (Spring Boot)" + E(API网关
Spring Cloud Gateway) + F[用户认证服务
Spring Security] + G[反馈管理模块] + H[任务管理模块] + I[智能分配模块
A*算法] + J[数据可视化模块] + K[文件存储模块] + end + + subgraph "数据与服务" + L[MySQL 8.x
主数据库] + M[火山引擎
AI大模型] + N[163邮箱
SMTP服务] + end + + A -->|HTTPS/REST API| E + B -->|HTTPS/REST API| E + C -->|HTTPS/REST API| E + D -->|HTTPS/REST API| E + + E --> F + E --> G + E --> H + E --> I + E --> J + E --> K + + F --> L + G --> L + H --> L + I --> L + J --> L + K --> L + + G -->|异步调用| M + F -->|SMTP| N + +``` + +### 4.2 核心数据流(问题处理流程) + +1. **提交**: 公众监督员通过NEPS端提交环境问题反馈,数据进入后端`反馈管理模块`,初始状态为`AI审核中`。 +2. **审核**: `反馈管理模块`异步调用`火山引擎AI服务`进行内容审核与信息提取。审核通过后,状态更新为`待处理`。 +3. **分配**: 主管在NEPM端查看`待处理`的反馈。`任务管理模块`调用`智能分配模块`的A*算法,推荐最优网格员。主管确认后,生成任务并分配。 +4. **处理**: 网格员在NEPG端接收任务,前往现场处理,并提交包含AQI数据的报告。 +5. **审核与闭环**: 主管在NEPM端审核任务报告。审核通过则任务完成,状态更新为`已完成`;不通过则打回给网格员。 +6. **可视化**: 所有处理完成的数据,由`数据可视化模块`进行聚合与分析,最终在NEPV大屏端展示。 + +## 5. 非功能性需求设计 + +### 5.1 性能需求 +- **核心API响应时间**: 95%的核心API请求(如登录、任务查询)应在500ms内响应。 +- **AI处理时间**: AI审核与信息提取的异步处理过程应在30秒内完成。 +- **并发用户数**: 系统应支持至少500个并发用户在线,核心功能稳定运行。 +- **大屏刷新率**: NEPV端数据应支持分钟级刷新。 + +### 5.2 安全性需求 +- **认证与授权**: 所有API均需通过Spring Security进行认证和授权检查,防止未授权访问。 +- **数据传输**: 全站强制使用HTTPS,确保数据在传输过程中的加密。 +- **数据存储**: 用户密码、API密钥等敏感信息必须加密存储。 +- **防SQL注入**: 采用JPA和参数化查询,从根本上防止SQL注入攻击。 +- **防XSS攻击**: 前端对用户输入进行严格过滤和转义,防止跨站脚本攻击。 + +### 5.3 可扩展性需求 +- **模块化设计**: 后端业务逻辑严格按照模块划分,降低耦合度,便于未来将单个模块拆分为微服务。 +- **无状态服务**: 后端核心业务服务设计为无状态,便于水平扩展和负载均衡。 +- **消息队列**: (未来规划)引入RabbitMQ或Kafka,实现服务间的异步解耦,提升系统的削峰填谷和弹性能力。 + +### 5.4 可维护性需求 +- **统一编码规范**: 全项目遵循统一的Java和TypeScript编码规范。 +- **日志记录**: 对关键业务流程和异常情况进行详细的结构化日志记录,便于问题排查。 +- **API文档**: 使用Swagger/OpenAPI自动生成并维护最新的API文档。 +- **模块化前端**: 前端代码按四端和功能模块组织,提高代码的可读性和复用性。 --- - + ### Design/API测试文档.md - -# EMS 后端 API 文档 (v3.0) - -本文档为EMS(环境监控系统)后端API提供了全面的接口说明。所有接口均基于RESTful设计原则,使用JSON格式进行数据交换,并通过JWT进行认证授权。 - -**基础URL**: `http://localhost:8080` - ---- - -## 1. 认证模块 (`/api/auth`) - -负责处理用户身份验证、注册、登出和密码管理等所有认证相关操作。 - -### 1.1 用户登录 - -- **功能**: 用户通过邮箱和密码登录,成功后返回JWT令牌及用户信息。 -- **URL**: `/api/auth/login` -- **方法**: `POST` -- **请求体** (`LoginRequest`): - ```json - { - "email": "admin@example.com", - "password": "password123" - } - ``` -- **成功响应** (`200 OK`, `JwtAuthenticationResponse`): - ```json - { - "token": "eyJhbGciOiJIUzI1NiJ9...", - "user": { - "id": 1, - "name": "Admin User", - "email": "admin@example.com", - "role": "ADMIN" - } - } - ``` -- **失败响应**: `401 Unauthorized` (凭证无效) - -### 1.2 用户注册 - -- **功能**: 使用提供的用户信息和验证码创建新用户账户。 -- **URL**: `/api/auth/signup` -- **方法**: `POST` -- **请求体** (`SignUpRequest`): - ```json - { - "name": "New User", - "email": "newuser@example.com", - "password": "password123", - "code": "123456" - } - ``` -- **成功响应**: `201 Created` -- **失败响应**: `400 Bad Request` (验证码错误、邮箱已存在等) - -### 1.3 用户登出 - -- **功能**: 服务端记录用户登出操作。 -- **URL**: `/api/auth/logout` -- **方法**: `POST` -- **认证**: 需要有效的JWT令牌 -- **成功响应**: `200 OK` - -### 1.4 发送注册验证码 - -- **功能**: 向指定邮箱发送用于账号注册的验证码。 -- **URL**: `/api/auth/send-verification-code` -- **方法**: `POST` -- **请求参数**: - - `email` (string, `query`): 接收验证码的邮箱地址。 -- **成功响应**: `200 OK` -- **失败响应**: `400 Bad Request` (邮箱格式不正确) - -### 1.5 发送密码重置验证码 - -- **功能**: 向指定邮箱发送用于重置密码的验证码。 -- **URL**: `/api/auth/send-password-reset-code` -- **方法**: `POST` -- **请求参数**: - - `email` (string, `query`): 注册时使用的邮箱地址。 -- **成功响应**: `200 OK` - -### 1.6 使用验证码重置密码 - -- **功能**: 校验验证码并重置为新密码。 -- **URL**: `/api/auth/reset-password-with-code` -- **方法**: `POST` -- **请求体** (`PasswordResetWithCodeDto`): - ```json - { - "email": "user@example.com", - "code": "654321", - "newPassword": "newSecurePassword456" - } - ``` -- **成功响应**: `200 OK` -- **失败响应**: `400 Bad Request` (验证码无效或密码不符合要求) - ---- - -## 2. 仪表盘模块 (`/api/dashboard`) - -提供决策支持的各类统计和可视化数据。所有端点都需要 `DECISION_MAKER` 或 `ADMIN` 角色权限,特殊权限会单独注明。 - -### 2.1 核心统计数据 - -- **功能**: 获取仪表盘的核心统计数据,如总任务数、待处理反馈数等。 -- **URL**: `/api/dashboard/stats` -- **方法**: `GET` -- **成功响应** (`200 OK`): `DashboardStatsDTO` - -### 2.2 报告与分析 - -#### AQI等级分布 -- **功能**: 获取AQI(空气质量指数)在不同等级下的分布情况。 -- **URL**: `/api/dashboard/reports/aqi-distribution` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -#### 月度超标趋势 -- **功能**: 获取过去几个月内环境指标超标事件的趋势。 -- **URL**: `/api/dashboard/reports/monthly-exceedance-trend` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -#### 网格覆盖情况 -- **功能**: 按城市统计网格的覆盖率和状态。 -- **URL**: `/api/dashboard/reports/grid-coverage` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -#### 污染物统计 -- **功能**: 获取主要污染物的统计数据报告。 -- **URL**: `/api/dashboard/reports/pollution-stats` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -#### 任务完成情况 -- **功能**: 统计任务的整体完成率和状态分布。 -- **URL**: `/api/dashboard/reports/task-completion-stats` -- **方法**: `GET` -- **成功响应** (`200 OK`): `TaskStatsDTO` - -#### 污染物月度趋势 -- **功能**: 获取不同污染物类型的月度变化趋势。 -- **URL**: `/api/dashboard/reports/pollutant-monthly-trends` -- **方法**: `GET` -- **成功响应** (`200 OK`): `Map>` - -### 2.3 地图数据 - -#### 反馈热力图 -- **功能**: 获取用于在地图上展示反馈问题地理分布的热力图数据。 -- **URL**: `/api/dashboard/map/heatmap` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -#### AQI热力图 -- **功能**: 获取用于在地图上展示AQI指数地理分布的热力图数据。 -- **URL**: `/api/dashboard/map/aqi-heatmap` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -### 2.4 污染物阈值管理 - -#### 获取所有阈值 -- **功能**: 获取所有污染物的阈值设置。 -- **URL**: `/api/dashboard/thresholds` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -#### 获取指定污染物阈值 -- **功能**: 根据污染物名称获取其特定的阈值设置。 -- **URL**: `/api/dashboard/thresholds/{pollutantName}` -- **方法**: `GET` -- **URL参数**: `pollutantName` (string) -- **成功响应** (`200 OK`): `PollutantThresholdDTO` - -#### 保存阈值 -- **功能**: 创建或更新一个污染物的阈值设置。 -- **权限**: **`ADMIN`** -- **URL**: `/api/dashboard/thresholds` -- **方法**: `POST` -- **请求体** (`PollutantThresholdDTO`): - ```json - { - "pollutantName": "PM2.5", - "goodThreshold": 35, - "moderateThreshold": 75, - "unhealthyThreshold": 115 - } - ``` -- **成功响应** (`200 OK`): `PollutantThresholdDTO` (已保存的数据) - ---- - -## 3. 反馈模块 (`/api/feedback`) - -处理公众和用户的环境问题反馈,包括提交、查询、统计和处理。 - -### 3.1 提交反馈 - -- **功能**: 已认证用户提交环境问题反馈,可附带文件。 -- **权限**: **`isAuthenticated()`** (任何已认证用户) -- **URL**: `/api/feedback/submit` -- **方法**: `POST` -- **内容类型**: `multipart/form-data` -- **请求部分**: - - `feedback` (JSON, `FeedbackSubmissionRequest`): - ```json - { - "title": "River Pollution", - "description": "The river near downtown is heavily polluted.", - "latitude": 34.0522, - "longitude": -118.2437, - "pollutionType": "WATER", - "severityLevel": "HIGH" - } - ``` - - `files` (File[], optional): (可选) 上传的图片或视频文件。 -- **成功响应** (`201 Created`): `Feedback` (包含ID的已创建反馈对象) - -### 3.2 获取反馈列表 (分页+过滤) - -- **功能**: 根据多种条件组合查询反馈列表,支持分页。 -- **权限**: **`isAuthenticated()`** -- **URL**: `/api/feedback` -- **方法**: `GET` -- **请求参数**: - - `page` (int, optional): 页码 (从0开始) - - `size` (int, optional): 每页数量 - - `sort` (string, optional): 排序字段,如 `createdAt,desc` - - `status` (string, optional): 状态 (e.g., `PENDING_REVIEW`, `PROCESSED`) - - `pollutionType` (string, optional): 污染类型 (e.g., `AIR`, `WATER`) - - `severityLevel` (string, optional): 严重程度 (e.g., `LOW`, `MEDIUM`) - - `cityName` (string, optional): 城市名称 - - `districtName` (string, optional): 区县名称 - - `startDate` (string, optional): 开始日期 (格式: `YYYY-MM-DD`) - - `endDate` (string, optional): 结束日期 (格式: `YYYY-MM-DD`) - - `keyword` (string, optional): 标题或描述中的关键词 -- **成功响应** (`200 OK`): `Page` (分页的反馈数据) - -### 3.3 获取反馈详情 - -- **功能**: 根据ID获取单个反馈的详细信息。 -- **权限**: **`ADMIN`**, **`SUPERVISOR`**, **`DECISION_MAKER`** -- **URL**: `/api/feedback/{id}` -- **方法**: `GET` -- **URL参数**: `id` (long) -- **成功响应** (`200 OK`): `FeedbackResponseDTO` - -### 3.4 获取反馈统计 - -- **功能**: 获取当前用户的反馈统计数据 (例如,不同状态下的反馈数量)。 -- **权限**: **`isAuthenticated()`** -- **URL**: `/api/feedback/stats` -- **方法**: `GET` -- **成功响应** (`200 OK`): `FeedbackStatsResponse` - -### 3.5 处理反馈 (审批) - -- **功能**: 主管或管理员对反馈进行处理,例如审批通过、驳回等。 -- **权限**: **`ADMIN`**, **`SUPERVISOR`** -- **URL**: `/api/feedback/{id}/process` -- **方法**: `POST` -- **URL参数**: `id` (long) -- **请求体** (`ProcessFeedbackRequest`): - ```json - { - "newStatus": "PROCESSED", - "remarks": "Feedback has been verified and assigned." - } - ``` -- **成功响应** (`200 OK`): `FeedbackResponseDTO` (更新后的反馈对象) - ---- - -## 4. 公共接口模块 (`/api/public`) - -无需认证即可访问的接口,主要用于公众参与。 - -### 4.1 提交公共反馈 - -- **功能**: 公众用户提交环境问题反馈,可附带文件。 -- **URL**: `/api/public/feedback` -- **方法**: `POST` -- **内容类型**: `multipart/form-data` -- **请求部分**: - - `feedback` (JSON, `PublicFeedbackRequest`): - ```json - { - "title": "Noise Complaint", - "description": "Loud construction noise at night.", - "latitude": 34.0522, - "longitude": -118.2437, - "contactInfo": "anonymous@example.com" - } - ``` - - `files` (File[], optional): 上传的图片或视频等附件。 -- **成功响应** (`201 Created`): `Feedback` (包含ID的已创建反馈对象) - ---- - -## 5. 文件模块 (`/api`) - -处理系统中文件的访问、下载和预览。 - -### 5.1 文件下载/预览 - -- **功能**: 根据文件名获取文件资源,既可用于下载,也可用于浏览器内联预览。 -- **URL**: - - `/api/files/{filename}` - - `/api/view/{filename}` -- **方法**: `GET` -- **URL参数**: - - `filename` (string): 完整的文件名,包含扩展名 (例如, `image.png`, `report.pdf`)。 -- **成功响应** (`200 OK`): - - **Content-Type**: 根据文件类型动态设置 (e.g., `image/jpeg`, `application/pdf`)。 - - **Content-Disposition**: `inline`,建议浏览器内联显示。 - - **Body**: 文件内容的二进制流。 -- **失败响应**: `404 Not Found` (文件不存在) - ---- - -## 6. 网格管理模块 (`/api/grids`) - -负责环境监督网格的查询、更新以及网格员的分配管理。 - -### 6.1 获取所有网格 - -- **功能**: 获取系统中所有的网格数据列表。 -- **权限**: `ADMIN`, `DECISION_MAKER`, `GRID_WORKER` -- **URL**: `/api/grids` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -### 6.2 更新网格信息 - -- **功能**: 更新指定ID的网格信息,例如修改其负责人或状态。 -- **权限**: `ADMIN` -- **URL**: `/api/grids/{id}` -- **方法**: `PATCH` -- **URL参数**: `id` (long) -- **请求体** (`GridUpdateRequest`): - ```json - { - "status": "ACTIVE", - "notes": "This grid is now actively monitored." - } - ``` -- **成功响应** (`200 OK`): `Grid` (更新后的网格对象) - -### 6.3 获取网格覆盖率统计 - -- **功能**: 按城市统计网格的覆盖情况。 -- **权限**: `ADMIN` -- **URL**: `/api/grids/coverage` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -### 6.4 分配/解分配网格员 (通过网格ID) - -#### 分配网格员 -- **功能**: 将一个网格员分配到指定的网格。 -- **权限**: `ADMIN` -- **URL**: `/api/grids/{gridId}/assign` -- **方法**: `POST` -- **URL参数**: `gridId` (long) -- **请求体**: - ```json - { - "userId": 123 - } - ``` -- **成功响应** (`200 OK`) - -#### 解分配网格员 -- **功能**: 将一个网格员从指定的网格中移除。 -- **权限**: `ADMIN` -- **URL**: `/api/grids/{gridId}/unassign` -- **方法**: `POST` -- **URL参数**: `gridId` (long) -- **成功响应** (`200 OK`) - -### 6.5 分配/解分配网格员 (通过坐标) - -#### 分配网格员 -- **功能**: 将一个网格员分配到指定坐标的网格。 -- **权限**: `ADMIN`, `GRID_WORKER` -- **URL**: `/api/grids/coordinates/{gridX}/{gridY}/assign` -- **方法**: `POST` -- **URL参数**: - - `gridX` (int) - - `gridY` (int) -- **请求体**: - ```json - { - "userId": 123 - } - ``` -- **成功响应** (`200 OK`) - -#### 解分配网格员 -- **功能**: 将一个网格员从指定坐标的网格中移除。 -- **权限**: `ADMIN`, `GRID_WORKER` -- **URL**: `/api/grids/coordinates/{gridX}/{gridY}/unassign` -- **方法**: `POST` -- **URL参数**: - - `gridX` (int) - - `gridY` (int) -- **成功响应** (`200 OK`) - ---- - -## 7. 人员管理模块 (`/api/personnel`) - -负责系统中所有用户账户的创建、查询、更新和删除 (CRUD)。 - -### 7.1 获取用户列表 (分页) - -- **功能**: 获取系统中的用户列表,支持按角色和姓名进行筛选,并提供分页。 -- **权限**: `ADMIN`, `GRID_WORKER` -- **URL**: `/api/personnel/users` -- **方法**: `GET` -- **请求参数**: - - `role` (string, optional): 角色名称 (e.g., `ADMIN`, `GRID_WORKER`) - - `name` (string, optional): 用户姓名 (模糊匹配) - - `page` (int, optional): 页码 - - `size` (int, optional): 每页数量 -- **成功响应** (`200 OK`): `PageDTO` - -### 7.2 创建新用户 - -- **功能**: 创建一个新的用户账户。 -- **权限**: `ADMIN` -- **URL**: `/api/personnel/users` -- **方法**: `POST` -- **请求体** (`UserCreationRequest`): - ```json - { - "name": "John Doe", - "email": "john.doe@example.com", - "password": "strongPassword123", - "role": "GRID_WORKER" - } - ``` -- **成功响应** (`201 Created`): `UserAccount` - -### 7.3 获取用户详情 - -- **功能**: 根据ID获取单个用户的详细信息。 -- **权限**: `ADMIN` -- **URL**: `/api/personnel/users/{userId}` -- **方法**: `GET` -- **URL参数**: `userId` (long) -- **成功响应** (`200 OK`): `UserAccount` (密码字段为空) - -### 7.4 更新用户信息 - -- **功能**: 更新指定用户的信息(例如姓名、邮箱,但不包括角色)。 -- **权限**: `ADMIN` -- **URL**: `/api/personnel/users/{userId}` -- **方法**: `PATCH` -- **URL参数**: `userId` (long) -- **请求体** (`UserUpdateRequest`): - ```json - { - "name": "Johnathan Doe", - "email": "johnathan.doe@example.com" - } - ``` -- **成功响应** (`200 OK`): `UserAccount` - -### 7.5 更新用户角色 - -- **功能**: 单独更新用户的角色。 -- **权限**: `ADMIN` -- **URL**: `/api/personnel/users/{userId}/role` -- **方法**: `PUT` -- **URL参数**: `userId` (long) -- **请求体** (`UserRoleUpdateRequest`): - ```json - { - "role": "SUPERVISOR" - } - ``` -- **成功响应** (`200 OK`): `UserAccount` (密码字段为空) - -### 7.6 删除用户 - -- **功能**: 从系统中永久删除一个用户。 -- **权限**: `ADMIN` -- **URL**: `/api/personnel/users/{userId}` -- **方法**: `DELETE` -- **URL参数**: `userId` (long) -- **成功响应** (`204 No Content`) - ---- - -## 8. 网格员任务模块 (`/api/worker`) - -专为网格员 (`GRID_WORKER`) 提供的任务管理接口。 - -### 8.1 获取我的任务列表 - -- **功能**: 获取分配给当前登录网格员的任务列表,支持按状态筛选和分页。 -- **权限**: `GRID_WORKER` -- **URL**: `/api/worker` -- **方法**: `GET` -- **请求参数**: - - `status` (string, optional): 任务状态 (e.g., `ASSIGNED`, `IN_PROGRESS`, `COMPLETED`) - - `page` (int, optional): 页码 - - `size` (int, optional): 每页数量 -- **成功响应** (`200 OK`): `Page` - -### 8.2 获取任务详情 - -- **功能**: 获取单个任务的详细信息。 -- **权限**: `GRID_WORKER` -- **URL**: `/api/worker/{taskId}` -- **方法**: `GET` -- **URL参数**: `taskId` (long) -- **成功响应** (`200 OK`): `TaskDetailDTO` - -### 8.3 接受任务 - -- **功能**: 网格员接受一个已分配给自己的任务。 -- **权限**: `GRID_WORKER` -- **URL**: `/api/worker/{taskId}/accept` -- **方法**: `POST` -- **URL参数**: `taskId` (long) -- **成功响应** (`200 OK`): `TaskSummaryDTO` (更新后的任务摘要) - -### 8.4 提交任务完成情况 - -- **功能**: 网格员提交任务的完成情况,可附带文字说明和文件。 -- **权限**: `GRID_WORKER` -- **URL**: `/api/worker/{taskId}/submit` -- **方法**: `POST` -- **内容类型**: `multipart/form-data` -- **URL参数**: `taskId` (long) -- **请求部分**: - - `comments` (string): 任务完成情况的文字说明。 - - `files` (File[], optional): (可选) 相关的证明文件或图片。 -- **成功响应** (`200 OK`): `TaskSummaryDTO` (更新后的任务摘要) - ---- - -## 9. 地图与寻路模块 (`/api/map`, `/api/pathfinding`) - -提供地图数据管理和基于A*算法的路径规划功能。 - -### 9.1 地图管理 (`/api/map`) - -#### 获取完整地图网格 -- **功能**: 获取用于渲染前端地图的完整网格数据。 -- **URL**: `/api/map/grid` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -#### 创建或更新网格单元 -- **功能**: 创建或更新单个网格单元的信息,如设置障碍物。 -- **URL**: `/api/map/grid` -- **方法**: `POST` -- **请求体** (`MapGrid`): - ```json - { - "x": 10, - "y": 15, - "obstacle": true, - "terrainType": "water" - } - ``` -- **成功响应** (`200 OK`): `MapGrid` (已保存的网格单元) - -#### 初始化地图 -- **功能**: 清空并重新生成指定大小的地图网格(主要用于开发和测试)。 -- **URL**: `/api/map/initialize` -- **方法**: `POST` -- **请求参数**: - - `width` (int, optional, default=20) - - `height` (int, optional, default=20) -- **成功响应** (`200 OK`): `String` (例如, "Initialized a 20x20 map.") - -### 9.2 路径规划 (`/api/pathfinding`) - -#### 查找路径 -- **功能**: 使用A*算法计算两点之间的最短路径,会避开障碍物。 -- **URL**: `/api/pathfinding/find` -- **方法**: `POST` -- **请求体** (`PathfindingRequest`): - ```json - { - "startX": 0, - "startY": 0, - "endX": 18, - "endY": 18 - } - ``` -- **成功响应** (`200 OK`): `List` (路径点坐标列表,若无路径则为空列表) - ---- - -## 10. 个人资料模块 (`/api/me`) - -处理与当前登录用户个人账户相关的操作。 - -### 10.1 获取我的反馈历史 - -- **功能**: 获取当前登录用户提交过的所有反馈记录,支持分页。 -- **权限**: `isAuthenticated()` (任何已认证用户) -- **URL**: `/api/me/feedback` -- **方法**: `GET` -- **请求参数**: - - `page` (int, optional): 页码 - - `size` (int, optional): 每页数量 -- **成功响应** (`200 OK`): `Page` - ---- - -## 11. 主管审核模块 (`/api/supervisor`) - -专为主管 (`SUPERVISOR`) 和管理员 (`ADMIN`) 提供的反馈审核接口。 - -### 11.1 获取待审核列表 - -- **功能**: 获取所有状态为"待审核"的反馈列表。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **URL**: `/api/supervisor/reviews` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -### 11.2 批准反馈 - -- **功能**: 将指定ID的反馈状态变更为"已批准"(通常意味着后续可被分配任务)。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **URL**: `/api/supervisor/reviews/{feedbackId}/approve` -- **方法**: `POST` -- **URL参数**: `feedbackId` (long) -- **成功响应** (`200 OK`) - -### 11.3 拒绝反馈 - -- **功能**: 拒绝一个反馈,并记录拒绝理由。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **URL**: `/api/supervisor/reviews/{feedbackId}/reject` -- **方法**: `POST` -- **URL参数**: `feedbackId` (long) -- **请求体** (`RejectFeedbackRequest`): - ```json - { - "reason": "Insufficient evidence provided." - } - ``` -- **成功响应** (`200 OK`) - ---- - -## 12. 任务分配模块 (`/api/tasks`) - -专为主管 (`SUPERVISOR`) 和管理员 (`ADMIN`) 提供的、将已审核通过的反馈分配为具体任务的接口。 - -### 12.1 获取未分配的任务 - -- **功能**: 获取所有已批准但尚未分配给任何网格员的反馈列表。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **URL**: `/api/tasks/unassigned` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -### 12.2 获取可用的网格员 - -- **功能**: 获取系统中所有角色为 `GRID_WORKER` 的用户列表,用于任务分配。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **URL**: `/api/tasks/grid-workers` -- **方法**: `GET` -- **成功响应** (`200 OK`): `List` - -### 12.3 分配任务 - -- **功能**: 将一个反馈分配给一个网格员,从而创建一个新任务。 -- **权限**: `SUPERVISOR`, `ADMIN` -- **URL**: `/api/tasks/assign` -- **方法**: `POST` -- **请求体** (`AssignmentRequest`): - ```json - { - "feedbackId": 10, - "assigneeId": 123 - } - ``` -- **成功响应** (`200 OK`): `Assignment` (创建的任务分配记录) - ---- - -## 13. 任务管理模块 (`/api/management/tasks`) + +# EMS 后端 API 文档 (v3.0) + +本文档为EMS(环境监控系统)后端API提供了全面的接口说明。所有接口均基于RESTful设计原则,使用JSON格式进行数据交换,并通过JWT进行认证授权。 + +**基础URL**: `http://localhost:8080` + +--- + +## 1. 认证模块 (`/api/auth`) + +负责处理用户身份验证、注册、登出和密码管理等所有认证相关操作。 + +### 1.1 用户登录 + +- **功能**: 用户通过邮箱和密码登录,成功后返回JWT令牌及用户信息。 +- **URL**: `/api/auth/login` +- **方法**: `POST` +- **请求体** (`LoginRequest`): + ```json + { + "email": "admin@example.com", + "password": "password123" + } + ``` +- **成功响应** (`200 OK`, `JwtAuthenticationResponse`): + ```json + { + "token": "eyJhbGciOiJIUzI1NiJ9...", + "user": { + "id": 1, + "name": "Admin User", + "email": "admin@example.com", + "role": "ADMIN" + } + } + ``` +- **失败响应**: `401 Unauthorized` (凭证无效) + +### 1.2 用户注册 + +- **功能**: 使用提供的用户信息和验证码创建新用户账户。 +- **URL**: `/api/auth/signup` +- **方法**: `POST` +- **请求体** (`SignUpRequest`): + ```json + { + "name": "New User", + "email": "newuser@example.com", + "password": "password123", + "code": "123456" + } + ``` +- **成功响应**: `201 Created` +- **失败响应**: `400 Bad Request` (验证码错误、邮箱已存在等) + +### 1.3 用户登出 + +- **功能**: 服务端记录用户登出操作。 +- **URL**: `/api/auth/logout` +- **方法**: `POST` +- **认证**: 需要有效的JWT令牌 +- **成功响应**: `200 OK` + +### 1.4 发送注册验证码 + +- **功能**: 向指定邮箱发送用于账号注册的验证码。 +- **URL**: `/api/auth/send-verification-code` +- **方法**: `POST` +- **请求参数**: + - `email` (string, `query`): 接收验证码的邮箱地址。 +- **成功响应**: `200 OK` +- **失败响应**: `400 Bad Request` (邮箱格式不正确) + +### 1.5 发送密码重置验证码 + +- **功能**: 向指定邮箱发送用于重置密码的验证码。 +- **URL**: `/api/auth/send-password-reset-code` +- **方法**: `POST` +- **请求参数**: + - `email` (string, `query`): 注册时使用的邮箱地址。 +- **成功响应**: `200 OK` + +### 1.6 使用验证码重置密码 + +- **功能**: 校验验证码并重置为新密码。 +- **URL**: `/api/auth/reset-password-with-code` +- **方法**: `POST` +- **请求体** (`PasswordResetWithCodeDto`): + ```json + { + "email": "user@example.com", + "code": "654321", + "newPassword": "newSecurePassword456" + } + ``` +- **成功响应**: `200 OK` +- **失败响应**: `400 Bad Request` (验证码无效或密码不符合要求) + +--- + +## 2. 仪表盘模块 (`/api/dashboard`) + +提供决策支持的各类统计和可视化数据。所有端点都需要 `DECISION_MAKER` 或 `ADMIN` 角色权限,特殊权限会单独注明。 + +### 2.1 核心统计数据 + +- **功能**: 获取仪表盘的核心统计数据,如总任务数、待处理反馈数等。 +- **URL**: `/api/dashboard/stats` +- **方法**: `GET` +- **成功响应** (`200 OK`): `DashboardStatsDTO` + +### 2.2 报告与分析 + +#### AQI等级分布 +- **功能**: 获取AQI(空气质量指数)在不同等级下的分布情况。 +- **URL**: `/api/dashboard/reports/aqi-distribution` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +#### 月度超标趋势 +- **功能**: 获取过去几个月内环境指标超标事件的趋势。 +- **URL**: `/api/dashboard/reports/monthly-exceedance-trend` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +#### 网格覆盖情况 +- **功能**: 按城市统计网格的覆盖率和状态。 +- **URL**: `/api/dashboard/reports/grid-coverage` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +#### 污染物统计 +- **功能**: 获取主要污染物的统计数据报告。 +- **URL**: `/api/dashboard/reports/pollution-stats` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +#### 任务完成情况 +- **功能**: 统计任务的整体完成率和状态分布。 +- **URL**: `/api/dashboard/reports/task-completion-stats` +- **方法**: `GET` +- **成功响应** (`200 OK`): `TaskStatsDTO` + +#### 污染物月度趋势 +- **功能**: 获取不同污染物类型的月度变化趋势。 +- **URL**: `/api/dashboard/reports/pollutant-monthly-trends` +- **方法**: `GET` +- **成功响应** (`200 OK`): `Map>` + +### 2.3 地图数据 + +#### 反馈热力图 +- **功能**: 获取用于在地图上展示反馈问题地理分布的热力图数据。 +- **URL**: `/api/dashboard/map/heatmap` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +#### AQI热力图 +- **功能**: 获取用于在地图上展示AQI指数地理分布的热力图数据。 +- **URL**: `/api/dashboard/map/aqi-heatmap` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +### 2.4 污染物阈值管理 + +#### 获取所有阈值 +- **功能**: 获取所有污染物的阈值设置。 +- **URL**: `/api/dashboard/thresholds` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +#### 获取指定污染物阈值 +- **功能**: 根据污染物名称获取其特定的阈值设置。 +- **URL**: `/api/dashboard/thresholds/{pollutantName}` +- **方法**: `GET` +- **URL参数**: `pollutantName` (string) +- **成功响应** (`200 OK`): `PollutantThresholdDTO` + +#### 保存阈值 +- **功能**: 创建或更新一个污染物的阈值设置。 +- **权限**: **`ADMIN`** +- **URL**: `/api/dashboard/thresholds` +- **方法**: `POST` +- **请求体** (`PollutantThresholdDTO`): + ```json + { + "pollutantName": "PM2.5", + "goodThreshold": 35, + "moderateThreshold": 75, + "unhealthyThreshold": 115 + } + ``` +- **成功响应** (`200 OK`): `PollutantThresholdDTO` (已保存的数据) + +--- + +## 3. 反馈模块 (`/api/feedback`) + +处理公众和用户的环境问题反馈,包括提交、查询、统计和处理。 + +### 3.1 提交反馈 + +- **功能**: 已认证用户提交环境问题反馈,可附带文件。 +- **权限**: **`isAuthenticated()`** (任何已认证用户) +- **URL**: `/api/feedback/submit` +- **方法**: `POST` +- **内容类型**: `multipart/form-data` +- **请求部分**: + - `feedback` (JSON, `FeedbackSubmissionRequest`): + ```json + { + "title": "River Pollution", + "description": "The river near downtown is heavily polluted.", + "latitude": 34.0522, + "longitude": -118.2437, + "pollutionType": "WATER", + "severityLevel": "HIGH" + } + ``` + - `files` (File[], optional): (可选) 上传的图片或视频文件。 +- **成功响应** (`201 Created`): `Feedback` (包含ID的已创建反馈对象) + +### 3.2 获取反馈列表 (分页+过滤) + +- **功能**: 根据多种条件组合查询反馈列表,支持分页。 +- **权限**: **`isAuthenticated()`** +- **URL**: `/api/feedback` +- **方法**: `GET` +- **请求参数**: + - `page` (int, optional): 页码 (从0开始) + - `size` (int, optional): 每页数量 + - `sort` (string, optional): 排序字段,如 `createdAt,desc` + - `status` (string, optional): 状态 (e.g., `PENDING_REVIEW`, `PROCESSED`) + - `pollutionType` (string, optional): 污染类型 (e.g., `AIR`, `WATER`) + - `severityLevel` (string, optional): 严重程度 (e.g., `LOW`, `MEDIUM`) + - `cityName` (string, optional): 城市名称 + - `districtName` (string, optional): 区县名称 + - `startDate` (string, optional): 开始日期 (格式: `YYYY-MM-DD`) + - `endDate` (string, optional): 结束日期 (格式: `YYYY-MM-DD`) + - `keyword` (string, optional): 标题或描述中的关键词 +- **成功响应** (`200 OK`): `Page` (分页的反馈数据) + +### 3.3 获取反馈详情 + +- **功能**: 根据ID获取单个反馈的详细信息。 +- **权限**: **`ADMIN`**, **`SUPERVISOR`**, **`DECISION_MAKER`** +- **URL**: `/api/feedback/{id}` +- **方法**: `GET` +- **URL参数**: `id` (long) +- **成功响应** (`200 OK`): `FeedbackResponseDTO` + +### 3.4 获取反馈统计 + +- **功能**: 获取当前用户的反馈统计数据 (例如,不同状态下的反馈数量)。 +- **权限**: **`isAuthenticated()`** +- **URL**: `/api/feedback/stats` +- **方法**: `GET` +- **成功响应** (`200 OK`): `FeedbackStatsResponse` + +### 3.5 处理反馈 (审批) + +- **功能**: 主管或管理员对反馈进行处理,例如审批通过、驳回等。 +- **权限**: **`ADMIN`**, **`SUPERVISOR`** +- **URL**: `/api/feedback/{id}/process` +- **方法**: `POST` +- **URL参数**: `id` (long) +- **请求体** (`ProcessFeedbackRequest`): + ```json + { + "newStatus": "PROCESSED", + "remarks": "Feedback has been verified and assigned." + } + ``` +- **成功响应** (`200 OK`): `FeedbackResponseDTO` (更新后的反馈对象) + +--- + +## 4. 公共接口模块 (`/api/public`) + +无需认证即可访问的接口,主要用于公众参与。 + +### 4.1 提交公共反馈 + +- **功能**: 公众用户提交环境问题反馈,可附带文件。 +- **URL**: `/api/public/feedback` +- **方法**: `POST` +- **内容类型**: `multipart/form-data` +- **请求部分**: + - `feedback` (JSON, `PublicFeedbackRequest`): + ```json + { + "title": "Noise Complaint", + "description": "Loud construction noise at night.", + "latitude": 34.0522, + "longitude": -118.2437, + "contactInfo": "anonymous@example.com" + } + ``` + - `files` (File[], optional): 上传的图片或视频等附件。 +- **成功响应** (`201 Created`): `Feedback` (包含ID的已创建反馈对象) + +--- + +## 5. 文件模块 (`/api`) + +处理系统中文件的访问、下载和预览。 + +### 5.1 文件下载/预览 + +- **功能**: 根据文件名获取文件资源,既可用于下载,也可用于浏览器内联预览。 +- **URL**: + - `/api/files/{filename}` + - `/api/view/{filename}` +- **方法**: `GET` +- **URL参数**: + - `filename` (string): 完整的文件名,包含扩展名 (例如, `image.png`, `report.pdf`)。 +- **成功响应** (`200 OK`): + - **Content-Type**: 根据文件类型动态设置 (e.g., `image/jpeg`, `application/pdf`)。 + - **Content-Disposition**: `inline`,建议浏览器内联显示。 + - **Body**: 文件内容的二进制流。 +- **失败响应**: `404 Not Found` (文件不存在) + +--- + +## 6. 网格管理模块 (`/api/grids`) + +负责环境监督网格的查询、更新以及网格员的分配管理。 + +### 6.1 获取所有网格 + +- **功能**: 获取系统中所有的网格数据列表。 +- **权限**: `ADMIN`, `DECISION_MAKER`, `GRID_WORKER` +- **URL**: `/api/grids` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +### 6.2 更新网格信息 + +- **功能**: 更新指定ID的网格信息,例如修改其负责人或状态。 +- **权限**: `ADMIN` +- **URL**: `/api/grids/{id}` +- **方法**: `PATCH` +- **URL参数**: `id` (long) +- **请求体** (`GridUpdateRequest`): + ```json + { + "status": "ACTIVE", + "notes": "This grid is now actively monitored." + } + ``` +- **成功响应** (`200 OK`): `Grid` (更新后的网格对象) + +### 6.3 获取网格覆盖率统计 + +- **功能**: 按城市统计网格的覆盖情况。 +- **权限**: `ADMIN` +- **URL**: `/api/grids/coverage` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +### 6.4 分配/解分配网格员 (通过网格ID) + +#### 分配网格员 +- **功能**: 将一个网格员分配到指定的网格。 +- **权限**: `ADMIN` +- **URL**: `/api/grids/{gridId}/assign` +- **方法**: `POST` +- **URL参数**: `gridId` (long) +- **请求体**: + ```json + { + "userId": 123 + } + ``` +- **成功响应** (`200 OK`) + +#### 解分配网格员 +- **功能**: 将一个网格员从指定的网格中移除。 +- **权限**: `ADMIN` +- **URL**: `/api/grids/{gridId}/unassign` +- **方法**: `POST` +- **URL参数**: `gridId` (long) +- **成功响应** (`200 OK`) + +### 6.5 分配/解分配网格员 (通过坐标) + +#### 分配网格员 +- **功能**: 将一个网格员分配到指定坐标的网格。 +- **权限**: `ADMIN`, `GRID_WORKER` +- **URL**: `/api/grids/coordinates/{gridX}/{gridY}/assign` +- **方法**: `POST` +- **URL参数**: + - `gridX` (int) + - `gridY` (int) +- **请求体**: + ```json + { + "userId": 123 + } + ``` +- **成功响应** (`200 OK`) + +#### 解分配网格员 +- **功能**: 将一个网格员从指定坐标的网格中移除。 +- **权限**: `ADMIN`, `GRID_WORKER` +- **URL**: `/api/grids/coordinates/{gridX}/{gridY}/unassign` +- **方法**: `POST` +- **URL参数**: + - `gridX` (int) + - `gridY` (int) +- **成功响应** (`200 OK`) + +--- + +## 7. 人员管理模块 (`/api/personnel`) + +负责系统中所有用户账户的创建、查询、更新和删除 (CRUD)。 + +### 7.1 获取用户列表 (分页) + +- **功能**: 获取系统中的用户列表,支持按角色和姓名进行筛选,并提供分页。 +- **权限**: `ADMIN`, `GRID_WORKER` +- **URL**: `/api/personnel/users` +- **方法**: `GET` +- **请求参数**: + - `role` (string, optional): 角色名称 (e.g., `ADMIN`, `GRID_WORKER`) + - `name` (string, optional): 用户姓名 (模糊匹配) + - `page` (int, optional): 页码 + - `size` (int, optional): 每页数量 +- **成功响应** (`200 OK`): `PageDTO` + +### 7.2 创建新用户 + +- **功能**: 创建一个新的用户账户。 +- **权限**: `ADMIN` +- **URL**: `/api/personnel/users` +- **方法**: `POST` +- **请求体** (`UserCreationRequest`): + ```json + { + "name": "John Doe", + "email": "john.doe@example.com", + "password": "strongPassword123", + "role": "GRID_WORKER" + } + ``` +- **成功响应** (`201 Created`): `UserAccount` + +### 7.3 获取用户详情 + +- **功能**: 根据ID获取单个用户的详细信息。 +- **权限**: `ADMIN` +- **URL**: `/api/personnel/users/{userId}` +- **方法**: `GET` +- **URL参数**: `userId` (long) +- **成功响应** (`200 OK`): `UserAccount` (密码字段为空) + +### 7.4 更新用户信息 + +- **功能**: 更新指定用户的信息(例如姓名、邮箱,但不包括角色)。 +- **权限**: `ADMIN` +- **URL**: `/api/personnel/users/{userId}` +- **方法**: `PATCH` +- **URL参数**: `userId` (long) +- **请求体** (`UserUpdateRequest`): + ```json + { + "name": "Johnathan Doe", + "email": "johnathan.doe@example.com" + } + ``` +- **成功响应** (`200 OK`): `UserAccount` + +### 7.5 更新用户角色 + +- **功能**: 单独更新用户的角色。 +- **权限**: `ADMIN` +- **URL**: `/api/personnel/users/{userId}/role` +- **方法**: `PUT` +- **URL参数**: `userId` (long) +- **请求体** (`UserRoleUpdateRequest`): + ```json + { + "role": "SUPERVISOR" + } + ``` +- **成功响应** (`200 OK`): `UserAccount` (密码字段为空) + +### 7.6 删除用户 + +- **功能**: 从系统中永久删除一个用户。 +- **权限**: `ADMIN` +- **URL**: `/api/personnel/users/{userId}` +- **方法**: `DELETE` +- **URL参数**: `userId` (long) +- **成功响应** (`204 No Content`) + +--- + +## 8. 网格员任务模块 (`/api/worker`) + +专为网格员 (`GRID_WORKER`) 提供的任务管理接口。 + +### 8.1 获取我的任务列表 + +- **功能**: 获取分配给当前登录网格员的任务列表,支持按状态筛选和分页。 +- **权限**: `GRID_WORKER` +- **URL**: `/api/worker` +- **方法**: `GET` +- **请求参数**: + - `status` (string, optional): 任务状态 (e.g., `ASSIGNED`, `IN_PROGRESS`, `COMPLETED`) + - `page` (int, optional): 页码 + - `size` (int, optional): 每页数量 +- **成功响应** (`200 OK`): `Page` + +### 8.2 获取任务详情 + +- **功能**: 获取单个任务的详细信息。 +- **权限**: `GRID_WORKER` +- **URL**: `/api/worker/{taskId}` +- **方法**: `GET` +- **URL参数**: `taskId` (long) +- **成功响应** (`200 OK`): `TaskDetailDTO` + +### 8.3 接受任务 + +- **功能**: 网格员接受一个已分配给自己的任务。 +- **权限**: `GRID_WORKER` +- **URL**: `/api/worker/{taskId}/accept` +- **方法**: `POST` +- **URL参数**: `taskId` (long) +- **成功响应** (`200 OK`): `TaskSummaryDTO` (更新后的任务摘要) + +### 8.4 提交任务完成情况 + +- **功能**: 网格员提交任务的完成情况,可附带文字说明和文件。 +- **权限**: `GRID_WORKER` +- **URL**: `/api/worker/{taskId}/submit` +- **方法**: `POST` +- **内容类型**: `multipart/form-data` +- **URL参数**: `taskId` (long) +- **请求部分**: + - `comments` (string): 任务完成情况的文字说明。 + - `files` (File[], optional): (可选) 相关的证明文件或图片。 +- **成功响应** (`200 OK`): `TaskSummaryDTO` (更新后的任务摘要) + +--- + +## 9. 地图与寻路模块 (`/api/map`, `/api/pathfinding`) + +提供地图数据管理和基于A*算法的路径规划功能。 + +### 9.1 地图管理 (`/api/map`) + +#### 获取完整地图网格 +- **功能**: 获取用于渲染前端地图的完整网格数据。 +- **URL**: `/api/map/grid` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +#### 创建或更新网格单元 +- **功能**: 创建或更新单个网格单元的信息,如设置障碍物。 +- **URL**: `/api/map/grid` +- **方法**: `POST` +- **请求体** (`MapGrid`): + ```json + { + "x": 10, + "y": 15, + "obstacle": true, + "terrainType": "water" + } + ``` +- **成功响应** (`200 OK`): `MapGrid` (已保存的网格单元) + +#### 初始化地图 +- **功能**: 清空并重新生成指定大小的地图网格(主要用于开发和测试)。 +- **URL**: `/api/map/initialize` +- **方法**: `POST` +- **请求参数**: + - `width` (int, optional, default=20) + - `height` (int, optional, default=20) +- **成功响应** (`200 OK`): `String` (例如, "Initialized a 20x20 map.") + +### 9.2 路径规划 (`/api/pathfinding`) + +#### 查找路径 +- **功能**: 使用A*算法计算两点之间的最短路径,会避开障碍物。 +- **URL**: `/api/pathfinding/find` +- **方法**: `POST` +- **请求体** (`PathfindingRequest`): + ```json + { + "startX": 0, + "startY": 0, + "endX": 18, + "endY": 18 + } + ``` +- **成功响应** (`200 OK`): `List` (路径点坐标列表,若无路径则为空列表) + +--- + +## 10. 个人资料模块 (`/api/me`) + +处理与当前登录用户个人账户相关的操作。 + +### 10.1 获取我的反馈历史 + +- **功能**: 获取当前登录用户提交过的所有反馈记录,支持分页。 +- **权限**: `isAuthenticated()` (任何已认证用户) +- **URL**: `/api/me/feedback` +- **方法**: `GET` +- **请求参数**: + - `page` (int, optional): 页码 + - `size` (int, optional): 每页数量 +- **成功响应** (`200 OK`): `Page` + +--- + +## 11. 主管审核模块 (`/api/supervisor`) + +专为主管 (`SUPERVISOR`) 和管理员 (`ADMIN`) 提供的反馈审核接口。 + +### 11.1 获取待审核列表 + +- **功能**: 获取所有状态为"待审核"的反馈列表。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **URL**: `/api/supervisor/reviews` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +### 11.2 批准反馈 + +- **功能**: 将指定ID的反馈状态变更为"已批准"(通常意味着后续可被分配任务)。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **URL**: `/api/supervisor/reviews/{feedbackId}/approve` +- **方法**: `POST` +- **URL参数**: `feedbackId` (long) +- **成功响应** (`200 OK`) + +### 11.3 拒绝反馈 + +- **功能**: 拒绝一个反馈,并记录拒绝理由。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **URL**: `/api/supervisor/reviews/{feedbackId}/reject` +- **方法**: `POST` +- **URL参数**: `feedbackId` (long) +- **请求体** (`RejectFeedbackRequest`): + ```json + { + "reason": "Insufficient evidence provided." + } + ``` +- **成功响应** (`200 OK`) + +--- + +## 12. 任务分配模块 (`/api/tasks`) + +专为主管 (`SUPERVISOR`) 和管理员 (`ADMIN`) 提供的、将已审核通过的反馈分配为具体任务的接口。 + +### 12.1 获取未分配的任务 + +- **功能**: 获取所有已批准但尚未分配给任何网格员的反馈列表。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **URL**: `/api/tasks/unassigned` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +### 12.2 获取可用的网格员 + +- **功能**: 获取系统中所有角色为 `GRID_WORKER` 的用户列表,用于任务分配。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **URL**: `/api/tasks/grid-workers` +- **方法**: `GET` +- **成功响应** (`200 OK`): `List` + +### 12.3 分配任务 + +- **功能**: 将一个反馈分配给一个网格员,从而创建一个新任务。 +- **权限**: `SUPERVISOR`, `ADMIN` +- **URL**: `/api/tasks/assign` +- **方法**: `POST` +- **请求体** (`AssignmentRequest`): + ```json + { + "feedbackId": 10, + "assigneeId": 123 + } + ``` +- **成功响应** (`200 OK`): `Assignment` (创建的任务分配记录) + +--- + +## 13. 任务管理模块 (`/api/management/tasks`) --- - + ### Design/PROJECT_README.md - -# Spring Boot 项目深度解析 - -本文档旨在深入解析当前 Spring Boot 项目的构建原理、核心技术以及业务逻辑,帮助您更好地理解和准备答疑。 - -## 一、Spring Boot 核心原理 - -Spring Boot 是一个用于简化新 Spring 应用的初始搭建以及开发过程的框架。其核心原理主要体现在以下几个方面: - -### 1. 自动配置 (Auto-Configuration) - 智能的“管家” - -您可以将 Spring Boot 的自动配置想象成一个非常智能的“管家”。在传统的 Spring 开发中,您需要手动配置很多东西,比如数据库连接、We你b 服务器等,就像您需要亲自告诉管家每一个细节:如何烧水、如何扫地。 - -而 Spring Boot 的“管家”则会观察您家里添置了什么新东西(即您在项目中加入了什么依赖),然后自动地把这些东西配置好,让它们能正常工作。 - -**它是如何工作的?** - -1. **观察您的“购物清单” (`pom.xml`)**:当您在项目的 `pom.xml` 文件中加入一个 `spring-boot-starter-*` 依赖,比如 `spring-boot-starter-web`,就等于告诉“管家”:“我需要开发一个网站。” - -2. **自动准备所需工具**:看到这个“购物清单”后,“管家”会立刻为您准备好一套完整的网站开发工具: - * 一个嵌入式的 **Tomcat 服务器**,这样您就不用自己安装和配置服务器了。 - * 配置好 **Spring MVC** 框架,这是处理网页请求的核心工具。 - * 设置好处理 JSON 数据的工具 (Jackson),方便前后端通信。 - -3. **背后的魔法 (`@EnableAutoConfiguration`)**:这一切的起点是您项目主启动类上的 `@SpringBootApplication` 注解,它内部包含了 `@EnableAutoConfiguration`。这个注解就是给“管家”下达“开始自动工作”命令的开关。它会去检查一个固定的清单(位于 `META-INF/spring.factories`),上面写着所有它认识的工具(自动配置类)以及何时应该启用它们。 - -**项目实例:** - -在本项目中,因为 `pom.xml` 包含了 `spring-boot-starter-web`,所以 Spring Boot 自动为您配置了处理 Web 请求的所有环境。这就是为什么您可以在 `com.dne.ems.controller` 包下的任何一个 Controller(例如 `AuthController`)中直接使用 `@RestController` 和 `@PostMapping` 这样的注解来定义 API 接口,而无需编写任何一行 XML 配置来启动 Web 服务或配置 Spring MVC。 - -简单来说,**自动配置就是 Spring Boot 替您完成了大部分繁琐、重复的配置工作,让您可以专注于编写业务代码。** - -### 2. 起步依赖 (Starter Dependencies) - “主题工具箱” - -如果说自动配置是“智能管家”,那么起步依赖就是“管家”使用的“主题工具箱”。您不需要告诉管家需要买钉子、锤子、螺丝刀……您只需要说:“我需要一个木工工具箱。” - -起步依赖 (`spring-boot-starter-*`) 就是这样的“主题工具箱”。每一个 starter 都包含了一整套用于某个特定任务的、经过测试、版本兼容的依赖。 - -**项目实例:** - -- **`spring-boot-starter-web`**: 这是“Web 开发工具箱”。它不仅包含了 Spring MVC,还包含了内嵌的 Tomcat 服务器和处理验证的库。您只需要引入这一个依赖,所有 Web 开发的基础环境就都准备好了。 - -- **`spring-boot-starter-data-jpa`**: 这是“数据库操作工具箱”。它打包了与数据库交互所需的一切,包括 Spring Data JPA、Hibernate (JPA 的一种实现) 和数据库连接池 (HikariCP)。您引入它之后,就可以直接在 `com.dne.ems.repository` 包下编写 `JpaRepository` 接口来进行数据库操作,而无需关心如何配置 Hibernate 或事务管理器。 - -- **`spring-boot-starter-security`**: 这是“安全管理工具箱”。引入它之后,Spring Security 框架就被集成进来,提供了认证和授权的基础功能。您可以在 `com.dne.ems.security.SecurityConfig` 中看到对这个“工具箱”的具体配置,比如定义哪些 API 需要登录才能访问。 - -**优势总结:** - -- **简化依赖**:您不必再手动添加一长串的单个依赖项。 -- **避免冲突**:Spring Boot 已经为您管理好了这些依赖之间的版本,您不必再担心因为版本不兼容而导致的各种奇怪错误。 - -通过使用这些“工具箱”,您可以非常快速地搭建起一个功能完备的应用程序框架。 - -### 3. 嵌入式 Web 服务器 - -Spring Boot 内嵌了常见的 Web 服务器,如 Tomcat、Jetty 或 Undertow。这意味着您不需要将应用程序打包成 WAR 文件部署到外部服务器,而是可以直接将应用程序打包成一个可执行的 JAR 文件,通过 `java -jar` 命令来运行。 - -- **优势**:简化了部署流程,便于持续集成和持续部署 (CI/CD)。 - -## 二、前后端分离与交互原理 - -本项目采用前后端分离的架构,前端(如 Vue.js)和后端(Spring Boot)是两个独立的项目,它们之间通过 API 进行通信。 - -### 1. RESTful API - -后端通过暴露一组 RESTful API 来提供服务。REST (Representational State Transfer) 是一种软件架构风格,它定义了一组用于创建 Web 服务的约束。核心思想是,将应用中的所有事物都看作资源,每个资源都有一个唯一的标识符 (URI)。 - -- **HTTP 方法**: - - `GET`:获取资源。 - - `POST`:创建新资源。 - - `PUT` / `PATCH`:更新资源。 - - `DELETE`:删除资源。 - -### 2. JSON (JavaScript Object Notation) - -前后端之间的数据交换格式通常是 JSON。当浏览器向后端发送请求时,它可能会在请求体中包含 JSON 数据。当后端响应时,它会将 Java 对象序列化为 JSON 字符串返回给前端。 - -- **Spring Boot 中的实现**:Spring Boot 默认使用 Jackson 库来处理 JSON 的序列化和反序列化。当 Controller 的方法参数有 `@RequestBody` 注解时,Spring 会自动将请求体中的 JSON 转换为 Java 对象。当方法有 `@ResponseBody` 或类有 `@RestController` 注解时,Spring 会自动将返回的 Java 对象转换为 JSON。 - -### 3. 认证与授权 (JWT) - -在前后端分离架构中,常用的认证机制是 JSON Web Token (JWT)。 - -- **流程**: - 1. 用户使用用户名和密码登录。 - 2. 后端验证凭据,如果成功,则生成一个 JWT 并返回给前端。 - 3. 前端将收到的 JWT 存储起来(例如,在 `localStorage` 或 `sessionStorage` 中)。 - 4. 在后续的每个请求中,前端都会在 HTTP 请求的 `Authorization` 头中携带这个 JWT。 - 5. 后端的安全过滤器会拦截每个请求,验证 JWT 的有效性。如果验证通过,则允许访问受保护的资源。 - -## 三、项目业务逻辑与分层结构分析 - -本项目是一个环境管理系统 (EMS),其代码结构遵循经典的分层架构模式。 - -### 1. 分层结构概述 - -``` -+----------------------------------------------------+ -| Controller (表现层) | -| (处理 HTTP 请求, 调用 Service, 返回 JSON 响应) | -+----------------------+-----------------------------+ - | (调用) -+----------------------v-----------------------------+ -| Service (业务逻辑层) | -| (实现核心业务逻辑, 事务管理) | -+----------------------+-----------------------------+ - | (调用) -+----------------------v-----------------------------+ -| Repository (数据访问层) | -| (通过 Spring Data JPA 与数据库交互) | -+----------------------+-----------------------------+ - | (操作) -+----------------------v-----------------------------+ -| Model (领域模型) | -| (定义数据结构, 对应数据库表) | -+----------------------------------------------------+ -``` - -### 2. 各层详细分析 - -#### a. Model (领域模型) - -位于 `com.dne.ems.model` 包下,是应用程序的核心。这些是简单的 Java 对象 (POJO),使用 JPA 注解(如 `@Entity`, `@Table`, `@Id`, `@Column`)映射到数据库中的表。 - -- **核心实体**: - - `UserAccount`: 用户账户信息,包含角色、状态等。 - - `Task`: 任务实体,包含任务状态、描述、位置等信息。 - - `Feedback`: 公众或用户提交的反馈信息。 - - `Grid`: 地理网格信息,用于管理和分配任务区域。 - - `Attachment`: 附件信息,如上传的图片。 - -#### b. Repository (数据访问层) - -位于 `com.dne.ems.repository` 包下。这些是接口,继承自 Spring Data JPA 的 `JpaRepository`。开发者只需要定义接口,Spring Data JPA 会在运行时自动为其提供实现,从而极大地简化了数据访问代码。 - -- **示例**:`TaskRepository` 提供了对 `Task` 实体的 CRUD (创建、读取、更新、删除) 操作,以及基于方法名约定的查询功能(如 `findByStatus(TaskStatus status)`)。 - -#### c. Service (业务逻辑层) - -位于 `com.dne.ems.service` 包下,是业务逻辑的核心实现。Service 层封装了应用程序的业务规则和流程,并负责协调多个 Repository 来完成一个完整的业务操作。事务管理也通常在这一层通过 `@Transactional` 注解来声明。 - -- **主要 Service 及其职责**: - - `AuthService`: 处理用户认证,如登录、注册。 - - `TaskManagementService`: 负责任务的创建、更新、查询等核心管理功能。 - - `TaskAssignmentService`: 负责任务的分配逻辑,如自动分配或手动指派。 - - `SupervisorService`: 主管相关的业务逻辑,如审核任务。 - - `GridWorkerTaskService`: 网格员的任务处理逻辑,如提交任务进度。 - - `FeedbackService`: 处理公众反馈,可能包括创建任务等后续操作。 - - `PersonnelService`: 员工管理,如添加、查询用户信息。 - -- **层间关系**:Service 通常会注入 (DI) 一个或多个 Repository 来操作数据。一个 Service 也可能调用另一个 Service 来复用业务逻辑。 - -#### d. Controller (表现层) - -位于 `com.dne.ems.controller` 包下。Controller 负责接收来自客户端的 HTTP 请求,解析请求参数,然后调用相应的 Service 来处理业务逻辑。最后,它将 Service 返回的数据(通常是 DTO)封装成 HTTP 响应(通常是 JSON 格式)返回给客户端。 - -- **主要 Controller 及其职责**: - - `AuthController`: 暴露 `/api/auth/login`, `/api/auth/register` 等认证相关的端点。 - - `TaskManagementController`: 提供任务管理的 RESTful API,如 `GET /api/tasks`, `POST /api/tasks`。 - - `SupervisorController`: 提供主管操作的 API,如审核、分配任务。 - - `FileController`: 处理文件上传和下载。 - -- **DTO (Data Transfer Object)**:位于 `com.dne.ems.dto` 包下。Controller 和 Service 之间,以及 Controller 和前端之间,通常使用 DTO 来传输数据。这样做的好处是可以避免直接暴露数据库实体,并且可以根据前端页面的需要来定制数据结构,避免传输不必要的信息。 - -### 3. 安全 (Security) - -位于 `com.dne.ems.security` 包下。使用 Spring Security 来提供全面的安全服务。 - -- `SecurityConfig`: 配置安全规则,如哪些 URL 是公开的,哪些需要认证,以及配置 JWT 过滤器。 -- `JwtAuthenticationFilter`: 一个自定义的过滤器,在每个请求到达 Controller 之前执行。它从请求头中提取 JWT,验证其有效性,并将用户信息加载到 Spring Security 的上下文中。 -- `UserDetailsServiceImpl`: 实现了 Spring Security 的 `UserDetailsService` 接口,负责根据用户名从数据库加载用户信息(包括密码和权限)。 - -## 四、总结 - -这个 Spring Boot 项目是一个典型的、结构清晰的、基于 RESTful API 的前后端分离应用。它有效地利用了 Spring Boot 的自动配置和起步依赖来简化开发,并通过分层架构将表现、业务逻辑和数据访问清晰地分离开来,使得项目易于理解、维护和扩展。 - -希望这份文档能帮助您在答疑中充满信心! - -## 五、为小白定制:项目代码结构与工作流程详解 - -为了让您彻底理解代码是如何组织的,我们把整个后端项目想象成一个分工明确的大公司。 - -### 1. 公司各部门职责 (包/文件夹的作用) - -- **`com.dne.ems.config` (后勤与配置部)** - - **职责**: 负责应用的各种基础配置。比如 `WebClientConfig` 是配置网络请求工具的,`WebSocketConfig` 是配置实时通讯的,`DataInitializer` 则是在项目启动时初始化一些默认数据(比如默认的管理员账号)。这个部门确保了其他所有部门的工具和环境都是准备好的。 - -- **`com.dne.ems.model` (产品设计部)** - - **职责**: 定义公司的核心“产品”是什么样的。这里的“产品”就是数据。例如,`UserAccount.java` 定义了“用户”有哪些属性(姓名、密码、角色等),`Task.java` 定义了“任务”有哪些属性(标题、状态、截止日期等)。它们是整个公司的业务基础,决定了公司要处理什么信息。这些文件直接对应数据库里的表结构。 - -- **`com.dne.ems.repository` (仓库管理部)** - - **职责**: 专门负责和数据库这个大“仓库”打交道。当需要存取数据时,其他部门不会直接去仓库,而是会通知仓库管理员。例如,`TaskRepository` 提供了所有操作 `Task` 表的方法,如保存一个新任务、根据ID查找一个任务。这个部门使用了 Spring Data JPA 技术,所以代码看起来很简单,但功能强大。 - -- **`com.dne.ems.service` (核心业务部)** - - **职责**: 这是公司的核心部门,负责处理所有实际的业务逻辑。一个业务操作可能很复杂,需要协调多个部门。例如,`TaskManagementService` (任务管理服务) 在创建一个新任务时,可能需要: - 1. 调用 `TaskRepository` 将任务信息存入数据库。 - 2. 调用 `UserAccountRepository` 查找合适的执行人。 - 3. 调用 `NotificationService` (如果存在的话) 发送通知。 - - **`impl` 子目录**: `service` 包下通常是接口 (定义了“能做什么”),而 `impl` (implementation) 包下是这些接口的具体实现 (定义了“具体怎么做”)。这是为了“面向接口编程”,是一种良好的编程习惯。 - -- **`com.dne.ems.controller` (客户服务与接待部)** - - **职责**: 这是公司的“前台”,直接面向客户(在这里就是前端浏览器或App)。它接收客户的请求(HTTP 请求),然后将请求传达给相应的业务部门 (`Service`) 去处理。处理完成后,它再把结果(通常是 JSON 格式的数据)返回给客户。 - - 例如,`TaskManagementController` 里的一个 `createTask` 方法,会接收前端发来的创建任务的请求,然后调用 `TaskManagementService` 的 `createTask` 方法来真正执行创建逻辑。 - -- **`com.dne.ems.dto` (数据包装与运输部)** - - **职责**: 负责数据的包装和运输。直接把数据库里的原始数据 (`Model`) 给客户看是不安全也不专业的。DTO (Data Transfer Object) 就是专门用于在各部门之间,特别是“前台”(`Controller`)和客户(前端)之间传递数据的“包装盒”。 - - 例如,`TaskDTO` 可能只包含任务的ID、标题和状态,而隐藏了创建时间、更新时间等前端不需要的敏感信息。 - -- **`com.dne.ems.security` (安保部)** - - **职责**: 负责整个公司的安全。`SecurityConfig` 定义了公司的安保规则(比如哪些地方需要门禁卡才能进),`JwtAuthenticationFilter` 就是门口的保安,每个请求进来都要检查它的“门禁卡”(JWT Token) 是否有效。 - -- **`com.dne.ems.exception` (风险与危机处理部)** - - **职责**: 处理各种突发异常情况。当业务流程中出现错误时(比如找不到用户、数据库连接失败),`GlobalExceptionHandler` 会捕获这些错误,并给出一个统一、友好的错误提示给前端,而不是让程序崩溃。 - -### 2. 一次典型的请求流程:用户登录 - -让我们通过“用户登录”这个简单的例子,看看这些部门是如何协同工作的: - -1. **客户上门 (前端 -> Controller)**: 用户在前端页面输入用户名和密码,点击登录。前端将这些信息打包成一个 JSON 对象,发送一个 HTTP POST 请求到后端的 `/api/auth/login` 地址。 - -2. **前台接待 (Controller)**: `AuthController` 接收到这个请求。它看到地址是 `/login`,于是调用 `login` 方法。它从请求中取出 JSON 数据,并将其转换为 `LoginRequest` 这个 DTO 对象。 - -3. **转交业务部门 (Controller -> Service)**: `AuthController` 自己不处理登录逻辑,它调用 `AuthService` 的 `login` 方法,并将 `LoginRequest` 这个 DTO 传递过去。 - -4. **核心业务处理 (Service)**: `AuthService` 开始处理登录: - a. 它首先需要验证用户名是否存在,于是它请求 **仓库管理部** (`UserAccountRepository`):“请帮我根据这个用户名 (`loginRequest.getUsername()`) 查一下有没有这个人。” - b. `UserAccountRepository` 从数据库中查找到对应的 `UserAccount` (Model) 信息,返回给 `AuthService`。 - c. `AuthService` 拿到用户信息后,会使用密码加密工具,验证用户提交的密码和数据库中存储的加密密码是否匹配。 - d. 如果验证成功,`AuthService` 会请求 **安保部** (`JwtService`):“请为这位用户生成一张有时效的门禁卡 (JWT Token)。” - -5. **返回处理结果 (Service -> Controller -> 前端)**: `JwtService` 生成 Token 后返回给 `AuthService`。`AuthService` 将 Token 包装在一个 `JwtAuthenticationResponse` DTO 中,返回给 `AuthController`。`AuthController` 再将这个包含 Token 的 DTO 转换成 JSON 格式,作为 HTTP 响应返回给前端。 - -6. **客户拿到凭证 (前端)**: 前端收到包含 Token 的响应,就知道登录成功了。它会把这个 Token 保存起来,在之后访问需要权限的页面时,都会带上这张“门禁卡”。 - -通过这个流程,您可以看到,一个简单的请求背后是多个“部门”按照明确的职责划分,层层调用、协同工作的结果。这种结构使得代码逻辑清晰,易于维护和扩展。 -嗯 -## 六、核心业务流程与关键机制分析 - -下面,我们将梳理出本项目的几个核心业务流程和支撑这些流程的关键技术机制,并详细说明它们涉及的层级和文件,让您清晰地看到数据和指令是如何在系统中流转的。 - -### (一) 核心业务流程 - -#### 流程一:用户认证与授权 (Authentication & Authorization) - -**目标**:保障系统安全,确保只有授权用户才能访问受保护的资源。 - -**相关API端点** (`/api/auth`): -- `POST /api/auth/signup`: 用户注册。 -- `POST /api/auth/login`: 用户登录。 -- `POST /api/auth/send-verification-code`: 发送邮箱验证码。 -- `POST /api/auth/request-password-reset`: 请求重置密码。 -- `POST /api/auth/reset-password`: 执行密码重置。 - -**执行路径**: - -1. **注册 (`signup`)** - * **入口**: `AuthController.signup()` 对应 `POST /api/auth/signup`。 - * **业务逻辑**: `AuthService.signup()` - * 验证邮箱和验证码的有效性 (`VerificationCodeService`)。 - * 检查邮箱是否已注册 (`UserAccountRepository`)。 - * 对密码进行加密 (`PasswordEncoder`)。 - * 创建并保存新的 `UserAccount` 实体。 - -2. **登录 (`login`)** - * **入口**: `AuthController.login()` 对应 `POST /api/auth/login`。 - * **业务逻辑**: `AuthService.login()` - * 通过 Spring Security 的 `AuthenticationManager` 验证用户名和密码。 - * 如果认证成功,调用 `JwtService` 生成 JWT。 - * 返回包含 JWT 的响应给前端。 - -3. **访问受保护资源** - * **安全过滤器**: `JwtAuthenticationFilter` - * 拦截所有请求,从 `Authorization` 请求头中提取 JWT。 - * 验证 JWT 的有效性 (`JwtService`)。 - * 如果有效,从 JWT 中解析出用户信息,并构建一个 `UsernamePasswordAuthenticationToken`,设置到 Spring Security 的 `SecurityContextHolder` 中,完成授权。 - -#### 流程二:公众反馈与任务转化 (Feedback to Task) - -**目标**:将公众通过开放接口提交的反馈(如环境问题)转化为系统内部的可追踪任务。 - -**相关API端点**: -- `POST /api/public/feedback`: 公众提交反馈。 -- `GET /api/supervisor/reviews`: 主管获取待审核的反馈列表。 -- `POST /api/supervisor/reviews/{feedbackId}/approve`: 主管批准反馈。 -- `POST /api/supervisor/reviews/{feedbackId}/reject`: 主管拒绝反馈。 -- `POST /api/management/tasks/feedback/{feedbackId}/create-task`: 从反馈创建任务。 - -**执行路径**: - -1. **提交反馈** - * **入口**: `PublicController.submitFeedback()` 对应 `POST /api/public/feedback`。 - * **业务逻辑**: `FeedbackService.createFeedback()` - * 接收 `PublicFeedbackRequest`,包含反馈内容和可选的附件。 - * 处理文件上传(如果存在),调用 `FileStorageService` 保存文件,并将附件信息与反馈关联。 - * 保存 `Feedback` 实体到数据库。 - -2. **反馈审核与任务创建 (由主管操作)** - * **入口**: `SupervisorController` 和 `TaskManagementController` 的相关方法。 - * **业务逻辑**: `SupervisorService.processFeedback()` 和 `TaskManagementService.createTaskFromFeedback()` - * 主管通过 `GET /api/supervisor/reviews` 查看待处理反馈。 - * 通过 `POST /api/supervisor/reviews/{feedbackId}/approve` 批准反馈。 - * 批准后,可通过 `POST /api/management/tasks/feedback/{feedbackId}/create-task` 基于反馈信息创建一个新的 `Task`。 - -#### 流程三:任务生命周期管理 (Task Lifecycle) - -**目标**:完整地管理一个任务从创建、分配、执行到完成的全过程。 - -**相关API端点**: -- **主管/管理员**: - - `POST /api/management/tasks`: 直接创建任务。 - - `GET /api/management/tasks`: 获取任务列表。 - - `POST /api/management/tasks/{taskId}/assign`: 分配任务。 - - `POST /api/management/tasks/{taskId}/review`: 审核任务。 - - `POST /api/management/tasks/{taskId}/cancel`: 取消任务。 -- **网格员**: - - `GET /api/worker`: 获取我的任务列表。 - - `GET /api/worker/{taskId}`: 获取任务详情。 - - `POST /api/worker/{taskId}/accept`: 接受任务。 - - `POST /api/worker/{taskId}/submit`: 提交任务成果。 - -**执行路径**: - -1. **任务创建** - * **入口**: `TaskManagementController.createTask()` 对应 `POST /api/management/tasks`。 - * **业务逻辑**: `TaskManagementService.createTask()` - * 创建 `Task` 实体并设置初始状态(如 `PENDING_ASSIGNMENT`)。 - -2. **任务分配** - * **入口**: `TaskManagementController.assignTask()` 对应 `POST /api/management/tasks/{taskId}/assign`。 - * **业务逻辑**: `TaskAssignmentService.assignTaskToWorker()` - * 将任务 (`Task`) 与一个网格员 (`GridWorker`) 关联起来,创建 `Assignment` 记录。 - * 更新任务状态为 `ASSIGNED`。 - -3. **任务执行与更新** - * **入口**: `GridWorkerTaskController` 的相关方法,如 `acceptTask()` 和 `submitTask()`。 - * **业务逻辑**: `GridWorkerTaskService.updateTaskProgress()` - * 网格员通过 `POST /api/worker/{taskId}/accept` 接受任务。 - * 通过 `POST /api/worker/{taskId}/submit` 提交任务进度或结果,可附带图片等附件 (`FileStorageService`)。 - * 更新任务状态(如 `IN_PROGRESS`, `COMPLETED`)。 - -4. **任务审核与关闭** - * **入口**: `TaskManagementController.reviewTask()` 对应 `POST /api/management/tasks/{taskId}/review`。 - * **业务逻辑**: `SupervisorService.reviewAndCloseTask()` - * 主管审核已完成的任务。 - * 如果合格,将任务状态更新为 `CLOSED`。如果不合格,可打回 (`REJECTED`)。 - -#### 流程四:数据可视化与决策支持 (Dashboard & AI) - -**目标**:为管理层提供数据洞察,并通过 AI 功能辅助决策。 - -**相关API端点**: -- **仪表盘** (`/api/dashboard`): - - `GET /api/dashboard/stats`: 获取核心统计数据。 - - `GET /api/dashboard/reports/aqi-distribution`: 获取 AQI 分布。 - - `GET /api/dashboard/map/heatmap`: 获取反馈热力图数据。 - - `GET /api/dashboard/reports/task-completion-stats`: 获取任务完成情况统计。 -- **路径规划** (`/api/pathfinding`): - - `POST /api/pathfinding/find`: 使用 A* 算法寻找路径。 - -**执行路径**: - -1. **仪表盘数据** - * **入口**: `DashboardController` 中的各个接口。 - * **业务逻辑**: `DashboardService` - * 调用多个 `Repository`(如 `TaskRepository`, `FeedbackRepository`)进行数据聚合查询,统计任务状态、反馈趋势等。 - * 返回 DTO 给前端进行图表展示。 - -2. **AI 辅助功能** - * **文本审核**: `AiReviewService.reviewText()` 可能被用于自动审核反馈内容或评论,识别敏感信息 (无直接API,内部调用)。 - * **路径规划**: `AStarService.findPath()` 对应 `POST /api/pathfinding/find`,用于计算两点之间的最优路径,可辅助网格员规划任务路线。 - -### (二) 关键支撑机制 - -除了核心业务流程,系统还包含一系列关键的技术机制来保证其健壮、安全和可维护性。 - -1. **文件上传与管理** - * **位置**: `FileStorageService`, `FileController` - * **作用**: 提供统一的文件存储和访问服务。无论是用户头像、反馈附件还是任务报告,都通过此服务进行处理,实现了与具体存储位置(本地文件系统或云存储)的解耦。 - -2. **邮件与验证码服务** - * **位置**: `MailService`, `VerificationCodeService` - * **作用**: 负责发送邮件(如注册验证、密码重置)和管理验证码的生成与校验,是保障账户安全的重要环节。 - -3. **登录安全与尝试锁定** - * **位置**: `LoginAttemptService`, `AuthenticationFailureEventListener` - * **作用**: 监听登录失败事件 (`AuthenticationFailureListener`),并通过 `LoginAttemptService` 记录特定 IP 的失败次数。当超过阈值时,会暂时锁定该 IP 的登录权限,有效防止暴力破解攻击。 - -4. **A* 路径规划算法** - * **位置**: `AStarService`, `PathfindingController` - * **作用**: 实现了 A* 寻路算法,这是一个独立的功能模块,可以根据地图数据(网格 `Grid`)计算最优路径,为其他业务(如任务路线规划)提供支持。 - -5. **系统事件处理机制** - * **位置**: `event` 和 `listener` 包 - * **作用**: 基于 Spring 的事件驱动模型,实现系统内各组件的解耦。例如,当一个用户注册成功后,可以发布一个 `UserRegistrationEvent` 事件,而邮件发送监听器 (`EmailNotificationListener`) 和新用户欢迎监听器 (`WelcomeBonusListener`) 可以分别监听并处理此事件,而无需在注册服务中硬编码这些逻辑。 - -6. **全局异常处理** - * **位置**: `GlobalExceptionHandler` - * **作用**: 捕获整个应用中抛出的未处理异常,并根据异常类型返回统一、规范的错误响应给前端。这避免了将原始的、可能包含敏感信息的堆栈跟踪暴露给用户,并提升了用户体验。 - -7. **自定义数据校验** - * **位置**: `validation` 包 (如 `PasswordValidator`) - * **作用**: 实现自定义的、复杂的校验逻辑。例如,`@ValidPassword` 注解可以确保用户设置的密码符合特定的强度要求(如长度、包含数字和特殊字符等)。 - -8. **数据持久化与查询 (JPA)** - * **位置**: `repository` 包 - * **作用**: 利用 Spring Data JPA,通过定义接口 (`extends JpaRepository`) 的方式,极大地简化了数据库的 CRUD 操作和查询。开发者无需编写 SQL 语句,JPA 会自动根据方法名或注解生成相应的查询。 - -9. **API 数据契约 (DTO)** - * **位置**: `dto` 包 - * **作用**: 数据传输对象 (Data Transfer Object) 是前后端分离架构中的关键实践。它作为 API 的“数据契约”,明确了接口需要什么数据、返回什么数据,实现了表现层与领域模型的解耦,使得双方可以独立演进,同时避免了敏感信息的泄露。 - -10. **应用配置与初始化** - * **位置**: `config` 包, `DataInitializer` - * **作用**: `config` 包集中管理了应用的所有配置类,如安全配置 (`SecurityConfig`)、Web 配置等。`DataInitializer` 则利用 `CommandLineRunner` 接口,在应用启动后执行一次性任务,如创建默认管理员账户、初始化基础数据等,确保了应用开箱即用的能力。 - * **密码加密**: 使用 `PasswordEncoder` 对用户密码进行加密,增强安全性。 - * **创建用户实体**: 创建一个新的 `UserAccount` 对象 (`Model`),并填充信息。 - -3. **Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.UserAccountRepository.java` - * **方法**: `save(UserAccount user)` - * **作用**: `AuthService` 调用此方法,将新创建的 `UserAccount` 对象持久化到数据库中。 - -4. **Model (领域模型)** - * **文件**: `com.dne.ems.model.UserAccount.java` - * **作用**: 定义了用户账户的数据结构,是数据库中 `user_accounts` 表的映射。 - -### 流程二:任务创建与分配 - -**目标**:主管或管理员根据一个已批准的反馈,创建一个新任务,并将其分配给一个网格员。 - -**执行路径**: - -1. **Controller (表现层)** - * **文件**: `com.dne.ems.controller.TaskManagementController.java` - * **方法**: `createTaskFromFeedback(long feedbackId, TaskFromFeedbackRequest request)` - * **作用**: 接收 `/api/management/tasks/feedback/{feedbackId}/create-task` POST 请求。它将反馈ID和任务创建请求 (`TaskFromFeedbackRequest` DTO) 传递给 Service 层。 - -2. **Service (业务逻辑层)** - * **文件**: `com.dne.ems.service.impl.TaskManagementServiceImpl.java` - * **方法**: `createTaskFromFeedback(long feedbackId, TaskFromFeedbackRequest request)` - * **作用**: 实现核心的创建和分配逻辑: - * **查找反馈**: 调用 `FeedbackRepository` 找到对应的 `Feedback` (`Model`)。 - * **查找负责人**: 调用 `UserAccountRepository` 找到指定的负责人 `UserAccount` (`Model`)。 - * **创建任务**: 创建一个新的 `Task` 对象 (`Model`),并从 `Feedback` 和请求 DTO 中填充任务信息(如描述、截止日期、严重性等)。 - * **设置状态**: 将任务的初始状态设置为 `ASSIGNED`。 - -3. **Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.TaskRepository.java` - * **方法**: `save(Task task)` - * **作用**: `TaskManagementService` 调用此方法,将新创建的 `Task` 对象保存到数据库。 - * **涉及的其他 Repository**: `FeedbackRepository` 和 `UserAccountRepository` 用于在业务逻辑中获取必要的数据。 - -4. **Model (领域模型)** - * **文件**: `com.dne.ems.model.Task.java` - * **作用**: 定义了任务的数据结构。 - * **涉及的其他 Model**: `Feedback.java` 和 `UserAccount.java` 作为数据来源。 - -### 流程三:网格员处理任务与提交反馈 - -**目标**:网格员查看自己的任务,执行任务,并提交处理结果(包括评论和附件)。 - -**执行路径**: - -1. **Controller (表现层)** - * **文件**: `com.dne.ems.controller.GridWorkerTaskController.java` - * **方法**: `submitTask(long taskId, String comments, List files)` - * **作用**: 接收 `/api/worker/{taskId}/submit` POST 请求。这是一个 `multipart/form-data` 请求,因为它同时包含了文本(评论)和文件。Controller 将这些信息传递给 Service 层。 - -2. **Service (业务逻辑层)** - * **文件**: `com.dne.ems.service.impl.GridWorkerTaskServiceImpl.java` - * **方法**: `submitTask(long taskId, String comments, List files)` - * **作用**: 处理任务提交的业务逻辑: - * **查找任务**: 调用 `TaskRepository` 找到当前需要处理的 `Task` (`Model`)。 - * **验证权限**: 确保当前登录的用户就是这个任务的负责人。 - * **处理文件上传**: 如果有附件,调用 `FileStorageService` 将文件保存到服务器,并创建 `Attachment` (`Model`) 对象。 - * **更新任务状态**: 将任务状态更新为 `SUBMITTED` 或 `PENDING_REVIEW`。 - * **保存评论和附件信息**: 将评论和附件信息关联到任务上。 - -3. **Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.TaskRepository.java` - * **方法**: `save(Task task)` - * **作用**: `GridWorkerTaskService` 在更新了任务状态和信息后,调用此方法将 `Task` 对象的变化持久化到数据库。 - * **涉及的其他 Repository**: `AttachmentRepository` (如果适用) 用于保存附件信息。 - -4. **Model (领域模型)** - * **文件**: `com.dne.ems.model.Task.java` - * **作用**: 任务数据在此流程中被更新。 - * **涉及的其他 Model**: `Attachment.java` 用于表示上传的文件。 - * **密码加密**: 使用 `PasswordEncoder` 对用户提交的明文密码进行加密,确保数据库中存储的是密文,保障安全。 - * **创建用户实体**: 创建一个新的 `UserAccount` (Model) 对象,并将注册信息(包括加密后的密码和角色)设置到该对象中。 - -3. **Service -> Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.UserAccountRepository.java` - * **方法**: `save(UserAccount userAccount)` - * **作用**: Service 层调用此方法,将填充好数据的 `UserAccount` 实体对象持久化到数据库中。Spring Data JPA 会自动生成对应的 SQL `INSERT` 语句来完成操作。 - -4. **返回结果**: 数据成功存入数据库后,操作结果会逐层返回,最终 `AuthController` 会向前端返回一个成功的响应消息。 - -### 流程二:任务的创建与自动分配 - -**目标**:系统管理员或主管创建一个新的环境问题任务,并由系统自动分配给相应网格内的网格员。 - -**执行路径**: - -1. **前端 -> Controller (表现层)** - * **文件**: `com.dne.ems.controller.TaskManagementController.java` - * **方法**: `createTask(TaskCreateRequest taskCreateRequest)` - * **作用**: 接收 `/api/tasks` POST 请求,请求体中包含了任务的详细信息(如描述、位置、图片附件ID等),封装在 `TaskCreateRequest` DTO 中。Controller 调用 `TaskManagementService` 来处理创建逻辑。 - -2. **Controller -> Service (业务逻辑层)** - * **文件**: `com.dne.ems.service.impl.TaskManagementServiceImpl.java` - * **方法**: `createTask(TaskCreateRequest taskCreateRequest)` - * **作用**: 创建任务的核心业务逻辑。 - * 创建一个新的 `Task` (Model) 实体。 - * 根据请求中的附件ID,调用 `AttachmentRepository` 查找并关联附件。 - * 设置任务的初始状态为 `PENDING` (待处理)。 - * 调用 `TaskRepository` 将新任务保存到数据库。 - * **触发任务分配**: 保存任务后,调用 `TaskAssignmentService` 来执行自动分配逻辑。 - -3. **Service -> Service (业务逻辑层内部调用)** - * **文件**: `com.dne.ems.service.impl.TaskAssignmentServiceImpl.java` - * **方法**: `assignTask(Task task)` - * **作用**: 处理任务的分配逻辑。 - * 根据任务的地理位置信息,调用 `GridRepository` 找到对应的地理网格 (`Grid`)。 - * 根据网格信息,调用 `UserAccountRepository` 找到负责该网格的网格员 (`GridWorker`)。 - * 如果找到合适的网格员,则更新 `Task` 实体的 `assignee` (执行人) 字段。 - * 更新任务状态为 `ASSIGNED` (已分配)。 - * 调用 `TaskRepository` 将更新后的任务信息保存回数据库。 - * (可选) 调用通知服务,向被分配的网格员发送新任务通知。 - -4. **Service -> Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.TaskRepository.java`, `com.dne.ems.repository.GridRepository.java`, `com.dne.ems.repository.UserAccountRepository.java` - * **作用**: 在整个流程中,Service 层会频繁地与这些 Repository 交互,以查询网格信息、查询用户信息、保存和更新任务数据。 - -### 流程三:网格员处理任务并提交反馈 - -**目标**:网格员在完成任务后,通过 App 或前端页面提交任务处理结果,包括文字描述和现场图片。 - -**执行路径**: - -1. **前端 -> Controller (表现层)** - * **文件**: `com.dne.ems.controller.GridWorkerTaskController.java` - * **方法**: `submitTaskFeedback(Long taskId, TaskFeedbackRequest feedbackRequest)` - * **作用**: 接收 `/api/worker/tasks/{taskId}/feedback` POST 请求。路径中的 `taskId` 指明了要为哪个任务提交反馈,请求体 `TaskFeedbackRequest` DTO 中包含了反馈内容和附件ID。 - -2. **Controller -> Service (业务逻辑层)** - * **文件**: `com.dne.ems.service.impl.GridWorkerTaskServiceImpl.java` - * **方法**: `submitTaskFeedback(Long taskId, TaskFeedbackRequest feedbackRequest)` - * **作用**: 处理网格员提交反馈的业务逻辑。 - * 调用 `TaskRepository` 检查任务是否存在以及当前用户是否有权限操作该任务。 - * 更新 `Task` 实体的状态为 `COMPLETED` (已完成) 或 `PENDING_REVIEW` (待审核)。 - * 创建一个新的 `Feedback` (Model) 实体,将反馈内容和关联的 `Task` 设置进去。 - * 调用 `FeedbackRepository` 将新的反馈信息保存到数据库。 - * 调用 `TaskRepository` 更新任务状态。 - -3. **Service -> Repository (数据访问层)** - * **文件**: `com.dne.ems.repository.TaskRepository.java`, `com.dne.ems.repository.FeedbackRepository.java` - * **作用**: `TaskRepository` 用于查询和更新任务信息,`FeedbackRepository` 用于保存新的反馈记录。 - -通过以上对核心业务流程的梳理,您可以清晰地看到,用户的每一个操作是如何触发后端系统中不同层、不同文件之间的一系列连锁反应,最终完成一个完整的业务闭环。这种清晰的、分层的架构是保证项目可维护性和扩展性的关键。 + +# Spring Boot 项目深度解析 + +本文档旨在深入解析当前 Spring Boot 项目的构建原理、核心技术以及业务逻辑,帮助您更好地理解和准备答疑。 + +## 一、Spring Boot 核心原理 + +Spring Boot 是一个用于简化新 Spring 应用的初始搭建以及开发过程的框架。其核心原理主要体现在以下几个方面: + +### 1. 自动配置 (Auto-Configuration) - 智能的“管家” + +您可以将 Spring Boot 的自动配置想象成一个非常智能的“管家”。在传统的 Spring 开发中,您需要手动配置很多东西,比如数据库连接、We你b 服务器等,就像您需要亲自告诉管家每一个细节:如何烧水、如何扫地。 + +而 Spring Boot 的“管家”则会观察您家里添置了什么新东西(即您在项目中加入了什么依赖),然后自动地把这些东西配置好,让它们能正常工作。 + +**它是如何工作的?** + +1. **观察您的“购物清单” (`pom.xml`)**:当您在项目的 `pom.xml` 文件中加入一个 `spring-boot-starter-*` 依赖,比如 `spring-boot-starter-web`,就等于告诉“管家”:“我需要开发一个网站。” + +2. **自动准备所需工具**:看到这个“购物清单”后,“管家”会立刻为您准备好一套完整的网站开发工具: + * 一个嵌入式的 **Tomcat 服务器**,这样您就不用自己安装和配置服务器了。 + * 配置好 **Spring MVC** 框架,这是处理网页请求的核心工具。 + * 设置好处理 JSON 数据的工具 (Jackson),方便前后端通信。 + +3. **背后的魔法 (`@EnableAutoConfiguration`)**:这一切的起点是您项目主启动类上的 `@SpringBootApplication` 注解,它内部包含了 `@EnableAutoConfiguration`。这个注解就是给“管家”下达“开始自动工作”命令的开关。它会去检查一个固定的清单(位于 `META-INF/spring.factories`),上面写着所有它认识的工具(自动配置类)以及何时应该启用它们。 + +**项目实例:** + +在本项目中,因为 `pom.xml` 包含了 `spring-boot-starter-web`,所以 Spring Boot 自动为您配置了处理 Web 请求的所有环境。这就是为什么您可以在 `com.dne.ems.controller` 包下的任何一个 Controller(例如 `AuthController`)中直接使用 `@RestController` 和 `@PostMapping` 这样的注解来定义 API 接口,而无需编写任何一行 XML 配置来启动 Web 服务或配置 Spring MVC。 + +简单来说,**自动配置就是 Spring Boot 替您完成了大部分繁琐、重复的配置工作,让您可以专注于编写业务代码。** + +### 2. 起步依赖 (Starter Dependencies) - “主题工具箱” + +如果说自动配置是“智能管家”,那么起步依赖就是“管家”使用的“主题工具箱”。您不需要告诉管家需要买钉子、锤子、螺丝刀……您只需要说:“我需要一个木工工具箱。” + +起步依赖 (`spring-boot-starter-*`) 就是这样的“主题工具箱”。每一个 starter 都包含了一整套用于某个特定任务的、经过测试、版本兼容的依赖。 + +**项目实例:** + +- **`spring-boot-starter-web`**: 这是“Web 开发工具箱”。它不仅包含了 Spring MVC,还包含了内嵌的 Tomcat 服务器和处理验证的库。您只需要引入这一个依赖,所有 Web 开发的基础环境就都准备好了。 + +- **`spring-boot-starter-data-jpa`**: 这是“数据库操作工具箱”。它打包了与数据库交互所需的一切,包括 Spring Data JPA、Hibernate (JPA 的一种实现) 和数据库连接池 (HikariCP)。您引入它之后,就可以直接在 `com.dne.ems.repository` 包下编写 `JpaRepository` 接口来进行数据库操作,而无需关心如何配置 Hibernate 或事务管理器。 + +- **`spring-boot-starter-security`**: 这是“安全管理工具箱”。引入它之后,Spring Security 框架就被集成进来,提供了认证和授权的基础功能。您可以在 `com.dne.ems.security.SecurityConfig` 中看到对这个“工具箱”的具体配置,比如定义哪些 API 需要登录才能访问。 + +**优势总结:** + +- **简化依赖**:您不必再手动添加一长串的单个依赖项。 +- **避免冲突**:Spring Boot 已经为您管理好了这些依赖之间的版本,您不必再担心因为版本不兼容而导致的各种奇怪错误。 + +通过使用这些“工具箱”,您可以非常快速地搭建起一个功能完备的应用程序框架。 + +### 3. 嵌入式 Web 服务器 + +Spring Boot 内嵌了常见的 Web 服务器,如 Tomcat、Jetty 或 Undertow。这意味着您不需要将应用程序打包成 WAR 文件部署到外部服务器,而是可以直接将应用程序打包成一个可执行的 JAR 文件,通过 `java -jar` 命令来运行。 + +- **优势**:简化了部署流程,便于持续集成和持续部署 (CI/CD)。 + +## 二、前后端分离与交互原理 + +本项目采用前后端分离的架构,前端(如 Vue.js)和后端(Spring Boot)是两个独立的项目,它们之间通过 API 进行通信。 + +### 1. RESTful API + +后端通过暴露一组 RESTful API 来提供服务。REST (Representational State Transfer) 是一种软件架构风格,它定义了一组用于创建 Web 服务的约束。核心思想是,将应用中的所有事物都看作资源,每个资源都有一个唯一的标识符 (URI)。 + +- **HTTP 方法**: + - `GET`:获取资源。 + - `POST`:创建新资源。 + - `PUT` / `PATCH`:更新资源。 + - `DELETE`:删除资源。 + +### 2. JSON (JavaScript Object Notation) + +前后端之间的数据交换格式通常是 JSON。当浏览器向后端发送请求时,它可能会在请求体中包含 JSON 数据。当后端响应时,它会将 Java 对象序列化为 JSON 字符串返回给前端。 + +- **Spring Boot 中的实现**:Spring Boot 默认使用 Jackson 库来处理 JSON 的序列化和反序列化。当 Controller 的方法参数有 `@RequestBody` 注解时,Spring 会自动将请求体中的 JSON 转换为 Java 对象。当方法有 `@ResponseBody` 或类有 `@RestController` 注解时,Spring 会自动将返回的 Java 对象转换为 JSON。 + +### 3. 认证与授权 (JWT) + +在前后端分离架构中,常用的认证机制是 JSON Web Token (JWT)。 + +- **流程**: + 1. 用户使用用户名和密码登录。 + 2. 后端验证凭据,如果成功,则生成一个 JWT 并返回给前端。 + 3. 前端将收到的 JWT 存储起来(例如,在 `localStorage` 或 `sessionStorage` 中)。 + 4. 在后续的每个请求中,前端都会在 HTTP 请求的 `Authorization` 头中携带这个 JWT。 + 5. 后端的安全过滤器会拦截每个请求,验证 JWT 的有效性。如果验证通过,则允许访问受保护的资源。 + +## 三、项目业务逻辑与分层结构分析 + +本项目是一个环境管理系统 (EMS),其代码结构遵循经典的分层架构模式。 + +### 1. 分层结构概述 + +``` ++----------------------------------------------------+ +| Controller (表现层) | +| (处理 HTTP 请求, 调用 Service, 返回 JSON 响应) | ++----------------------+-----------------------------+ + | (调用) ++----------------------v-----------------------------+ +| Service (业务逻辑层) | +| (实现核心业务逻辑, 事务管理) | ++----------------------+-----------------------------+ + | (调用) ++----------------------v-----------------------------+ +| Repository (数据访问层) | +| (通过 Spring Data JPA 与数据库交互) | ++----------------------+-----------------------------+ + | (操作) ++----------------------v-----------------------------+ +| Model (领域模型) | +| (定义数据结构, 对应数据库表) | ++----------------------------------------------------+ +``` + +### 2. 各层详细分析 + +#### a. Model (领域模型) + +位于 `com.dne.ems.model` 包下,是应用程序的核心。这些是简单的 Java 对象 (POJO),使用 JPA 注解(如 `@Entity`, `@Table`, `@Id`, `@Column`)映射到数据库中的表。 + +- **核心实体**: + - `UserAccount`: 用户账户信息,包含角色、状态等。 + - `Task`: 任务实体,包含任务状态、描述、位置等信息。 + - `Feedback`: 公众或用户提交的反馈信息。 + - `Grid`: 地理网格信息,用于管理和分配任务区域。 + - `Attachment`: 附件信息,如上传的图片。 + +#### b. Repository (数据访问层) + +位于 `com.dne.ems.repository` 包下。这些是接口,继承自 Spring Data JPA 的 `JpaRepository`。开发者只需要定义接口,Spring Data JPA 会在运行时自动为其提供实现,从而极大地简化了数据访问代码。 + +- **示例**:`TaskRepository` 提供了对 `Task` 实体的 CRUD (创建、读取、更新、删除) 操作,以及基于方法名约定的查询功能(如 `findByStatus(TaskStatus status)`)。 + +#### c. Service (业务逻辑层) + +位于 `com.dne.ems.service` 包下,是业务逻辑的核心实现。Service 层封装了应用程序的业务规则和流程,并负责协调多个 Repository 来完成一个完整的业务操作。事务管理也通常在这一层通过 `@Transactional` 注解来声明。 + +- **主要 Service 及其职责**: + - `AuthService`: 处理用户认证,如登录、注册。 + - `TaskManagementService`: 负责任务的创建、更新、查询等核心管理功能。 + - `TaskAssignmentService`: 负责任务的分配逻辑,如自动分配或手动指派。 + - `SupervisorService`: 主管相关的业务逻辑,如审核任务。 + - `GridWorkerTaskService`: 网格员的任务处理逻辑,如提交任务进度。 + - `FeedbackService`: 处理公众反馈,可能包括创建任务等后续操作。 + - `PersonnelService`: 员工管理,如添加、查询用户信息。 + +- **层间关系**:Service 通常会注入 (DI) 一个或多个 Repository 来操作数据。一个 Service 也可能调用另一个 Service 来复用业务逻辑。 + +#### d. Controller (表现层) + +位于 `com.dne.ems.controller` 包下。Controller 负责接收来自客户端的 HTTP 请求,解析请求参数,然后调用相应的 Service 来处理业务逻辑。最后,它将 Service 返回的数据(通常是 DTO)封装成 HTTP 响应(通常是 JSON 格式)返回给客户端。 + +- **主要 Controller 及其职责**: + - `AuthController`: 暴露 `/api/auth/login`, `/api/auth/register` 等认证相关的端点。 + - `TaskManagementController`: 提供任务管理的 RESTful API,如 `GET /api/tasks`, `POST /api/tasks`。 + - `SupervisorController`: 提供主管操作的 API,如审核、分配任务。 + - `FileController`: 处理文件上传和下载。 + +- **DTO (Data Transfer Object)**:位于 `com.dne.ems.dto` 包下。Controller 和 Service 之间,以及 Controller 和前端之间,通常使用 DTO 来传输数据。这样做的好处是可以避免直接暴露数据库实体,并且可以根据前端页面的需要来定制数据结构,避免传输不必要的信息。 + +### 3. 安全 (Security) + +位于 `com.dne.ems.security` 包下。使用 Spring Security 来提供全面的安全服务。 + +- `SecurityConfig`: 配置安全规则,如哪些 URL 是公开的,哪些需要认证,以及配置 JWT 过滤器。 +- `JwtAuthenticationFilter`: 一个自定义的过滤器,在每个请求到达 Controller 之前执行。它从请求头中提取 JWT,验证其有效性,并将用户信息加载到 Spring Security 的上下文中。 +- `UserDetailsServiceImpl`: 实现了 Spring Security 的 `UserDetailsService` 接口,负责根据用户名从数据库加载用户信息(包括密码和权限)。 + +## 四、总结 + +这个 Spring Boot 项目是一个典型的、结构清晰的、基于 RESTful API 的前后端分离应用。它有效地利用了 Spring Boot 的自动配置和起步依赖来简化开发,并通过分层架构将表现、业务逻辑和数据访问清晰地分离开来,使得项目易于理解、维护和扩展。 + +希望这份文档能帮助您在答疑中充满信心! + +## 五、为小白定制:项目代码结构与工作流程详解 + +为了让您彻底理解代码是如何组织的,我们把整个后端项目想象成一个分工明确的大公司。 + +### 1. 公司各部门职责 (包/文件夹的作用) + +- **`com.dne.ems.config` (后勤与配置部)** + - **职责**: 负责应用的各种基础配置。比如 `WebClientConfig` 是配置网络请求工具的,`WebSocketConfig` 是配置实时通讯的,`DataInitializer` 则是在项目启动时初始化一些默认数据(比如默认的管理员账号)。这个部门确保了其他所有部门的工具和环境都是准备好的。 + +- **`com.dne.ems.model` (产品设计部)** + - **职责**: 定义公司的核心“产品”是什么样的。这里的“产品”就是数据。例如,`UserAccount.java` 定义了“用户”有哪些属性(姓名、密码、角色等),`Task.java` 定义了“任务”有哪些属性(标题、状态、截止日期等)。它们是整个公司的业务基础,决定了公司要处理什么信息。这些文件直接对应数据库里的表结构。 + +- **`com.dne.ems.repository` (仓库管理部)** + - **职责**: 专门负责和数据库这个大“仓库”打交道。当需要存取数据时,其他部门不会直接去仓库,而是会通知仓库管理员。例如,`TaskRepository` 提供了所有操作 `Task` 表的方法,如保存一个新任务、根据ID查找一个任务。这个部门使用了 Spring Data JPA 技术,所以代码看起来很简单,但功能强大。 + +- **`com.dne.ems.service` (核心业务部)** + - **职责**: 这是公司的核心部门,负责处理所有实际的业务逻辑。一个业务操作可能很复杂,需要协调多个部门。例如,`TaskManagementService` (任务管理服务) 在创建一个新任务时,可能需要: + 1. 调用 `TaskRepository` 将任务信息存入数据库。 + 2. 调用 `UserAccountRepository` 查找合适的执行人。 + 3. 调用 `NotificationService` (如果存在的话) 发送通知。 + - **`impl` 子目录**: `service` 包下通常是接口 (定义了“能做什么”),而 `impl` (implementation) 包下是这些接口的具体实现 (定义了“具体怎么做”)。这是为了“面向接口编程”,是一种良好的编程习惯。 + +- **`com.dne.ems.controller` (客户服务与接待部)** + - **职责**: 这是公司的“前台”,直接面向客户(在这里就是前端浏览器或App)。它接收客户的请求(HTTP 请求),然后将请求传达给相应的业务部门 (`Service`) 去处理。处理完成后,它再把结果(通常是 JSON 格式的数据)返回给客户。 + - 例如,`TaskManagementController` 里的一个 `createTask` 方法,会接收前端发来的创建任务的请求,然后调用 `TaskManagementService` 的 `createTask` 方法来真正执行创建逻辑。 + +- **`com.dne.ems.dto` (数据包装与运输部)** + - **职责**: 负责数据的包装和运输。直接把数据库里的原始数据 (`Model`) 给客户看是不安全也不专业的。DTO (Data Transfer Object) 就是专门用于在各部门之间,特别是“前台”(`Controller`)和客户(前端)之间传递数据的“包装盒”。 + - 例如,`TaskDTO` 可能只包含任务的ID、标题和状态,而隐藏了创建时间、更新时间等前端不需要的敏感信息。 + +- **`com.dne.ems.security` (安保部)** + - **职责**: 负责整个公司的安全。`SecurityConfig` 定义了公司的安保规则(比如哪些地方需要门禁卡才能进),`JwtAuthenticationFilter` 就是门口的保安,每个请求进来都要检查它的“门禁卡”(JWT Token) 是否有效。 + +- **`com.dne.ems.exception` (风险与危机处理部)** + - **职责**: 处理各种突发异常情况。当业务流程中出现错误时(比如找不到用户、数据库连接失败),`GlobalExceptionHandler` 会捕获这些错误,并给出一个统一、友好的错误提示给前端,而不是让程序崩溃。 + +### 2. 一次典型的请求流程:用户登录 + +让我们通过“用户登录”这个简单的例子,看看这些部门是如何协同工作的: + +1. **客户上门 (前端 -> Controller)**: 用户在前端页面输入用户名和密码,点击登录。前端将这些信息打包成一个 JSON 对象,发送一个 HTTP POST 请求到后端的 `/api/auth/login` 地址。 + +2. **前台接待 (Controller)**: `AuthController` 接收到这个请求。它看到地址是 `/login`,于是调用 `login` 方法。它从请求中取出 JSON 数据,并将其转换为 `LoginRequest` 这个 DTO 对象。 + +3. **转交业务部门 (Controller -> Service)**: `AuthController` 自己不处理登录逻辑,它调用 `AuthService` 的 `login` 方法,并将 `LoginRequest` 这个 DTO 传递过去。 + +4. **核心业务处理 (Service)**: `AuthService` 开始处理登录: + a. 它首先需要验证用户名是否存在,于是它请求 **仓库管理部** (`UserAccountRepository`):“请帮我根据这个用户名 (`loginRequest.getUsername()`) 查一下有没有这个人。” + b. `UserAccountRepository` 从数据库中查找到对应的 `UserAccount` (Model) 信息,返回给 `AuthService`。 + c. `AuthService` 拿到用户信息后,会使用密码加密工具,验证用户提交的密码和数据库中存储的加密密码是否匹配。 + d. 如果验证成功,`AuthService` 会请求 **安保部** (`JwtService`):“请为这位用户生成一张有时效的门禁卡 (JWT Token)。” + +5. **返回处理结果 (Service -> Controller -> 前端)**: `JwtService` 生成 Token 后返回给 `AuthService`。`AuthService` 将 Token 包装在一个 `JwtAuthenticationResponse` DTO 中,返回给 `AuthController`。`AuthController` 再将这个包含 Token 的 DTO 转换成 JSON 格式,作为 HTTP 响应返回给前端。 + +6. **客户拿到凭证 (前端)**: 前端收到包含 Token 的响应,就知道登录成功了。它会把这个 Token 保存起来,在之后访问需要权限的页面时,都会带上这张“门禁卡”。 + +通过这个流程,您可以看到,一个简单的请求背后是多个“部门”按照明确的职责划分,层层调用、协同工作的结果。这种结构使得代码逻辑清晰,易于维护和扩展。 +嗯 +## 六、核心业务流程与关键机制分析 + +下面,我们将梳理出本项目的几个核心业务流程和支撑这些流程的关键技术机制,并详细说明它们涉及的层级和文件,让您清晰地看到数据和指令是如何在系统中流转的。 + +### (一) 核心业务流程 + +#### 流程一:用户认证与授权 (Authentication & Authorization) + +**目标**:保障系统安全,确保只有授权用户才能访问受保护的资源。 + +**相关API端点** (`/api/auth`): +- `POST /api/auth/signup`: 用户注册。 +- `POST /api/auth/login`: 用户登录。 +- `POST /api/auth/send-verification-code`: 发送邮箱验证码。 +- `POST /api/auth/request-password-reset`: 请求重置密码。 +- `POST /api/auth/reset-password`: 执行密码重置。 + +**执行路径**: + +1. **注册 (`signup`)** + * **入口**: `AuthController.signup()` 对应 `POST /api/auth/signup`。 + * **业务逻辑**: `AuthService.signup()` + * 验证邮箱和验证码的有效性 (`VerificationCodeService`)。 + * 检查邮箱是否已注册 (`UserAccountRepository`)。 + * 对密码进行加密 (`PasswordEncoder`)。 + * 创建并保存新的 `UserAccount` 实体。 + +2. **登录 (`login`)** + * **入口**: `AuthController.login()` 对应 `POST /api/auth/login`。 + * **业务逻辑**: `AuthService.login()` + * 通过 Spring Security 的 `AuthenticationManager` 验证用户名和密码。 + * 如果认证成功,调用 `JwtService` 生成 JWT。 + * 返回包含 JWT 的响应给前端。 + +3. **访问受保护资源** + * **安全过滤器**: `JwtAuthenticationFilter` + * 拦截所有请求,从 `Authorization` 请求头中提取 JWT。 + * 验证 JWT 的有效性 (`JwtService`)。 + * 如果有效,从 JWT 中解析出用户信息,并构建一个 `UsernamePasswordAuthenticationToken`,设置到 Spring Security 的 `SecurityContextHolder` 中,完成授权。 + +#### 流程二:公众反馈与任务转化 (Feedback to Task) + +**目标**:将公众通过开放接口提交的反馈(如环境问题)转化为系统内部的可追踪任务。 + +**相关API端点**: +- `POST /api/public/feedback`: 公众提交反馈。 +- `GET /api/supervisor/reviews`: 主管获取待审核的反馈列表。 +- `POST /api/supervisor/reviews/{feedbackId}/approve`: 主管批准反馈。 +- `POST /api/supervisor/reviews/{feedbackId}/reject`: 主管拒绝反馈。 +- `POST /api/management/tasks/feedback/{feedbackId}/create-task`: 从反馈创建任务。 + +**执行路径**: + +1. **提交反馈** + * **入口**: `PublicController.submitFeedback()` 对应 `POST /api/public/feedback`。 + * **业务逻辑**: `FeedbackService.createFeedback()` + * 接收 `PublicFeedbackRequest`,包含反馈内容和可选的附件。 + * 处理文件上传(如果存在),调用 `FileStorageService` 保存文件,并将附件信息与反馈关联。 + * 保存 `Feedback` 实体到数据库。 + +2. **反馈审核与任务创建 (由主管操作)** + * **入口**: `SupervisorController` 和 `TaskManagementController` 的相关方法。 + * **业务逻辑**: `SupervisorService.processFeedback()` 和 `TaskManagementService.createTaskFromFeedback()` + * 主管通过 `GET /api/supervisor/reviews` 查看待处理反馈。 + * 通过 `POST /api/supervisor/reviews/{feedbackId}/approve` 批准反馈。 + * 批准后,可通过 `POST /api/management/tasks/feedback/{feedbackId}/create-task` 基于反馈信息创建一个新的 `Task`。 + +#### 流程三:任务生命周期管理 (Task Lifecycle) + +**目标**:完整地管理一个任务从创建、分配、执行到完成的全过程。 + +**相关API端点**: +- **主管/管理员**: + - `POST /api/management/tasks`: 直接创建任务。 + - `GET /api/management/tasks`: 获取任务列表。 + - `POST /api/management/tasks/{taskId}/assign`: 分配任务。 + - `POST /api/management/tasks/{taskId}/review`: 审核任务。 + - `POST /api/management/tasks/{taskId}/cancel`: 取消任务。 +- **网格员**: + - `GET /api/worker`: 获取我的任务列表。 + - `GET /api/worker/{taskId}`: 获取任务详情。 + - `POST /api/worker/{taskId}/accept`: 接受任务。 + - `POST /api/worker/{taskId}/submit`: 提交任务成果。 + +**执行路径**: + +1. **任务创建** + * **入口**: `TaskManagementController.createTask()` 对应 `POST /api/management/tasks`。 + * **业务逻辑**: `TaskManagementService.createTask()` + * 创建 `Task` 实体并设置初始状态(如 `PENDING_ASSIGNMENT`)。 + +2. **任务分配** + * **入口**: `TaskManagementController.assignTask()` 对应 `POST /api/management/tasks/{taskId}/assign`。 + * **业务逻辑**: `TaskAssignmentService.assignTaskToWorker()` + * 将任务 (`Task`) 与一个网格员 (`GridWorker`) 关联起来,创建 `Assignment` 记录。 + * 更新任务状态为 `ASSIGNED`。 + +3. **任务执行与更新** + * **入口**: `GridWorkerTaskController` 的相关方法,如 `acceptTask()` 和 `submitTask()`。 + * **业务逻辑**: `GridWorkerTaskService.updateTaskProgress()` + * 网格员通过 `POST /api/worker/{taskId}/accept` 接受任务。 + * 通过 `POST /api/worker/{taskId}/submit` 提交任务进度或结果,可附带图片等附件 (`FileStorageService`)。 + * 更新任务状态(如 `IN_PROGRESS`, `COMPLETED`)。 + +4. **任务审核与关闭** + * **入口**: `TaskManagementController.reviewTask()` 对应 `POST /api/management/tasks/{taskId}/review`。 + * **业务逻辑**: `SupervisorService.reviewAndCloseTask()` + * 主管审核已完成的任务。 + * 如果合格,将任务状态更新为 `CLOSED`。如果不合格,可打回 (`REJECTED`)。 + +#### 流程四:数据可视化与决策支持 (Dashboard & AI) + +**目标**:为管理层提供数据洞察,并通过 AI 功能辅助决策。 + +**相关API端点**: +- **仪表盘** (`/api/dashboard`): + - `GET /api/dashboard/stats`: 获取核心统计数据。 + - `GET /api/dashboard/reports/aqi-distribution`: 获取 AQI 分布。 + - `GET /api/dashboard/map/heatmap`: 获取反馈热力图数据。 + - `GET /api/dashboard/reports/task-completion-stats`: 获取任务完成情况统计。 +- **路径规划** (`/api/pathfinding`): + - `POST /api/pathfinding/find`: 使用 A* 算法寻找路径。 + +**执行路径**: + +1. **仪表盘数据** + * **入口**: `DashboardController` 中的各个接口。 + * **业务逻辑**: `DashboardService` + * 调用多个 `Repository`(如 `TaskRepository`, `FeedbackRepository`)进行数据聚合查询,统计任务状态、反馈趋势等。 + * 返回 DTO 给前端进行图表展示。 + +2. **AI 辅助功能** + * **文本审核**: `AiReviewService.reviewText()` 可能被用于自动审核反馈内容或评论,识别敏感信息 (无直接API,内部调用)。 + * **路径规划**: `AStarService.findPath()` 对应 `POST /api/pathfinding/find`,用于计算两点之间的最优路径,可辅助网格员规划任务路线。 + +### (二) 关键支撑机制 + +除了核心业务流程,系统还包含一系列关键的技术机制来保证其健壮、安全和可维护性。 + +1. **文件上传与管理** + * **位置**: `FileStorageService`, `FileController` + * **作用**: 提供统一的文件存储和访问服务。无论是用户头像、反馈附件还是任务报告,都通过此服务进行处理,实现了与具体存储位置(本地文件系统或云存储)的解耦。 + +2. **邮件与验证码服务** + * **位置**: `MailService`, `VerificationCodeService` + * **作用**: 负责发送邮件(如注册验证、密码重置)和管理验证码的生成与校验,是保障账户安全的重要环节。 + +3. **登录安全与尝试锁定** + * **位置**: `LoginAttemptService`, `AuthenticationFailureEventListener` + * **作用**: 监听登录失败事件 (`AuthenticationFailureListener`),并通过 `LoginAttemptService` 记录特定 IP 的失败次数。当超过阈值时,会暂时锁定该 IP 的登录权限,有效防止暴力破解攻击。 + +4. **A* 路径规划算法** + * **位置**: `AStarService`, `PathfindingController` + * **作用**: 实现了 A* 寻路算法,这是一个独立的功能模块,可以根据地图数据(网格 `Grid`)计算最优路径,为其他业务(如任务路线规划)提供支持。 + +5. **系统事件处理机制** + * **位置**: `event` 和 `listener` 包 + * **作用**: 基于 Spring 的事件驱动模型,实现系统内各组件的解耦。例如,当一个用户注册成功后,可以发布一个 `UserRegistrationEvent` 事件,而邮件发送监听器 (`EmailNotificationListener`) 和新用户欢迎监听器 (`WelcomeBonusListener`) 可以分别监听并处理此事件,而无需在注册服务中硬编码这些逻辑。 + +6. **全局异常处理** + * **位置**: `GlobalExceptionHandler` + * **作用**: 捕获整个应用中抛出的未处理异常,并根据异常类型返回统一、规范的错误响应给前端。这避免了将原始的、可能包含敏感信息的堆栈跟踪暴露给用户,并提升了用户体验。 + +7. **自定义数据校验** + * **位置**: `validation` 包 (如 `PasswordValidator`) + * **作用**: 实现自定义的、复杂的校验逻辑。例如,`@ValidPassword` 注解可以确保用户设置的密码符合特定的强度要求(如长度、包含数字和特殊字符等)。 + +8. **数据持久化与查询 (JPA)** + * **位置**: `repository` 包 + * **作用**: 利用 Spring Data JPA,通过定义接口 (`extends JpaRepository`) 的方式,极大地简化了数据库的 CRUD 操作和查询。开发者无需编写 SQL 语句,JPA 会自动根据方法名或注解生成相应的查询。 + +9. **API 数据契约 (DTO)** + * **位置**: `dto` 包 + * **作用**: 数据传输对象 (Data Transfer Object) 是前后端分离架构中的关键实践。它作为 API 的“数据契约”,明确了接口需要什么数据、返回什么数据,实现了表现层与领域模型的解耦,使得双方可以独立演进,同时避免了敏感信息的泄露。 + +10. **应用配置与初始化** + * **位置**: `config` 包, `DataInitializer` + * **作用**: `config` 包集中管理了应用的所有配置类,如安全配置 (`SecurityConfig`)、Web 配置等。`DataInitializer` 则利用 `CommandLineRunner` 接口,在应用启动后执行一次性任务,如创建默认管理员账户、初始化基础数据等,确保了应用开箱即用的能力。 + * **密码加密**: 使用 `PasswordEncoder` 对用户密码进行加密,增强安全性。 + * **创建用户实体**: 创建一个新的 `UserAccount` 对象 (`Model`),并填充信息。 + +3. **Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.UserAccountRepository.java` + * **方法**: `save(UserAccount user)` + * **作用**: `AuthService` 调用此方法,将新创建的 `UserAccount` 对象持久化到数据库中。 + +4. **Model (领域模型)** + * **文件**: `com.dne.ems.model.UserAccount.java` + * **作用**: 定义了用户账户的数据结构,是数据库中 `user_accounts` 表的映射。 + +### 流程二:任务创建与分配 + +**目标**:主管或管理员根据一个已批准的反馈,创建一个新任务,并将其分配给一个网格员。 + +**执行路径**: + +1. **Controller (表现层)** + * **文件**: `com.dne.ems.controller.TaskManagementController.java` + * **方法**: `createTaskFromFeedback(long feedbackId, TaskFromFeedbackRequest request)` + * **作用**: 接收 `/api/management/tasks/feedback/{feedbackId}/create-task` POST 请求。它将反馈ID和任务创建请求 (`TaskFromFeedbackRequest` DTO) 传递给 Service 层。 + +2. **Service (业务逻辑层)** + * **文件**: `com.dne.ems.service.impl.TaskManagementServiceImpl.java` + * **方法**: `createTaskFromFeedback(long feedbackId, TaskFromFeedbackRequest request)` + * **作用**: 实现核心的创建和分配逻辑: + * **查找反馈**: 调用 `FeedbackRepository` 找到对应的 `Feedback` (`Model`)。 + * **查找负责人**: 调用 `UserAccountRepository` 找到指定的负责人 `UserAccount` (`Model`)。 + * **创建任务**: 创建一个新的 `Task` 对象 (`Model`),并从 `Feedback` 和请求 DTO 中填充任务信息(如描述、截止日期、严重性等)。 + * **设置状态**: 将任务的初始状态设置为 `ASSIGNED`。 + +3. **Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.TaskRepository.java` + * **方法**: `save(Task task)` + * **作用**: `TaskManagementService` 调用此方法,将新创建的 `Task` 对象保存到数据库。 + * **涉及的其他 Repository**: `FeedbackRepository` 和 `UserAccountRepository` 用于在业务逻辑中获取必要的数据。 + +4. **Model (领域模型)** + * **文件**: `com.dne.ems.model.Task.java` + * **作用**: 定义了任务的数据结构。 + * **涉及的其他 Model**: `Feedback.java` 和 `UserAccount.java` 作为数据来源。 + +### 流程三:网格员处理任务与提交反馈 + +**目标**:网格员查看自己的任务,执行任务,并提交处理结果(包括评论和附件)。 + +**执行路径**: + +1. **Controller (表现层)** + * **文件**: `com.dne.ems.controller.GridWorkerTaskController.java` + * **方法**: `submitTask(long taskId, String comments, List files)` + * **作用**: 接收 `/api/worker/{taskId}/submit` POST 请求。这是一个 `multipart/form-data` 请求,因为它同时包含了文本(评论)和文件。Controller 将这些信息传递给 Service 层。 + +2. **Service (业务逻辑层)** + * **文件**: `com.dne.ems.service.impl.GridWorkerTaskServiceImpl.java` + * **方法**: `submitTask(long taskId, String comments, List files)` + * **作用**: 处理任务提交的业务逻辑: + * **查找任务**: 调用 `TaskRepository` 找到当前需要处理的 `Task` (`Model`)。 + * **验证权限**: 确保当前登录的用户就是这个任务的负责人。 + * **处理文件上传**: 如果有附件,调用 `FileStorageService` 将文件保存到服务器,并创建 `Attachment` (`Model`) 对象。 + * **更新任务状态**: 将任务状态更新为 `SUBMITTED` 或 `PENDING_REVIEW`。 + * **保存评论和附件信息**: 将评论和附件信息关联到任务上。 + +3. **Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.TaskRepository.java` + * **方法**: `save(Task task)` + * **作用**: `GridWorkerTaskService` 在更新了任务状态和信息后,调用此方法将 `Task` 对象的变化持久化到数据库。 + * **涉及的其他 Repository**: `AttachmentRepository` (如果适用) 用于保存附件信息。 + +4. **Model (领域模型)** + * **文件**: `com.dne.ems.model.Task.java` + * **作用**: 任务数据在此流程中被更新。 + * **涉及的其他 Model**: `Attachment.java` 用于表示上传的文件。 + * **密码加密**: 使用 `PasswordEncoder` 对用户提交的明文密码进行加密,确保数据库中存储的是密文,保障安全。 + * **创建用户实体**: 创建一个新的 `UserAccount` (Model) 对象,并将注册信息(包括加密后的密码和角色)设置到该对象中。 + +3. **Service -> Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.UserAccountRepository.java` + * **方法**: `save(UserAccount userAccount)` + * **作用**: Service 层调用此方法,将填充好数据的 `UserAccount` 实体对象持久化到数据库中。Spring Data JPA 会自动生成对应的 SQL `INSERT` 语句来完成操作。 + +4. **返回结果**: 数据成功存入数据库后,操作结果会逐层返回,最终 `AuthController` 会向前端返回一个成功的响应消息。 + +### 流程二:任务的创建与自动分配 + +**目标**:系统管理员或主管创建一个新的环境问题任务,并由系统自动分配给相应网格内的网格员。 + +**执行路径**: + +1. **前端 -> Controller (表现层)** + * **文件**: `com.dne.ems.controller.TaskManagementController.java` + * **方法**: `createTask(TaskCreateRequest taskCreateRequest)` + * **作用**: 接收 `/api/tasks` POST 请求,请求体中包含了任务的详细信息(如描述、位置、图片附件ID等),封装在 `TaskCreateRequest` DTO 中。Controller 调用 `TaskManagementService` 来处理创建逻辑。 + +2. **Controller -> Service (业务逻辑层)** + * **文件**: `com.dne.ems.service.impl.TaskManagementServiceImpl.java` + * **方法**: `createTask(TaskCreateRequest taskCreateRequest)` + * **作用**: 创建任务的核心业务逻辑。 + * 创建一个新的 `Task` (Model) 实体。 + * 根据请求中的附件ID,调用 `AttachmentRepository` 查找并关联附件。 + * 设置任务的初始状态为 `PENDING` (待处理)。 + * 调用 `TaskRepository` 将新任务保存到数据库。 + * **触发任务分配**: 保存任务后,调用 `TaskAssignmentService` 来执行自动分配逻辑。 + +3. **Service -> Service (业务逻辑层内部调用)** + * **文件**: `com.dne.ems.service.impl.TaskAssignmentServiceImpl.java` + * **方法**: `assignTask(Task task)` + * **作用**: 处理任务的分配逻辑。 + * 根据任务的地理位置信息,调用 `GridRepository` 找到对应的地理网格 (`Grid`)。 + * 根据网格信息,调用 `UserAccountRepository` 找到负责该网格的网格员 (`GridWorker`)。 + * 如果找到合适的网格员,则更新 `Task` 实体的 `assignee` (执行人) 字段。 + * 更新任务状态为 `ASSIGNED` (已分配)。 + * 调用 `TaskRepository` 将更新后的任务信息保存回数据库。 + * (可选) 调用通知服务,向被分配的网格员发送新任务通知。 + +4. **Service -> Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.TaskRepository.java`, `com.dne.ems.repository.GridRepository.java`, `com.dne.ems.repository.UserAccountRepository.java` + * **作用**: 在整个流程中,Service 层会频繁地与这些 Repository 交互,以查询网格信息、查询用户信息、保存和更新任务数据。 + +### 流程三:网格员处理任务并提交反馈 + +**目标**:网格员在完成任务后,通过 App 或前端页面提交任务处理结果,包括文字描述和现场图片。 + +**执行路径**: + +1. **前端 -> Controller (表现层)** + * **文件**: `com.dne.ems.controller.GridWorkerTaskController.java` + * **方法**: `submitTaskFeedback(Long taskId, TaskFeedbackRequest feedbackRequest)` + * **作用**: 接收 `/api/worker/tasks/{taskId}/feedback` POST 请求。路径中的 `taskId` 指明了要为哪个任务提交反馈,请求体 `TaskFeedbackRequest` DTO 中包含了反馈内容和附件ID。 + +2. **Controller -> Service (业务逻辑层)** + * **文件**: `com.dne.ems.service.impl.GridWorkerTaskServiceImpl.java` + * **方法**: `submitTaskFeedback(Long taskId, TaskFeedbackRequest feedbackRequest)` + * **作用**: 处理网格员提交反馈的业务逻辑。 + * 调用 `TaskRepository` 检查任务是否存在以及当前用户是否有权限操作该任务。 + * 更新 `Task` 实体的状态为 `COMPLETED` (已完成) 或 `PENDING_REVIEW` (待审核)。 + * 创建一个新的 `Feedback` (Model) 实体,将反馈内容和关联的 `Task` 设置进去。 + * 调用 `FeedbackRepository` 将新的反馈信息保存到数据库。 + * 调用 `TaskRepository` 更新任务状态。 + +3. **Service -> Repository (数据访问层)** + * **文件**: `com.dne.ems.repository.TaskRepository.java`, `com.dne.ems.repository.FeedbackRepository.java` + * **作用**: `TaskRepository` 用于查询和更新任务信息,`FeedbackRepository` 用于保存新的反馈记录。 + +通过以上对核心业务流程的梳理,您可以清晰地看到,用户的每一个操作是如何触发后端系统中不同层、不同文件之间的一系列连锁反应,最终完成一个完整的业务闭环。这种清晰的、分层的架构是保证项目可维护性和扩展性的关键。 --- - + ### Design/Vue界面设计规约.md - -# 东软环保公众监督系统 - Vue 界面设计规约 - -## 1. 设计原则 -- **一致性**: 所有界面元素、组件和交互行为应保持高度一致。 -- **清晰性**: 界面布局、信息层级和操作指引清晰明确,避免用户困惑。 -- **效率性**: 简化操作流程,减少不必要的点击和输入,让用户高效完成任务。 -- **响应式**: 确保在不同尺寸的设备上(特别是PC端主流分辨率)都能获得良好的视觉和交互体验。 -- **美观性**: 遵循现代化的审美标准,界面简洁、美观、专业。 - -## 2. 颜色系统 (Color Palette) -颜色方案以"科技蓝"为主色调,辅以中性灰和功能色,营造专业、冷静、可靠的视觉感受。 - -- **主色 (Primary)** - - `Brand Blue`: `#409EFF` - 用于关键操作按钮、Logo、导航高亮等。 -- **功能色 (Functional)** - - `Success Green`: `#67C23A` - 用于成功提示、操作完成状态。 - - `Warning Orange`: `#E6A23C` - 用于警告信息、需要用户注意的操作。 - - `Danger Red`: `#F56C6C` - 用于错误提示、删除、高危操作。 - - `Info Gray`: `#909399` - 用于普通信息、辅助说明。 -- **中性色 (Neutral)** - - `Text Primary`: `#303133` - 主要文字颜色。 - - `Text Regular`: `#606266` - 常规文字、次要信息。 - - `Text Secondary`: `#909399` - 辅助、占位文字。 - - `Border Color`: `#DCDFE6` - 边框、分割线。 - - `Background`: `#F5F7FA` - 页面背景色。 - - `Container Background`: `#FFFFFF` - 卡片、表格等容器背景色。 - -## 3. 字体与排版 (Typography) -- **字体族**: `Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif` -- **字号体系**: - - `H1 / 页面主标题`: 24px (Bold) - - `H2 / 区域标题`: 20px (Bold) - - `H3 / 卡片/弹窗标题`: 18px (Medium) - - `正文/表格内容`: 14px (Regular) - - `辅助/说明文字`: 12px (Regular) -- **行高**: `1.6` - `1.8`,保证阅读舒适性。 -- **段落间距**: `16px` - -## 4. 布局与间距 (Layout & Spacing) -- **主布局**: 采用经典的侧边栏导航 + 内容区的布局方式。 - - `NEPM (管理端)` / `NEPV (大屏端)`: 左侧为固定菜单栏,右侧为内容展示区。 - - `NEPS (公众端)` / `NEPG (网格员端)`: 根据移动端优先原则,可采用底部Tab栏或顶部导航。 -- **间距单位**: 以 `8px` 为基础栅格单位。 - - `xs`: 4px - - `sm`: 8px - - `md`: 16px - - `lg`: 24px - - `xl`: 32px -- **页面内边距 (Padding)**: 主要内容区域应有 `24px` 的内边距。 -- **组件间距 (Gap)**: 卡片、表单项之间保持 `16px` 或 `24px` 的间距。 - -## 5. 核心组件样式规约 - -### 5.1 按钮 (Button) -- **主按钮 (Primary)**: `background: #409EFF`, `color: #FFFFFF`。用于页面核心操作。 -- **次按钮 (Default)**: `border: 1px solid #DCDFE6`, `background: #FFFFFF`。用于非核心或次要操作。 -- **危险按钮 (Danger)**: `background: #F56C6C`, `color: #FFFFFF`。用于删除等高危操作。 -- **尺寸**: 高度分为 `大 (40px)`, `中 (32px)`, `小 (24px)` 三档。 - -### 5.2 表格 (Table) -- **表头**: 背景色 `#FAFAFA`, 字体加粗。 -- **行**: 偶数行可设置 `#F5F7FA` 的背景色以增强区分度 (斑马纹)。 -- **操作区**: 表格最后一列通常为操作区,放置`查看`、`编辑`、`删除`等按钮,建议使用文字按钮或小尺寸图标按钮。 - -### 5.3 表单 (Form) -- **标签 (Label)**: 右对齐,与输入框保持 `8px` 距离。 -- **输入框 (Input)**: 统一高度,`hover` 和 `focus` 状态有明显视觉反馈 (边框颜色变为 `#409EFF`)。 -- **必填项**: 在标签前用红色 `*` 标示。 - -### 5.4 卡片 (Card) -- **样式**: `background: #FFFFFF`, `border-radius: 8px`, `box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1)`。 -- **内边距**: `20px`。 - -## 6. 编码与实现规约 -- **组件化**: 遵循Vue的组件化思想,将可复用的UI和逻辑封装成组件。 -- **命名规范**: - - 组件文件名: `PascalCase` (e.g., `UserTable.vue`) - - props/data/methods: `camelCase` (e.g., `tableData`) -- **状态管理**: 使用 `Pinia` 统一管理全局状态,如用户信息、角色权限等。 -- **API请求**: 封装 `Axios`,统一处理请求头、响应拦截和错误处理。 -- **TypeScript**: 全程使用TypeScript,为`props`, `emits`, `data` 和 `store` 提供明确的类型定义。 -- **代码风格**: 遵循 `ESLint + Prettier` 的配置,确保代码风格统一。 + +# 东软环保公众监督系统 - Vue 界面设计规约 + +## 1. 设计原则 +- **一致性**: 所有界面元素、组件和交互行为应保持高度一致。 +- **清晰性**: 界面布局、信息层级和操作指引清晰明确,避免用户困惑。 +- **效率性**: 简化操作流程,减少不必要的点击和输入,让用户高效完成任务。 +- **响应式**: 确保在不同尺寸的设备上(特别是PC端主流分辨率)都能获得良好的视觉和交互体验。 +- **美观性**: 遵循现代化的审美标准,界面简洁、美观、专业。 + +## 2. 颜色系统 (Color Palette) +颜色方案以"科技蓝"为主色调,辅以中性灰和功能色,营造专业、冷静、可靠的视觉感受。 + +- **主色 (Primary)** + - `Brand Blue`: `#409EFF` - 用于关键操作按钮、Logo、导航高亮等。 +- **功能色 (Functional)** + - `Success Green`: `#67C23A` - 用于成功提示、操作完成状态。 + - `Warning Orange`: `#E6A23C` - 用于警告信息、需要用户注意的操作。 + - `Danger Red`: `#F56C6C` - 用于错误提示、删除、高危操作。 + - `Info Gray`: `#909399` - 用于普通信息、辅助说明。 +- **中性色 (Neutral)** + - `Text Primary`: `#303133` - 主要文字颜色。 + - `Text Regular`: `#606266` - 常规文字、次要信息。 + - `Text Secondary`: `#909399` - 辅助、占位文字。 + - `Border Color`: `#DCDFE6` - 边框、分割线。 + - `Background`: `#F5F7FA` - 页面背景色。 + - `Container Background`: `#FFFFFF` - 卡片、表格等容器背景色。 + +## 3. 字体与排版 (Typography) +- **字体族**: `Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif` +- **字号体系**: + - `H1 / 页面主标题`: 24px (Bold) + - `H2 / 区域标题`: 20px (Bold) + - `H3 / 卡片/弹窗标题`: 18px (Medium) + - `正文/表格内容`: 14px (Regular) + - `辅助/说明文字`: 12px (Regular) +- **行高**: `1.6` - `1.8`,保证阅读舒适性。 +- **段落间距**: `16px` + +## 4. 布局与间距 (Layout & Spacing) +- **主布局**: 采用经典的侧边栏导航 + 内容区的布局方式。 + - `NEPM (管理端)` / `NEPV (大屏端)`: 左侧为固定菜单栏,右侧为内容展示区。 + - `NEPS (公众端)` / `NEPG (网格员端)`: 根据移动端优先原则,可采用底部Tab栏或顶部导航。 +- **间距单位**: 以 `8px` 为基础栅格单位。 + - `xs`: 4px + - `sm`: 8px + - `md`: 16px + - `lg`: 24px + - `xl`: 32px +- **页面内边距 (Padding)**: 主要内容区域应有 `24px` 的内边距。 +- **组件间距 (Gap)**: 卡片、表单项之间保持 `16px` 或 `24px` 的间距。 + +## 5. 核心组件样式规约 + +### 5.1 按钮 (Button) +- **主按钮 (Primary)**: `background: #409EFF`, `color: #FFFFFF`。用于页面核心操作。 +- **次按钮 (Default)**: `border: 1px solid #DCDFE6`, `background: #FFFFFF`。用于非核心或次要操作。 +- **危险按钮 (Danger)**: `background: #F56C6C`, `color: #FFFFFF`。用于删除等高危操作。 +- **尺寸**: 高度分为 `大 (40px)`, `中 (32px)`, `小 (24px)` 三档。 + +### 5.2 表格 (Table) +- **表头**: 背景色 `#FAFAFA`, 字体加粗。 +- **行**: 偶数行可设置 `#F5F7FA` 的背景色以增强区分度 (斑马纹)。 +- **操作区**: 表格最后一列通常为操作区,放置`查看`、`编辑`、`删除`等按钮,建议使用文字按钮或小尺寸图标按钮。 + +### 5.3 表单 (Form) +- **标签 (Label)**: 右对齐,与输入框保持 `8px` 距离。 +- **输入框 (Input)**: 统一高度,`hover` 和 `focus` 状态有明显视觉反馈 (边框颜色变为 `#409EFF`)。 +- **必填项**: 在标签前用红色 `*` 标示。 + +### 5.4 卡片 (Card) +- **样式**: `background: #FFFFFF`, `border-radius: 8px`, `box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1)`。 +- **内边距**: `20px`。 + +## 6. 编码与实现规约 +- **组件化**: 遵循Vue的组件化思想,将可复用的UI和逻辑封装成组件。 +- **命名规范**: + - 组件文件名: `PascalCase` (e.g., `UserTable.vue`) + - props/data/methods: `camelCase` (e.g., `tableData`) +- **状态管理**: 使用 `Pinia` 统一管理全局状态,如用户信息、角色权限等。 +- **API请求**: 封装 `Axios`,统一处理请求头、响应拦截和错误处理。 +- **TypeScript**: 全程使用TypeScript,为`props`, `emits`, `data` 和 `store` 提供明确的类型定义。 +- **代码风格**: 遵循 `ESLint + Prettier` 的配置,确保代码风格统一。 --- - + ## 前端页面设计 - + ### Design/frontend_Design/DashboardPage_Design.md - -# 仪表盘页面设计文档 - -## 1. 页面概述 - -仪表盘页面是用户登录后看到的第一个界面,旨在提供系统关键信息的概览。它主要面向管理员、决策者和主管,展示核心业务指标、任务状态分布、近期活动等,以便用户快速掌握系统整体状况并做出决策。 - -## 2. 页面布局 - -![仪表盘页面布局示意图](https://placeholder-for-dashboard-page-mockup.png) - -### 2.1 布局结构 - -页面采用响应式网格布局,确保在不同屏幕尺寸下都有良好的可读性。 -- **顶部**: 页面标题和一个时间范围选择器,用于筛选仪表盘数据。 -- **中部**: 关键指标卡片区域,展示核心数据(如总任务数、待处理反馈等)。 -- **下部**: - - **左侧**: 图表区域,通过饼图和柱状图展示任务状态和类型的分布。 - - **右侧**: 近期活动或任务列表,展示最新的任务分配或状态变更。 - -### 2.2 响应式设计 - -- **桌面端**: 采用多列网格布局,充分利用屏幕空间。 -- **平板端**: 网格列数减少,卡片和图表垂直堆叠。 -- **移动端**: 单列布局,所有模块垂直排列,保证可读性。 - -## 3. 组件结构 - -```vue - -``` - -## 4. 数据结构 - -```typescript -// 仪表盘统计数据 -interface DashboardStats { - totalTasks: number; - pendingTasks: number; - pendingFeedback: number; - activeUsers: number; -} - -// 图表数据项 -interface ChartDataItem { - name: string; - value: number; -} - -// 近期任务项 -interface RecentTask { - id: number; - title: string; - assignee: string; - status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'REVIEWED'; - updatedAt: string; -} - -// 仪表盘完整数据 -interface DashboardData { - stats: DashboardStats; - taskStatusDistribution: ChartDataItem[]; - taskTypeDistribution: ChartDataItem[]; - recentTasks: RecentTask[]; -} -``` - -## 5. 状态管理 - -```typescript -// 组件内状态 -const dateRange = ref<[Date, Date]>(); -const loading = ref(true); -const error = ref(null); - -// 全局状态 (Pinia Store) -const dashboardStore = useDashboardStore(); -const { stats, taskStatusChartData, taskTypeChartData, recentTasks } = storeToRefs(dashboardStore); -``` - -## 6. 交互逻辑 - -### 6.1 数据加载 - -```typescript -// 在组件挂载时加载数据 -onMounted(() => { - fetchDashboardData(); -}); - -const fetchDashboardData = async () => { - try { - loading.value = true; - error.value = null; - - // 从 store 中调用 action 加载数据 - await dashboardStore.fetchDashboardData({ - startDate: dateRange.value?.[0], - endDate: dateRange.value?.[1], - }); - - } catch (err) { - error.value = '仪表盘数据加载失败,请稍后重试。'; - } finally { - loading.value = false; - } -}; - -// 当日期范围变化时重新加载数据 -const handleDateChange = () => { - fetchDashboardData(); -}; -``` - -### 6.2 辅助函数 - -```typescript -// 根据任务状态返回不同的标签类型 -const getStatusTagType = (status: RecentTask['status']) => { - switch (status) { - case 'PENDING': return 'warning'; - case 'IN_PROGRESS': return 'primary'; - case 'COMPLETED': return 'success'; - case 'REVIEWED': return 'info'; - default: return 'info'; - } -}; -``` - -## 7. API 调用 - -### 7.1 仪表盘 API - -```typescript -// api/dashboard.ts -export const dashboardApi = { - getDashboardData: (params: { startDate?: Date; endDate?: Date }) => - apiClient.get('/dashboard', { params }) -}; - -// stores/dashboard.ts -export const useDashboardStore = defineStore('dashboard', { - state: (): DashboardData => ({ - stats: { totalTasks: 0, pendingTasks: 0, pendingFeedback: 0, activeUsers: 0 }, - taskStatusDistribution: [], - taskTypeDistribution: [], - recentTasks: [], - }), - getters: { - // 将后端数据转换为 ECharts 需要的格式 - taskStatusChartData: (state) => ({ - legend: state.taskStatusDistribution.map(item => item.name), - series: [{ data: state.taskStatusDistribution }] - }), - taskTypeChartData: (state) => ({ - xAxis: state.taskTypeDistribution.map(item => item.name), - series: [{ data: state.taskTypeDistribution.map(item => item.value) }] - }) - }, - actions: { - async fetchDashboardData(params) { - try { - const { data } = await dashboardApi.getDashboardData(params); - // 更新整个 state - this.$patch(data); - return data; - } catch (error) { - console.error('获取仪表盘数据失败:', error); - throw error; - } - } - } -}); -``` - -## 8. 样式设计 - -```scss -.dashboard-page { - padding: 24px; - - .page-header { - margin-bottom: 24px; - } - - .dashboard-filters { - margin-bottom: 24px; - } - - .summary-cards { - margin-bottom: 20px; - } - - .charts-section { - margin-bottom: 20px; - } - - .el-card { - border: none; - border-radius: 8px; - } - - .loading-state, - .error-state { - padding: 40px; - } -} - -// 响应式样式 -@media screen and (max-width: 768px) { - .dashboard-page { - padding: 16px; - - .summary-cards .el-col, - .charts-section .el-col { - margin-bottom: 20px; - } - } -} -``` - -## 9. 测试用例 - -1. **单元测试** - - 测试 `getStatusTagType` 辅助函数的正确性。 - - 测试 Pinia store中getters的数据转换逻辑。 - - 测试 `StatisticCard`、`PieChart`、`BarChart` 组件的渲染是否正确。 - -2. **集成测试** - - 测试页面在加载、成功、失败状态下的显示。 - - 测试日期选择器筛选功能是否能正确触发数据重新加载。 - - 模拟 API 调用,验证数据是否正确渲染到页面上。 - -## 10. 性能优化 - -1. **懒加载图表**: 使用 `v-if` 或动态导入,确保图表库只在需要时加载和渲染。 -2. **骨架屏**: 在数据加载时使用骨架屏,提升用户体验。 -3. **数据缓存**: 对不经常变化的数据(如任务类型),可以在 Pinia store 中设置缓存策略,避免重复请求。 -4. **节流/防抖**: 对日期选择器的 `change` 事件使用节流或防抖,防止用户快速操作导致频繁的 API 请求。 + +# 仪表盘页面设计文档 + +## 1. 页面概述 + +仪表盘页面是用户登录后看到的第一个界面,旨在提供系统关键信息的概览。它主要面向管理员、决策者和主管,展示核心业务指标、任务状态分布、近期活动等,以便用户快速掌握系统整体状况并做出决策。 + +## 2. 页面布局 + +![仪表盘页面布局示意图](https://placeholder-for-dashboard-page-mockup.png) + +### 2.1 布局结构 + +页面采用响应式网格布局,确保在不同屏幕尺寸下都有良好的可读性。 +- **顶部**: 页面标题和一个时间范围选择器,用于筛选仪表盘数据。 +- **中部**: 关键指标卡片区域,展示核心数据(如总任务数、待处理反馈等)。 +- **下部**: + - **左侧**: 图表区域,通过饼图和柱状图展示任务状态和类型的分布。 + - **右侧**: 近期活动或任务列表,展示最新的任务分配或状态变更。 + +### 2.2 响应式设计 + +- **桌面端**: 采用多列网格布局,充分利用屏幕空间。 +- **平板端**: 网格列数减少,卡片和图表垂直堆叠。 +- **移动端**: 单列布局,所有模块垂直排列,保证可读性。 + +## 3. 组件结构 + +```vue + +``` + +## 4. 数据结构 + +```typescript +// 仪表盘统计数据 +interface DashboardStats { + totalTasks: number; + pendingTasks: number; + pendingFeedback: number; + activeUsers: number; +} + +// 图表数据项 +interface ChartDataItem { + name: string; + value: number; +} + +// 近期任务项 +interface RecentTask { + id: number; + title: string; + assignee: string; + status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'REVIEWED'; + updatedAt: string; +} + +// 仪表盘完整数据 +interface DashboardData { + stats: DashboardStats; + taskStatusDistribution: ChartDataItem[]; + taskTypeDistribution: ChartDataItem[]; + recentTasks: RecentTask[]; +} +``` + +## 5. 状态管理 + +```typescript +// 组件内状态 +const dateRange = ref<[Date, Date]>(); +const loading = ref(true); +const error = ref(null); + +// 全局状态 (Pinia Store) +const dashboardStore = useDashboardStore(); +const { stats, taskStatusChartData, taskTypeChartData, recentTasks } = storeToRefs(dashboardStore); +``` + +## 6. 交互逻辑 + +### 6.1 数据加载 + +```typescript +// 在组件挂载时加载数据 +onMounted(() => { + fetchDashboardData(); +}); + +const fetchDashboardData = async () => { + try { + loading.value = true; + error.value = null; + + // 从 store 中调用 action 加载数据 + await dashboardStore.fetchDashboardData({ + startDate: dateRange.value?.[0], + endDate: dateRange.value?.[1], + }); + + } catch (err) { + error.value = '仪表盘数据加载失败,请稍后重试。'; + } finally { + loading.value = false; + } +}; + +// 当日期范围变化时重新加载数据 +const handleDateChange = () => { + fetchDashboardData(); +}; +``` + +### 6.2 辅助函数 + +```typescript +// 根据任务状态返回不同的标签类型 +const getStatusTagType = (status: RecentTask['status']) => { + switch (status) { + case 'PENDING': return 'warning'; + case 'IN_PROGRESS': return 'primary'; + case 'COMPLETED': return 'success'; + case 'REVIEWED': return 'info'; + default: return 'info'; + } +}; +``` + +## 7. API 调用 + +### 7.1 仪表盘 API + +```typescript +// api/dashboard.ts +export const dashboardApi = { + getDashboardData: (params: { startDate?: Date; endDate?: Date }) => + apiClient.get('/dashboard', { params }) +}; + +// stores/dashboard.ts +export const useDashboardStore = defineStore('dashboard', { + state: (): DashboardData => ({ + stats: { totalTasks: 0, pendingTasks: 0, pendingFeedback: 0, activeUsers: 0 }, + taskStatusDistribution: [], + taskTypeDistribution: [], + recentTasks: [], + }), + getters: { + // 将后端数据转换为 ECharts 需要的格式 + taskStatusChartData: (state) => ({ + legend: state.taskStatusDistribution.map(item => item.name), + series: [{ data: state.taskStatusDistribution }] + }), + taskTypeChartData: (state) => ({ + xAxis: state.taskTypeDistribution.map(item => item.name), + series: [{ data: state.taskTypeDistribution.map(item => item.value) }] + }) + }, + actions: { + async fetchDashboardData(params) { + try { + const { data } = await dashboardApi.getDashboardData(params); + // 更新整个 state + this.$patch(data); + return data; + } catch (error) { + console.error('获取仪表盘数据失败:', error); + throw error; + } + } + } +}); +``` + +## 8. 样式设计 + +```scss +.dashboard-page { + padding: 24px; + + .page-header { + margin-bottom: 24px; + } + + .dashboard-filters { + margin-bottom: 24px; + } + + .summary-cards { + margin-bottom: 20px; + } + + .charts-section { + margin-bottom: 20px; + } + + .el-card { + border: none; + border-radius: 8px; + } + + .loading-state, + .error-state { + padding: 40px; + } +} + +// 响应式样式 +@media screen and (max-width: 768px) { + .dashboard-page { + padding: 16px; + + .summary-cards .el-col, + .charts-section .el-col { + margin-bottom: 20px; + } + } +} +``` + +## 9. 测试用例 + +1. **单元测试** + - 测试 `getStatusTagType` 辅助函数的正确性。 + - 测试 Pinia store中getters的数据转换逻辑。 + - 测试 `StatisticCard`、`PieChart`、`BarChart` 组件的渲染是否正确。 + +2. **集成测试** + - 测试页面在加载、成功、失败状态下的显示。 + - 测试日期选择器筛选功能是否能正确触发数据重新加载。 + - 模拟 API 调用,验证数据是否正确渲染到页面上。 + +## 10. 性能优化 + +1. **懒加载图表**: 使用 `v-if` 或动态导入,确保图表库只在需要时加载和渲染。 +2. **骨架屏**: 在数据加载时使用骨架屏,提升用户体验。 +3. **数据缓存**: 对不经常变化的数据(如任务类型),可以在 Pinia store 中设置缓存策略,避免重复请求。 +4. **节流/防抖**: 对日期选择器的 `change` 事件使用节流或防抖,防止用户快速操作导致频繁的 API 请求。 --- - + ### Design/frontend_Design/FeedbackManagementPage_Design.md - -# 反馈管理页面设计文档 - -## 1. 页面概述 - -反馈管理页面是管理员和主管处理用户提交的环境问题报告、建议或其他反馈的核心平台。此页面旨在提供一个高效的工作流程,用于审查、分类、回复和跟踪所有用户反馈,确保问题得到及时响应和解决。 - -## 2. 页面布局 - -![反馈管理页面布局示意图](https://placeholder-for-feedback-page-mockup.png) - -### 2.1 布局结构 - -页面布局与任务管理页面类似,遵循标准的后台管理界面设计: -- **顶部**: 筛选区域,允许管理员按反馈状态(待处理、已处理)、反馈类型或提交时间进行过滤。 -- **中部**: 反馈列表,以表格或卡片列表的形式展示反馈信息。 -- **底部**: 分页控件。 - -### 2.2 响应式设计 - -- **桌面端**: 使用多列数据表格,清晰展示反馈的各项信息。 -- **移动端**: 转换为卡片式列表,每张卡片突出显示反馈标题、提交人和状态,并提供点击查看详情的入口。 - -## 3. 组件结构 - -```vue - -``` - -## 4. 数据结构 - -```typescript -// 反馈筛选条件 -interface FeedbackFilters { - status?: 'PENDING' | 'RESOLVED'; - submitter?: string; -} - -// 反馈列表项 -interface FeedbackListItem { - id: number; - title: string; - submitterName: string; - createdAt: string; - status: 'PENDING' | 'RESOLVED'; -} - -// 反馈详情 (用于对话框) -interface FeedbackDetail extends FeedbackListItem { - content: string; - images: string[]; // 图片 URL 列表 - location: string; - contact: string; - handlerNotes?: string; // 处理备注 -} -``` - -## 5. 状态管理 - -```typescript -// 组件内状态 -const filters = ref({}); -const pagination = ref({ page: 1, pageSize: 10 }); -const dialogVisible = ref(false); -const selectedFeedbackId = ref(null); - -// 全局状态 (Pinia Store) -const feedbackStore = useFeedbackStore(); -const { feedbackList, total, loading } = storeToRefs(feedbackStore); -``` - -## 6. 交互逻辑 - -### 6.1 数据获取与筛选 - -```typescript -const fetchFeedback = () => { - feedbackStore.fetchFeedbackList({ ...filters.value, ...pagination.value }); -}; - -onMounted(fetchFeedback); - -const handleSearch = () => { - pagination.value.page = 1; - fetchFeedback(); -}; - -// 分页逻辑 (handleSizeChange, handleCurrentChange) 与任务管理页面类似 -``` - -### 6.2 查看详情与处理 - -```typescript -const handleViewDetails = (feedback: FeedbackListItem) => { - selectedFeedbackId.value = feedback.id; - dialogVisible.value = true; -}; - -// 当详情对话框中完成处理后被调用 -const handleProcessed = () => { - dialogVisible.value = false; - fetchFeedback(); // 刷新列表以更新状态 -}; -``` - -## 7. API 调用 - -### 7.1 反馈管理 API - -```typescript -// api/feedback.ts -export const feedbackApi = { - getFeedbackList: (params) => apiClient.get('/management/feedback', { params }), - getFeedbackDetail: (id: number) => apiClient.get(`/management/feedback/${id}`), - processFeedback: (id: number, data: { notes: string }) => apiClient.post(`/management/feedback/${id}/process`, data), -}; -``` - -### 7.2 Pinia Store - -```typescript -// stores/feedback.ts -export const useFeedbackStore = defineStore('feedback', { - state: () => ({ - feedbackList: [] as FeedbackListItem[], - total: 0, - loading: false, - currentFeedbackDetail: null as FeedbackDetail | null, - }), - actions: { - async fetchFeedbackList(params) { - this.loading = true; - try { - const { data } = await feedbackApi.getFeedbackList(params); - this.feedbackList = data.items; - this.total = data.total; - } finally { - this.loading = false; - } - }, - async fetchFeedbackDetail(id: number) { - const { data } = await feedbackApi.getFeedbackDetail(id); - this.currentFeedbackDetail = data; - }, - async processFeedback(payload: { id: number; notes: string }) { - await feedbackApi.processFeedback(payload.id, { notes: payload.notes }); - } - } -}); -``` - -## 8. 样式设计 - -```scss -.feedback-management-page { - padding: 24px; - - .page-container { - margin-top: 24px; - padding: 24px; - border-radius: 8px; - border: none; - } - - .filter-container { - margin-bottom: 20px; - } - - .pagination-container { - margin-top: 24px; - display: flex; - justify-content: flex-end; - } -} -``` - -## 9. 关联组件 - -### `FeedbackDetailDialog.vue` - -这是一个关键的子组件,用于显示反馈的完整信息,并提供处理功能。 -- **Props**: `modelValue` (for v-model), `feedbackId` -- **功能**: - - 接收 `feedbackId`,在对话框打开时调用 store action 获取反馈详情。 - - 展示反馈的文本内容、联系方式、地理位置和图片(图片可点击放大预览)。 - - 提供一个文本域供管理员填写处理备注。 - - 提供一个"标记为已处理"的按钮,点击后调用 store action 提交处理结果。 - - 操作成功后,emit 一个 `processed` 事件通知父组件刷新列表。 - -## 10. 测试用例 - -1. **单元测试**: - - 测试 Pinia store 的各个 action 是否能正常工作。 - - 测试 `FeedbackDetailDialog` 组件在接收到 `feedbackId` 后是否能正确加载和显示数据。 - -2. **集成测试**: - - 测试筛选功能是否能正确过滤反馈列表。 - - 测试分页是否正常。 - - 测试点击"查看详情"按钮是否能打开对话框并加载正确的反馈信息。 - - 测试在详情对话框中提交处理意见后,列表中的反馈状态是否更新。 + +# 反馈管理页面设计文档 + +## 1. 页面概述 + +反馈管理页面是管理员和主管处理用户提交的环境问题报告、建议或其他反馈的核心平台。此页面旨在提供一个高效的工作流程,用于审查、分类、回复和跟踪所有用户反馈,确保问题得到及时响应和解决。 + +## 2. 页面布局 + +![反馈管理页面布局示意图](https://placeholder-for-feedback-page-mockup.png) + +### 2.1 布局结构 + +页面布局与任务管理页面类似,遵循标准的后台管理界面设计: +- **顶部**: 筛选区域,允许管理员按反馈状态(待处理、已处理)、反馈类型或提交时间进行过滤。 +- **中部**: 反馈列表,以表格或卡片列表的形式展示反馈信息。 +- **底部**: 分页控件。 + +### 2.2 响应式设计 + +- **桌面端**: 使用多列数据表格,清晰展示反馈的各项信息。 +- **移动端**: 转换为卡片式列表,每张卡片突出显示反馈标题、提交人和状态,并提供点击查看详情的入口。 + +## 3. 组件结构 + +```vue + +``` + +## 4. 数据结构 + +```typescript +// 反馈筛选条件 +interface FeedbackFilters { + status?: 'PENDING' | 'RESOLVED'; + submitter?: string; +} + +// 反馈列表项 +interface FeedbackListItem { + id: number; + title: string; + submitterName: string; + createdAt: string; + status: 'PENDING' | 'RESOLVED'; +} + +// 反馈详情 (用于对话框) +interface FeedbackDetail extends FeedbackListItem { + content: string; + images: string[]; // 图片 URL 列表 + location: string; + contact: string; + handlerNotes?: string; // 处理备注 +} +``` + +## 5. 状态管理 + +```typescript +// 组件内状态 +const filters = ref({}); +const pagination = ref({ page: 1, pageSize: 10 }); +const dialogVisible = ref(false); +const selectedFeedbackId = ref(null); + +// 全局状态 (Pinia Store) +const feedbackStore = useFeedbackStore(); +const { feedbackList, total, loading } = storeToRefs(feedbackStore); +``` + +## 6. 交互逻辑 + +### 6.1 数据获取与筛选 + +```typescript +const fetchFeedback = () => { + feedbackStore.fetchFeedbackList({ ...filters.value, ...pagination.value }); +}; + +onMounted(fetchFeedback); + +const handleSearch = () => { + pagination.value.page = 1; + fetchFeedback(); +}; + +// 分页逻辑 (handleSizeChange, handleCurrentChange) 与任务管理页面类似 +``` + +### 6.2 查看详情与处理 + +```typescript +const handleViewDetails = (feedback: FeedbackListItem) => { + selectedFeedbackId.value = feedback.id; + dialogVisible.value = true; +}; + +// 当详情对话框中完成处理后被调用 +const handleProcessed = () => { + dialogVisible.value = false; + fetchFeedback(); // 刷新列表以更新状态 +}; +``` + +## 7. API 调用 + +### 7.1 反馈管理 API + +```typescript +// api/feedback.ts +export const feedbackApi = { + getFeedbackList: (params) => apiClient.get('/management/feedback', { params }), + getFeedbackDetail: (id: number) => apiClient.get(`/management/feedback/${id}`), + processFeedback: (id: number, data: { notes: string }) => apiClient.post(`/management/feedback/${id}/process`, data), +}; +``` + +### 7.2 Pinia Store + +```typescript +// stores/feedback.ts +export const useFeedbackStore = defineStore('feedback', { + state: () => ({ + feedbackList: [] as FeedbackListItem[], + total: 0, + loading: false, + currentFeedbackDetail: null as FeedbackDetail | null, + }), + actions: { + async fetchFeedbackList(params) { + this.loading = true; + try { + const { data } = await feedbackApi.getFeedbackList(params); + this.feedbackList = data.items; + this.total = data.total; + } finally { + this.loading = false; + } + }, + async fetchFeedbackDetail(id: number) { + const { data } = await feedbackApi.getFeedbackDetail(id); + this.currentFeedbackDetail = data; + }, + async processFeedback(payload: { id: number; notes: string }) { + await feedbackApi.processFeedback(payload.id, { notes: payload.notes }); + } + } +}); +``` + +## 8. 样式设计 + +```scss +.feedback-management-page { + padding: 24px; + + .page-container { + margin-top: 24px; + padding: 24px; + border-radius: 8px; + border: none; + } + + .filter-container { + margin-bottom: 20px; + } + + .pagination-container { + margin-top: 24px; + display: flex; + justify-content: flex-end; + } +} +``` + +## 9. 关联组件 + +### `FeedbackDetailDialog.vue` + +这是一个关键的子组件,用于显示反馈的完整信息,并提供处理功能。 +- **Props**: `modelValue` (for v-model), `feedbackId` +- **功能**: + - 接收 `feedbackId`,在对话框打开时调用 store action 获取反馈详情。 + - 展示反馈的文本内容、联系方式、地理位置和图片(图片可点击放大预览)。 + - 提供一个文本域供管理员填写处理备注。 + - 提供一个"标记为已处理"的按钮,点击后调用 store action 提交处理结果。 + - 操作成功后,emit 一个 `processed` 事件通知父组件刷新列表。 + +## 10. 测试用例 + +1. **单元测试**: + - 测试 Pinia store 的各个 action 是否能正常工作。 + - 测试 `FeedbackDetailDialog` 组件在接收到 `feedbackId` 后是否能正确加载和显示数据。 + +2. **集成测试**: + - 测试筛选功能是否能正确过滤反馈列表。 + - 测试分页是否正常。 + - 测试点击"查看详情"按钮是否能打开对话框并加载正确的反馈信息。 + - 测试在详情对话框中提交处理意见后,列表中的反馈状态是否更新。 --- - + ### Design/frontend_Design/FileManagementPage_Design.md - -# 文件管理页面设计文档 - -## 1. 页面概述 - -文件管理页面为系统管理员提供了一个集中管理所有上传文件的界面。这主要包括用户在提交反馈、任务报告等流程中上传的图片或其他附件。管理员能够在此页面上预览、搜索、筛选和删除文件,以维护系统存储的整洁和合规性。 - -## 2. 页面布局 - -![文件管理页面布局示意图](https://placeholder-for-files-page-mockup.png) - -### 2.1 布局结构 - -页面将采用现代化的卡片式画廊(Gallery)布局,以优化图片等视觉文件的预览体验。 -- **顶部**: 操作和筛选区域。包含一个上传按钮(如果允许管理员直接上传)和按文件名、上传者或关联模块(如"反馈"、"任务")进行筛选的控件。 -- **中部**: 文件画廊/列表。以卡片网格的形式展示文件,每个卡片包含文件缩略图、文件名、大小、上传时间等信息。 -- **底部**: 分页控件。 - -## 3. 组件结构 - -```vue - -``` - -## 4. 数据结构 - -```typescript -// 文件筛选条件 -interface FileFilters { - filename?: string; -} - -// 文件数据 -interface FileItem { - id: number; - url: string; // 文件的访问 URL - originalName: string; // 原始文件名 - size: number; // 文件大小 (bytes) - mimeType: string; - uploadedAt: string; -} -``` - -## 5. 状态管理 - -```typescript -// 组件内状态 -const filters = ref({}); -const pagination = ref({ page: 1, pageSize: 20 }); - -// 全局状态 (Pinia Store) -const fileStore = useFileStore(); -const { files, total, loading } = storeToRefs(fileStore); -``` - -## 6. 交互逻辑 - -### 6.1 数据获取 - -```typescript -const fetchFiles = () => { - fileStore.fetchFiles({ ...filters.value, ...pagination.value }); -}; - -onMounted(fetchFiles); - -const handleSearch = () => { - pagination.value.page = 1; - fetchFiles(); -}; -// ... 分页逻辑 -``` - -### 6.2 文件操作 - -```typescript -const handleDelete = async (file: FileItem) => { - await fileStore.deleteFile(file.id); - ElMessage.success('文件删除成功'); - fetchFiles(); // 刷新列表 -}; -``` - -### 6.3 辅助函数 - -```typescript -const formatFileSize = (bytes: number): string => { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -}; -``` - -## 7. API 调用 - -```typescript -// api/files.ts -export const fileApi = { - getFiles: (params) => apiClient.get('/files', { params }), - deleteFile: (id: number) => apiClient.delete(`/files/${id}`), -}; - -// stores/file.ts -export const useFileStore = defineStore('file', { - state: () => ({ - files: [] as FileItem[], - total: 0, - loading: false, - }), - actions: { - async fetchFiles(params) { - this.loading = true; - try { - const { data } = await fileApi.getFiles(params); - this.files = data.items; - this.total = data.total; - } finally { - this.loading = false; - } - }, - async deleteFile(id: number) { - await fileApi.deleteFile(id); - } - } -}); -``` - -## 8. 样式设计 - -```scss -.file-management-page { - .page-container { - margin-top: 24px; - } - - .file-gallery { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 20px; - } - - .file-card { - .file-thumbnail { - width: 100%; - height: 150px; - display: block; - } - - .file-info { - padding: 10px; - display: flex; - align-items: center; - justify-content: space-between; - - .file-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex-grow: 1; - margin-right: 10px; - } - - .file-size { - font-size: 12px; - color: #909399; - flex-shrink: 0; - } - - .delete-btn { - margin-left: 10px; - padding: 2px; - } - } - } - - .pagination-container { - margin-top: 24px; - } -} -``` - -## 9. 测试用例 - -- **集成测试**: - - 测试页面是否能正确加载并以卡片形式展示文件列表。 - - 测试搜索功能是否能正确过滤文件。 - - 测试分页功能。 - - 测试点击图片是否能触发大图预览。 - - 测试删除文件功能,并验证文件是否从列表中移除。 + +# 文件管理页面设计文档 + +## 1. 页面概述 + +文件管理页面为系统管理员提供了一个集中管理所有上传文件的界面。这主要包括用户在提交反馈、任务报告等流程中上传的图片或其他附件。管理员能够在此页面上预览、搜索、筛选和删除文件,以维护系统存储的整洁和合规性。 + +## 2. 页面布局 + +![文件管理页面布局示意图](https://placeholder-for-files-page-mockup.png) + +### 2.1 布局结构 + +页面将采用现代化的卡片式画廊(Gallery)布局,以优化图片等视觉文件的预览体验。 +- **顶部**: 操作和筛选区域。包含一个上传按钮(如果允许管理员直接上传)和按文件名、上传者或关联模块(如"反馈"、"任务")进行筛选的控件。 +- **中部**: 文件画廊/列表。以卡片网格的形式展示文件,每个卡片包含文件缩略图、文件名、大小、上传时间等信息。 +- **底部**: 分页控件。 + +## 3. 组件结构 + +```vue + +``` + +## 4. 数据结构 + +```typescript +// 文件筛选条件 +interface FileFilters { + filename?: string; +} + +// 文件数据 +interface FileItem { + id: number; + url: string; // 文件的访问 URL + originalName: string; // 原始文件名 + size: number; // 文件大小 (bytes) + mimeType: string; + uploadedAt: string; +} +``` + +## 5. 状态管理 + +```typescript +// 组件内状态 +const filters = ref({}); +const pagination = ref({ page: 1, pageSize: 20 }); + +// 全局状态 (Pinia Store) +const fileStore = useFileStore(); +const { files, total, loading } = storeToRefs(fileStore); +``` + +## 6. 交互逻辑 + +### 6.1 数据获取 + +```typescript +const fetchFiles = () => { + fileStore.fetchFiles({ ...filters.value, ...pagination.value }); +}; + +onMounted(fetchFiles); + +const handleSearch = () => { + pagination.value.page = 1; + fetchFiles(); +}; +// ... 分页逻辑 +``` + +### 6.2 文件操作 + +```typescript +const handleDelete = async (file: FileItem) => { + await fileStore.deleteFile(file.id); + ElMessage.success('文件删除成功'); + fetchFiles(); // 刷新列表 +}; +``` + +### 6.3 辅助函数 + +```typescript +const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; +``` + +## 7. API 调用 + +```typescript +// api/files.ts +export const fileApi = { + getFiles: (params) => apiClient.get('/files', { params }), + deleteFile: (id: number) => apiClient.delete(`/files/${id}`), +}; + +// stores/file.ts +export const useFileStore = defineStore('file', { + state: () => ({ + files: [] as FileItem[], + total: 0, + loading: false, + }), + actions: { + async fetchFiles(params) { + this.loading = true; + try { + const { data } = await fileApi.getFiles(params); + this.files = data.items; + this.total = data.total; + } finally { + this.loading = false; + } + }, + async deleteFile(id: number) { + await fileApi.deleteFile(id); + } + } +}); +``` + +## 8. 样式设计 + +```scss +.file-management-page { + .page-container { + margin-top: 24px; + } + + .file-gallery { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 20px; + } + + .file-card { + .file-thumbnail { + width: 100%; + height: 150px; + display: block; + } + + .file-info { + padding: 10px; + display: flex; + align-items: center; + justify-content: space-between; + + .file-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex-grow: 1; + margin-right: 10px; + } + + .file-size { + font-size: 12px; + color: #909399; + flex-shrink: 0; + } + + .delete-btn { + margin-left: 10px; + padding: 2px; + } + } + } + + .pagination-container { + margin-top: 24px; + } +} +``` + +## 9. 测试用例 + +- **集成测试**: + - 测试页面是否能正确加载并以卡片形式展示文件列表。 + - 测试搜索功能是否能正确过滤文件。 + - 测试分页功能。 + - 测试点击图片是否能触发大图预览。 + - 测试删除文件功能,并验证文件是否从列表中移除。 --- - + ### Design/frontend_Design/Frontend_Design.md - -# EMS 前端设计文档 - -## 1. 技术栈 - -- **前端框架**: Vue 3 -- **构建工具**: Vite -- **状态管理**: Pinia -- **路由管理**: Vue Router -- **UI 组件库**: Element Plus -- **HTTP 客户端**: Axios -- **CSS 预处理器**: SCSS -- **类型检查**: TypeScript -- **地图组件**: Leaflet.js -- **图表库**: ECharts -- **表单验证**: Vee-Validate -- **国际化**: Vue I18n -- **测试框架**: Vitest - -## 2. 架构设计 - -### 2.1 目录结构 - -``` -ems-frontend/ -├── public/ # 静态资源 -├── src/ -│ ├── api/ # API 请求模块 -│ │ ├── auth.ts # 认证相关 API -│ │ ├── dashboard.ts # 仪表盘相关 API -│ │ └── ... -│ ├── assets/ # 静态资源 -│ │ ├── styles/ # 全局样式 -│ │ └── images/ # 图片资源 -│ ├── components/ # 通用组件 -│ │ ├── common/ # 基础组件 -│ │ ├── layout/ # 布局组件 -│ │ └── business/ # 业务组件 -│ ├── composables/ # 组合式函数 -│ ├── config/ # 配置文件 -│ ├── constants/ # 常量定义 -│ ├── directives/ # 自定义指令 -│ ├── hooks/ # 自定义 Hooks -│ ├── locales/ # 国际化资源 -│ ├── router/ # 路由配置 -│ ├── stores/ # Pinia 状态管理 -│ ├── types/ # TypeScript 类型定义 -│ ├── utils/ # 工具函数 -│ ├── views/ # 页面组件 -│ │ ├── auth/ # 认证相关页面 -│ │ ├── dashboard/ # 仪表盘页面 -│ │ └── ... -│ ├── App.vue # 根组件 -│ ├── main.ts # 入口文件 -│ └── env.d.ts # 环境变量类型声明 -├── .env # 环境变量 -├── .env.development # 开发环境变量 -├── .env.production # 生产环境变量 -├── index.html # HTML 模板 -├── package.json # 依赖配置 -├── tsconfig.json # TypeScript 配置 -└── vite.config.ts # Vite 配置 -``` - -### 2.2 核心模块 - -1. **认证模块**: 处理用户登录、注册、密码重置等功能 -2. **仪表盘模块**: 展示系统概览、数据统计和图表 -3. **地图模块**: 展示地理信息、网格分布和污染热图 -4. **任务管理模块**: 处理任务分配、查看和审核 -5. **反馈管理模块**: 处理用户反馈的提交和管理 -6. **人员管理模块**: 管理系统用户、角色和权限 -7. **个人中心模块**: 查看和修改个人信息 - -## 3. 页面结构 - -### 3.1 基础布局 - -系统将采用以下几种基础布局: - -1. **主布局**: 包含侧边导航栏、顶部导航栏和内容区域 -2. **认证布局**: 用于登录、注册等不需要导航的页面 -3. **空白布局**: 用于全屏展示的页面,如地图全屏模式 - -### 3.2 响应式设计 - -- 采用移动优先的响应式设计 -- 断点设置: - - 移动端: < 768px - - 平板: 768px - 1024px - - 桌面: > 1024px -- 关键组件的响应式行为: - - 侧边栏在移动端变为可折叠抽屉 - - 表格在移动端优化为卡片式布局 - - 表单在移动端采用单列布局 - -## 4. 路由设计 - -### 4.1 路由结构 - -```javascript -const routes = [ - // 认证相关路由 - { - path: '/auth', - component: AuthLayout, - children: [ - { path: 'login', component: LoginView }, - { path: 'register', component: RegisterView }, - { path: 'forgot-password', component: ForgotPasswordView }, - { path: 'reset-password', component: ResetPasswordView }, - ], - meta: { requiresAuth: false } - }, - - // 主应用路由 - { - path: '/', - component: MainLayout, - children: [ - { path: '', component: DashboardView, meta: { requiresAuth: true, roles: ['ADMIN', 'DECISION_MAKER'] } }, - { path: 'map', component: MapView, meta: { requiresAuth: false } }, - { path: 'tasks', component: TasksView, meta: { requiresAuth: true } }, - { path: 'feedback', component: FeedbackView, meta: { requiresAuth: true } }, - { path: 'personnel', component: PersonnelView, meta: { requiresAuth: true, roles: ['ADMIN'] } }, - { path: 'profile', component: ProfileView, meta: { requiresAuth: true } }, - ] - }, - - // 错误页面路由 - { path: '/404', component: NotFoundView }, - { path: '/403', component: ForbiddenView }, - { path: '/:pathMatch(.*)*', redirect: '/404' } -] -``` - -### 4.2 路由守卫 - -```javascript -router.beforeEach((to, from, next) => { - const authStore = useAuthStore(); - - // 检查路由是否需要认证 - const requiresAuth = to.matched.some(record => record.meta.requiresAuth !== false); - - // 检查用户角色权限 - const hasRequiredRole = to.matched.every(record => { - if (!record.meta.roles) return true; - return authStore.user && record.meta.roles.includes(authStore.user.role); - }); - - if (requiresAuth && !authStore.isAuthenticated) { - // 未登录,重定向到登录页 - next({ path: '/auth/login', query: { redirect: to.fullPath } }); - } else if (requiresAuth && !hasRequiredRole) { - // 无权限访问 - next({ path: '/403' }); - } else { - // 正常导航 - next(); - } -}); -``` - -## 5. 状态管理 - -### 5.1 Pinia Store 设计 - -```typescript -// 认证状态管理 -export const useAuthStore = defineStore('auth', { - state: () => ({ - user: null, - token: localStorage.getItem('token'), - error: null, - }), - getters: { - isAuthenticated: state => !!state.token, - userRole: state => state.user?.role || null, - }, - actions: { - async login(credentials) { /* 实现登录逻辑 */ }, - async logout() { /* 实现登出逻辑 */ }, - async fetchUserProfile() { /* 获取用户信息 */ }, - } -}); - -// 任务状态管理 -export const useTaskStore = defineStore('task', { - state: () => ({ - tasks: [], - currentTask: null, - loading: false, - error: null, - }), - getters: { - tasksByStatus: state => status => state.tasks.filter(task => task.status === status), - }, - actions: { - async fetchTasks(filters) { /* 获取任务列表 */ }, - async fetchTaskById(id) { /* 获取任务详情 */ }, - async assignTask(taskId, userId) { /* 分配任务 */ }, - async submitTask(taskId, data) { /* 提交任务 */ }, - } -}); - -// 其他模块状态管理... -``` - -### 5.2 持久化策略 - -- 使用 localStorage 存储认证 token -- 关键用户设置使用 localStorage 持久化 -- 敏感数据不进行持久化,每次会话重新获取 - -## 6. API 请求封装 - -### 6.1 基础请求客户端 - -```typescript -// api/client.ts -import axios from 'axios'; -import { useAuthStore } from '@/stores/auth'; - -const apiClient = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL, - timeout: 10000, - headers: { - 'Content-Type': 'application/json', - } -}); - -// 请求拦截器 -apiClient.interceptors.request.use( - config => { - const authStore = useAuthStore(); - if (authStore.token) { - config.headers.Authorization = `Bearer ${authStore.token}`; - } - return config; - }, - error => Promise.reject(error) -); - -// 响应拦截器 -apiClient.interceptors.response.use( - response => response, - error => { - const authStore = useAuthStore(); - - // 处理 401 未授权错误 - if (error.response && error.response.status === 401) { - authStore.logout(); - router.push('/auth/login'); - } - - return Promise.reject(error); - } -); - -export default apiClient; -``` - -### 6.2 模块化 API 封装 - -```typescript -// api/auth.ts -import apiClient from './client'; - -export const authApi = { - login: (credentials) => apiClient.post('/auth/login', credentials), - register: (userData) => apiClient.post('/auth/signup', userData), - requestPasswordReset: (email) => apiClient.post('/auth/request-password-reset', { email }), - resetPassword: (data) => apiClient.post('/auth/reset-password', data), -}; - -// api/tasks.ts -import apiClient from './client'; - -export const taskApi = { - getTasks: (params) => apiClient.get('/management/tasks', { params }), - getTaskById: (id) => apiClient.get(`/management/tasks/${id}`), - assignTask: (taskId, data) => apiClient.post(`/management/tasks/${taskId}/assign`, data), - reviewTask: (taskId, data) => apiClient.post(`/management/tasks/${taskId}/review`, data), -}; - -// 其他模块 API... -``` - -## 7. 组件设计 - -### 7.1 基础组件 - -- **AppButton**: 统一按钮样式和行为 -- **AppCard**: 卡片容器组件 -- **AppForm**: 表单容器组件 -- **AppInput**: 输入框组件 -- **AppSelect**: 下拉选择组件 -- **AppTable**: 表格组件 -- **AppPagination**: 分页组件 -- **AppModal**: 模态框组件 -- **AppAlert**: 提示框组件 - -### 7.2 业务组件 - -- **TaskCard**: 任务卡片组件 -- **FeedbackForm**: 反馈提交表单 -- **UserSelector**: 用户选择器 -- **StatusBadge**: 状态标签组件 -- **MapGrid**: 地图网格组件 -- **PollutionTypeIcon**: 污染类型图标 -- **TaskFilterPanel**: 任务筛选面板 -- **StatisticCard**: 统计数据卡片 - -## 8. 主题设计 - -### 8.1 色彩系统 - -- **主色**: #1890ff (蓝色) -- **成功色**: #52c41a (绿色) -- **警告色**: #faad14 (黄色) -- **错误色**: #f5222d (红色) -- **中性色**: - - 标题: #262626 - - 正文: #595959 - - 辅助文字: #8c8c8c - - 禁用: #bfbfbf - - 边框: #d9d9d9 - - 分割线: #f0f0f0 - - 背景: #f5f5f5 - - 表格头部: #fafafa - -### 8.2 字体系统 - -- **主字体**: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif -- **代码字体**: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace -- **字体大小**: - - 小号: 12px - - 正文: 14px - - 标题: 16px, 18px, 20px, 24px - -### 8.3 间距系统 - -- **基础单位**: 4px -- **间距尺寸**: - - xs: 4px - - sm: 8px - - md: 16px - - lg: 24px - - xl: 32px - - xxl: 48px - -### 8.4 阴影系统 - -- **轻微阴影**: 0 2px 8px rgba(0, 0, 0, 0.15) -- **中等阴影**: 0 4px 12px rgba(0, 0, 0, 0.15) -- **深度阴影**: 0 8px 16px rgba(0, 0, 0, 0.15) - -## 9. 国际化 - -支持中文和英文两种语言,使用 Vue I18n 实现国际化。 - -```typescript -// locales/zh-CN.json -{ - "auth": { - "login": "登录", - "register": "注册", - "forgotPassword": "忘记密码", - "email": "邮箱", - "password": "密码", - "confirmPassword": "确认密码" - }, - "dashboard": { - "title": "仪表盘", - "stats": "统计数据", - "tasks": "任务概览", - "feedback": "反馈概览" - } -} - -// locales/en-US.json -{ - "auth": { - "login": "Login", - "register": "Register", - "forgotPassword": "Forgot Password", - "email": "Email", - "password": "Password", - "confirmPassword": "Confirm Password" - }, - "dashboard": { - "title": "Dashboard", - "stats": "Statistics", - "tasks": "Task Overview", - "feedback": "Feedback Overview" - } -} -``` - -## 10. 权限控制 - -### 10.1 角色定义 - -- **ADMIN**: 系统管理员,拥有所有权限 -- **SUPERVISOR**: 主管,负责任务分配和审核 -- **GRID_WORKER**: 网格员,负责执行任务 -- **DECISION_MAKER**: 决策者,查看数据和报表 - -### 10.2 权限指令 - -```typescript -// 创建自定义指令控制元素的显示/隐藏 -app.directive('permission', { - mounted(el, binding) { - const { value } = binding; - const authStore = useAuthStore(); - - if (value && !authStore.hasPermission(value)) { - el.parentNode && el.parentNode.removeChild(el); - } - } -}); - -// 使用示例 - -
管理内容
-``` - -## 11. 错误处理 - -### 11.1 全局错误处理 - -```typescript -// 全局错误处理 -app.config.errorHandler = (err, vm, info) => { - console.error('全局错误:', err); - - // 上报错误到监控系统 - errorReportingService.report(err, { - component: vm?.$options?.name, - info, - user: useAuthStore().user?.id - }); - - // 显示用户友好的错误提示 - ElMessage.error('操作失败,请稍后重试'); -}; -``` - -### 11.2 API 错误处理 - -```typescript -// 统一处理 API 错误 -export const handleApiError = (error) => { - if (error.response) { - // 服务器响应错误 - const { status, data } = error.response; - - switch (status) { - case 400: - ElMessage.error(data.message || '请求参数错误'); - break; - case 401: - ElMessage.error('会话已过期,请重新登录'); - break; - case 403: - ElMessage.error('没有权限执行此操作'); - break; - case 404: - ElMessage.error('请求的资源不存在'); - break; - case 500: - ElMessage.error('服务器内部错误,请稍后重试'); - break; - default: - ElMessage.error('请求失败,请稍后重试'); - } - } else if (error.request) { - // 请求发出但没有收到响应 - ElMessage.error('网络连接失败,请检查网络设置'); - } else { - // 请求配置错误 - ElMessage.error('请求配置错误'); - } - - return Promise.reject(error); -}; -``` - -## 12. 性能优化 - -### 12.1 代码分割 - -- 使用动态导入实现路由级别的代码分割 -- 将大型第三方库单独打包 - -### 12.2 资源优化 - -- 图片资源使用 WebP 格式并进行压缩 -- 使用 SVG 图标代替图片图标 -- 使用字体图标减少 HTTP 请求 - -### 12.3 渲染优化 - -- 使用虚拟滚动处理长列表 -- 合理使用 `v-show` 和 `v-if` -- 大型表格使用分页加载 -- 使用 `keep-alive` 缓存组件状态 - -## 13. 测试策略 - -### 13.1 单元测试 - -- 使用 Vitest 进行单元测试 -- 重点测试工具函数、Hooks 和小型组件 -- 使用 Vue Test Utils 测试组件 - -### 13.2 端到端测试 - -- 使用 Cypress 进行端到端测试 -- 覆盖关键用户流程: - - 用户注册和登录 - - 任务创建和分配 - - 反馈提交和处理 - -## 14. 部署策略 - -### 14.1 构建配置 - -- 开发环境: `vite build --mode development` -- 测试环境: `vite build --mode staging` -- 生产环境: `vite build --mode production` - -### 14.2 环境变量 - -``` -# .env.development -VITE_API_BASE_URL=http://localhost:8080/api -VITE_APP_TITLE=EMS开发环境 - -# .env.production -VITE_API_BASE_URL=/api -VITE_APP_TITLE=环境监测系统 -``` - -### 14.3 CI/CD 流程 - -- 使用 GitHub Actions 自动化构建和部署 -- 提交到 `develop` 分支自动部署到测试环境 -- 提交到 `main` 分支自动部署到生产环境 + +# EMS 前端设计文档 + +## 1. 技术栈 + +- **前端框架**: Vue 3 +- **构建工具**: Vite +- **状态管理**: Pinia +- **路由管理**: Vue Router +- **UI 组件库**: Element Plus +- **HTTP 客户端**: Axios +- **CSS 预处理器**: SCSS +- **类型检查**: TypeScript +- **地图组件**: Leaflet.js +- **图表库**: ECharts +- **表单验证**: Vee-Validate +- **国际化**: Vue I18n +- **测试框架**: Vitest + +## 2. 架构设计 + +### 2.1 目录结构 + +``` +ems-frontend/ +├── public/ # 静态资源 +├── src/ +│ ├── api/ # API 请求模块 +│ │ ├── auth.ts # 认证相关 API +│ │ ├── dashboard.ts # 仪表盘相关 API +│ │ └── ... +│ ├── assets/ # 静态资源 +│ │ ├── styles/ # 全局样式 +│ │ └── images/ # 图片资源 +│ ├── components/ # 通用组件 +│ │ ├── common/ # 基础组件 +│ │ ├── layout/ # 布局组件 +│ │ └── business/ # 业务组件 +│ ├── composables/ # 组合式函数 +│ ├── config/ # 配置文件 +│ ├── constants/ # 常量定义 +│ ├── directives/ # 自定义指令 +│ ├── hooks/ # 自定义 Hooks +│ ├── locales/ # 国际化资源 +│ ├── router/ # 路由配置 +│ ├── stores/ # Pinia 状态管理 +│ ├── types/ # TypeScript 类型定义 +│ ├── utils/ # 工具函数 +│ ├── views/ # 页面组件 +│ │ ├── auth/ # 认证相关页面 +│ │ ├── dashboard/ # 仪表盘页面 +│ │ └── ... +│ ├── App.vue # 根组件 +│ ├── main.ts # 入口文件 +│ └── env.d.ts # 环境变量类型声明 +├── .env # 环境变量 +├── .env.development # 开发环境变量 +├── .env.production # 生产环境变量 +├── index.html # HTML 模板 +├── package.json # 依赖配置 +├── tsconfig.json # TypeScript 配置 +└── vite.config.ts # Vite 配置 +``` + +### 2.2 核心模块 + +1. **认证模块**: 处理用户登录、注册、密码重置等功能 +2. **仪表盘模块**: 展示系统概览、数据统计和图表 +3. **地图模块**: 展示地理信息、网格分布和污染热图 +4. **任务管理模块**: 处理任务分配、查看和审核 +5. **反馈管理模块**: 处理用户反馈的提交和管理 +6. **人员管理模块**: 管理系统用户、角色和权限 +7. **个人中心模块**: 查看和修改个人信息 + +## 3. 页面结构 + +### 3.1 基础布局 + +系统将采用以下几种基础布局: + +1. **主布局**: 包含侧边导航栏、顶部导航栏和内容区域 +2. **认证布局**: 用于登录、注册等不需要导航的页面 +3. **空白布局**: 用于全屏展示的页面,如地图全屏模式 + +### 3.2 响应式设计 + +- 采用移动优先的响应式设计 +- 断点设置: + - 移动端: < 768px + - 平板: 768px - 1024px + - 桌面: > 1024px +- 关键组件的响应式行为: + - 侧边栏在移动端变为可折叠抽屉 + - 表格在移动端优化为卡片式布局 + - 表单在移动端采用单列布局 + +## 4. 路由设计 + +### 4.1 路由结构 + +```javascript +const routes = [ + // 认证相关路由 + { + path: '/auth', + component: AuthLayout, + children: [ + { path: 'login', component: LoginView }, + { path: 'register', component: RegisterView }, + { path: 'forgot-password', component: ForgotPasswordView }, + { path: 'reset-password', component: ResetPasswordView }, + ], + meta: { requiresAuth: false } + }, + + // 主应用路由 + { + path: '/', + component: MainLayout, + children: [ + { path: '', component: DashboardView, meta: { requiresAuth: true, roles: ['ADMIN', 'DECISION_MAKER'] } }, + { path: 'map', component: MapView, meta: { requiresAuth: false } }, + { path: 'tasks', component: TasksView, meta: { requiresAuth: true } }, + { path: 'feedback', component: FeedbackView, meta: { requiresAuth: true } }, + { path: 'personnel', component: PersonnelView, meta: { requiresAuth: true, roles: ['ADMIN'] } }, + { path: 'profile', component: ProfileView, meta: { requiresAuth: true } }, + ] + }, + + // 错误页面路由 + { path: '/404', component: NotFoundView }, + { path: '/403', component: ForbiddenView }, + { path: '/:pathMatch(.*)*', redirect: '/404' } +] +``` + +### 4.2 路由守卫 + +```javascript +router.beforeEach((to, from, next) => { + const authStore = useAuthStore(); + + // 检查路由是否需要认证 + const requiresAuth = to.matched.some(record => record.meta.requiresAuth !== false); + + // 检查用户角色权限 + const hasRequiredRole = to.matched.every(record => { + if (!record.meta.roles) return true; + return authStore.user && record.meta.roles.includes(authStore.user.role); + }); + + if (requiresAuth && !authStore.isAuthenticated) { + // 未登录,重定向到登录页 + next({ path: '/auth/login', query: { redirect: to.fullPath } }); + } else if (requiresAuth && !hasRequiredRole) { + // 无权限访问 + next({ path: '/403' }); + } else { + // 正常导航 + next(); + } +}); +``` + +## 5. 状态管理 + +### 5.1 Pinia Store 设计 + +```typescript +// 认证状态管理 +export const useAuthStore = defineStore('auth', { + state: () => ({ + user: null, + token: localStorage.getItem('token'), + error: null, + }), + getters: { + isAuthenticated: state => !!state.token, + userRole: state => state.user?.role || null, + }, + actions: { + async login(credentials) { /* 实现登录逻辑 */ }, + async logout() { /* 实现登出逻辑 */ }, + async fetchUserProfile() { /* 获取用户信息 */ }, + } +}); + +// 任务状态管理 +export const useTaskStore = defineStore('task', { + state: () => ({ + tasks: [], + currentTask: null, + loading: false, + error: null, + }), + getters: { + tasksByStatus: state => status => state.tasks.filter(task => task.status === status), + }, + actions: { + async fetchTasks(filters) { /* 获取任务列表 */ }, + async fetchTaskById(id) { /* 获取任务详情 */ }, + async assignTask(taskId, userId) { /* 分配任务 */ }, + async submitTask(taskId, data) { /* 提交任务 */ }, + } +}); + +// 其他模块状态管理... +``` + +### 5.2 持久化策略 + +- 使用 localStorage 存储认证 token +- 关键用户设置使用 localStorage 持久化 +- 敏感数据不进行持久化,每次会话重新获取 + +## 6. API 请求封装 + +### 6.1 基础请求客户端 + +```typescript +// api/client.ts +import axios from 'axios'; +import { useAuthStore } from '@/stores/auth'; + +const apiClient = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + } +}); + +// 请求拦截器 +apiClient.interceptors.request.use( + config => { + const authStore = useAuthStore(); + if (authStore.token) { + config.headers.Authorization = `Bearer ${authStore.token}`; + } + return config; + }, + error => Promise.reject(error) +); + +// 响应拦截器 +apiClient.interceptors.response.use( + response => response, + error => { + const authStore = useAuthStore(); + + // 处理 401 未授权错误 + if (error.response && error.response.status === 401) { + authStore.logout(); + router.push('/auth/login'); + } + + return Promise.reject(error); + } +); + +export default apiClient; +``` + +### 6.2 模块化 API 封装 + +```typescript +// api/auth.ts +import apiClient from './client'; + +export const authApi = { + login: (credentials) => apiClient.post('/auth/login', credentials), + register: (userData) => apiClient.post('/auth/signup', userData), + requestPasswordReset: (email) => apiClient.post('/auth/request-password-reset', { email }), + resetPassword: (data) => apiClient.post('/auth/reset-password', data), +}; + +// api/tasks.ts +import apiClient from './client'; + +export const taskApi = { + getTasks: (params) => apiClient.get('/management/tasks', { params }), + getTaskById: (id) => apiClient.get(`/management/tasks/${id}`), + assignTask: (taskId, data) => apiClient.post(`/management/tasks/${taskId}/assign`, data), + reviewTask: (taskId, data) => apiClient.post(`/management/tasks/${taskId}/review`, data), +}; + +// 其他模块 API... +``` + +## 7. 组件设计 + +### 7.1 基础组件 + +- **AppButton**: 统一按钮样式和行为 +- **AppCard**: 卡片容器组件 +- **AppForm**: 表单容器组件 +- **AppInput**: 输入框组件 +- **AppSelect**: 下拉选择组件 +- **AppTable**: 表格组件 +- **AppPagination**: 分页组件 +- **AppModal**: 模态框组件 +- **AppAlert**: 提示框组件 + +### 7.2 业务组件 + +- **TaskCard**: 任务卡片组件 +- **FeedbackForm**: 反馈提交表单 +- **UserSelector**: 用户选择器 +- **StatusBadge**: 状态标签组件 +- **MapGrid**: 地图网格组件 +- **PollutionTypeIcon**: 污染类型图标 +- **TaskFilterPanel**: 任务筛选面板 +- **StatisticCard**: 统计数据卡片 + +## 8. 主题设计 + +### 8.1 色彩系统 + +- **主色**: #1890ff (蓝色) +- **成功色**: #52c41a (绿色) +- **警告色**: #faad14 (黄色) +- **错误色**: #f5222d (红色) +- **中性色**: + - 标题: #262626 + - 正文: #595959 + - 辅助文字: #8c8c8c + - 禁用: #bfbfbf + - 边框: #d9d9d9 + - 分割线: #f0f0f0 + - 背景: #f5f5f5 + - 表格头部: #fafafa + +### 8.2 字体系统 + +- **主字体**: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Helvetica Neue', Arial, sans-serif +- **代码字体**: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace +- **字体大小**: + - 小号: 12px + - 正文: 14px + - 标题: 16px, 18px, 20px, 24px + +### 8.3 间距系统 + +- **基础单位**: 4px +- **间距尺寸**: + - xs: 4px + - sm: 8px + - md: 16px + - lg: 24px + - xl: 32px + - xxl: 48px + +### 8.4 阴影系统 + +- **轻微阴影**: 0 2px 8px rgba(0, 0, 0, 0.15) +- **中等阴影**: 0 4px 12px rgba(0, 0, 0, 0.15) +- **深度阴影**: 0 8px 16px rgba(0, 0, 0, 0.15) + +## 9. 国际化 + +支持中文和英文两种语言,使用 Vue I18n 实现国际化。 + +```typescript +// locales/zh-CN.json +{ + "auth": { + "login": "登录", + "register": "注册", + "forgotPassword": "忘记密码", + "email": "邮箱", + "password": "密码", + "confirmPassword": "确认密码" + }, + "dashboard": { + "title": "仪表盘", + "stats": "统计数据", + "tasks": "任务概览", + "feedback": "反馈概览" + } +} + +// locales/en-US.json +{ + "auth": { + "login": "Login", + "register": "Register", + "forgotPassword": "Forgot Password", + "email": "Email", + "password": "Password", + "confirmPassword": "Confirm Password" + }, + "dashboard": { + "title": "Dashboard", + "stats": "Statistics", + "tasks": "Task Overview", + "feedback": "Feedback Overview" + } +} +``` + +## 10. 权限控制 + +### 10.1 角色定义 + +- **ADMIN**: 系统管理员,拥有所有权限 +- **SUPERVISOR**: 主管,负责任务分配和审核 +- **GRID_WORKER**: 网格员,负责执行任务 +- **DECISION_MAKER**: 决策者,查看数据和报表 + +### 10.2 权限指令 + +```typescript +// 创建自定义指令控制元素的显示/隐藏 +app.directive('permission', { + mounted(el, binding) { + const { value } = binding; + const authStore = useAuthStore(); + + if (value && !authStore.hasPermission(value)) { + el.parentNode && el.parentNode.removeChild(el); + } + } +}); + +// 使用示例 + +
管理内容
+``` + +## 11. 错误处理 + +### 11.1 全局错误处理 + +```typescript +// 全局错误处理 +app.config.errorHandler = (err, vm, info) => { + console.error('全局错误:', err); + + // 上报错误到监控系统 + errorReportingService.report(err, { + component: vm?.$options?.name, + info, + user: useAuthStore().user?.id + }); + + // 显示用户友好的错误提示 + ElMessage.error('操作失败,请稍后重试'); +}; +``` + +### 11.2 API 错误处理 + +```typescript +// 统一处理 API 错误 +export const handleApiError = (error) => { + if (error.response) { + // 服务器响应错误 + const { status, data } = error.response; + + switch (status) { + case 400: + ElMessage.error(data.message || '请求参数错误'); + break; + case 401: + ElMessage.error('会话已过期,请重新登录'); + break; + case 403: + ElMessage.error('没有权限执行此操作'); + break; + case 404: + ElMessage.error('请求的资源不存在'); + break; + case 500: + ElMessage.error('服务器内部错误,请稍后重试'); + break; + default: + ElMessage.error('请求失败,请稍后重试'); + } + } else if (error.request) { + // 请求发出但没有收到响应 + ElMessage.error('网络连接失败,请检查网络设置'); + } else { + // 请求配置错误 + ElMessage.error('请求配置错误'); + } + + return Promise.reject(error); +}; +``` + +## 12. 性能优化 + +### 12.1 代码分割 + +- 使用动态导入实现路由级别的代码分割 +- 将大型第三方库单独打包 + +### 12.2 资源优化 + +- 图片资源使用 WebP 格式并进行压缩 +- 使用 SVG 图标代替图片图标 +- 使用字体图标减少 HTTP 请求 + +### 12.3 渲染优化 + +- 使用虚拟滚动处理长列表 +- 合理使用 `v-show` 和 `v-if` +- 大型表格使用分页加载 +- 使用 `keep-alive` 缓存组件状态 + +## 13. 测试策略 + +### 13.1 单元测试 + +- 使用 Vitest 进行单元测试 +- 重点测试工具函数、Hooks 和小型组件 +- 使用 Vue Test Utils 测试组件 + +### 13.2 端到端测试 + +- 使用 Cypress 进行端到端测试 +- 覆盖关键用户流程: + - 用户注册和登录 + - 任务创建和分配 + - 反馈提交和处理 + +## 14. 部署策略 + +### 14.1 构建配置 + +- 开发环境: `vite build --mode development` +- 测试环境: `vite build --mode staging` +- 生产环境: `vite build --mode production` + +### 14.2 环境变量 + +``` +# .env.development +VITE_API_BASE_URL=http://localhost:8080/api +VITE_APP_TITLE=EMS开发环境 + +# .env.production +VITE_API_BASE_URL=/api +VITE_APP_TITLE=环境监测系统 +``` + +### 14.3 CI/CD 流程 + +- 使用 GitHub Actions 自动化构建和部署 +- 提交到 `develop` 分支自动部署到测试环境 +- 提交到 `main` 分支自动部署到生产环境 --- - + ### Design/frontend_Design/GridManagementPage_Design.md - -# 网格管理页面设计文档 - -## 1. 页面概述 - -网格管理页面允许系统管理员创建、查看、编辑和删除地理网格。每个网格代表一个责任区域,可以关联到特定的主管或网格员。此功能是任务分配和地理数据可视化的基础。 - -## 2. 页面布局 - -![网格管理页面布局示意图](https://placeholder-for-grid-page-mockup.png) - -### 2.1 布局结构 - -页面由两部分组成: -- **左侧**: 网格列表,以表格形式展示所有已定义的网格及其基本信息(名称、负责人等)。 -- **右侧**: 交互式地图,用于可视化展示所选网格的地理边界。当创建或编辑网格时,地图将进入绘图模式。 - -## 3. 组件结构 - -```vue - -``` - -## 4. 数据结构 - -```typescript -// 网格列表项 -interface GridListItem { - id: number; - name: string; - manager?: string; - // GeoJSON 字符串 - geometry: string; -} - -// 网格表单数据 (用于对话框) -interface GridFormData { - id?: number; - name: string; - managerId?: number; - // GeoJSON 字符串 - geometry: string; -} -``` - -## 5. 状态管理 - -```typescript -// 地图实例 -let mapInstance = null; -let currentGridLayer = null; - -// 组件内状态 -const mapContainer = ref(); -const dialogVisible = ref(false); -const selectedGridId = ref(null); - -// 全局状态 (Pinia Store) -const gridStore = useGridStore(); -const { grids, loading } = storeToRefs(gridStore); -``` - -## 6. 交互逻辑 - -### 6.1 地图与列表交互 - -```typescript -onMounted(() => { - gridStore.fetchGrids(); - initializeMap(); -}); - -const initializeMap = () => { - // ... 地图初始化逻辑 -}; - -// 点击表格行,在地图上高亮显示对应网格 -const handleRowClick = (row: GridListItem) => { - if (currentGridLayer) { - mapInstance.removeLayer(currentGridLayer); - } - if (row.geometry) { - const geoJson = JSON.parse(row.geometry); - currentGridLayer = L.geoJSON(geoJson).addTo(mapInstance); - mapInstance.fitBounds(currentGridLayer.getBounds()); - } -}; -``` - -### 6.2 CRUD 操作 - -```typescript -const handleCreate = () => { - selectedGridId.value = null; - dialogVisible.value = true; -}; - -const handleEdit = (grid: GridListItem) => { - selectedGridId.value = grid.id; - dialogVisible.value = true; -}; - -const handleDelete = async (grid: GridListItem) => { - await gridStore.deleteGrid(grid.id); - ElMessage.success('网格删除成功'); - gridStore.fetchGrids(); -}; - -const onFormSuccess = () => { - dialogVisible.value = false; - gridStore.fetchGrids(); -}; -``` - -## 7. API 调用 - -```typescript -// api/grid.ts -export const gridApi = { - getGrids: () => apiClient.get('/grid'), - getGridById: (id: number) => apiClient.get(`/grid/${id}`), - createGrid: (data: GridFormData) => apiClient.post('/grid', data), - updateGrid: (id: number, data: GridFormData) => apiClient.put(`/grid/${id}`, data), - deleteGrid: (id: number) => apiClient.delete(`/grid/${id}`), -}; - -// stores/grid.ts -export const useGridStore = defineStore('grid', { - state: () => ({ - grids: [] as GridListItem[], - loading: false, - }), - actions: { - async fetchGrids() { - this.loading = true; - try { - const { data } = await gridApi.getGrids(); - this.grids = data; - } finally { - this.loading = false; - } - }, - // ... create, update, delete actions - } -}); -``` - -## 8. 样式设计 - -```scss -.grid-management-page { - padding: 24px; - - .page-container { - margin-top: 24px; - } - - .table-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - } - - .map-container { - height: 600px; - } -} -``` - -## 9. 关联组件 - -### `GridFormDialog.vue` - -这是本页面的核心复杂组件,用于创建和编辑网格。 -- **Props**: `modelValue`, `gridId` -- **内部组件**: - - 一个表单,包含网格名称、负责人(下拉选择)等字段。 - - 一个内嵌的 Leaflet 地图,用于绘制和编辑网格边界。 -- **功能**: - - **地图绘制**: 提供多边形(Polygon)绘制工具。用户可以在地图上绘制闭合区域来定义网格边界。 - - **数据加载**: 在编辑模式下,根据 `gridId` 加载网格数据,并在表单和内嵌地图上显示。 - - **数据保存**: 用户完成绘制和表单填写后,点击保存。组件将绘制的多边形转换为 GeoJSON 字符串,连同表单数据一起,调用 store 的 action 进行保存。 - -## 10. 测试用例 - -- **集成测试**: - - 测试能否成功创建一个新网格,包括在地图上绘制边界和填写信息。 - - 测试点击列表中的网格,地图上是否正确显示其边界。 - - 测试编辑功能,包括修改网格信息和在地图上重新编辑边界。 - - 测试删除网格功能。 + +# 网格管理页面设计文档 + +## 1. 页面概述 + +网格管理页面允许系统管理员创建、查看、编辑和删除地理网格。每个网格代表一个责任区域,可以关联到特定的主管或网格员。此功能是任务分配和地理数据可视化的基础。 + +## 2. 页面布局 + +![网格管理页面布局示意图](https://placeholder-for-grid-page-mockup.png) + +### 2.1 布局结构 + +页面由两部分组成: +- **左侧**: 网格列表,以表格形式展示所有已定义的网格及其基本信息(名称、负责人等)。 +- **右侧**: 交互式地图,用于可视化展示所选网格的地理边界。当创建或编辑网格时,地图将进入绘图模式。 + +## 3. 组件结构 + +```vue + +``` + +## 4. 数据结构 + +```typescript +// 网格列表项 +interface GridListItem { + id: number; + name: string; + manager?: string; + // GeoJSON 字符串 + geometry: string; +} + +// 网格表单数据 (用于对话框) +interface GridFormData { + id?: number; + name: string; + managerId?: number; + // GeoJSON 字符串 + geometry: string; +} +``` + +## 5. 状态管理 + +```typescript +// 地图实例 +let mapInstance = null; +let currentGridLayer = null; + +// 组件内状态 +const mapContainer = ref(); +const dialogVisible = ref(false); +const selectedGridId = ref(null); + +// 全局状态 (Pinia Store) +const gridStore = useGridStore(); +const { grids, loading } = storeToRefs(gridStore); +``` + +## 6. 交互逻辑 + +### 6.1 地图与列表交互 + +```typescript +onMounted(() => { + gridStore.fetchGrids(); + initializeMap(); +}); + +const initializeMap = () => { + // ... 地图初始化逻辑 +}; + +// 点击表格行,在地图上高亮显示对应网格 +const handleRowClick = (row: GridListItem) => { + if (currentGridLayer) { + mapInstance.removeLayer(currentGridLayer); + } + if (row.geometry) { + const geoJson = JSON.parse(row.geometry); + currentGridLayer = L.geoJSON(geoJson).addTo(mapInstance); + mapInstance.fitBounds(currentGridLayer.getBounds()); + } +}; +``` + +### 6.2 CRUD 操作 + +```typescript +const handleCreate = () => { + selectedGridId.value = null; + dialogVisible.value = true; +}; + +const handleEdit = (grid: GridListItem) => { + selectedGridId.value = grid.id; + dialogVisible.value = true; +}; + +const handleDelete = async (grid: GridListItem) => { + await gridStore.deleteGrid(grid.id); + ElMessage.success('网格删除成功'); + gridStore.fetchGrids(); +}; + +const onFormSuccess = () => { + dialogVisible.value = false; + gridStore.fetchGrids(); +}; +``` + +## 7. API 调用 + +```typescript +// api/grid.ts +export const gridApi = { + getGrids: () => apiClient.get('/grid'), + getGridById: (id: number) => apiClient.get(`/grid/${id}`), + createGrid: (data: GridFormData) => apiClient.post('/grid', data), + updateGrid: (id: number, data: GridFormData) => apiClient.put(`/grid/${id}`, data), + deleteGrid: (id: number) => apiClient.delete(`/grid/${id}`), +}; + +// stores/grid.ts +export const useGridStore = defineStore('grid', { + state: () => ({ + grids: [] as GridListItem[], + loading: false, + }), + actions: { + async fetchGrids() { + this.loading = true; + try { + const { data } = await gridApi.getGrids(); + this.grids = data; + } finally { + this.loading = false; + } + }, + // ... create, update, delete actions + } +}); +``` + +## 8. 样式设计 + +```scss +.grid-management-page { + padding: 24px; + + .page-container { + margin-top: 24px; + } + + .table-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + } + + .map-container { + height: 600px; + } +} +``` + +## 9. 关联组件 + +### `GridFormDialog.vue` + +这是本页面的核心复杂组件,用于创建和编辑网格。 +- **Props**: `modelValue`, `gridId` +- **内部组件**: + - 一个表单,包含网格名称、负责人(下拉选择)等字段。 + - 一个内嵌的 Leaflet 地图,用于绘制和编辑网格边界。 +- **功能**: + - **地图绘制**: 提供多边形(Polygon)绘制工具。用户可以在地图上绘制闭合区域来定义网格边界。 + - **数据加载**: 在编辑模式下,根据 `gridId` 加载网格数据,并在表单和内嵌地图上显示。 + - **数据保存**: 用户完成绘制和表单填写后,点击保存。组件将绘制的多边形转换为 GeoJSON 字符串,连同表单数据一起,调用 store 的 action 进行保存。 + +## 10. 测试用例 + +- **集成测试**: + - 测试能否成功创建一个新网格,包括在地图上绘制边界和填写信息。 + - 测试点击列表中的网格,地图上是否正确显示其边界。 + - 测试编辑功能,包括修改网格信息和在地图上重新编辑边界。 + - 测试删除网格功能。 --- - + ### Design/frontend_Design/LoginPage_Design.md - -# 登录页面设计文档 - -## 1. 页面概述 - -登录页面是用户进入系统的主要入口,提供账号密码登录功能,同时支持记住登录状态和找回密码功能。 - -## 2. 页面布局 - -![登录页面布局示意图](https://placeholder-for-login-page-mockup.png) - -### 2.1 布局结构 - -登录页面采用居中卡片式布局,包含以下主要区域: -- 顶部 Logo 和系统名称区域 -- 中部登录表单区域 -- 底部版权信息和帮助链接区域 - -### 2.2 响应式设计 - -- **桌面端**:居中卡片,宽度 400px,两侧留白 -- **平板端**:居中卡片,宽度 80%,两侧留白 -- **移动端**:全宽卡片,上下留白,左右边距 16px - -## 3. 组件结构 - -```vue - -``` - -## 4. 数据结构 - -```typescript -interface LoginForm { - username: string; - password: string; - remember: boolean; -} - -interface LoginResponse { - token: string; - user: { - id: number; - username: string; - role: string; - permissions: string[]; - }; -} -``` - -## 5. 状态管理 - -```typescript -// 组件内状态 -const loginForm = ref({ - username: '', - password: '', - remember: false -}); - -const loading = ref(false); -const loginFormRef = ref(); -const enableOtherLoginMethods = ref(true); - -// 全局状态 (Pinia Store) -const authStore = useAuthStore(); -``` - -## 6. 交互逻辑 - -### 6.1 表单验证规则 - -```typescript -const loginRules = { - username: [ - { required: true, message: '请输入用户名', trigger: 'blur' }, - { min: 3, max: 20, message: '用户名长度应为 3-20 个字符', trigger: 'blur' } - ], - password: [ - { required: true, message: '请输入密码', trigger: 'blur' }, - { min: 6, max: 20, message: '密码长度应为 6-20 个字符', trigger: 'blur' } - ] -}; -``` - -### 6.2 登录流程 - -```typescript -const handleLogin = async () => { - if (!loginFormRef.value) return; - - await loginFormRef.value.validate(async (valid) => { - if (!valid) return; - - try { - loading.value = true; - - // 调用登录 API - await authStore.login({ - username: loginForm.value.username, - password: loginForm.value.password - }); - - // 如果选择记住我,设置本地存储 - if (loginForm.value.remember) { - localStorage.setItem('remember_username', loginForm.value.username); - } else { - localStorage.removeItem('remember_username'); - } - - // 登录成功提示 - ElMessage.success('登录成功'); - - // 获取重定向地址或跳转到首页 - const redirect = route.query.redirect as string || '/'; - router.push(redirect); - } catch (error) { - // 错误处理 - ElMessage.error(error.response?.data?.message || '登录失败,请检查用户名和密码'); - } finally { - loading.value = false; - } - }); -}; -``` - -### 6.3 其他交互功能 - -```typescript -// 跳转到注册页面 -const goToRegister = () => { - router.push('/auth/register'); -}; - -// 跳转到忘记密码页面 -const forgotPassword = () => { - router.push('/auth/forgot-password'); -}; - -// 生命周期钩子,检查是否有记住的用户名 -onMounted(() => { - const rememberedUsername = localStorage.getItem('remember_username'); - if (rememberedUsername) { - loginForm.value.username = rememberedUsername; - loginForm.value.remember = true; - } -}); -``` - -## 7. API 调用 - -### 7.1 登录 API - -```typescript -// api/auth.ts -export const authApi = { - login: (credentials: { username: string; password: string }) => - apiClient.post('/auth/login', credentials) -}; - -// stores/auth.ts -export const useAuthStore = defineStore('auth', { - // ... 其他状态和 getters - - actions: { - async login(credentials) { - try { - const { data } = await authApi.login(credentials); - this.token = data.token; - this.user = data.user; - - // 存储 token 到本地存储 - localStorage.setItem('token', data.token); - - return data; - } catch (error) { - this.error = error.response?.data?.message || '登录失败'; - throw error; - } - } - } -}); -``` - -## 8. 样式设计 - -```scss -.login-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - min-height: 100vh; - background-color: #f5f7fa; - padding: 20px; - - .login-header { - text-align: center; - margin-bottom: 30px; - - .login-logo { - width: 80px; - height: 80px; - } - - .login-title { - font-size: 24px; - color: #303133; - margin-top: 16px; - } - } - - .login-card { - width: 400px; - max-width: 100%; - - .login-card-title { - font-size: 20px; - text-align: center; - margin-bottom: 30px; - font-weight: 500; - } - - .login-form { - margin-bottom: 20px; - - .login-options { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; - } - - .login-button { - width: 100%; - height: 40px; - font-size: 16px; - } - } - - .other-login-methods { - margin-top: 24px; - - .divider { - display: flex; - align-items: center; - margin: 16px 0; - - &::before, - &::after { - content: ''; - flex: 1; - height: 1px; - background-color: #dcdfe6; - } - - span { - padding: 0 16px; - color: #909399; - font-size: 14px; - } - } - - .login-icons { - display: flex; - justify-content: center; - gap: 20px; - margin-top: 16px; - - .icon-button { - font-size: 20px; - - &.wechat { - color: #07c160; - background-color: #f0f9eb; - border-color: #e1f3d8; - - &:hover { - background-color: #e1f3d8; - } - } - } - } - } - - .register-link { - text-align: center; - margin-top: 24px; - font-size: 14px; - } - } - - .login-footer { - margin-top: 40px; - text-align: center; - color: #909399; - font-size: 12px; - - p { - margin: 8px 0; - } - } -} - -// 响应式样式 -@media screen and (max-width: 768px) { - .login-container { - padding: 16px; - - .login-card { - width: 100%; - } - } -} -``` - -## 9. 安全考虑 - -1. **密码安全** - - 密码输入框使用 `type="password"` 和 `show-password` 属性 - - 密码不在前端存储,只在登录时传输 - - 密码传输使用 HTTPS 加密 - -2. **Token 安全** - - Token 存储在 localStorage 中,有 XSS 风险,可考虑使用 HttpOnly Cookie - - Token 过期处理在响应拦截器中统一处理 - -3. **表单安全** - - 实施前端输入验证,防止基本的注入攻击 - - 防止表单重复提交(登录按钮 loading 状态) - -## 10. 测试用例 - -1. **单元测试** - - 测试表单验证逻辑 - - 测试记住用户名功能 - -2. **集成测试** - - 测试登录成功流程 - - 测试登录失败处理 - - 测试重定向功能 - -3. **端到端测试** - - 测试完整登录流程 - - 测试页面响应式布局 - -## 11. 性能优化 - -1. **延迟加载** - - 第三方图标库按需引入 - - 忘记密码页面使用动态导入 - -2. **缓存策略** - - 记住用户名使用 localStorage - - 静态资源(logo 等)设置适当的缓存策略 + +# 登录页面设计文档 + +## 1. 页面概述 + +登录页面是用户进入系统的主要入口,提供账号密码登录功能,同时支持记住登录状态和找回密码功能。 + +## 2. 页面布局 + +![登录页面布局示意图](https://placeholder-for-login-page-mockup.png) + +### 2.1 布局结构 + +登录页面采用居中卡片式布局,包含以下主要区域: +- 顶部 Logo 和系统名称区域 +- 中部登录表单区域 +- 底部版权信息和帮助链接区域 + +### 2.2 响应式设计 + +- **桌面端**:居中卡片,宽度 400px,两侧留白 +- **平板端**:居中卡片,宽度 80%,两侧留白 +- **移动端**:全宽卡片,上下留白,左右边距 16px + +## 3. 组件结构 + +```vue + +``` + +## 4. 数据结构 + +```typescript +interface LoginForm { + username: string; + password: string; + remember: boolean; +} + +interface LoginResponse { + token: string; + user: { + id: number; + username: string; + role: string; + permissions: string[]; + }; +} +``` + +## 5. 状态管理 + +```typescript +// 组件内状态 +const loginForm = ref({ + username: '', + password: '', + remember: false +}); + +const loading = ref(false); +const loginFormRef = ref(); +const enableOtherLoginMethods = ref(true); + +// 全局状态 (Pinia Store) +const authStore = useAuthStore(); +``` + +## 6. 交互逻辑 + +### 6.1 表单验证规则 + +```typescript +const loginRules = { + username: [ + { required: true, message: '请输入用户名', trigger: 'blur' }, + { min: 3, max: 20, message: '用户名长度应为 3-20 个字符', trigger: 'blur' } + ], + password: [ + { required: true, message: '请输入密码', trigger: 'blur' }, + { min: 6, max: 20, message: '密码长度应为 6-20 个字符', trigger: 'blur' } + ] +}; +``` + +### 6.2 登录流程 + +```typescript +const handleLogin = async () => { + if (!loginFormRef.value) return; + + await loginFormRef.value.validate(async (valid) => { + if (!valid) return; + + try { + loading.value = true; + + // 调用登录 API + await authStore.login({ + username: loginForm.value.username, + password: loginForm.value.password + }); + + // 如果选择记住我,设置本地存储 + if (loginForm.value.remember) { + localStorage.setItem('remember_username', loginForm.value.username); + } else { + localStorage.removeItem('remember_username'); + } + + // 登录成功提示 + ElMessage.success('登录成功'); + + // 获取重定向地址或跳转到首页 + const redirect = route.query.redirect as string || '/'; + router.push(redirect); + } catch (error) { + // 错误处理 + ElMessage.error(error.response?.data?.message || '登录失败,请检查用户名和密码'); + } finally { + loading.value = false; + } + }); +}; +``` + +### 6.3 其他交互功能 + +```typescript +// 跳转到注册页面 +const goToRegister = () => { + router.push('/auth/register'); +}; + +// 跳转到忘记密码页面 +const forgotPassword = () => { + router.push('/auth/forgot-password'); +}; + +// 生命周期钩子,检查是否有记住的用户名 +onMounted(() => { + const rememberedUsername = localStorage.getItem('remember_username'); + if (rememberedUsername) { + loginForm.value.username = rememberedUsername; + loginForm.value.remember = true; + } +}); +``` + +## 7. API 调用 + +### 7.1 登录 API + +```typescript +// api/auth.ts +export const authApi = { + login: (credentials: { username: string; password: string }) => + apiClient.post('/auth/login', credentials) +}; + +// stores/auth.ts +export const useAuthStore = defineStore('auth', { + // ... 其他状态和 getters + + actions: { + async login(credentials) { + try { + const { data } = await authApi.login(credentials); + this.token = data.token; + this.user = data.user; + + // 存储 token 到本地存储 + localStorage.setItem('token', data.token); + + return data; + } catch (error) { + this.error = error.response?.data?.message || '登录失败'; + throw error; + } + } + } +}); +``` + +## 8. 样式设计 + +```scss +.login-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: #f5f7fa; + padding: 20px; + + .login-header { + text-align: center; + margin-bottom: 30px; + + .login-logo { + width: 80px; + height: 80px; + } + + .login-title { + font-size: 24px; + color: #303133; + margin-top: 16px; + } + } + + .login-card { + width: 400px; + max-width: 100%; + + .login-card-title { + font-size: 20px; + text-align: center; + margin-bottom: 30px; + font-weight: 500; + } + + .login-form { + margin-bottom: 20px; + + .login-options { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + } + + .login-button { + width: 100%; + height: 40px; + font-size: 16px; + } + } + + .other-login-methods { + margin-top: 24px; + + .divider { + display: flex; + align-items: center; + margin: 16px 0; + + &::before, + &::after { + content: ''; + flex: 1; + height: 1px; + background-color: #dcdfe6; + } + + span { + padding: 0 16px; + color: #909399; + font-size: 14px; + } + } + + .login-icons { + display: flex; + justify-content: center; + gap: 20px; + margin-top: 16px; + + .icon-button { + font-size: 20px; + + &.wechat { + color: #07c160; + background-color: #f0f9eb; + border-color: #e1f3d8; + + &:hover { + background-color: #e1f3d8; + } + } + } + } + } + + .register-link { + text-align: center; + margin-top: 24px; + font-size: 14px; + } + } + + .login-footer { + margin-top: 40px; + text-align: center; + color: #909399; + font-size: 12px; + + p { + margin: 8px 0; + } + } +} + +// 响应式样式 +@media screen and (max-width: 768px) { + .login-container { + padding: 16px; + + .login-card { + width: 100%; + } + } +} +``` + +## 9. 安全考虑 + +1. **密码安全** + - 密码输入框使用 `type="password"` 和 `show-password` 属性 + - 密码不在前端存储,只在登录时传输 + - 密码传输使用 HTTPS 加密 + +2. **Token 安全** + - Token 存储在 localStorage 中,有 XSS 风险,可考虑使用 HttpOnly Cookie + - Token 过期处理在响应拦截器中统一处理 + +3. **表单安全** + - 实施前端输入验证,防止基本的注入攻击 + - 防止表单重复提交(登录按钮 loading 状态) + +## 10. 测试用例 + +1. **单元测试** + - 测试表单验证逻辑 + - 测试记住用户名功能 + +2. **集成测试** + - 测试登录成功流程 + - 测试登录失败处理 + - 测试重定向功能 + +3. **端到端测试** + - 测试完整登录流程 + - 测试页面响应式布局 + +## 11. 性能优化 + +1. **延迟加载** + - 第三方图标库按需引入 + - 忘记密码页面使用动态导入 + +2. **缓存策略** + - 记住用户名使用 localStorage + - 静态资源(logo 等)设置适当的缓存策略 --- - + ### Design/frontend_Design/MapPage_Design.md - -# 地图页面设计文档 - -## 1. 页面概述 - -地图页面是 EMS 系统的核心可视化界面,提供基于地理位置的直观信息展示。它主要用于显示城市的环境网格划分、实时污染数据(如热力图)、网格员位置以及特定任务的地理标记。此页面帮助主管和决策者监控区域状态,并为网格员提供清晰的工作区域指引。 - -## 2. 页面布局 - -![地图页面布局示意图](https://placeholder-for-map-page-mockup.png) - -### 2.1 布局结构 - -页面采用全屏地图为主体的布局,辅以侧边栏和浮动控件。 -- **主区域**: 全屏交互式地图,作为所有地理信息的载体。 -- **左侧边栏**: 用于控制地图上显示的图层,如网格边界、热力图、网格员位置等。同时,也可能包含一个可搜索的兴趣点列表。 -- **地图控件**: 地图右上角或左上角放置缩放、定位、全屏等标准地图控件。 -- **信息弹窗 (Popup)**: 点击地图上的特定元素(如网格、标记点)时,会弹出信息窗口,显示该元素的详细信息。 - -### 2.2 响应式设计 - -- **桌面端**: 显示完整的左侧边栏和地图。 -- **平板端/移动端**: 左侧边栏默认收起,可通过按钮展开为抽屉或浮动面板,以最大化地图可视区域。 - -## 3. 组件结构 - -```vue - -``` - -## 4. 数据结构 - -```typescript -// 网格数据结构 -interface GridData { - id: number; - name: string; - // GeoJSON 格式的多边形坐标 - geometry: { - type: 'Polygon'; - coordinates: number[][][]; - }; - manager: string; // 网格负责人 -} - -// 热力图数据点 -interface HeatmapPoint { - lat: number; - lng: number; - value: number; // 污染指数 -} - -// 网格员位置 -interface WorkerPosition { - id: number; - name: string; - lat: number; - lng: number; - lastUpdated: string; -} - -// 任务标记 -interface TaskMarker { - id: number; - title: string; - lat: number; - lng: number; - status: string; -} -``` - -## 5. 状态管理 - -```typescript -// 地图服务实例 -let mapInstance = null; -let gridLayerGroup = null; -let heatmapLayer = null; -let workerMarkersGroup = null; - -// 组件内状态 -const mapContainer = ref(null); -const mapLoading = ref(true); -const isPanelVisible = ref(true); -const visibleLayers = ref(['grids']); // 默认显示的图层 -const gridSearch = ref(''); - -// 全局状态 (Pinia Store) -const mapStore = useMapStore(); -const { grids, heatmapData, workers } = storeToRefs(mapStore); - -// 监听可见图层变化,动态更新地图 -watch(visibleLayers, (newLayers, oldLayers) => { - // ... 调用地图服务方法显示/隐藏图层 -}); -``` - -## 6. 交互逻辑 - -### 6.1 地图初始化 - -```typescript -onMounted(() => { - initializeMap(); - mapStore.fetchInitialMapData(); -}); - -const initializeMap = () => { - if (mapContainer.value) { - mapInstance = L.map(mapContainer.value).setView([39.9042, 116.4074], 10); // 默认北京 - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(mapInstance); - mapLoading.value = false; - } -}; -``` - -### 6.2 图层控制 - -```typescript -const updateLayers = () => { - // 网格图层 - if (visibleLayers.value.includes('grids') && !mapInstance.hasLayer(gridLayerGroup)) { - gridLayerGroup.addTo(mapInstance); - } else if (!visibleLayers.value.includes('grids') && mapInstance.hasLayer(gridLayerGroup)) { - gridLayerGroup.removeFrom(mapInstance); - } - // 其他图层同理... -}; - -// 监听 Pinia store 中数据的变化,并更新地图 -watch(grids, (newGrids) => { - if (gridLayerGroup) { - gridLayerGroup.clearLayers(); - // 添加新的网格到图层 - newGrids.forEach(grid => { - const polygon = L.geoJSON(grid.geometry); - polygon.bindPopup(`${grid.name}
负责人: ${grid.manager}`); - gridLayerGroup.addLayer(polygon); - }); - } -}); -``` - -### 6.3 面板交互 - -```typescript -const togglePanel = () => { - isPanelVisible.value = !isPanelVisible.value; -}; - -const searchGrid = () => { - const targetGrid = grids.value.find(g => g.name === gridSearch.value); - if (targetGrid) { - // 飞到目标网格位置 - const bounds = L.geoJSON(targetGrid.geometry).getBounds(); - mapInstance.flyToBounds(bounds); - } else { - ElMessage.info('未找到该网格'); - } -}; -``` - -## 7. API 调用 - -```typescript -// api/map.ts -export const mapApi = { - getGrids: () => apiClient.get('/map/grids'), - getHeatmapData: () => apiClient.get('/map/heatmap'), - getWorkerPositions: () => apiClient.get('/map/workers'), -}; - -// stores/map.ts -export const useMapStore = defineStore('map', { - state: () => ({ - grids: [] as GridData[], - heatmapData: [] as HeatmapPoint[], - workers: [] as WorkerPosition[], - }), - actions: { - async fetchInitialMapData() { - // 并发请求所有地图数据 - const [gridsRes, heatmapRes, workersRes] = await Promise.all([ - mapApi.getGrids(), - mapApi.getHeatmapData(), - mapApi.getWorkerPositions(), - ]); - this.grids = gridsRes.data; - this.heatmapData = heatmapRes.data; - this.workers = workersRes.data; - } - } -}); -``` - -## 8. 样式设计 - -```scss -.map-page { - position: relative; - width: 100%; - height: calc(100vh - 50px); /* 减去顶部导航栏高度 */ - - .map-container { - width: 100%; - height: 100%; - z-index: 1; - } - - .layer-control-panel { - position: absolute; - top: 20px; - left: 20px; - z-index: 2; - width: 250px; - background: white; - transition: transform 0.3s ease; - - .panel-header { - display: flex; - justify-content: space-between; - align-items: center; - } - } - - .map-loading-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(255, 255, 255, 0.7); - display: flex; - justify-content: center; - align-items: center; - z-index: 3; - flex-direction: column; - gap: 10px; - } -} -``` - -## 9. 测试用例 - -1. **单元测试**: - - 测试 Pinia store 的 action 是否能正确获取和存储地图数据。 - - 测试地图交互函数,如 `searchGrid` 的逻辑。 - -2. **集成测试**: - - 验证地图是否能成功初始化并显示底图。 - - 验证图层控制复选框是否能正确显示/隐藏对应的地理要素。 - - 验证点击网格是否能弹出正确的信息窗口。 - - 验证搜索功能是否能正确地将地图视图定位到指定网格。 - -## 10. 性能优化 - -1. **大数据渲染**: - - 对于大量的标记点(如网格员、任务),使用 `L.markerClusterGroup` 插件进行聚合,提高性能。 - - 对于复杂的网格边界(Polygon),在低缩放级别下使用简化的几何图形(可通过后端服务实现)。 -2. **懒加载**: 地图库(Leaflet)和相关插件(热力图、聚合)应按需异步加载。 -3. **数据更新**: 网格员位置等实时数据应使用 WebSocket 或定时轮询(配合 `requestAnimationFrame`)进行高效更新,而不是频繁重绘整个图层。 + +# 地图页面设计文档 + +## 1. 页面概述 + +地图页面是 EMS 系统的核心可视化界面,提供基于地理位置的直观信息展示。它主要用于显示城市的环境网格划分、实时污染数据(如热力图)、网格员位置以及特定任务的地理标记。此页面帮助主管和决策者监控区域状态,并为网格员提供清晰的工作区域指引。 + +## 2. 页面布局 + +![地图页面布局示意图](https://placeholder-for-map-page-mockup.png) + +### 2.1 布局结构 + +页面采用全屏地图为主体的布局,辅以侧边栏和浮动控件。 +- **主区域**: 全屏交互式地图,作为所有地理信息的载体。 +- **左侧边栏**: 用于控制地图上显示的图层,如网格边界、热力图、网格员位置等。同时,也可能包含一个可搜索的兴趣点列表。 +- **地图控件**: 地图右上角或左上角放置缩放、定位、全屏等标准地图控件。 +- **信息弹窗 (Popup)**: 点击地图上的特定元素(如网格、标记点)时,会弹出信息窗口,显示该元素的详细信息。 + +### 2.2 响应式设计 + +- **桌面端**: 显示完整的左侧边栏和地图。 +- **平板端/移动端**: 左侧边栏默认收起,可通过按钮展开为抽屉或浮动面板,以最大化地图可视区域。 + +## 3. 组件结构 + +```vue + +``` + +## 4. 数据结构 + +```typescript +// 网格数据结构 +interface GridData { + id: number; + name: string; + // GeoJSON 格式的多边形坐标 + geometry: { + type: 'Polygon'; + coordinates: number[][][]; + }; + manager: string; // 网格负责人 +} + +// 热力图数据点 +interface HeatmapPoint { + lat: number; + lng: number; + value: number; // 污染指数 +} + +// 网格员位置 +interface WorkerPosition { + id: number; + name: string; + lat: number; + lng: number; + lastUpdated: string; +} + +// 任务标记 +interface TaskMarker { + id: number; + title: string; + lat: number; + lng: number; + status: string; +} +``` + +## 5. 状态管理 + +```typescript +// 地图服务实例 +let mapInstance = null; +let gridLayerGroup = null; +let heatmapLayer = null; +let workerMarkersGroup = null; + +// 组件内状态 +const mapContainer = ref(null); +const mapLoading = ref(true); +const isPanelVisible = ref(true); +const visibleLayers = ref(['grids']); // 默认显示的图层 +const gridSearch = ref(''); + +// 全局状态 (Pinia Store) +const mapStore = useMapStore(); +const { grids, heatmapData, workers } = storeToRefs(mapStore); + +// 监听可见图层变化,动态更新地图 +watch(visibleLayers, (newLayers, oldLayers) => { + // ... 调用地图服务方法显示/隐藏图层 +}); +``` + +## 6. 交互逻辑 + +### 6.1 地图初始化 + +```typescript +onMounted(() => { + initializeMap(); + mapStore.fetchInitialMapData(); +}); + +const initializeMap = () => { + if (mapContainer.value) { + mapInstance = L.map(mapContainer.value).setView([39.9042, 116.4074], 10); // 默认北京 + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(mapInstance); + mapLoading.value = false; + } +}; +``` + +### 6.2 图层控制 + +```typescript +const updateLayers = () => { + // 网格图层 + if (visibleLayers.value.includes('grids') && !mapInstance.hasLayer(gridLayerGroup)) { + gridLayerGroup.addTo(mapInstance); + } else if (!visibleLayers.value.includes('grids') && mapInstance.hasLayer(gridLayerGroup)) { + gridLayerGroup.removeFrom(mapInstance); + } + // 其他图层同理... +}; + +// 监听 Pinia store 中数据的变化,并更新地图 +watch(grids, (newGrids) => { + if (gridLayerGroup) { + gridLayerGroup.clearLayers(); + // 添加新的网格到图层 + newGrids.forEach(grid => { + const polygon = L.geoJSON(grid.geometry); + polygon.bindPopup(`${grid.name}
负责人: ${grid.manager}`); + gridLayerGroup.addLayer(polygon); + }); + } +}); +``` + +### 6.3 面板交互 + +```typescript +const togglePanel = () => { + isPanelVisible.value = !isPanelVisible.value; +}; + +const searchGrid = () => { + const targetGrid = grids.value.find(g => g.name === gridSearch.value); + if (targetGrid) { + // 飞到目标网格位置 + const bounds = L.geoJSON(targetGrid.geometry).getBounds(); + mapInstance.flyToBounds(bounds); + } else { + ElMessage.info('未找到该网格'); + } +}; +``` + +## 7. API 调用 + +```typescript +// api/map.ts +export const mapApi = { + getGrids: () => apiClient.get('/map/grids'), + getHeatmapData: () => apiClient.get('/map/heatmap'), + getWorkerPositions: () => apiClient.get('/map/workers'), +}; + +// stores/map.ts +export const useMapStore = defineStore('map', { + state: () => ({ + grids: [] as GridData[], + heatmapData: [] as HeatmapPoint[], + workers: [] as WorkerPosition[], + }), + actions: { + async fetchInitialMapData() { + // 并发请求所有地图数据 + const [gridsRes, heatmapRes, workersRes] = await Promise.all([ + mapApi.getGrids(), + mapApi.getHeatmapData(), + mapApi.getWorkerPositions(), + ]); + this.grids = gridsRes.data; + this.heatmapData = heatmapRes.data; + this.workers = workersRes.data; + } + } +}); +``` + +## 8. 样式设计 + +```scss +.map-page { + position: relative; + width: 100%; + height: calc(100vh - 50px); /* 减去顶部导航栏高度 */ + + .map-container { + width: 100%; + height: 100%; + z-index: 1; + } + + .layer-control-panel { + position: absolute; + top: 20px; + left: 20px; + z-index: 2; + width: 250px; + background: white; + transition: transform 0.3s ease; + + .panel-header { + display: flex; + justify-content: space-between; + align-items: center; + } + } + + .map-loading-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 3; + flex-direction: column; + gap: 10px; + } +} +``` + +## 9. 测试用例 + +1. **单元测试**: + - 测试 Pinia store 的 action 是否能正确获取和存储地图数据。 + - 测试地图交互函数,如 `searchGrid` 的逻辑。 + +2. **集成测试**: + - 验证地图是否能成功初始化并显示底图。 + - 验证图层控制复选框是否能正确显示/隐藏对应的地理要素。 + - 验证点击网格是否能弹出正确的信息窗口。 + - 验证搜索功能是否能正确地将地图视图定位到指定网格。 + +## 10. 性能优化 + +1. **大数据渲染**: + - 对于大量的标记点(如网格员、任务),使用 `L.markerClusterGroup` 插件进行聚合,提高性能。 + - 对于复杂的网格边界(Polygon),在低缩放级别下使用简化的几何图形(可通过后端服务实现)。 +2. **懒加载**: 地图库(Leaflet)和相关插件(热力图、聚合)应按需异步加载。 +3. **数据更新**: 网格员位置等实时数据应使用 WebSocket 或定时轮询(配合 `requestAnimationFrame`)进行高效更新,而不是频繁重绘整个图层。 --- - + ### Design/frontend_Design/PersonnelManagementPage_Design.md - -# 人员管理页面设计文档 - -## 1. 页面概述 - -人员管理页面是提供给系统管理员(ADMIN)用于管理所有用户账户的界面。其核心功能包括展示用户列表、添加新用户、编辑现有用户信息、更改用户角色与状态,以及搜索和筛选用户。 - -## 2. 页面布局 - -![人员管理页面布局示意图](https://placeholder-for-personnel-page-mockup.png) - -### 2.1 布局结构 - -页面采用经典的后台管理布局,与任务管理、反馈管理页面保持一致: -- **顶部**: 操作区域,包含"添加用户"按钮。 -- **中部**: 筛选和搜索区域,支持按用户名、角色或状态进行搜索。 -- **下部**: 用户数据表格,展示所有用户及其关键信息,并提供行内操作。 -- **底部**: 分页组件。 - -## 3. 组件结构 - -```vue - -``` - -## 4. 数据结构 - -```typescript -// 用户筛选条件 -interface UserFilters { - name?: string; - role?: 'ADMIN' | 'SUPERVISOR' | 'GRID_WORKER' | 'DECISION_MAKER'; -} - -// 用户列表项 -interface UserListItem { - id: number; - username: string; - name: string; - email: string; - role: string; - isActive: boolean; - statusChanging?: boolean; // 用于控制 Switch 的 loading 状态 -} - -// 用户表单数据 (用于对话框) -interface UserFormData { - id?: number; - username: string; - name: string; - email: string; - role: string; - password?: string; // 创建时需要,编辑时可选 -} -``` - -## 5. 状态管理 - -```typescript -// 组件内状态 -const filters = ref({}); -const pagination = ref({ page: 1, pageSize: 10 }); -const dialogVisible = ref(false); -const selectedUserId = ref(null); - -// 全局状态 (Pinia Store) -const personnelStore = usePersonnelStore(); -const { users, total, loading } = storeToRefs(personnelStore); -``` - -## 6. 交互逻辑 - -### 6.1 CRUD 操作 - -```typescript -const fetchUsers = () => { - personnelStore.fetchUsers({ ...filters.value, ...pagination.value }); -}; - -onMounted(fetchUsers); - -const handleSearch = () => { - pagination.value.page = 1; - fetchUsers(); -}; - -const handleCreateUser = () => { - selectedUserId.value = null; - dialogVisible.value = true; -}; - -const handleEdit = (user: UserListItem) => { - selectedUserId.value = user.id; - dialogVisible.value = true; -}; - -const handleDelete = async (user: UserListItem) => { - await personnelStore.deleteUser(user.id); - ElMessage.success('用户删除成功'); - fetchUsers(); // 刷新列表 -}; - -const handleStatusChange = async (user: UserListItem) => { - user.statusChanging = true; - try { - await personnelStore.updateUserStatus(user.id, user.isActive); - ElMessage.success('状态更新成功'); - } catch { - // 失败时将开关拨回原位 - user.isActive = !user.isActive; - } finally { - user.statusChanging = false; - } -}; - -const onFormSuccess = () => { - dialogVisible.value = false; - fetchUsers(); -}; -``` - -### 6.2 辅助函数 - -```typescript -const formatRole = (role: string) => { - const roleMap = { - ADMIN: '管理员', - SUPERVISOR: '主管', - GRID_WORKER: '网格员', - DECISION_MAKER: '决策者' - }; - return roleMap[role] || '未知角色'; -}; -``` - -## 7. API 调用 - -```typescript -// api/personnel.ts -export const personnelApi = { - getUsers: (params) => apiClient.get('/personnel', { params }), - getUserById: (id: number) => apiClient.get(`/personnel/${id}`), - createUser: (data: UserFormData) => apiClient.post('/personnel', data), - updateUser: (id: number, data: UserFormData) => apiClient.put(`/personnel/${id}`, data), - deleteUser: (id: number) => apiClient.delete(`/personnel/${id}`), -}; - -// stores/personnel.ts -export const usePersonnelStore = defineStore('personnel', { - state: () => ({ - users: [] as UserListItem[], - total: 0, - loading: false, - }), - actions: { - async fetchUsers(params) { - this.loading = true; - try { - const { data } = await personnelApi.getUsers(params); - this.users = data.items.map(u => ({ ...u, statusChanging: false })); - this.total = data.total; - } finally { - this.loading = false; - } - }, - // ...其他 createUser, updateUser, deleteUser 等 actions - } -}); -``` - -## 8. 样式设计 - -```scss -.personnel-management-page { - padding: 24px; - - .page-container { - margin-top: 24px; - } - - .table-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - } - - .pagination-container { - margin-top: 24px; - display: flex; - justify-content: flex-end; - } -} -``` - -## 9. 关联组件 - -### `UserFormDialog.vue` - -用于创建和编辑用户的对话框组件。 -- **Props**: `modelValue`, `userId` -- **功能**: - - 如果 `userId` 存在,则为编辑模式,对话框打开时会根据 ID 加载用户信息。 - - 如果 `userId` 为空,则为创建模式。 - - 包含用户名、姓名、邮箱、角色和密码的表单字段。 - - 密码字段在编辑模式下为可选,并提示"留空则不修改密码"。 - - 表单提交时进行验证,并调用 store 中对应的 `createUser` 或 `updateUser` action。 - - 操作成功后,发出 `success` 事件。 - -## 10. 测试用例 - -- **集成测试**: - - 测试能否成功添加一个新用户。 - - 测试能否成功编辑一个现有用户的信息(包括修改密码和不修改密码两种情况)。 - - 测试能否成功删除一个用户。 - - 测试启用/禁用开关是否能正确更新用户状态。 - - 测试搜索和筛选功能是否能正确过滤用户列表。 + +# 人员管理页面设计文档 + +## 1. 页面概述 + +人员管理页面是提供给系统管理员(ADMIN)用于管理所有用户账户的界面。其核心功能包括展示用户列表、添加新用户、编辑现有用户信息、更改用户角色与状态,以及搜索和筛选用户。 + +## 2. 页面布局 + +![人员管理页面布局示意图](https://placeholder-for-personnel-page-mockup.png) + +### 2.1 布局结构 + +页面采用经典的后台管理布局,与任务管理、反馈管理页面保持一致: +- **顶部**: 操作区域,包含"添加用户"按钮。 +- **中部**: 筛选和搜索区域,支持按用户名、角色或状态进行搜索。 +- **下部**: 用户数据表格,展示所有用户及其关键信息,并提供行内操作。 +- **底部**: 分页组件。 + +## 3. 组件结构 + +```vue + +``` + +## 4. 数据结构 + +```typescript +// 用户筛选条件 +interface UserFilters { + name?: string; + role?: 'ADMIN' | 'SUPERVISOR' | 'GRID_WORKER' | 'DECISION_MAKER'; +} + +// 用户列表项 +interface UserListItem { + id: number; + username: string; + name: string; + email: string; + role: string; + isActive: boolean; + statusChanging?: boolean; // 用于控制 Switch 的 loading 状态 +} + +// 用户表单数据 (用于对话框) +interface UserFormData { + id?: number; + username: string; + name: string; + email: string; + role: string; + password?: string; // 创建时需要,编辑时可选 +} +``` + +## 5. 状态管理 + +```typescript +// 组件内状态 +const filters = ref({}); +const pagination = ref({ page: 1, pageSize: 10 }); +const dialogVisible = ref(false); +const selectedUserId = ref(null); + +// 全局状态 (Pinia Store) +const personnelStore = usePersonnelStore(); +const { users, total, loading } = storeToRefs(personnelStore); +``` + +## 6. 交互逻辑 + +### 6.1 CRUD 操作 + +```typescript +const fetchUsers = () => { + personnelStore.fetchUsers({ ...filters.value, ...pagination.value }); +}; + +onMounted(fetchUsers); + +const handleSearch = () => { + pagination.value.page = 1; + fetchUsers(); +}; + +const handleCreateUser = () => { + selectedUserId.value = null; + dialogVisible.value = true; +}; + +const handleEdit = (user: UserListItem) => { + selectedUserId.value = user.id; + dialogVisible.value = true; +}; + +const handleDelete = async (user: UserListItem) => { + await personnelStore.deleteUser(user.id); + ElMessage.success('用户删除成功'); + fetchUsers(); // 刷新列表 +}; + +const handleStatusChange = async (user: UserListItem) => { + user.statusChanging = true; + try { + await personnelStore.updateUserStatus(user.id, user.isActive); + ElMessage.success('状态更新成功'); + } catch { + // 失败时将开关拨回原位 + user.isActive = !user.isActive; + } finally { + user.statusChanging = false; + } +}; + +const onFormSuccess = () => { + dialogVisible.value = false; + fetchUsers(); +}; +``` + +### 6.2 辅助函数 + +```typescript +const formatRole = (role: string) => { + const roleMap = { + ADMIN: '管理员', + SUPERVISOR: '主管', + GRID_WORKER: '网格员', + DECISION_MAKER: '决策者' + }; + return roleMap[role] || '未知角色'; +}; +``` + +## 7. API 调用 + +```typescript +// api/personnel.ts +export const personnelApi = { + getUsers: (params) => apiClient.get('/personnel', { params }), + getUserById: (id: number) => apiClient.get(`/personnel/${id}`), + createUser: (data: UserFormData) => apiClient.post('/personnel', data), + updateUser: (id: number, data: UserFormData) => apiClient.put(`/personnel/${id}`, data), + deleteUser: (id: number) => apiClient.delete(`/personnel/${id}`), +}; + +// stores/personnel.ts +export const usePersonnelStore = defineStore('personnel', { + state: () => ({ + users: [] as UserListItem[], + total: 0, + loading: false, + }), + actions: { + async fetchUsers(params) { + this.loading = true; + try { + const { data } = await personnelApi.getUsers(params); + this.users = data.items.map(u => ({ ...u, statusChanging: false })); + this.total = data.total; + } finally { + this.loading = false; + } + }, + // ...其他 createUser, updateUser, deleteUser 等 actions + } +}); +``` + +## 8. 样式设计 + +```scss +.personnel-management-page { + padding: 24px; + + .page-container { + margin-top: 24px; + } + + .table-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + } + + .pagination-container { + margin-top: 24px; + display: flex; + justify-content: flex-end; + } +} +``` + +## 9. 关联组件 + +### `UserFormDialog.vue` + +用于创建和编辑用户的对话框组件。 +- **Props**: `modelValue`, `userId` +- **功能**: + - 如果 `userId` 存在,则为编辑模式,对话框打开时会根据 ID 加载用户信息。 + - 如果 `userId` 为空,则为创建模式。 + - 包含用户名、姓名、邮箱、角色和密码的表单字段。 + - 密码字段在编辑模式下为可选,并提示"留空则不修改密码"。 + - 表单提交时进行验证,并调用 store 中对应的 `createUser` 或 `updateUser` action。 + - 操作成功后,发出 `success` 事件。 + +## 10. 测试用例 + +- **集成测试**: + - 测试能否成功添加一个新用户。 + - 测试能否成功编辑一个现有用户的信息(包括修改密码和不修改密码两种情况)。 + - 测试能否成功删除一个用户。 + - 测试启用/禁用开关是否能正确更新用户状态。 + - 测试搜索和筛选功能是否能正确过滤用户列表。 --- - + ### Design/frontend_Design/ProfilePage_Design.md - -# 个人中心页面设计文档 - -## 1. 页面概述 - -个人中心页面允许当前登录的用户查看和修改自己的个人信息,以及更改密码。此页面旨在提供一个简单、安全的界面来管理个人账户资料。 - -## 2. 页面布局 - -![个人中心页面布局示意图](https://placeholder-for-profile-page-mockup.png) - -### 2.1 布局结构 - -页面通常采用多标签页(Tabs)布局,将不同功能的表单分开,以保持界面整洁。 -- **左侧**: 用户头像和基本信息展示。 -- **右侧**: 包含两个标签页: - - **基本资料**: 用于修改用户名、姓名、邮箱等信息。 - - **修改密码**: 用于更改当前用户的登录密码。 - -## 3. 组件结构 - -```vue - -``` - -## 4. 数据结构 - -```typescript -// 用户信息 (来自 Store) -interface UserProfile { - id: number; - username: string; - name: string; - email: string; - role: string; - avatarUrl?: string; -} - -// 修改密码表单 -interface PasswordForm { - oldPassword: string; - newPassword: string; - confirmPassword: string; -} -``` - -## 5. 状态管理 - -```typescript -// 组件内状态 -const activeTab = ref('profile'); -const profileForm = ref>({}); -const passwordForm = ref({ oldPassword: '', newPassword: '', confirmPassword: '' }); -const profileLoading = ref(false); -const passwordLoading = ref(false); -const profileFormRef = ref(); -const passwordFormRef = ref(); - -// 全局状态 (Pinia Store) -const authStore = useAuthStore(); -const { user } = storeToRefs(authStore); - -// 当 user 数据从 store 加载后,填充表单 -watch(user, (currentUser) => { - if (currentUser) { - profileForm.value = { ...currentUser }; - } -}, { immediate: true }); -``` - -## 6. 交互逻辑 - -### 6.1 更新个人资料 - -```typescript -const updateProfile = async () => { - await profileFormRef.value?.validate(async (valid) => { - if (!valid) return; - profileLoading.value = true; - try { - await authStore.updateProfile({ - name: profileForm.value.name, - email: profileForm.value.email, - }); - ElMessage.success('个人资料更新成功'); - } finally { - profileLoading.value = false; - } - }); -}; -``` - -### 6.2 修改密码 - -```typescript -const changePassword = async () => { - await passwordFormRef.value?.validate(async (valid) => { - if (!valid) return; - passwordLoading.value = true; - try { - await authStore.changePassword(passwordForm.value); - ElMessage.success('密码修改成功,请重新登录'); - // 密码修改成功后通常需要用户重新登录 - authStore.logout(); - router.push('/auth/login'); - } catch (error) { - ElMessage.error(error.response?.data?.message || '密码修改失败'); - } finally { - passwordLoading.value = false; - } - }); -}; - -// 自定义密码验证规则 -const validateConfirmPassword = (rule: any, value: any, callback: any) => { - if (value !== passwordForm.value.newPassword) { - callback(new Error('两次输入的新密码不一致')); - } else { - callback(); - } -}; - -const passwordRules = { - // ... oldPassword 和 newPassword 的必填和长度规则 - confirmPassword: [ - { required: true, message: '请再次输入新密码', trigger: 'blur' }, - { validator: validateConfirmPassword, trigger: 'blur' } - ] -}; -``` - -## 7. API 调用 - -```typescript -// api/me.ts -export const meApi = { - getProfile: () => apiClient.get('/me'), - updateProfile: (data: Partial) => apiClient.put('/me', data), - changePassword: (data: PasswordForm) => apiClient.post('/me/change-password', data), -}; - -// stores/auth.ts -export const useAuthStore = defineStore('auth', { - // ... state, getters - actions: { - async fetchUserProfile() { - // (在App启动或登录后调用) - const { data } = await meApi.getProfile(); - this.user = data; - }, - async updateProfile(data) { - const { data: updatedUser } = await meApi.updateProfile(data); - this.user = { ...this.user, ...updatedUser }; - }, - async changePassword(data) { - await meApi.changePassword(data); - } - } -}); -``` - -## 8. 样式设计 - -```scss -.profile-page { - padding: 24px; - - .page-container { - margin-top: 24px; - } - - .user-card { - text-align: center; - .user-avatar { - margin-bottom: 20px; - } - .user-info h2 { - font-size: 20px; - margin-bottom: 8px; - } - .user-info p { - color: #909399; - } - } -} -``` - -## 9. 测试用例 - -1. **集成测试**: - - 测试页面是否能正确显示当前用户的个人信息。 - - 测试更新个人资料功能是否成功,并验证 store 中的用户信息是否同步更新。 - - 测试修改密码的成功流程,验证是否提示成功并跳转到登录页。 - - 测试修改密码的失败流程(如旧密码错误),验证是否显示正确的错误提示。 - - 测试所有表单验证规则是否生效(如邮箱格式、密码长度、两次密码一致性等)。 + +# 个人中心页面设计文档 + +## 1. 页面概述 + +个人中心页面允许当前登录的用户查看和修改自己的个人信息,以及更改密码。此页面旨在提供一个简单、安全的界面来管理个人账户资料。 + +## 2. 页面布局 + +![个人中心页面布局示意图](https://placeholder-for-profile-page-mockup.png) + +### 2.1 布局结构 + +页面通常采用多标签页(Tabs)布局,将不同功能的表单分开,以保持界面整洁。 +- **左侧**: 用户头像和基本信息展示。 +- **右侧**: 包含两个标签页: + - **基本资料**: 用于修改用户名、姓名、邮箱等信息。 + - **修改密码**: 用于更改当前用户的登录密码。 + +## 3. 组件结构 + +```vue + +``` + +## 4. 数据结构 + +```typescript +// 用户信息 (来自 Store) +interface UserProfile { + id: number; + username: string; + name: string; + email: string; + role: string; + avatarUrl?: string; +} + +// 修改密码表单 +interface PasswordForm { + oldPassword: string; + newPassword: string; + confirmPassword: string; +} +``` + +## 5. 状态管理 + +```typescript +// 组件内状态 +const activeTab = ref('profile'); +const profileForm = ref>({}); +const passwordForm = ref({ oldPassword: '', newPassword: '', confirmPassword: '' }); +const profileLoading = ref(false); +const passwordLoading = ref(false); +const profileFormRef = ref(); +const passwordFormRef = ref(); + +// 全局状态 (Pinia Store) +const authStore = useAuthStore(); +const { user } = storeToRefs(authStore); + +// 当 user 数据从 store 加载后,填充表单 +watch(user, (currentUser) => { + if (currentUser) { + profileForm.value = { ...currentUser }; + } +}, { immediate: true }); +``` + +## 6. 交互逻辑 + +### 6.1 更新个人资料 + +```typescript +const updateProfile = async () => { + await profileFormRef.value?.validate(async (valid) => { + if (!valid) return; + profileLoading.value = true; + try { + await authStore.updateProfile({ + name: profileForm.value.name, + email: profileForm.value.email, + }); + ElMessage.success('个人资料更新成功'); + } finally { + profileLoading.value = false; + } + }); +}; +``` + +### 6.2 修改密码 + +```typescript +const changePassword = async () => { + await passwordFormRef.value?.validate(async (valid) => { + if (!valid) return; + passwordLoading.value = true; + try { + await authStore.changePassword(passwordForm.value); + ElMessage.success('密码修改成功,请重新登录'); + // 密码修改成功后通常需要用户重新登录 + authStore.logout(); + router.push('/auth/login'); + } catch (error) { + ElMessage.error(error.response?.data?.message || '密码修改失败'); + } finally { + passwordLoading.value = false; + } + }); +}; + +// 自定义密码验证规则 +const validateConfirmPassword = (rule: any, value: any, callback: any) => { + if (value !== passwordForm.value.newPassword) { + callback(new Error('两次输入的新密码不一致')); + } else { + callback(); + } +}; + +const passwordRules = { + // ... oldPassword 和 newPassword 的必填和长度规则 + confirmPassword: [ + { required: true, message: '请再次输入新密码', trigger: 'blur' }, + { validator: validateConfirmPassword, trigger: 'blur' } + ] +}; +``` + +## 7. API 调用 + +```typescript +// api/me.ts +export const meApi = { + getProfile: () => apiClient.get('/me'), + updateProfile: (data: Partial) => apiClient.put('/me', data), + changePassword: (data: PasswordForm) => apiClient.post('/me/change-password', data), +}; + +// stores/auth.ts +export const useAuthStore = defineStore('auth', { + // ... state, getters + actions: { + async fetchUserProfile() { + // (在App启动或登录后调用) + const { data } = await meApi.getProfile(); + this.user = data; + }, + async updateProfile(data) { + const { data: updatedUser } = await meApi.updateProfile(data); + this.user = { ...this.user, ...updatedUser }; + }, + async changePassword(data) { + await meApi.changePassword(data); + } + } +}); +``` + +## 8. 样式设计 + +```scss +.profile-page { + padding: 24px; + + .page-container { + margin-top: 24px; + } + + .user-card { + text-align: center; + .user-avatar { + margin-bottom: 20px; + } + .user-info h2 { + font-size: 20px; + margin-bottom: 8px; + } + .user-info p { + color: #909399; + } + } +} +``` + +## 9. 测试用例 + +1. **集成测试**: + - 测试页面是否能正确显示当前用户的个人信息。 + - 测试更新个人资料功能是否成功,并验证 store 中的用户信息是否同步更新。 + - 测试修改密码的成功流程,验证是否提示成功并跳转到登录页。 + - 测试修改密码的失败流程(如旧密码错误),验证是否显示正确的错误提示。 + - 测试所有表单验证规则是否生效(如邮箱格式、密码长度、两次密码一致性等)。 --- - + ### Design/frontend_Design/TaskManagementPage_Design.md - -# 任务管理页面设计文档 - -## 1. 页面概述 - -任务管理页面是系统的核心功能模块之一,允许主管和管理员对任务进行全面的管理。用户可以在此页面上查看所有任务,并使用筛选、排序和搜索功能快速定位任务。此外,页面还支持任务的分配、审核以及查看任务详情等操作。 - -## 2. 页面布局 - -![任务管理页面布局示意图](https://placeholder-for-task-page-mockup.png) - -### 2.1 布局结构 - -页面采用经典的后台管理布局: -- **顶部**: 包含页面标题和操作按钮(如"新建任务")。 -- **中部**: 筛选和搜索区域,提供多种条件来过滤任务列表。 -- **下部**: 任务数据表格,以列表形式展示任务,并包含操作列。 -- **底部**: 分页组件,用于浏览大量任务数据。 - -### 2.2 响应式设计 - -- **桌面端**: 多列表格,所有筛选条件水平排列。 -- **平板端**: 表格列数减少,部分次要信息可能隐藏,筛选条件折叠或垂直排列。 -- **移动端**: 表格转为卡片列表视图,每个卡片显示一个任务的核心信息,筛选功能通过抽屉或模态框提供。 - -## 3. 组件结构 - -```vue - -``` - -## 4. 数据结构 - -```typescript -// 任务筛选条件 -interface TaskFilters { - status?: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'REVIEWED'; - assignee?: string; -} - -// 分页信息 -interface Pagination { - page: number; - pageSize: number; -} - -// 任务列表项 -interface TaskItem { - id: number; - title: string; - grid: string; - assignee: string | null; - status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'REVIEWED'; - createdAt: string; -} - -// 对话框模式 -type DialogMode = 'create' | 'assign' | 'review' | 'view'; -``` - -## 5. 状态管理 - -```typescript -// 组件内状态 -const filters = ref({}); -const pagination = ref({ page: 1, pageSize: 10 }); -const dialogVisible = ref(false); -const selectedTaskId = ref(null); -const dialogMode = ref('view'); - -// 全局状态 (Pinia Store) -const taskStore = useTaskStore(); -const { tasks, total, loading } = storeToRefs(taskStore); -const authStore = useAuthStore(); -const { user } = storeToRefs(authStore); -``` - -## 6. 交互逻辑 - -### 6.1 数据获取与筛选 - -```typescript -const fetchTasks = () => { - taskStore.fetchTasks({ ...filters.value, ...pagination.value }); -}; - -onMounted(fetchTasks); - -const handleSearch = () => { - pagination.value.page = 1; - fetchTasks(); -}; - -const handleReset = () => { - filters.value = {}; - handleSearch(); -}; - -const handleSizeChange = (size: number) => { - pagination.value.pageSize = size; - fetchTasks(); -}; - -const handleCurrentChange = (page: number) => { - pagination.value.page = page; - fetchTasks(); -}; -``` - -### 6.2 表格操作 - -```typescript -const handleView = (task: TaskItem) => { - selectedTaskId.value = task.id; - dialogMode.value = 'view'; - dialogVisible.value = true; -}; - -const handleAssign = (task: TaskItem) => { - selectedTaskId.value = task.id; - dialogMode.value = 'assign'; - dialogVisible.value = true; -}; - -const handleReview = (task: TaskItem) => { - selectedTaskId.value = task.id; - dialogMode.value = 'review'; - dialogVisible.value = true; -}; - -const handleCreateTask = () => { - selectedTaskId.value = null; - dialogMode.value = 'create'; - dialogVisible.value = true; -}; - -const handleSuccess = () => { - dialogVisible.value = false; - fetchTasks(); // 操作成功后刷新列表 -}; -``` - -### 6.3 权限控制 - -```typescript -const canAssign = (task: TaskItem) => { - return user.value?.role === 'SUPERVISOR' && task.status === 'PENDING'; -}; - -const canReview = (task: TaskItem) => { - return user.value?.role === 'SUPERVISOR' && task.status === 'COMPLETED'; -}; -``` - -### 6.4 辅助函数 - -```typescript -const formatStatus = (status: TaskItem['status']) => { - const map = { PENDING: '待处理', IN_PROGRESS: '进行中', COMPLETED: '已完成', REVIEWED: '已审核' }; - return map[status] || '未知'; -}; - -// getStatusTagType 函数同仪表盘页面设计 -``` - -## 7. API 调用 - -```typescript -// api/tasks.ts -export const taskApi = { - getTasks: (params) => apiClient.get('/management/tasks', { params }), - // ... 其他任务相关API -}; - -// stores/task.ts -export const useTaskStore = defineStore('task', { - state: () => ({ - tasks: [] as TaskItem[], - total: 0, - loading: false, - }), - actions: { - async fetchTasks(params) { - this.loading = true; - try { - const { data } = await taskApi.getTasks(params); - this.tasks = data.items; - this.total = data.total; - } catch (error) { - console.error('获取任务列表失败', error); - } finally { - this.loading = false; - } - } - } -}); -``` - -## 8. 样式设计 - -```scss -.task-management-page { - padding: 24px; - - .page-container { - margin-top: 24px; - padding: 24px; - border-radius: 8px; - border: none; - } - - .filter-container { - margin-bottom: 20px; - } - - .action-buttons { - margin-bottom: 20px; - } - - .pagination-container { - margin-top: 24px; - display: flex; - justify-content: flex-end; - } -} -``` - -## 9. 测试用例 - -1. **单元测试**: - - 测试权限控制函数 `canAssign` 和 `canReview` 的逻辑。 - - 测试 `formatStatus` 辅助函数的正确性。 - - 测试 Pinia store 的 action 是否能正确调用 API 并更新 state。 - -2. **集成测试**: - - 测试筛选、搜索和重置功能是否正常工作。 - - 测试分页组件是否能正确切换页面和每页数量。 - - 测试"查看"、"分配"、"审核"、"新建"按钮能否正确打开对应模式的对话框。 - - 测试对话框操作成功后,任务列表是否刷新。 - -## 10. 性能优化 - -1. **后端分页**: 必须使用后端分页来处理大量任务数据。 -2. **防抖**: 对搜索输入框使用防抖处理,避免用户输入时频繁触发 API 请求。 -3. **组件懒加载**: `TaskFormDialog` 组件可以通过动态导入实现懒加载,只在需要时才加载。 + +# 任务管理页面设计文档 + +## 1. 页面概述 + +任务管理页面是系统的核心功能模块之一,允许主管和管理员对任务进行全面的管理。用户可以在此页面上查看所有任务,并使用筛选、排序和搜索功能快速定位任务。此外,页面还支持任务的分配、审核以及查看任务详情等操作。 + +## 2. 页面布局 + +![任务管理页面布局示意图](https://placeholder-for-task-page-mockup.png) + +### 2.1 布局结构 + +页面采用经典的后台管理布局: +- **顶部**: 包含页面标题和操作按钮(如"新建任务")。 +- **中部**: 筛选和搜索区域,提供多种条件来过滤任务列表。 +- **下部**: 任务数据表格,以列表形式展示任务,并包含操作列。 +- **底部**: 分页组件,用于浏览大量任务数据。 + +### 2.2 响应式设计 + +- **桌面端**: 多列表格,所有筛选条件水平排列。 +- **平板端**: 表格列数减少,部分次要信息可能隐藏,筛选条件折叠或垂直排列。 +- **移动端**: 表格转为卡片列表视图,每个卡片显示一个任务的核心信息,筛选功能通过抽屉或模态框提供。 + +## 3. 组件结构 + +```vue + +``` + +## 4. 数据结构 + +```typescript +// 任务筛选条件 +interface TaskFilters { + status?: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'REVIEWED'; + assignee?: string; +} + +// 分页信息 +interface Pagination { + page: number; + pageSize: number; +} + +// 任务列表项 +interface TaskItem { + id: number; + title: string; + grid: string; + assignee: string | null; + status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'REVIEWED'; + createdAt: string; +} + +// 对话框模式 +type DialogMode = 'create' | 'assign' | 'review' | 'view'; +``` + +## 5. 状态管理 + +```typescript +// 组件内状态 +const filters = ref({}); +const pagination = ref({ page: 1, pageSize: 10 }); +const dialogVisible = ref(false); +const selectedTaskId = ref(null); +const dialogMode = ref('view'); + +// 全局状态 (Pinia Store) +const taskStore = useTaskStore(); +const { tasks, total, loading } = storeToRefs(taskStore); +const authStore = useAuthStore(); +const { user } = storeToRefs(authStore); +``` + +## 6. 交互逻辑 + +### 6.1 数据获取与筛选 + +```typescript +const fetchTasks = () => { + taskStore.fetchTasks({ ...filters.value, ...pagination.value }); +}; + +onMounted(fetchTasks); + +const handleSearch = () => { + pagination.value.page = 1; + fetchTasks(); +}; + +const handleReset = () => { + filters.value = {}; + handleSearch(); +}; + +const handleSizeChange = (size: number) => { + pagination.value.pageSize = size; + fetchTasks(); +}; + +const handleCurrentChange = (page: number) => { + pagination.value.page = page; + fetchTasks(); +}; +``` + +### 6.2 表格操作 + +```typescript +const handleView = (task: TaskItem) => { + selectedTaskId.value = task.id; + dialogMode.value = 'view'; + dialogVisible.value = true; +}; + +const handleAssign = (task: TaskItem) => { + selectedTaskId.value = task.id; + dialogMode.value = 'assign'; + dialogVisible.value = true; +}; + +const handleReview = (task: TaskItem) => { + selectedTaskId.value = task.id; + dialogMode.value = 'review'; + dialogVisible.value = true; +}; + +const handleCreateTask = () => { + selectedTaskId.value = null; + dialogMode.value = 'create'; + dialogVisible.value = true; +}; + +const handleSuccess = () => { + dialogVisible.value = false; + fetchTasks(); // 操作成功后刷新列表 +}; +``` + +### 6.3 权限控制 + +```typescript +const canAssign = (task: TaskItem) => { + return user.value?.role === 'SUPERVISOR' && task.status === 'PENDING'; +}; + +const canReview = (task: TaskItem) => { + return user.value?.role === 'SUPERVISOR' && task.status === 'COMPLETED'; +}; +``` + +### 6.4 辅助函数 + +```typescript +const formatStatus = (status: TaskItem['status']) => { + const map = { PENDING: '待处理', IN_PROGRESS: '进行中', COMPLETED: '已完成', REVIEWED: '已审核' }; + return map[status] || '未知'; +}; + +// getStatusTagType 函数同仪表盘页面设计 +``` + +## 7. API 调用 + +```typescript +// api/tasks.ts +export const taskApi = { + getTasks: (params) => apiClient.get('/management/tasks', { params }), + // ... 其他任务相关API +}; + +// stores/task.ts +export const useTaskStore = defineStore('task', { + state: () => ({ + tasks: [] as TaskItem[], + total: 0, + loading: false, + }), + actions: { + async fetchTasks(params) { + this.loading = true; + try { + const { data } = await taskApi.getTasks(params); + this.tasks = data.items; + this.total = data.total; + } catch (error) { + console.error('获取任务列表失败', error); + } finally { + this.loading = false; + } + } + } +}); +``` + +## 8. 样式设计 + +```scss +.task-management-page { + padding: 24px; + + .page-container { + margin-top: 24px; + padding: 24px; + border-radius: 8px; + border: none; + } + + .filter-container { + margin-bottom: 20px; + } + + .action-buttons { + margin-bottom: 20px; + } + + .pagination-container { + margin-top: 24px; + display: flex; + justify-content: flex-end; + } +} +``` + +## 9. 测试用例 + +1. **单元测试**: + - 测试权限控制函数 `canAssign` 和 `canReview` 的逻辑。 + - 测试 `formatStatus` 辅助函数的正确性。 + - 测试 Pinia store 的 action 是否能正确调用 API 并更新 state。 + +2. **集成测试**: + - 测试筛选、搜索和重置功能是否正常工作。 + - 测试分页组件是否能正确切换页面和每页数量。 + - 测试"查看"、"分配"、"审核"、"新建"按钮能否正确打开对应模式的对话框。 + - 测试对话框操作成功后,任务列表是否刷新。 + +## 10. 性能优化 + +1. **后端分页**: 必须使用后端分页来处理大量任务数据。 +2. **防抖**: 对搜索输入框使用防抖处理,避免用户输入时频繁触发 API 请求。 +3. **组件懒加载**: `TaskFormDialog` 组件可以通过动态导入实现懒加载,只在需要时才加载。 --- - + ## 报告与材料 - + ### Report/报告格式.md - -实践目的 -首先对项目内容进行“概述”,并明确说明本人从事的工作; -然后,写出本人通过本次实践,达成了以下哪些能力的培养,要求简洁清楚。可以从以下几点来说明:(不要照抄以下内容,说明自己的能力达成情况) -设计/开发解决方案的能力: -(1)掌握软件生命周期要素(理解、掌握,并能够按照面向对象的思想对系统进行设计),熟悉软件需求分析、设计、实现、测试的方法和技术(能够使用UML类图对系统整体建模,会抽取类、属性、方法、关联,设计合理,符合系统要求;能够合理持久化存储;界面设计美观实用等); -(2)能够设计满足特定功能需求与性能需求的解决方案,并体现创新意识; -研究能力: -(3)能够理解系统软件的设计思路和基本原理,掌握应用软件技术、科学方法,具备创新性地解决软件工程具体问题的能力; -使用现代工具的能力: -(4)能够选择恰当的技术、资源、现代工程工具和信息技术工具,对复杂软件工程问题进行分析、计算或设计; -沟通能力: -(5)具备一定的社交技能和技巧,能够就与本专业相关的当前热点问题发表自己的观点,能够以口头、文稿、图表等方式与业界同行进行技术交流与沟通,能使用通俗易懂的语言与社会公众进行表达与沟通; - -1. 相关技术基础 -写出本次实践过程中你所用到的相关技术,包括与项目相关的理论基础,项目开发方法、开发工具、开发环境等关键技术的介绍; - -2. 实践结果 -此部分属报告的主要部分。包括: -3.1 需求定义 -“系统分析”也可以看成是需求定义,包括对整个项目的介绍分析及本人工作内容的详细分析,如业务分析、功能分析(可使用例图、活动图来描述)、可行性分析等; -3.2 系统设计 -“系统设计”包括总体设计和详细设计,"总体设计"包括系统架构设计、功能模块划分等,"详细设计"要围绕本人工作内容展开,包括功能模块详细设计、类和对象的设计、动态模型设计(时序图、状态图、协作图等)、算法设计、数据库设计等; -3.3 系统实现 -“系统实现”也要围绕本人工作内容展开,从编码实现角度论述相应功能模块的实现细节,并展示自己所完成的主要成果及实际应用情况等。可通过“程序流程图”、“关键代码”和“界面”进行直观论述。 -3.4 系统测试 -“系统测试”包括测试方案设计、测试用例和测试结果、最终的测试结论或评价等。 - -3. 实践总结 -简述你在实践过程中的内容完成情况,重点介绍创新点及不足(也就是可以再完善的部分,只是时间不允许了。不足不代表不好,也说明你思考了,但是来不及完成实现) - -4. 参考资料 -例: -[1] 数据结构、算法与应用:C++语言描述 [Data Structures,Algorithms,and Applications in C++][M].机械工业出版社出版时间:2000-01-01. -[2] 数据结构(C语言版) [M].北京: 中国铁道出版社, 2011-08-01. - - - - - -基础编程实训成绩评定表 - -考核内容 考核标准 分值 得分 - -面向对象设计能力 1. 能够很好的按照面向对象的思想对系统进行设计和实现,设计合理的实体关系,正确使用UML类图对系统整体建模,设计合理,符合系统要求; -2. 能够很好的划分模块和提取方法,程序设计具有高内聚低耦合的特点; -3. 能够合理应用多种设计模式,提高系统的灵活性和可用性; 20 -面向对象编程能力 1. 能够应用面向对象程序设计语言进行系统实现; -2. 能够很好的使用继承和多态,提升代码复用性; -3. 程序设计逻辑结构清晰合理; -4. 代码规范,遵照Java的代码规范。 20 -解决问题能力 1. 能够正确使用已有的架构完成系统功能,如MVC架构; -2. 能够很好的设计持久化存储结构。正确使用Java语言实现设计的系统,程序结构清晰,代码书写规范,简洁,实现的系统功能完善,运行稳定,基本无bug。 -3. 界面美观,符合用户习惯; 20 -学习能力 1. 能够自学Java语言中关于图形用户界面的知识,并能为开发的系统构造图形用户界面。能够挑选合适GUI插件(如window builder插件)快速构建图形用户界面。 -2. 能够在集成开发环境中对系统进行开发和调试,能够使用代码检查工具提升代码的正确性。 -3. 能够通过网络、课堂、书籍等多处获取所需的知识,并应用这些知识解决相应的问题。 -4. 在解决问题的过程中有自己独到的见解,并能够有所创新。 20 -报告质量 1. 实践报告格式规范,报告内容充实、正确,报告叙述逻辑严密,可准确反映出设计和实现的结果。 -2. 实践报告能够体现实践过程中出现的问题和解决方案,以及独立分析问题和解决问题的能力。 -3. 报告格式统一,图表使用规范,报告用词准确,符合科技文档写作要求。 20 -总 分(百分制) - - + +实践目的 +首先对项目内容进行“概述”,并明确说明本人从事的工作; +然后,写出本人通过本次实践,达成了以下哪些能力的培养,要求简洁清楚。可以从以下几点来说明:(不要照抄以下内容,说明自己的能力达成情况) +设计/开发解决方案的能力: +(1)掌握软件生命周期要素(理解、掌握,并能够按照面向对象的思想对系统进行设计),熟悉软件需求分析、设计、实现、测试的方法和技术(能够使用UML类图对系统整体建模,会抽取类、属性、方法、关联,设计合理,符合系统要求;能够合理持久化存储;界面设计美观实用等); +(2)能够设计满足特定功能需求与性能需求的解决方案,并体现创新意识; +研究能力: +(3)能够理解系统软件的设计思路和基本原理,掌握应用软件技术、科学方法,具备创新性地解决软件工程具体问题的能力; +使用现代工具的能力: +(4)能够选择恰当的技术、资源、现代工程工具和信息技术工具,对复杂软件工程问题进行分析、计算或设计; +沟通能力: +(5)具备一定的社交技能和技巧,能够就与本专业相关的当前热点问题发表自己的观点,能够以口头、文稿、图表等方式与业界同行进行技术交流与沟通,能使用通俗易懂的语言与社会公众进行表达与沟通; + +1. 相关技术基础 +写出本次实践过程中你所用到的相关技术,包括与项目相关的理论基础,项目开发方法、开发工具、开发环境等关键技术的介绍; + +2. 实践结果 +此部分属报告的主要部分。包括: +3.1 需求定义 +“系统分析”也可以看成是需求定义,包括对整个项目的介绍分析及本人工作内容的详细分析,如业务分析、功能分析(可使用例图、活动图来描述)、可行性分析等; +3.2 系统设计 +“系统设计”包括总体设计和详细设计,"总体设计"包括系统架构设计、功能模块划分等,"详细设计"要围绕本人工作内容展开,包括功能模块详细设计、类和对象的设计、动态模型设计(时序图、状态图、协作图等)、算法设计、数据库设计等; +3.3 系统实现 +“系统实现”也要围绕本人工作内容展开,从编码实现角度论述相应功能模块的实现细节,并展示自己所完成的主要成果及实际应用情况等。可通过“程序流程图”、“关键代码”和“界面”进行直观论述。 +3.4 系统测试 +“系统测试”包括测试方案设计、测试用例和测试结果、最终的测试结论或评价等。 + +3. 实践总结 +简述你在实践过程中的内容完成情况,重点介绍创新点及不足(也就是可以再完善的部分,只是时间不允许了。不足不代表不好,也说明你思考了,但是来不及完成实现) + +4. 参考资料 +例: +[1] 数据结构、算法与应用:C++语言描述 [Data Structures,Algorithms,and Applications in C++][M].机械工业出版社出版时间:2000-01-01. +[2] 数据结构(C语言版) [M].北京: 中国铁道出版社, 2011-08-01. + + + + + +基础编程实训成绩评定表 + +考核内容 考核标准 分值 得分 + +面向对象设计能力 1. 能够很好的按照面向对象的思想对系统进行设计和实现,设计合理的实体关系,正确使用UML类图对系统整体建模,设计合理,符合系统要求; +2. 能够很好的划分模块和提取方法,程序设计具有高内聚低耦合的特点; +3. 能够合理应用多种设计模式,提高系统的灵活性和可用性; 20 +面向对象编程能力 1. 能够应用面向对象程序设计语言进行系统实现; +2. 能够很好的使用继承和多态,提升代码复用性; +3. 程序设计逻辑结构清晰合理; +4. 代码规范,遵照Java的代码规范。 20 +解决问题能力 1. 能够正确使用已有的架构完成系统功能,如MVC架构; +2. 能够很好的设计持久化存储结构。正确使用Java语言实现设计的系统,程序结构清晰,代码书写规范,简洁,实现的系统功能完善,运行稳定,基本无bug。 +3. 界面美观,符合用户习惯; 20 +学习能力 1. 能够自学Java语言中关于图形用户界面的知识,并能为开发的系统构造图形用户界面。能够挑选合适GUI插件(如window builder插件)快速构建图形用户界面。 +2. 能够在集成开发环境中对系统进行开发和调试,能够使用代码检查工具提升代码的正确性。 +3. 能够通过网络、课堂、书籍等多处获取所需的知识,并应用这些知识解决相应的问题。 +4. 在解决问题的过程中有自己独到的见解,并能够有所创新。 20 +报告质量 1. 实践报告格式规范,报告内容充实、正确,报告叙述逻辑严密,可准确反映出设计和实现的结果。 +2. 实践报告能够体现实践过程中出现的问题和解决方案,以及独立分析问题和解决问题的能力。 +3. 报告格式统一,图表使用规范,报告用词准确,符合科技文档写作要求。 20 +总 分(百分制) + + --- - + ### Report/参考资料.md - -# 4. 参考资料 - -[1] Spring Boot. (2023). Spring Boot Reference Documentation. [https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/) - -[2] Vue.js. (2023). The Vue.js Guide. [https://vuejs.org/guide/introduction.html](https://vuejs.org/guide/introduction.html) - -[3] Baeldung. (2023). Spring Security. [https://www.baeldung.com/security-spring](https://www.baeldung.com/security-spring) - -[4] Vaughn, V. (2013). *Implementing Domain-Driven Design*. Addison-Wesley Professional. + +# 4. 参考资料 + +[1] Spring Boot. (2023). Spring Boot Reference Documentation. [https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/) + +[2] Vue.js. (2023). The Vue.js Guide. [https://vuejs.org/guide/introduction.html](https://vuejs.org/guide/introduction.html) + +[3] Baeldung. (2023). Spring Security. [https://www.baeldung.com/security-spring](https://www.baeldung.com/security-spring) + +[4] Vaughn, V. (2013). *Implementing Domain-Driven Design*. Addison-Wesley Professional. --- - + ### Report/基本要求.md - -(1)所有数据以文件格式保存,文件存储在工程目录中。 -(2)文件数据格式可以是JSON格式,也可以是以对象序列化的方式存储。 -(3)东软环保公众监督系统主要功能为:汇总不同地区的公众监督员提供的空气质量信息,由系统管理员将这些信息指派给专业的环保检测网格员,进行实地考察和检测,从而得到不同地区的空气质量AQI(空气质量指数)的实时数据。再将这些AQI数据进行统计,统计结果最终成为环保方面决策者进行决策的依据。 - + +(1)所有数据以文件格式保存,文件存储在工程目录中。 +(2)文件数据格式可以是JSON格式,也可以是以对象序列化的方式存储。 +(3)东软环保公众监督系统主要功能为:汇总不同地区的公众监督员提供的空气质量信息,由系统管理员将这些信息指派给专业的环保检测网格员,进行实地考察和检测,从而得到不同地区的空气质量AQI(空气质量指数)的实时数据。再将这些AQI数据进行统计,统计结果最终成为环保方面决策者进行决策的依据。 + --- - + ### Report/目前内容.md - -# 1. 实践目的 - -## 1.1 项目概述与个人贡献 - -### 1.1.1 项目概述 - -本项目旨在解决传统环保监督渠道反馈不畅、处理流程不透明、以及公众参与度不高等痛点,开发一个高效、透明的**环保公众监督平台**。作为《东软环保应急》系统的重要子模块,它不仅是技术上的实现,更是业务流程上的一次重要创新。 - -系统的核心目标是打通从"问题发现"到"问题解决"的全链路。它通过提供便捷的移动端/网页端入口,鼓励公众随时随地上传图文并茂的环境问题反馈。系统接收到反馈后,将**自动触发一系列内部流程**:首先通过AI服务进行初步的智能分类与严重性评估;随后,经由主管人员审核确认后,反馈将**自动转化为一个结构化的处理任务**;最后,系统基于**智能分配算法**,综合考量网格员的实时位置、当前负载等因素,将任务精准派发给最优的执行者。整个过程的状态(待处理、执行中、已完成、已归档)对管理者和反馈者全程可见,极大地提升了环保工作的透明度与处理效率。 - -### 1.1.2 个人贡献 - -在本次实践中,本人担任**核心的系统设计与后端开发角色**,全面负责了从技术选型到项目落地的完整技术实现。我的工作不仅是代码的编写者,更是整个后端架构的**设计者**和**构建者**。具体贡献如下: - -* **架构设计与技术决策**:在项目初期,我主导了技术栈的选型,力主采用`Spring Boot` + `Vue.js`的前后端分离架构,以应对未来快速迭代和功能扩展的需求。同时,我独立设计了后端**Controller-Service-Repository**的三层应用架构,并规划了项目的整体模块(如安全、事件、仓储等),为整个项目的稳固开发奠定了基础。 - -* **核心难题攻关**:面对"所有数据需以文件形式存储"这一核心且棘手的约束,我没有采取简单的文件读写,而是独立设计并实现了一套**模拟JPA规范的泛型JSON仓储层**。这个创新方案不仅完美地解决了数据持久化问题,其遵循的"依赖倒置原则"也使得代码高度解耦,极大地提升了系统的可维护性和可测试性,是本次项目中技术含量最高的突破之一。 - -* **全栈功能实现**:我负责了全部后端模块的编码实现,包括但不限于基于`Spring Security`和`JWT`的**用户认证与授权系统**、**任务全生命周期的状态机管理**、以及基于**A*算法的智能任务分配模型**等。此外,我也参与了部分前端页面的开发,并利用`Swagger`和`Postman`完成了所有API的设计、文档化与测试工作,确保了前后端的顺畅联调。 - -## 1.2 个人能力培养 - -通过本次实践,本人将软件工程理论与开发实践深度结合,在设计与开发复杂问题的解决方案方面获得了显著提升。 - -* **软件工程全周期掌控能力**:通过完整地参与从需求分析、系统设计、编码实现到测试部署的全过程,深刻掌握了现代软件开发的生命周期要素和方法。能够运用面向对象的思想,通过UML对系统进行建模,合理地划分模块,确保了系统设计的高内聚、低耦合。 - -* **复杂问题解决方案的设计与创新能力**: - * 面对"数据以文件格式保存"的核心约束,没有采用简单的序列化读写,而是创新性地设计并实现了一套**基于JSON的泛型仓储层(Repository Pattern)**。该方案遵循了依赖倒置原则,不仅满足了功能要求,更保证了代码的可维护性与未来向数据库迁移的可行性。 - * 针对任务分配场景,设计了**基于多维因素(地理位置、负载)的智能调度算法**,并集成了**A*寻路算法**进行路径规划,展现了运用科学算法解决实际工程问题的能力。 - * 在系统中有效运用了**事件驱动模型**,将耗时的AI分析流程解耦为异步操作,提升了系统的响应速度和健壮性。 - -* **技术沟通与文档化能力**:能够通过撰写系统设计方案、绘制UML图表等方式,系统、清晰地阐述项目的技术架构与实现细节,并具备良好的口头技术交流能力。 - -# 2. 相关技术基础 - -本项目综合运用了业界主流的开发技术与环境,构建了一个现代化Web应用。整体技术体系由以下五部分构成: - -* **理论基础**:遵循**面向对象编程(OOP)**、**SOLID设计原则(尤其是DIP)**、**MVC分层架构**及**RESTful API**设计规范。 -* **后端技术栈**:以 **`Spring Boot 3`** 为核心,整合 **`Spring Security`** 与 **`JWT`** 进行安全控制,并创新性地设计了**自定义的泛型JSON仓储层**作为持久化方案。 -* **前端技术栈**:采用 **`Vue 3`** 作为核心框架,配合 **`Vite`** 进行构建,使用 **`Pinia`** 进行状态管理,**`Element Plus`** 作为UI组件库,并对 **`Axios`** 进行了封装。 -* **测试技术**:后端采用 **`JUnit 5`** 和 **`Mockito`** 进行单元/集成测试;接口层使用 **`Swagger UI`** 和 **`Postman`** 进行测试与调试。 -* **开发工具与环境**:使用 **`IntelliJ IDEA`** 和 **`VS Code`** 作为主力IDE,通过 **`Maven`** 和 **`npm`** 管理项目,并利用 **`Git`** 进行版本控制。 - -以下是各部分的详细介绍。 - -## 2.1 理论基础 - -项目的架构和代码设计遵循了下述成熟的软件工程理论,以确保系统的可维护性和扩展性。 - -* **面向对象编程 (OOP):** - 项目的核心编程思想,其封装、继承、多态特性在后端代码设计中得到了深入应用。 - * **封装 (Encapsulation):** 核心业务实体(如`User`, `Task`)被抽象为Java类,其属性私有化,仅通过公共方法暴露,保证了对象状态的完整性。 - * **继承 (Inheritance) 与多态 (Polymorphism):** 在自定义的JSON仓储层设计中,通过定义泛型接口`JsonRepository`和实现该接口的泛型基类`JsonRepositoryImpl`,使得具体的实体仓储(如`JsonUserRepositoryImpl`)能自动获得通用的数据操作能力。业务逻辑层依赖于抽象接口而非具体实现,实现了"面向接口编程",提高了代码的灵活性。 - -* **SOLID设计原则,尤其是依赖倒置原则 (DIP):** - 项目架构遵循SOLID原则,特别是依赖倒置原则,即"高层模块不应依赖于低层模块,两者都应依赖于抽象"。 - * **实践应用:** `Service`层(高层模块)依赖的是`UserRepository`等抽象接口,而不是`JsonRepositoryImpl`这样的具体实现(低层模块)。这种设计倒置了传统的依赖关系,极大提升了系统的可替换性和可测试性。未来更换数据源或进行单元测试时,只需更换实现或模拟接口,上层代码无需改动。 - -* **模型-视图-控制器 (MVC) 分层架构:** - 项目遵循MVC分层思想,并扩展为更精细的四层架构:Controller -> Service -> Repository -> Entity。 - * **Controller (控制器层):** 作为HTTP请求的入口,负责解析请求参数,调用`Service`层执行业务逻辑,并返回响应。 - * **Service (业务逻辑层):** 包含所有业务规则和处理流程,通过依赖注入(DI)调用`Repository`层。 - * **Repository (数据访问层):** 数据持久化的抽象,负责与数据源(本项目中为JSON文件)交互,实现业务与数据的解耦。 - * **Entity/DTO (模型层):** POJO对象,`Entity`映射数据存储结构,`DTO`(Data Transfer Object)用于各层之间的数据传输,避免持久化实体直接暴露给外部。 - -* **RESTful API 设计原则:** - 前后端通信完全基于RESTful风格的API进行。 - * **统一接口 (Uniform Interface):** 使用HTTP标准方法(`GET`, `POST`, `PUT`, `DELETE`)表达对资源的操作。 - * **无状态 (Stateless):** 服务端不保存客户端会话状态,每次请求都包含所有必要信息(如JWT),提高了系统的可伸缩性。 - * **资源导向 (Resource-Oriented):** API围绕"资源"展开,使用名词(如`/api/users`)标识资源,而非动词。 - -## 2.2 后端技术栈 - -* **核心框架 - `Spring Boot 3.x`:** - 作为后端应用基石,它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发、配置和部署。其强大的生态整合能力,使得集成`Spring Security`、`Lombok`、`Swagger`等第三方库变得非常简单。 - -* **安全框架 - `Spring Security 6.x` & `JWT`:** - 采用`Spring Security`结合`JSON Web Tokens (JWT)`,构建无状态的认证与授权体系。 - * **`Spring Security` Filter链:** 自定义`JwtAuthenticationFilter`过滤器,用于在每个请求中校验JWT并设置安全上下文。 - * **声明式权限控制:** 使用`@PreAuthorize`注解对Service方法进行方法级别的权限声明,实现了安全逻辑与业务逻辑的解耦。 - * **无状态会话 (Stateless Session):** 配置会话管理策略为`STATELESS`,使后端服务成为真正的无状态服务,提升了系统的可伸缩性。 - -* **持久化方案 - 自定义泛型JSON仓储层:** - 为满足"数据存储在JSON文件中"的需求,设计并实现了一套模拟`Spring Data JPA`接口规范的、可复用的泛型JSON仓储层。 - * **`JsonStorageService`:** 将所有文件I/O操作封装在此服务中,并使用`synchronized`块确保并发写操作的线程安全。 - * **`JsonRepositoryImpl`:** 泛型基类,实现了通用的CRUD功能,具体的实体仓储通过继承该类来复用代码。 - * 该方案遵循**依赖倒置原则**,业务层仅依赖抽象接口,为未来平滑迁移持久化方案提供了便利。 - -* **数据校验 - `Jakarta Bean Validation` (`Hibernate Validator`):** - 在DTO的字段上使用`@NotNull`, `@Size`等声明式注解,并配合Controller层的`@Valid`注解,实现对请求参数的自动化校验。 - -* **对象映射 - `MapStruct`:** - 一个编译期的代码生成器,通过定义`@Mapper`接口,自动生成Entity与DTO之间转换的高性能实现代码,提升了开发效率。 - -* **编程语言与辅助工具:** - * **`Java 17 (LTS)`:** 使用其`record`、`switch`表达式等新特性编写更简洁的代码。 - * **`Lombok`:** 通过`@Data`, `@Builder`等注解,消除样板代码。 - * **`SLF4J` & `Logback`:** 灵活的日志系统,通过配置文件实现分环境、分级别的日志输出。 - -## 2.3 前端技术栈 - -采用`Vue.js`生态的最新技术,构建响应迅速、代码可维护的现代化单页应用(SPA)。 - -* **核心框架 - `Vue.js 3.x`:** - 利用其两大核心新特性: - * **`Composition API` (组合式API):** 将相关逻辑组织在同一个`setup`函数内,提高了代码的可读性、可维护性和逻辑复用性。 - * **`Proxy`-based Reactivity:** 基于`Proxy`重写的响应式系统,解决了`Vue 2`中无法监听对象属性新增/删除等痛点。 - -* **构建工具 - `Vite`:** - 新一代前端构建工具,优势在于: - * 利用浏览器原生ESM支持,实现开发环境下的极速冷启动和闪电般的热模块替换(HMR)。 - * 生产环境使用`Rollup`打包,生成高度优化的静态资源。 - -* **状态管理 - `Pinia`:** - `Vue`官方推荐的下一代状态管理库,特点是API直观、类型支持完美,且天生模块化。废除了`Vuex`中复杂的`Mutations`等概念,心智负担小。 - -* **UI组件库 - `Element Plus`:** - `Element UI`的`Vue 3`版本,是一套高质量的企业级UI组件库,提供了丰富的组件,极大地加速了界面的开发进程。 - -* **HTTP客户端 - `Axios`封装:** - 对流行的`Axios`库进行二次封装: - * **创建实例:** 为API服务设置不同的`baseURL`、`timeout`等。 - * **请求拦截器:** 统一添加认证`token`。 - * **响应拦截器:** 统一处理业务数据和错误状态码。 - -## 2.4 测试技术 - -* **后端单元与集成测试 (`JUnit 5`, `Mockito`, `Spring Boot Test`):** - 使用`spring-boot-starter-test`集成的测试套件保障业务逻辑正确性。 - * **`JUnit 5`:** 测试的基础框架。 - * **`Mockito`:** 在单元测试中用于模拟依赖对象(如Repository),实现测试隔离。 - * **`Spring Boot Test`:** 用于集成测试,加载完整应用上下文,测试跨层级的真实交互场景。 - -* **API接口测试 (`Postman` / `Swagger UI`):** - * **`Swagger UI`:** 通过集成`SpringDoc`,根据代码注解自动生成交互式API文档,方便调试。 - * **`Postman`:** 用于执行更复杂的API测试场景、编写测试用例和自动化测试。 - -## 2.5 开发工具与环境 - -* **IDE:** 后端使用`IntelliJ IDEA Ultimate`,前端使用`Visual Studio Code`。 -* **项目管理与构建:** 后端使用`Maven`,前端使用`npm`。 -* **版本控制:** `Git` & `GitHub`,遵循`Git Flow`工作流。 - -# 3. 实践结果 - -此部分属报告的主要部分。包括: - -## 3.1 需求定义 - -"系统分析"也可以看成是需求定义,包括对整个项目的介绍分析及本人工作内容的详细分析,如业务分析、功能分析(可使用例图、活动图来描述)、可行性分析等; - -## 3.2 系统设计 - -"系统设计"包括总体设计和详细设计,"总体设计"包括系统架构设计、功能模块划分等,"详细设计"要围绕本人工作内容展开,包括功能模块详细设计、类和对象的设计、动态模型设计(时序图、状态图、协作图等)、算法设计、数据库设计等; - -## 3.3 系统实现 - -"系统实现"也要围绕本人工作内容展开,从编码实现角度论述相应功能模块的实现细节,并展示自己所完成的主要成果及实际应用情况等。可通过"程序流程图"、"关键代码"和"界面"进行直观论述。 - -## 3.4 系统测试 - -"系统测试"包括测试方案设计、测试用例和测试结果、最终的测试结论或评价等。 - -# 4. 实践总结 - -简述你在实践过程中的内容完成情况,重点介绍创新点及不足(也就是可以再完善的部分,只是时间不允许了。不足不代表不好,也说明你思考了,但是来不及完成实现) - -# 5. 参考资料 - -例: -[1] 数据结构、算法与应用:C++语言描述 [Data Structures,Algorithms,and Applications in C++][M].机械工业出版社出版时间:2000-01-01. -[2] 数据结构(C语言版) [M].北京: 中国铁道出版社, 2011-08-01. - + +# 1. 实践目的 + +## 1.1 项目概述与个人贡献 + +### 1.1.1 项目概述 + +本项目旨在解决传统环保监督渠道反馈不畅、处理流程不透明、以及公众参与度不高等痛点,开发一个高效、透明的**环保公众监督平台**。作为《东软环保应急》系统的重要子模块,它不仅是技术上的实现,更是业务流程上的一次重要创新。 + +系统的核心目标是打通从"问题发现"到"问题解决"的全链路。它通过提供便捷的移动端/网页端入口,鼓励公众随时随地上传图文并茂的环境问题反馈。系统接收到反馈后,将**自动触发一系列内部流程**:首先通过AI服务进行初步的智能分类与严重性评估;随后,经由主管人员审核确认后,反馈将**自动转化为一个结构化的处理任务**;最后,系统基于**智能分配算法**,综合考量网格员的实时位置、当前负载等因素,将任务精准派发给最优的执行者。整个过程的状态(待处理、执行中、已完成、已归档)对管理者和反馈者全程可见,极大地提升了环保工作的透明度与处理效率。 + +### 1.1.2 个人贡献 + +在本次实践中,本人担任**核心的系统设计与后端开发角色**,全面负责了从技术选型到项目落地的完整技术实现。我的工作不仅是代码的编写者,更是整个后端架构的**设计者**和**构建者**。具体贡献如下: + +* **架构设计与技术决策**:在项目初期,我主导了技术栈的选型,力主采用`Spring Boot` + `Vue.js`的前后端分离架构,以应对未来快速迭代和功能扩展的需求。同时,我独立设计了后端**Controller-Service-Repository**的三层应用架构,并规划了项目的整体模块(如安全、事件、仓储等),为整个项目的稳固开发奠定了基础。 + +* **核心难题攻关**:面对"所有数据需以文件形式存储"这一核心且棘手的约束,我没有采取简单的文件读写,而是独立设计并实现了一套**模拟JPA规范的泛型JSON仓储层**。这个创新方案不仅完美地解决了数据持久化问题,其遵循的"依赖倒置原则"也使得代码高度解耦,极大地提升了系统的可维护性和可测试性,是本次项目中技术含量最高的突破之一。 + +* **全栈功能实现**:我负责了全部后端模块的编码实现,包括但不限于基于`Spring Security`和`JWT`的**用户认证与授权系统**、**任务全生命周期的状态机管理**、以及基于**A*算法的智能任务分配模型**等。此外,我也参与了部分前端页面的开发,并利用`Swagger`和`Postman`完成了所有API的设计、文档化与测试工作,确保了前后端的顺畅联调。 + +## 1.2 个人能力培养 + +通过本次实践,本人将软件工程理论与开发实践深度结合,在设计与开发复杂问题的解决方案方面获得了显著提升。 + +* **软件工程全周期掌控能力**:通过完整地参与从需求分析、系统设计、编码实现到测试部署的全过程,深刻掌握了现代软件开发的生命周期要素和方法。能够运用面向对象的思想,通过UML对系统进行建模,合理地划分模块,确保了系统设计的高内聚、低耦合。 + +* **复杂问题解决方案的设计与创新能力**: + * 面对"数据以文件格式保存"的核心约束,没有采用简单的序列化读写,而是创新性地设计并实现了一套**基于JSON的泛型仓储层(Repository Pattern)**。该方案遵循了依赖倒置原则,不仅满足了功能要求,更保证了代码的可维护性与未来向数据库迁移的可行性。 + * 针对任务分配场景,设计了**基于多维因素(地理位置、负载)的智能调度算法**,并集成了**A*寻路算法**进行路径规划,展现了运用科学算法解决实际工程问题的能力。 + * 在系统中有效运用了**事件驱动模型**,将耗时的AI分析流程解耦为异步操作,提升了系统的响应速度和健壮性。 + +* **技术沟通与文档化能力**:能够通过撰写系统设计方案、绘制UML图表等方式,系统、清晰地阐述项目的技术架构与实现细节,并具备良好的口头技术交流能力。 + +# 2. 相关技术基础 + +本项目综合运用了业界主流的开发技术与环境,构建了一个现代化Web应用。整体技术体系由以下五部分构成: + +* **理论基础**:遵循**面向对象编程(OOP)**、**SOLID设计原则(尤其是DIP)**、**MVC分层架构**及**RESTful API**设计规范。 +* **后端技术栈**:以 **`Spring Boot 3`** 为核心,整合 **`Spring Security`** 与 **`JWT`** 进行安全控制,并创新性地设计了**自定义的泛型JSON仓储层**作为持久化方案。 +* **前端技术栈**:采用 **`Vue 3`** 作为核心框架,配合 **`Vite`** 进行构建,使用 **`Pinia`** 进行状态管理,**`Element Plus`** 作为UI组件库,并对 **`Axios`** 进行了封装。 +* **测试技术**:后端采用 **`JUnit 5`** 和 **`Mockito`** 进行单元/集成测试;接口层使用 **`Swagger UI`** 和 **`Postman`** 进行测试与调试。 +* **开发工具与环境**:使用 **`IntelliJ IDEA`** 和 **`VS Code`** 作为主力IDE,通过 **`Maven`** 和 **`npm`** 管理项目,并利用 **`Git`** 进行版本控制。 + +以下是各部分的详细介绍。 + +## 2.1 理论基础 + +项目的架构和代码设计遵循了下述成熟的软件工程理论,以确保系统的可维护性和扩展性。 + +* **面向对象编程 (OOP):** + 项目的核心编程思想,其封装、继承、多态特性在后端代码设计中得到了深入应用。 + * **封装 (Encapsulation):** 核心业务实体(如`User`, `Task`)被抽象为Java类,其属性私有化,仅通过公共方法暴露,保证了对象状态的完整性。 + * **继承 (Inheritance) 与多态 (Polymorphism):** 在自定义的JSON仓储层设计中,通过定义泛型接口`JsonRepository`和实现该接口的泛型基类`JsonRepositoryImpl`,使得具体的实体仓储(如`JsonUserRepositoryImpl`)能自动获得通用的数据操作能力。业务逻辑层依赖于抽象接口而非具体实现,实现了"面向接口编程",提高了代码的灵活性。 + +* **SOLID设计原则,尤其是依赖倒置原则 (DIP):** + 项目架构遵循SOLID原则,特别是依赖倒置原则,即"高层模块不应依赖于低层模块,两者都应依赖于抽象"。 + * **实践应用:** `Service`层(高层模块)依赖的是`UserRepository`等抽象接口,而不是`JsonRepositoryImpl`这样的具体实现(低层模块)。这种设计倒置了传统的依赖关系,极大提升了系统的可替换性和可测试性。未来更换数据源或进行单元测试时,只需更换实现或模拟接口,上层代码无需改动。 + +* **模型-视图-控制器 (MVC) 分层架构:** + 项目遵循MVC分层思想,并扩展为更精细的四层架构:Controller -> Service -> Repository -> Entity。 + * **Controller (控制器层):** 作为HTTP请求的入口,负责解析请求参数,调用`Service`层执行业务逻辑,并返回响应。 + * **Service (业务逻辑层):** 包含所有业务规则和处理流程,通过依赖注入(DI)调用`Repository`层。 + * **Repository (数据访问层):** 数据持久化的抽象,负责与数据源(本项目中为JSON文件)交互,实现业务与数据的解耦。 + * **Entity/DTO (模型层):** POJO对象,`Entity`映射数据存储结构,`DTO`(Data Transfer Object)用于各层之间的数据传输,避免持久化实体直接暴露给外部。 + +* **RESTful API 设计原则:** + 前后端通信完全基于RESTful风格的API进行。 + * **统一接口 (Uniform Interface):** 使用HTTP标准方法(`GET`, `POST`, `PUT`, `DELETE`)表达对资源的操作。 + * **无状态 (Stateless):** 服务端不保存客户端会话状态,每次请求都包含所有必要信息(如JWT),提高了系统的可伸缩性。 + * **资源导向 (Resource-Oriented):** API围绕"资源"展开,使用名词(如`/api/users`)标识资源,而非动词。 + +## 2.2 后端技术栈 + +* **核心框架 - `Spring Boot 3.x`:** + 作为后端应用基石,它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发、配置和部署。其强大的生态整合能力,使得集成`Spring Security`、`Lombok`、`Swagger`等第三方库变得非常简单。 + +* **安全框架 - `Spring Security 6.x` & `JWT`:** + 采用`Spring Security`结合`JSON Web Tokens (JWT)`,构建无状态的认证与授权体系。 + * **`Spring Security` Filter链:** 自定义`JwtAuthenticationFilter`过滤器,用于在每个请求中校验JWT并设置安全上下文。 + * **声明式权限控制:** 使用`@PreAuthorize`注解对Service方法进行方法级别的权限声明,实现了安全逻辑与业务逻辑的解耦。 + * **无状态会话 (Stateless Session):** 配置会话管理策略为`STATELESS`,使后端服务成为真正的无状态服务,提升了系统的可伸缩性。 + +* **持久化方案 - 自定义泛型JSON仓储层:** + 为满足"数据存储在JSON文件中"的需求,设计并实现了一套模拟`Spring Data JPA`接口规范的、可复用的泛型JSON仓储层。 + * **`JsonStorageService`:** 将所有文件I/O操作封装在此服务中,并使用`synchronized`块确保并发写操作的线程安全。 + * **`JsonRepositoryImpl`:** 泛型基类,实现了通用的CRUD功能,具体的实体仓储通过继承该类来复用代码。 + * 该方案遵循**依赖倒置原则**,业务层仅依赖抽象接口,为未来平滑迁移持久化方案提供了便利。 + +* **数据校验 - `Jakarta Bean Validation` (`Hibernate Validator`):** + 在DTO的字段上使用`@NotNull`, `@Size`等声明式注解,并配合Controller层的`@Valid`注解,实现对请求参数的自动化校验。 + +* **对象映射 - `MapStruct`:** + 一个编译期的代码生成器,通过定义`@Mapper`接口,自动生成Entity与DTO之间转换的高性能实现代码,提升了开发效率。 + +* **编程语言与辅助工具:** + * **`Java 17 (LTS)`:** 使用其`record`、`switch`表达式等新特性编写更简洁的代码。 + * **`Lombok`:** 通过`@Data`, `@Builder`等注解,消除样板代码。 + * **`SLF4J` & `Logback`:** 灵活的日志系统,通过配置文件实现分环境、分级别的日志输出。 + +## 2.3 前端技术栈 + +采用`Vue.js`生态的最新技术,构建响应迅速、代码可维护的现代化单页应用(SPA)。 + +* **核心框架 - `Vue.js 3.x`:** + 利用其两大核心新特性: + * **`Composition API` (组合式API):** 将相关逻辑组织在同一个`setup`函数内,提高了代码的可读性、可维护性和逻辑复用性。 + * **`Proxy`-based Reactivity:** 基于`Proxy`重写的响应式系统,解决了`Vue 2`中无法监听对象属性新增/删除等痛点。 + +* **构建工具 - `Vite`:** + 新一代前端构建工具,优势在于: + * 利用浏览器原生ESM支持,实现开发环境下的极速冷启动和闪电般的热模块替换(HMR)。 + * 生产环境使用`Rollup`打包,生成高度优化的静态资源。 + +* **状态管理 - `Pinia`:** + `Vue`官方推荐的下一代状态管理库,特点是API直观、类型支持完美,且天生模块化。废除了`Vuex`中复杂的`Mutations`等概念,心智负担小。 + +* **UI组件库 - `Element Plus`:** + `Element UI`的`Vue 3`版本,是一套高质量的企业级UI组件库,提供了丰富的组件,极大地加速了界面的开发进程。 + +* **HTTP客户端 - `Axios`封装:** + 对流行的`Axios`库进行二次封装: + * **创建实例:** 为API服务设置不同的`baseURL`、`timeout`等。 + * **请求拦截器:** 统一添加认证`token`。 + * **响应拦截器:** 统一处理业务数据和错误状态码。 + +## 2.4 测试技术 + +* **后端单元与集成测试 (`JUnit 5`, `Mockito`, `Spring Boot Test`):** + 使用`spring-boot-starter-test`集成的测试套件保障业务逻辑正确性。 + * **`JUnit 5`:** 测试的基础框架。 + * **`Mockito`:** 在单元测试中用于模拟依赖对象(如Repository),实现测试隔离。 + * **`Spring Boot Test`:** 用于集成测试,加载完整应用上下文,测试跨层级的真实交互场景。 + +* **API接口测试 (`Postman` / `Swagger UI`):** + * **`Swagger UI`:** 通过集成`SpringDoc`,根据代码注解自动生成交互式API文档,方便调试。 + * **`Postman`:** 用于执行更复杂的API测试场景、编写测试用例和自动化测试。 + +## 2.5 开发工具与环境 + +* **IDE:** 后端使用`IntelliJ IDEA Ultimate`,前端使用`Visual Studio Code`。 +* **项目管理与构建:** 后端使用`Maven`,前端使用`npm`。 +* **版本控制:** `Git` & `GitHub`,遵循`Git Flow`工作流。 + +# 3. 实践结果 + +此部分属报告的主要部分。包括: + +## 3.1 需求定义 + +"系统分析"也可以看成是需求定义,包括对整个项目的介绍分析及本人工作内容的详细分析,如业务分析、功能分析(可使用例图、活动图来描述)、可行性分析等; + +## 3.2 系统设计 + +"系统设计"包括总体设计和详细设计,"总体设计"包括系统架构设计、功能模块划分等,"详细设计"要围绕本人工作内容展开,包括功能模块详细设计、类和对象的设计、动态模型设计(时序图、状态图、协作图等)、算法设计、数据库设计等; + +## 3.3 系统实现 + +"系统实现"也要围绕本人工作内容展开,从编码实现角度论述相应功能模块的实现细节,并展示自己所完成的主要成果及实际应用情况等。可通过"程序流程图"、"关键代码"和"界面"进行直观论述。 + +## 3.4 系统测试 + +"系统测试"包括测试方案设计、测试用例和测试结果、最终的测试结论或评价等。 + +# 4. 实践总结 + +简述你在实践过程中的内容完成情况,重点介绍创新点及不足(也就是可以再完善的部分,只是时间不允许了。不足不代表不好,也说明你思考了,但是来不及完成实现) + +# 5. 参考资料 + +例: +[1] 数据结构、算法与应用:C++语言描述 [Data Structures,Algorithms,and Applications in C++][M].机械工业出版社出版时间:2000-01-01. +[2] 数据结构(C语言版) [M].北京: 中国铁道出版社, 2011-08-01. + --- - + ### Report/时序图.md - -# EMS后端系统技术图表文档 - -## 项目概述 - -环境管理系统(EMS)后端是一个基于Spring Boot的Java应用程序,用于处理环境问题反馈、任务分配和用户管理。系统采用分层架构,包含控制器、服务、仓库和模型层。 - -## 1. 系统时序图 - -### 1.1 用户登录认证流程 - -```mermaid -sequenceDiagram - participant User as 用户 - participant AuthController as 认证控制器 - participant AuthService as 认证服务 - participant UserRepository as 用户仓库 - participant JwtUtil as JWT工具 - participant Database as JSON持久化存储 - - User->>AuthController: POST /api/auth/login - AuthController->>AuthService: signIn(LoginRequest) - AuthService->>UserRepository: findByEmailOrPhone() - UserRepository->>Database: 查询用户信息 - Database-->>UserRepository: 返回用户数据 - UserRepository-->>AuthService: 返回UserAccount - AuthService->>AuthService: 验证密码 - AuthService->>JwtUtil: 生成JWT令牌 - JwtUtil-->>AuthService: 返回JWT - AuthService-->>AuthController: JwtAuthenticationResponse - AuthController-->>User: 返回JWT令牌 -``` - -### 1.2 反馈提交处理流程 - -```mermaid -sequenceDiagram - participant User as 用户 - participant FeedbackController as 反馈控制器 - participant FeedbackService as 反馈服务 - participant FeedbackRepository as 反馈仓库 - participant AIService as AI服务 - participant EventPublisher as 事件发布器 - participant Database as JSON持久化存储 - - User->>FeedbackController: POST /api/feedback/submit - FeedbackController->>FeedbackService: submitFeedback(request, files) - FeedbackService->>FeedbackService: 生成事件ID - FeedbackService->>FeedbackRepository: save(feedback) - FeedbackRepository->>Database: 保存反馈数据 - Database-->>FeedbackRepository: 返回保存结果 - FeedbackRepository-->>FeedbackService: 返回Feedback实体 - FeedbackService->>EventPublisher: 发布反馈创建事件 - EventPublisher->>AIService: 触发AI处理 - AIService->>AIService: 分析反馈内容 - FeedbackService-->>FeedbackController: 返回Feedback - FeedbackController-->>User: 201 Created -``` - -### 1.3 任务分配流程 - -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant TaskController as 任务控制器 - participant TaskService as 任务服务 - participant AssignmentService as 分配服务 - participant UserRepository as 用户仓库 - participant TaskRepository as 任务仓库 - participant GridWorker as 网格工作人员 - - Supervisor->>TaskController: POST /api/tasks/assign - TaskController->>TaskService: assignTask(taskId, workerId) - TaskService->>UserRepository: findById(workerId) - UserRepository-->>TaskService: 返回GridWorker - TaskService->>AssignmentService: createAssignment() - AssignmentService->>TaskRepository: updateTaskStatus() - TaskRepository-->>AssignmentService: 更新成功 - AssignmentService-->>TaskService: Assignment创建成功 - TaskService->>TaskService: 发送通知给工作人员 - TaskService-->>TaskController: 分配成功 - TaskController-->>Supervisor: 200 OK - Note over GridWorker: 接收任务通知 -``` - -### 1.4 主管审核反馈并创建任务 - -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant FeedbackController as 反馈控制器 - participant FeedbackService as 反馈服务 - participant TaskService as 任务服务 - participant TaskRepository as 任务仓库 - participant Database as JSON持久化存储 - - Supervisor->>FeedbackController: POST /api/feedback/{id}/process - FeedbackController->>FeedbackService: processFeedback(feedbackId, request) - FeedbackService->>FeedbackService: 验证反馈状态和主管权限 - alt 同意反馈并创建任务 - FeedbackService->>TaskService: createTaskFromFeedback(feedback) - TaskService->>TaskRepository: save(task) - TaskRepository->>Database: 保存新任务 - Database-->>TaskRepository: 返回保存的任务 - TaskRepository-->>TaskService: 返回Task实体 - TaskService->>FeedbackService: 更新反馈状态为PENDING_ASSIGNMENT - FeedbackService-->>FeedbackController: 返回处理结果 - else 拒绝反馈 - FeedbackService->>FeedbackService: 更新反馈状态为REJECTED - FeedbackService-->>FeedbackController: 返回处理结果 - end - FeedbackController-->>Supervisor: 200 OK -``` - -### 1.5 用户密码重置流程 - -```mermaid -sequenceDiagram - participant User as 用户 - participant AuthController as 认证控制器 - participant AuthService as 认证服务 - participant VerificationCodeService as 验证码服务 - participant MailService as 邮件服务 - participant UserRepository as 用户仓库 - - User->>AuthController: POST /api/auth/send-password-reset-code (email) - AuthController->>AuthService: requestPasswordReset(email) - AuthService->>UserRepository: findByEmail(email) - UserRepository-->>AuthService: 返回UserAccount - AuthService->>VerificationCodeService: createAndSendPasswordResetCode(user) - VerificationCodeService->>MailService: sendEmail(to, subject, content) - MailService-->>VerificationCodeService: 邮件发送成功 - VerificationCodeService-->>AuthService: 验证码发送成功 - AuthService-->>AuthController: 200 OK - AuthController-->>User: 提示验证码已发送 - - User->>AuthController: POST /api/auth/reset-password-with-code (email, code, newPassword) - AuthController->>AuthService: resetPasswordWithCode(email, code, newPassword) - AuthService->>VerificationCodeService: validateCode(email, code) - VerificationCodeService-->>AuthService: 验证码有效 - AuthService->>UserRepository: save(user) with new password - UserRepository-->>AuthService: 用户密码更新成功 - AuthService-->>AuthController: 200 OK - AuthController-->>User: 密码重置成功 -``` - -### 1.6 用户获取自己的反馈历史 - -```mermaid -sequenceDiagram - participant User as 用户 - participant ProfileController as 个人资料控制器 - participant UserFeedbackService as 用户反馈服务 - participant FeedbackRepository as 反馈仓库 - participant Database as JSON持久化存储 - - User->>ProfileController: GET /api/me/feedback - ProfileController->>UserFeedbackService: getFeedbackHistoryByUserId(userId, pageable) - UserFeedbackService->>FeedbackRepository: findBySubmitterId(userId, pageable) - FeedbackRepository->>Database: 查询用户反馈数据 - Database-->>FeedbackRepository: 返回反馈数据 - FeedbackRepository-->>UserFeedbackService: 返回Page - UserFeedbackService->>UserFeedbackService: 转换为UserFeedbackSummaryDTO列表 - UserFeedbackService-->>ProfileController: 返回Page - ProfileController-->>User: 返回反馈历史列表 -``` - -### 1.7 网格员获取和管理任务 - -```mermaid -sequenceDiagram - participant GridWorker as 网格员 - participant GridWorkerTaskController as 网格员任务控制器 - participant GridWorkerTaskService as 网格员任务服务 - participant TaskRepository as 任务仓库 - participant Database as JSON持久化存储 - - alt 获取任务列表 - GridWorker->>GridWorkerTaskController: GET /api/worker/tasks - GridWorkerTaskController->>GridWorkerTaskService: getAssignedTasks(workerId, status, pageable) - GridWorkerTaskService->>TaskRepository: findByAssigneeIdAndStatus(workerId, status, pageable) - TaskRepository->>Database: 查询任务数据 - Database-->>TaskRepository: 返回任务数据 - TaskRepository-->>GridWorkerTaskService: 返回Page - GridWorkerTaskService->>GridWorkerTaskService: 转换为TaskSummaryDTO列表 - GridWorkerTaskService-->>GridWorkerTaskController: 返回Page - GridWorkerTaskController-->>GridWorker: 返回任务列表 - end - - alt 接受任务 - GridWorker->>GridWorkerTaskController: POST /api/worker/tasks/{taskId}/accept - GridWorkerTaskController->>GridWorkerTaskService: acceptTask(taskId, workerId) - GridWorkerTaskService->>TaskRepository: findById(taskId) - TaskRepository->>Database: 查询任务 - Database-->>TaskRepository: 返回任务 - TaskRepository-->>GridWorkerTaskService: 返回Task - GridWorkerTaskService->>GridWorkerTaskService: 验证任务状态并更新为ACCEPTED - GridWorkerTaskService->>TaskRepository: save(task) - TaskRepository->>Database: 更新任务状态 - Database-->>TaskRepository: 返回更新后的任务 - TaskRepository-->>GridWorkerTaskService: 返回更新后的Task - GridWorkerTaskService->>GridWorkerTaskService: 转换为TaskSummaryDTO - GridWorkerTaskService-->>GridWorkerTaskController: 返回TaskSummaryDTO - GridWorkerTaskController-->>GridWorker: 返回更新后的任务摘要 - end - - alt 提交任务 - GridWorker->>GridWorkerTaskController: POST /api/worker/tasks/{taskId}/submit - GridWorkerTaskController->>GridWorkerTaskService: submitTaskCompletion(taskId, workerId, request, files) - GridWorkerTaskService->>TaskRepository: findById(taskId) - TaskRepository->>Database: 查询任务 - Database-->>TaskRepository: 返回任务 - TaskRepository-->>GridWorkerTaskService: 返回Task - GridWorkerTaskService->>GridWorkerTaskService: 验证并更新任务状态为COMPLETED - GridWorkerTaskService->>GridWorkerTaskService: (如果存在)处理附件上传 - GridWorkerTaskService->>TaskRepository: save(task) - TaskRepository->>Database: 更新任务 - Database-->>TaskRepository: 返回更新后的任务 - TaskRepository-->>GridWorkerTaskService: 返回更新后的Task - GridWorkerTaskService->>GridWorkerTaskService: 转换为TaskSummaryDTO - GridWorkerTaskService-->>GridWorkerTaskController: 返回TaskSummaryDTO - GridWorkerTaskController-->>GridWorker: 返回更新后的任务摘要 - end -``` - -### 1.8 决策者获取仪表盘数据 - -```mermaid -sequenceDiagram - participant DecisionMaker as 决策者 - participant DashboardController as 仪表盘控制器 - participant DashboardService as 仪表盘服务 - participant variousRepositories as 各类仓库 - participant Database as JSON持久化存储 - - alt 获取核心统计数据 - DecisionMaker->>DashboardController: GET /api/dashboard/stats - DashboardController->>DashboardService: getDashboardStats() - DashboardService->>variousRepositories: (并行)调用多个仓库方法获取数据 - variousRepositories->>Database: 查询统计数据 - Database-->>variousRepositories: 返回数据 - variousRepositories-->>DashboardService: 返回统计结果 - DashboardService->>DashboardService: 聚合数据为DashboardStatsDTO - DashboardService-->>DashboardController: 返回DashboardStatsDTO - DashboardController-->>DecisionMaker: 返回核心统计数据 - end - - alt 获取AQI分布 - DecisionMaker->>DashboardController: GET /api/dashboard/reports/aqi-distribution - DashboardController->>DashboardService: getAqiDistribution() - DashboardService->>variousRepositories: 查询AQI数据并分组统计 - variousRepositories->>Database: 查询AQI数据 - Database-->>variousRepositories: 返回数据 - variousRepositories-->>DashboardService: 返回统计结果 - DashboardService->>DashboardService: 转换为AqiDistributionDTO列表 - DashboardService-->>DashboardController: 返回List - DashboardController-->>DecisionMaker: 返回AQI等级分布数据 - end - - alt 获取热力图数据 - DecisionMaker->>DashboardController: GET /api/dashboard/map/heatmap - DashboardController->>DashboardService: getHeatmapData() - DashboardService->>variousRepositories: 查询反馈的地理位置数据 - variousRepositories->>Database: 查询地理位置数据 - Database-->>variousRepositories: 返回数据 - variousRepositories-->>DashboardService: 返回位置数据列表 - DashboardService->>DashboardService: 转换为HeatmapPointDTO列表 - DashboardService-->>DashboardController: 返回List - DashboardController-->>DecisionMaker: 返回热力图数据 - end -``` - -### 1.9 主管审核反馈 - -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant SupervisorController as 主管控制器 - participant SupervisorService as 主管服务 - participant FeedbackRepository as 反馈仓库 - participant Database as JSON持久化存储 - - alt 获取待审核列表 - Supervisor->>SupervisorController: GET /api/supervisor/reviews - SupervisorController->>SupervisorService: getFeedbackForReview() - SupervisorService->>FeedbackRepository: findByStatus(PENDING_REVIEW) - FeedbackRepository->>Database: 查询待审核反馈 - Database-->>FeedbackRepository: 返回反馈列表 - FeedbackRepository-->>SupervisorService: 返回List - SupervisorService-->>SupervisorController: 返回List - SupervisorController-->>Supervisor: 显示待审核列表 - end - - alt 批准反馈 - Supervisor->>SupervisorController: POST /api/supervisor/reviews/{feedbackId}/approve - SupervisorController->>SupervisorService: approveFeedback(feedbackId) - SupervisorService->>FeedbackRepository: findById(feedbackId) - FeedbackRepository->>Database: 查询反馈 - Database-->>FeedbackRepository: 返回反馈 - FeedbackRepository-->>SupervisorService: 返回Feedback - SupervisorService->>SupervisorService: 更新反馈状态为APPROVED - SupervisorService->>FeedbackRepository: save(feedback) - TaskRepository->>Database: 更新反馈状态 - Database-->>TaskRepository: 返回更新后的反馈 - TaskRepository-->>SupervisorService: 返回更新后的Feedback - SupervisorService-->>SupervisorController: 返回成功响应 - SupervisorController-->>Supervisor: 操作成功 - end - - alt 拒绝反馈 - Supervisor->>SupervisorController: POST /api/supervisor/reviews/{feedbackId}/reject - SupervisorController->>SupervisorService: rejectFeedback(feedbackId, request) - SupervisorService->>FeedbackRepository: findById(feedbackId) - FeedbackRepository->>Database: 查询反馈 - Database-->>FeedbackRepository: 返回反馈 - FeedbackRepository-->>SupervisorService: 返回Feedback - SupervisorService->>SupervisorService: 更新反馈状态为REJECTED并记录原因 - SupervisorService->>FeedbackRepository: save(feedback) - TaskRepository->>Database: 更新反馈状态 - Database-->>TaskRepository: 返回更新后的反馈 - TaskRepository-->>SupervisorService: 返回更新后的Feedback - SupervisorService-->>SupervisorController: 返回成功响应 - SupervisorController-->>Supervisor: 操作成功 - end -``` - -### 1.10 路径规划 (A* 寻路算法) - -```mermaid -sequenceDiagram - participant User as 用户 - participant PathfindingController as 寻路控制器 - participant AStarService as A*服务 - participant MapData as 地图数据 - - User->>+PathfindingController: POST /api/pathfinding/find (起点, 终点) - PathfindingController->>+AStarService: findPath(start, end) - AStarService->>+MapData: getObstacles() - MapData-->>-AStarService: 返回障碍物信息 - AStarService->>AStarService: 执行A*算法计算路径 - AStarService-->>-PathfindingController: 返回计算出的路径 - PathfindingController-->>-User: 200 OK (路径坐标列表) -``` - -### 1.11 公众提交反馈 - -```mermaid -sequenceDiagram - participant User as 用户 - participant FeedbackController as 反馈控制器 - participant FeedbackService as 反馈服务 - participant FeedbackRepository as 反馈仓库 - participant FileService as 文件服务 - participant Database as JSON持久化存储 - - User->>+FeedbackController: POST /api/feedback/submit (反馈信息, 附件) - FeedbackController->>+FeedbackService: submitFeedback(request, files) - alt 包含附件 - FeedbackService->>+FileService: store(files) - FileService-->>-FeedbackService: 返回文件存储路径 - end - FeedbackService->>+FeedbackRepository: save(feedback) - FeedbackRepository->>+Database: INSERT INTO feedbacks - Database-->>-FeedbackRepository: 返回已保存的反馈 - FeedbackRepository-->>-FeedbackService: 返回已保存的反馈 - FeedbackService-->>-FeedbackController: 返回创建的反馈 - FeedbackController-->>-User: 201 CREATED (反馈详情) -``` - -### 1.12 文件下载/预览 - -```mermaid -sequenceDiagram - participant User as 用户 - participant FileController as 文件控制器 - participant FileStorageService as 文件存储服务 - participant FileSystem as 文件系统 - - User->>+FileController: GET /api/files/{filename} 或 /api/view/{filename} - FileController->>+FileStorageService: loadFileAsResource(filename) - FileStorageService->>+FileSystem: 读取文件 - FileSystem-->>-FileStorageService: 返回文件资源 - FileStorageService-->>-FileController: 返回文件资源 - FileController->>FileController: 设置HTTP响应头 (Content-Type, Content-Disposition) - FileController-->>-User: 200 OK (文件内容) -``` - -### 1.13 管理员分配网格员 - -```mermaid -sequenceDiagram - participant Admin as 管理员 - participant GridController as 网格控制器 - participant UserAccountService as 用户账户服务 - participant GridService as 网格服务 - participant OperationLogService as 操作日志服务 - participant Database as JSON持久化存储 - - Admin->>GridController: POST /api/grids/assign-worker (userId, gridId) - activate GridController - - GridController->>UserAccountService: getUserById(userId) - activate UserAccountService - UserAccountService->>Database: 查询用户 - Database-->>UserAccountService: 返回用户信息 - UserAccountService-->>GridController: 返回用户 - deactivate UserAccountService - - alt 用户角色是网格员 (role == 'GRID_WORKER') - GridController->>GridService: assignGridToUser(userId, gridId) - activate GridService - GridService->>Database: 更新用户的 grid_id - Database-->>GridService: 更新成功 - GridService-->>GridController: 分配成功 - deactivate GridService - - GridController->>OperationLogService: recordOperation(adminId, 'ASSIGN_GRID', '...details...') - activate OperationLogService - OperationLogService->>Database: 记录日志 - Database-->>OperationLogService: 记录成功 - OperationLogService-->>GridController: 记录完成 - deactivate OperationLogService - - GridController-->>Admin: 200 OK - 分配成功 - else 用户角色不是网格员 - GridController-->>Admin: 400 Bad Request - 用户不是网格员 - end - deactivate GridController -``` - -### 1.14 初始化地图 - -```mermaid -sequenceDiagram - participant Admin as 管理员 - participant MapController as 地图控制器 - participant MapGridRepository as 地图网格仓库 - participant Database as JSON持久化存储 - - Admin->>+MapController: POST /api/map/initialize (width, height) - MapController->>+MapGridRepository: deleteAll() - MapGridRepository->>+Database: DELETE FROM map_grids - Database-->>-MapGridRepository: 删除成功 - MapGridRepository-->>-MapController: 返回 - loop for y from 0 to height-1 - loop for x from 0 to width-1 - MapController->>+MapGridRepository: save(cell) - MapGridRepository->>+Database: INSERT INTO map_grids - Database-->>-MapGridRepository: 插入成功 - MapGridRepository-->>-MapController: 返回 - end - end - MapController-->>-Admin: 200 OK (Initialized a WxH map.) -``` - -### 1.15 管理员获取操作日志 - -```mermaid -sequenceDiagram - participant Admin as 管理员 - participant OperationLogController as 操作日志控制器 - participant OperationLogService as 操作日志服务 - participant OperationLogRepository as 操作日志仓库 -``` + +# EMS后端系统技术图表文档 + +## 项目概述 + +环境管理系统(EMS)后端是一个基于Spring Boot的Java应用程序,用于处理环境问题反馈、任务分配和用户管理。系统采用分层架构,包含控制器、服务、仓库和模型层。 + +## 1. 系统时序图 + +### 1.1 用户登录认证流程 + +```mermaid +sequenceDiagram + participant User as 用户 + participant AuthController as 认证控制器 + participant AuthService as 认证服务 + participant UserRepository as 用户仓库 + participant JwtUtil as JWT工具 + participant Database as JSON持久化存储 + + User->>AuthController: POST /api/auth/login + AuthController->>AuthService: signIn(LoginRequest) + AuthService->>UserRepository: findByEmailOrPhone() + UserRepository->>Database: 查询用户信息 + Database-->>UserRepository: 返回用户数据 + UserRepository-->>AuthService: 返回UserAccount + AuthService->>AuthService: 验证密码 + AuthService->>JwtUtil: 生成JWT令牌 + JwtUtil-->>AuthService: 返回JWT + AuthService-->>AuthController: JwtAuthenticationResponse + AuthController-->>User: 返回JWT令牌 +``` + +### 1.2 反馈提交处理流程 + +```mermaid +sequenceDiagram + participant User as 用户 + participant FeedbackController as 反馈控制器 + participant FeedbackService as 反馈服务 + participant FeedbackRepository as 反馈仓库 + participant AIService as AI服务 + participant EventPublisher as 事件发布器 + participant Database as JSON持久化存储 + + User->>FeedbackController: POST /api/feedback/submit + FeedbackController->>FeedbackService: submitFeedback(request, files) + FeedbackService->>FeedbackService: 生成事件ID + FeedbackService->>FeedbackRepository: save(feedback) + FeedbackRepository->>Database: 保存反馈数据 + Database-->>FeedbackRepository: 返回保存结果 + FeedbackRepository-->>FeedbackService: 返回Feedback实体 + FeedbackService->>EventPublisher: 发布反馈创建事件 + EventPublisher->>AIService: 触发AI处理 + AIService->>AIService: 分析反馈内容 + FeedbackService-->>FeedbackController: 返回Feedback + FeedbackController-->>User: 201 Created +``` + +### 1.3 任务分配流程 + +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant TaskController as 任务控制器 + participant TaskService as 任务服务 + participant AssignmentService as 分配服务 + participant UserRepository as 用户仓库 + participant TaskRepository as 任务仓库 + participant GridWorker as 网格工作人员 + + Supervisor->>TaskController: POST /api/tasks/assign + TaskController->>TaskService: assignTask(taskId, workerId) + TaskService->>UserRepository: findById(workerId) + UserRepository-->>TaskService: 返回GridWorker + TaskService->>AssignmentService: createAssignment() + AssignmentService->>TaskRepository: updateTaskStatus() + TaskRepository-->>AssignmentService: 更新成功 + AssignmentService-->>TaskService: Assignment创建成功 + TaskService->>TaskService: 发送通知给工作人员 + TaskService-->>TaskController: 分配成功 + TaskController-->>Supervisor: 200 OK + Note over GridWorker: 接收任务通知 +``` + +### 1.4 主管审核反馈并创建任务 + +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant FeedbackController as 反馈控制器 + participant FeedbackService as 反馈服务 + participant TaskService as 任务服务 + participant TaskRepository as 任务仓库 + participant Database as JSON持久化存储 + + Supervisor->>FeedbackController: POST /api/feedback/{id}/process + FeedbackController->>FeedbackService: processFeedback(feedbackId, request) + FeedbackService->>FeedbackService: 验证反馈状态和主管权限 + alt 同意反馈并创建任务 + FeedbackService->>TaskService: createTaskFromFeedback(feedback) + TaskService->>TaskRepository: save(task) + TaskRepository->>Database: 保存新任务 + Database-->>TaskRepository: 返回保存的任务 + TaskRepository-->>TaskService: 返回Task实体 + TaskService->>FeedbackService: 更新反馈状态为PENDING_ASSIGNMENT + FeedbackService-->>FeedbackController: 返回处理结果 + else 拒绝反馈 + FeedbackService->>FeedbackService: 更新反馈状态为REJECTED + FeedbackService-->>FeedbackController: 返回处理结果 + end + FeedbackController-->>Supervisor: 200 OK +``` + +### 1.5 用户密码重置流程 + +```mermaid +sequenceDiagram + participant User as 用户 + participant AuthController as 认证控制器 + participant AuthService as 认证服务 + participant VerificationCodeService as 验证码服务 + participant MailService as 邮件服务 + participant UserRepository as 用户仓库 + + User->>AuthController: POST /api/auth/send-password-reset-code (email) + AuthController->>AuthService: requestPasswordReset(email) + AuthService->>UserRepository: findByEmail(email) + UserRepository-->>AuthService: 返回UserAccount + AuthService->>VerificationCodeService: createAndSendPasswordResetCode(user) + VerificationCodeService->>MailService: sendEmail(to, subject, content) + MailService-->>VerificationCodeService: 邮件发送成功 + VerificationCodeService-->>AuthService: 验证码发送成功 + AuthService-->>AuthController: 200 OK + AuthController-->>User: 提示验证码已发送 + + User->>AuthController: POST /api/auth/reset-password-with-code (email, code, newPassword) + AuthController->>AuthService: resetPasswordWithCode(email, code, newPassword) + AuthService->>VerificationCodeService: validateCode(email, code) + VerificationCodeService-->>AuthService: 验证码有效 + AuthService->>UserRepository: save(user) with new password + UserRepository-->>AuthService: 用户密码更新成功 + AuthService-->>AuthController: 200 OK + AuthController-->>User: 密码重置成功 +``` + +### 1.6 用户获取自己的反馈历史 + +```mermaid +sequenceDiagram + participant User as 用户 + participant ProfileController as 个人资料控制器 + participant UserFeedbackService as 用户反馈服务 + participant FeedbackRepository as 反馈仓库 + participant Database as JSON持久化存储 + + User->>ProfileController: GET /api/me/feedback + ProfileController->>UserFeedbackService: getFeedbackHistoryByUserId(userId, pageable) + UserFeedbackService->>FeedbackRepository: findBySubmitterId(userId, pageable) + FeedbackRepository->>Database: 查询用户反馈数据 + Database-->>FeedbackRepository: 返回反馈数据 + FeedbackRepository-->>UserFeedbackService: 返回Page + UserFeedbackService->>UserFeedbackService: 转换为UserFeedbackSummaryDTO列表 + UserFeedbackService-->>ProfileController: 返回Page + ProfileController-->>User: 返回反馈历史列表 +``` + +### 1.7 网格员获取和管理任务 + +```mermaid +sequenceDiagram + participant GridWorker as 网格员 + participant GridWorkerTaskController as 网格员任务控制器 + participant GridWorkerTaskService as 网格员任务服务 + participant TaskRepository as 任务仓库 + participant Database as JSON持久化存储 + + alt 获取任务列表 + GridWorker->>GridWorkerTaskController: GET /api/worker/tasks + GridWorkerTaskController->>GridWorkerTaskService: getAssignedTasks(workerId, status, pageable) + GridWorkerTaskService->>TaskRepository: findByAssigneeIdAndStatus(workerId, status, pageable) + TaskRepository->>Database: 查询任务数据 + Database-->>TaskRepository: 返回任务数据 + TaskRepository-->>GridWorkerTaskService: 返回Page + GridWorkerTaskService->>GridWorkerTaskService: 转换为TaskSummaryDTO列表 + GridWorkerTaskService-->>GridWorkerTaskController: 返回Page + GridWorkerTaskController-->>GridWorker: 返回任务列表 + end + + alt 接受任务 + GridWorker->>GridWorkerTaskController: POST /api/worker/tasks/{taskId}/accept + GridWorkerTaskController->>GridWorkerTaskService: acceptTask(taskId, workerId) + GridWorkerTaskService->>TaskRepository: findById(taskId) + TaskRepository->>Database: 查询任务 + Database-->>TaskRepository: 返回任务 + TaskRepository-->>GridWorkerTaskService: 返回Task + GridWorkerTaskService->>GridWorkerTaskService: 验证任务状态并更新为ACCEPTED + GridWorkerTaskService->>TaskRepository: save(task) + TaskRepository->>Database: 更新任务状态 + Database-->>TaskRepository: 返回更新后的任务 + TaskRepository-->>GridWorkerTaskService: 返回更新后的Task + GridWorkerTaskService->>GridWorkerTaskService: 转换为TaskSummaryDTO + GridWorkerTaskService-->>GridWorkerTaskController: 返回TaskSummaryDTO + GridWorkerTaskController-->>GridWorker: 返回更新后的任务摘要 + end + + alt 提交任务 + GridWorker->>GridWorkerTaskController: POST /api/worker/tasks/{taskId}/submit + GridWorkerTaskController->>GridWorkerTaskService: submitTaskCompletion(taskId, workerId, request, files) + GridWorkerTaskService->>TaskRepository: findById(taskId) + TaskRepository->>Database: 查询任务 + Database-->>TaskRepository: 返回任务 + TaskRepository-->>GridWorkerTaskService: 返回Task + GridWorkerTaskService->>GridWorkerTaskService: 验证并更新任务状态为COMPLETED + GridWorkerTaskService->>GridWorkerTaskService: (如果存在)处理附件上传 + GridWorkerTaskService->>TaskRepository: save(task) + TaskRepository->>Database: 更新任务 + Database-->>TaskRepository: 返回更新后的任务 + TaskRepository-->>GridWorkerTaskService: 返回更新后的Task + GridWorkerTaskService->>GridWorkerTaskService: 转换为TaskSummaryDTO + GridWorkerTaskService-->>GridWorkerTaskController: 返回TaskSummaryDTO + GridWorkerTaskController-->>GridWorker: 返回更新后的任务摘要 + end +``` + +### 1.8 决策者获取仪表盘数据 + +```mermaid +sequenceDiagram + participant DecisionMaker as 决策者 + participant DashboardController as 仪表盘控制器 + participant DashboardService as 仪表盘服务 + participant variousRepositories as 各类仓库 + participant Database as JSON持久化存储 + + alt 获取核心统计数据 + DecisionMaker->>DashboardController: GET /api/dashboard/stats + DashboardController->>DashboardService: getDashboardStats() + DashboardService->>variousRepositories: (并行)调用多个仓库方法获取数据 + variousRepositories->>Database: 查询统计数据 + Database-->>variousRepositories: 返回数据 + variousRepositories-->>DashboardService: 返回统计结果 + DashboardService->>DashboardService: 聚合数据为DashboardStatsDTO + DashboardService-->>DashboardController: 返回DashboardStatsDTO + DashboardController-->>DecisionMaker: 返回核心统计数据 + end + + alt 获取AQI分布 + DecisionMaker->>DashboardController: GET /api/dashboard/reports/aqi-distribution + DashboardController->>DashboardService: getAqiDistribution() + DashboardService->>variousRepositories: 查询AQI数据并分组统计 + variousRepositories->>Database: 查询AQI数据 + Database-->>variousRepositories: 返回数据 + variousRepositories-->>DashboardService: 返回统计结果 + DashboardService->>DashboardService: 转换为AqiDistributionDTO列表 + DashboardService-->>DashboardController: 返回List + DashboardController-->>DecisionMaker: 返回AQI等级分布数据 + end + + alt 获取热力图数据 + DecisionMaker->>DashboardController: GET /api/dashboard/map/heatmap + DashboardController->>DashboardService: getHeatmapData() + DashboardService->>variousRepositories: 查询反馈的地理位置数据 + variousRepositories->>Database: 查询地理位置数据 + Database-->>variousRepositories: 返回数据 + variousRepositories-->>DashboardService: 返回位置数据列表 + DashboardService->>DashboardService: 转换为HeatmapPointDTO列表 + DashboardService-->>DashboardController: 返回List + DashboardController-->>DecisionMaker: 返回热力图数据 + end +``` + +### 1.9 主管审核反馈 + +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant SupervisorController as 主管控制器 + participant SupervisorService as 主管服务 + participant FeedbackRepository as 反馈仓库 + participant Database as JSON持久化存储 + + alt 获取待审核列表 + Supervisor->>SupervisorController: GET /api/supervisor/reviews + SupervisorController->>SupervisorService: getFeedbackForReview() + SupervisorService->>FeedbackRepository: findByStatus(PENDING_REVIEW) + FeedbackRepository->>Database: 查询待审核反馈 + Database-->>FeedbackRepository: 返回反馈列表 + FeedbackRepository-->>SupervisorService: 返回List + SupervisorService-->>SupervisorController: 返回List + SupervisorController-->>Supervisor: 显示待审核列表 + end + + alt 批准反馈 + Supervisor->>SupervisorController: POST /api/supervisor/reviews/{feedbackId}/approve + SupervisorController->>SupervisorService: approveFeedback(feedbackId) + SupervisorService->>FeedbackRepository: findById(feedbackId) + FeedbackRepository->>Database: 查询反馈 + Database-->>FeedbackRepository: 返回反馈 + FeedbackRepository-->>SupervisorService: 返回Feedback + SupervisorService->>SupervisorService: 更新反馈状态为APPROVED + SupervisorService->>FeedbackRepository: save(feedback) + TaskRepository->>Database: 更新反馈状态 + Database-->>TaskRepository: 返回更新后的反馈 + TaskRepository-->>SupervisorService: 返回更新后的Feedback + SupervisorService-->>SupervisorController: 返回成功响应 + SupervisorController-->>Supervisor: 操作成功 + end + + alt 拒绝反馈 + Supervisor->>SupervisorController: POST /api/supervisor/reviews/{feedbackId}/reject + SupervisorController->>SupervisorService: rejectFeedback(feedbackId, request) + SupervisorService->>FeedbackRepository: findById(feedbackId) + FeedbackRepository->>Database: 查询反馈 + Database-->>FeedbackRepository: 返回反馈 + FeedbackRepository-->>SupervisorService: 返回Feedback + SupervisorService->>SupervisorService: 更新反馈状态为REJECTED并记录原因 + SupervisorService->>FeedbackRepository: save(feedback) + TaskRepository->>Database: 更新反馈状态 + Database-->>TaskRepository: 返回更新后的反馈 + TaskRepository-->>SupervisorService: 返回更新后的Feedback + SupervisorService-->>SupervisorController: 返回成功响应 + SupervisorController-->>Supervisor: 操作成功 + end +``` + +### 1.10 路径规划 (A* 寻路算法) + +```mermaid +sequenceDiagram + participant User as 用户 + participant PathfindingController as 寻路控制器 + participant AStarService as A*服务 + participant MapData as 地图数据 + + User->>+PathfindingController: POST /api/pathfinding/find (起点, 终点) + PathfindingController->>+AStarService: findPath(start, end) + AStarService->>+MapData: getObstacles() + MapData-->>-AStarService: 返回障碍物信息 + AStarService->>AStarService: 执行A*算法计算路径 + AStarService-->>-PathfindingController: 返回计算出的路径 + PathfindingController-->>-User: 200 OK (路径坐标列表) +``` + +### 1.11 公众提交反馈 + +```mermaid +sequenceDiagram + participant User as 用户 + participant FeedbackController as 反馈控制器 + participant FeedbackService as 反馈服务 + participant FeedbackRepository as 反馈仓库 + participant FileService as 文件服务 + participant Database as JSON持久化存储 + + User->>+FeedbackController: POST /api/feedback/submit (反馈信息, 附件) + FeedbackController->>+FeedbackService: submitFeedback(request, files) + alt 包含附件 + FeedbackService->>+FileService: store(files) + FileService-->>-FeedbackService: 返回文件存储路径 + end + FeedbackService->>+FeedbackRepository: save(feedback) + FeedbackRepository->>+Database: INSERT INTO feedbacks + Database-->>-FeedbackRepository: 返回已保存的反馈 + FeedbackRepository-->>-FeedbackService: 返回已保存的反馈 + FeedbackService-->>-FeedbackController: 返回创建的反馈 + FeedbackController-->>-User: 201 CREATED (反馈详情) +``` + +### 1.12 文件下载/预览 + +```mermaid +sequenceDiagram + participant User as 用户 + participant FileController as 文件控制器 + participant FileStorageService as 文件存储服务 + participant FileSystem as 文件系统 + + User->>+FileController: GET /api/files/{filename} 或 /api/view/{filename} + FileController->>+FileStorageService: loadFileAsResource(filename) + FileStorageService->>+FileSystem: 读取文件 + FileSystem-->>-FileStorageService: 返回文件资源 + FileStorageService-->>-FileController: 返回文件资源 + FileController->>FileController: 设置HTTP响应头 (Content-Type, Content-Disposition) + FileController-->>-User: 200 OK (文件内容) +``` + +### 1.13 管理员分配网格员 + +```mermaid +sequenceDiagram + participant Admin as 管理员 + participant GridController as 网格控制器 + participant UserAccountService as 用户账户服务 + participant GridService as 网格服务 + participant OperationLogService as 操作日志服务 + participant Database as JSON持久化存储 + + Admin->>GridController: POST /api/grids/assign-worker (userId, gridId) + activate GridController + + GridController->>UserAccountService: getUserById(userId) + activate UserAccountService + UserAccountService->>Database: 查询用户 + Database-->>UserAccountService: 返回用户信息 + UserAccountService-->>GridController: 返回用户 + deactivate UserAccountService + + alt 用户角色是网格员 (role == 'GRID_WORKER') + GridController->>GridService: assignGridToUser(userId, gridId) + activate GridService + GridService->>Database: 更新用户的 grid_id + Database-->>GridService: 更新成功 + GridService-->>GridController: 分配成功 + deactivate GridService + + GridController->>OperationLogService: recordOperation(adminId, 'ASSIGN_GRID', '...details...') + activate OperationLogService + OperationLogService->>Database: 记录日志 + Database-->>OperationLogService: 记录成功 + OperationLogService-->>GridController: 记录完成 + deactivate OperationLogService + + GridController-->>Admin: 200 OK - 分配成功 + else 用户角色不是网格员 + GridController-->>Admin: 400 Bad Request - 用户不是网格员 + end + deactivate GridController +``` + +### 1.14 初始化地图 + +```mermaid +sequenceDiagram + participant Admin as 管理员 + participant MapController as 地图控制器 + participant MapGridRepository as 地图网格仓库 + participant Database as JSON持久化存储 + + Admin->>+MapController: POST /api/map/initialize (width, height) + MapController->>+MapGridRepository: deleteAll() + MapGridRepository->>+Database: DELETE FROM map_grids + Database-->>-MapGridRepository: 删除成功 + MapGridRepository-->>-MapController: 返回 + loop for y from 0 to height-1 + loop for x from 0 to width-1 + MapController->>+MapGridRepository: save(cell) + MapGridRepository->>+Database: INSERT INTO map_grids + Database-->>-MapGridRepository: 插入成功 + MapGridRepository-->>-MapController: 返回 + end + end + MapController-->>-Admin: 200 OK (Initialized a WxH map.) +``` + +### 1.15 管理员获取操作日志 + +```mermaid +sequenceDiagram + participant Admin as 管理员 + participant OperationLogController as 操作日志控制器 + participant OperationLogService as 操作日志服务 + participant OperationLogRepository as 操作日志仓库 +``` --- - + ### Report/实践结果.md - -# 2. 实践结果 - -本章节将详细阐述“东软环保公众监督系统”从需求定义到系统测试的完整实践过程。 + +# 2. 实践结果 + +本章节将详细阐述“东软环保公众监督系统”从需求定义到系统测试的完整实践过程。 --- - + ### Report/实践目的.md - -# 《东软环保公众监督平台》项目实践目的 - -### 一、项目概述与本人工作 - -本项目旨在开发一个高效、透明的环保公众监督平台,作为《东软环保应急》系统的重要子模块。系统的核心业务流程覆盖了从公众反馈、智能初审、任务转化、智能分配到最终处理的全生命周期管理,旨在拓宽环保监督渠道,增加工作透明度,为环保决策提供数据支持。 - -在本次实践中,**本人担任核心的系统设计与开发角色**,是项目的主要贡献者。具体工作内容涵盖了整个软件生命周期,包括但不限于: - -* **需求分析与建模**:深入分析项目需求,将模糊的业务概念转化为精确的功能规格,并运用UML类图对系统核心实体进行建模。 -* **系统架构设计**:主导了项目技术选型,确定了采用`Spring Boot` + `Vue.js`前后端分离的现代Web架构,并设计了后端的三层(Controller, Service, Repository)分层结构。 -* **数据持久化方案设计**:针对"数据存储于文件"的核心约束,独立设计并实现了一套基于JSON文件的泛型仓储层(Repository Pattern),巧妙地解决了数据持久化问题,保证了代码的可维护性。 -* **核心模块编码实现**:独立完成了用户认证(JWT)、反馈管理、任务全生命周期管理、智能任务分配算法等多个核心业务模块的后端代码编写工作。 -* **API接口定义**:负责设计并编写了项目整体的RESTful API接口,并利用Swagger工具生成了清晰、规范的API文档,为前后端高效协作提供了保障。 -* **技术文档撰写**:作为主要作者,撰写了《系统设计方案》等核心技术文档,系统性地阐述了项目的技术实现细节。 - -### 二、能力培养达成情况 - -通过本次毕业设计实践,本人在指导老师的帮助下,系统性地将软件工程理论知识与项目实践相结合,在以下方面取得了显著的进步,达成了毕业设计所要求的各项能力培养目标: - -#### 1. 设计/开发解决方案的能力 - -* **(1)掌握软件生命周期要素,熟悉软件工程方法与技术:** - * **全周期理解与实践**:深刻理解并实践了从需求分析、系统设计、编码实现到测试维护的软件全生命周期。 - * **面向对象设计**:在项目设计阶段,始终贯彻面向对象的思想,对系统的核心业务(如用户账户`UserAccount`、环境反馈`Feedback`、处理任务`Task`)进行高度抽象,设计出了高内聚、低耦合的类结构。 - * **UML系统建模**:能够熟练运用PlantUML工具,通过绘制UML类图,对系统中的十余个核心实体及其关联的枚举类型进行整体建模,清晰地表达了类之间的关联、聚合与依赖关系,为后续开发提供了清晰、规范的蓝图。 - * **数据持久化设计**:根据"所有数据以文件格式保存"的核心要求,设计并实现了一套基于JSON文件的数据持久化方案。通过为不同实体创建独立的JSON文件,并设计相应的读写服务,确保了数据的合理组织与一致性。 - * **界面设计**:在前后端分离的架构下,与前端开发紧密配合,确保了API接口设计能够满足前端对美观、实用、符合用户习惯的界面的数据需求。 - -* **(2)设计满足特定需求的解决方案并体现创新意识:** - * **任务分配算法设计**:为满足高效调度的需求,不止于简单的手动任务指派,而是进一步设计了一套基于"地理邻近度"和"当前工作负载"的加权评分模型,提出了具体的智能任务分配算法解决方案,体现了解决特定业务需求时的创新性。 - * **仓储层模式创新**:为解决直接操作JSON文件导致业务逻辑混乱的问题,创新性地设计并实现了一套模拟`Spring Data JPA`接口的泛型仓储层(Repository Pattern)。该方案通过引入泛型和反射机制,极大地提升了数据访问层的代码整洁性与可维护性,是本次实践中的一个重要技术创新点。 - -#### 2. 研究能力 - -* **(3)理解系统设计原理,掌握科学方法解决问题:** - * **深入理解架构原理**:通过实践,不仅熟练掌握了Spring Boot框架的应用,更深入理解了其背后的分层架构(Controller-Service-Repository)、依赖注入(DI)、面向切面编程(AOP)等核心设计思想。 - * **掌握事件驱动模型**:在设计AI对反馈进行初审的功能时,引入了Spring的事件发布/监听机制,将耗时的AI分析流程解耦为异步处理,既提升了API的响应速度,也增强了系统的健壮性和可扩展性。 - * **科学方法应用**:在设计"网格员任务路径规划"功能时,主动研究并集成了经典的A*寻路算法,能够运用科学的、经过验证的算法来解决工程中的最短路径问题,展现了将理论算法应用于工程实践的能力。 - -#### 3. 使用现代工具的能力 - -* **(4)能够选择并熟练使用现代工程工具:** - * 在整个开发过程中,能够根据项目需要,熟练选择和运用一整套现代工程工具对复杂的软件工程问题进行分析与设计: - * **后端技术栈**: Java, Spring Boot, Spring Security (for JWT) - * **项目管理与构建**: Maven - * **版本控制**: Git, GitHub - * **设计与文档工具**: PlantUML, Mermaid.js, Markdown - * **集成开发环境 (IDE)**: IntelliJ IDEA, VS Code - -#### 4. 沟通能力 - -* **(5)具备通过多种方式进行技术沟通的能力:** - * **文稿沟通**:能够通过撰写《系统设计方案》、《实践目的》等多种技术文档,系统性、结构化地阐述项目背景、设计思路和技术方案,文字表达清晰、逻辑严密。 - * **图表沟通**:在技术文档中,能够熟练运用UML类图、时序图等多种图表,将复杂的系统静态结构、对象交互时序等信息进行高度可视化、标准化的表达,极大地提升了与业界同行进行技术交流的效率和准确性。 - * **口头沟通**:在项目讨论中,能够清晰地向指导老师和项目组成员阐述自己的技术见解和设计方案,展现了良好的口头表达与技术沟通能力。 + +# 《东软环保公众监督平台》项目实践目的 + +### 一、项目概述与本人工作 + +本项目旨在开发一个高效、透明的环保公众监督平台,作为《东软环保应急》系统的重要子模块。系统的核心业务流程覆盖了从公众反馈、智能初审、任务转化、智能分配到最终处理的全生命周期管理,旨在拓宽环保监督渠道,增加工作透明度,为环保决策提供数据支持。 + +在本次实践中,**本人担任核心的系统设计与开发角色**,是项目的主要贡献者。具体工作内容涵盖了整个软件生命周期,包括但不限于: + +* **需求分析与建模**:深入分析项目需求,将模糊的业务概念转化为精确的功能规格,并运用UML类图对系统核心实体进行建模。 +* **系统架构设计**:主导了项目技术选型,确定了采用`Spring Boot` + `Vue.js`前后端分离的现代Web架构,并设计了后端的三层(Controller, Service, Repository)分层结构。 +* **数据持久化方案设计**:针对"数据存储于文件"的核心约束,独立设计并实现了一套基于JSON文件的泛型仓储层(Repository Pattern),巧妙地解决了数据持久化问题,保证了代码的可维护性。 +* **核心模块编码实现**:独立完成了用户认证(JWT)、反馈管理、任务全生命周期管理、智能任务分配算法等多个核心业务模块的后端代码编写工作。 +* **API接口定义**:负责设计并编写了项目整体的RESTful API接口,并利用Swagger工具生成了清晰、规范的API文档,为前后端高效协作提供了保障。 +* **技术文档撰写**:作为主要作者,撰写了《系统设计方案》等核心技术文档,系统性地阐述了项目的技术实现细节。 + +### 二、能力培养达成情况 + +通过本次毕业设计实践,本人在指导老师的帮助下,系统性地将软件工程理论知识与项目实践相结合,在以下方面取得了显著的进步,达成了毕业设计所要求的各项能力培养目标: + +#### 1. 设计/开发解决方案的能力 + +* **(1)掌握软件生命周期要素,熟悉软件工程方法与技术:** + * **全周期理解与实践**:深刻理解并实践了从需求分析、系统设计、编码实现到测试维护的软件全生命周期。 + * **面向对象设计**:在项目设计阶段,始终贯彻面向对象的思想,对系统的核心业务(如用户账户`UserAccount`、环境反馈`Feedback`、处理任务`Task`)进行高度抽象,设计出了高内聚、低耦合的类结构。 + * **UML系统建模**:能够熟练运用PlantUML工具,通过绘制UML类图,对系统中的十余个核心实体及其关联的枚举类型进行整体建模,清晰地表达了类之间的关联、聚合与依赖关系,为后续开发提供了清晰、规范的蓝图。 + * **数据持久化设计**:根据"所有数据以文件格式保存"的核心要求,设计并实现了一套基于JSON文件的数据持久化方案。通过为不同实体创建独立的JSON文件,并设计相应的读写服务,确保了数据的合理组织与一致性。 + * **界面设计**:在前后端分离的架构下,与前端开发紧密配合,确保了API接口设计能够满足前端对美观、实用、符合用户习惯的界面的数据需求。 + +* **(2)设计满足特定需求的解决方案并体现创新意识:** + * **任务分配算法设计**:为满足高效调度的需求,不止于简单的手动任务指派,而是进一步设计了一套基于"地理邻近度"和"当前工作负载"的加权评分模型,提出了具体的智能任务分配算法解决方案,体现了解决特定业务需求时的创新性。 + * **仓储层模式创新**:为解决直接操作JSON文件导致业务逻辑混乱的问题,创新性地设计并实现了一套模拟`Spring Data JPA`接口的泛型仓储层(Repository Pattern)。该方案通过引入泛型和反射机制,极大地提升了数据访问层的代码整洁性与可维护性,是本次实践中的一个重要技术创新点。 + +#### 2. 研究能力 + +* **(3)理解系统设计原理,掌握科学方法解决问题:** + * **深入理解架构原理**:通过实践,不仅熟练掌握了Spring Boot框架的应用,更深入理解了其背后的分层架构(Controller-Service-Repository)、依赖注入(DI)、面向切面编程(AOP)等核心设计思想。 + * **掌握事件驱动模型**:在设计AI对反馈进行初审的功能时,引入了Spring的事件发布/监听机制,将耗时的AI分析流程解耦为异步处理,既提升了API的响应速度,也增强了系统的健壮性和可扩展性。 + * **科学方法应用**:在设计"网格员任务路径规划"功能时,主动研究并集成了经典的A*寻路算法,能够运用科学的、经过验证的算法来解决工程中的最短路径问题,展现了将理论算法应用于工程实践的能力。 + +#### 3. 使用现代工具的能力 + +* **(4)能够选择并熟练使用现代工程工具:** + * 在整个开发过程中,能够根据项目需要,熟练选择和运用一整套现代工程工具对复杂的软件工程问题进行分析与设计: + * **后端技术栈**: Java, Spring Boot, Spring Security (for JWT) + * **项目管理与构建**: Maven + * **版本控制**: Git, GitHub + * **设计与文档工具**: PlantUML, Mermaid.js, Markdown + * **集成开发环境 (IDE)**: IntelliJ IDEA, VS Code + +#### 4. 沟通能力 + +* **(5)具备通过多种方式进行技术沟通的能力:** + * **文稿沟通**:能够通过撰写《系统设计方案》、《实践目的》等多种技术文档,系统性、结构化地阐述项目背景、设计思路和技术方案,文字表达清晰、逻辑严密。 + * **图表沟通**:在技术文档中,能够熟练运用UML类图、时序图等多种图表,将复杂的系统静态结构、对象交互时序等信息进行高度可视化、标准化的表达,极大地提升了与业界同行进行技术交流的效率和准确性。 + * **口头沟通**:在项目讨论中,能够清晰地向指导老师和项目组成员阐述自己的技术见解和设计方案,展现了良好的口头表达与技术沟通能力。 --- - + ### Report/实践总结.md - -# 4. 实践总结与展望 - -本次"东软环保公众监督系统"的课程设计,对我而言,远不止是一次课程任务的完成,更是一场从理论到实践、从单一技能到综合工程能力的深度淬炼。通过从零开始,亲历一个完整软件项目的生命周期——从模糊的需求雏形到清晰的系统蓝图,从优雅的架构设计到严谨的代码实现,再到全面的测试交付——我不仅高质量地达成了所有预设目标,更在思想认知、技术栈深度和工程素养上实现了跨越式的成长。 - -## 4.1 收获与体会:从"会用"到"精通"的蜕变 - -### 1. 宏观架构观的树立与深化 - -本次实践最大的收获,莫过于在真实场景中主导并落地了**前后端分离架构**。这让我对现代Web应用架构的理解,从"知道其然"深入到了"知其所以然"。 - -* **深刻理解"解耦"的价值**:我不再将"解耦"视为一个抽象概念,而是亲身体会到它带来的巨大工程优势:后端可以专注于业务逻辑与数据服务,不受前端界面迭代的影响;前端则可以自由选择技术栈(Vue 3全家桶),聚焦于用户体验的打磨。这种并行的开发模式极大地提升了团队协作的效率。 -* **掌握RESTful API设计精髓**:我学习并实践了一套规范的RESTful API设计原则,包括使用HTTP动词(GET, POST, PUT, DELETE)表达操作,通过URI定位资源,以及设计统一的响应数据结构(包含状态码、消息和数据体)。这使得前后端的数据交互变得清晰、可预测且易于调试。 -* **拥抱"无状态服务"理念**:通过引入JWT进行认证授权,我设计的后端服务实现了无状态化。服务器不再需要存储用户的Session信息,每一次请求都包含了完整的认证信息,这为未来系统的水平扩展和负载均衡奠定了坚实的基础。 - -### 2. 设计能力与代码匠艺的磨练 - -面对"基于文件存储"这一核心约束,我没有选择简单的、面向过程的文件I/O操作,而是进行了一次富有创造性的技术探索,设计并实现了一套**模拟JPA思想的泛型JSON仓储层(Repository Layer)**。这成为我本次实践中最具价值的技术沉淀。 - -* **设计模式的实战应用**:该仓储层的实现,是对**SOLID原则**的一次综合演练。通过定义泛型接口`JsonRepository`,我实践了**依赖倒置原则**;通过`JsonStorageService`的封装,实现了数据读写逻辑的单一职责;通过为不同实体提供具体的Repository实现,遵循了**接口隔离原则**。这让我真正领会到"面向接口编程"如何带来代码的灵活性、可测试性和可扩展性。 -* **并发编程的初探**:为了解决多用户并发写文件可能导致的数据覆盖或错乱问题,我深入研究了Java的并发控制机制,并最终选用`synchronized`关键字对核心的写入方法进行加锁。通过压力测试,我验证了该机制的有效性,这让我对线程安全问题有了具体而深刻的认识,也为未来学习更高级的并发技术(如`ReentrantLock`、`CAS`)打下了基础。 - -### 3. 解决复杂技术难题的信心与方法论 - -项目开发过程并非一帆风顺,我遭遇并攻克了多个技术难点,这个过程极大地锻炼了我独立解决问题的能力。 - -* **攻坚`Spring Security`**:配置`Spring Security`与JWT的整合是一大挑战。我通过深入阅读官方文档和优秀开源项目的源码,理解了其Filter链的工作机制,并成功自定义了`JwtAuthenticationFilter`,实现了从请求头解析Token、验证签名、加载用户权限,最终将其置入`SecurityContextHolder`的完整流程。我还学会了使用`@PreAuthorize`注解,实现了精确到方法级别的声明式权限控制。 -* **构建优雅的全局异常处理**:为了避免在每个Controller方法中都充斥着大量的`try-catch`块,我利用Spring MVC提供的`@ControllerAdvice`和`@ExceptionHandler`注解,构建了一个统一的全局异常处理器。它可以捕获不同类型的业务异常(如`ResourceNotFoundException`)和系统异常,并将其转换为标准化的API错误响应返回给前端。这不仅净化了业务代码,也提升了API的健壮性和用户体验。 - -### 4. 软件工程全景视野的拓展 - -这次经历让我彻底摆脱了"代码工人"的思维定式,开始以一名"软件工程师"的视角来审视整个项目。我认识到,高质量的软件交付,远不止代码本身。 - -* **文档的价值**:我投入了大量精力编写清晰、规范的Markdown项目文档,包括需求文档、设计文档、API文档(Swagger)和测试报告。我发现,高质量的文档是团队沟通的基石,是项目知识传承的载体,更是保证项目长期可维护性的关键。 -* **测试的左移**:我主动引入了单元测试,并坚持在开发阶段就为核心模块编写测试用例。这让我体会到"测试左移"的重要性——越早发现Bug,修复的成本越低。全面的API测试和集成测试则构成了产品质量的最后一道防线。 - -## 4.2 不足与展望:迈向更高阶的工程师之路 - -在收获满满的同时,我也清醒地看到了当前项目的局限性和个人能力的待提升之处,这为我规划了清晰的未来学习路径。 - -* **构建成熟的自动化测试体系**:目前项目的自动化测试还处于初级阶段。我的下一步计划是: - * **后端**:深入学习`Mockito`的高级用法(如`ArgumentCaptor`),并引入集成测试框架(如`Testcontainers`),在CI/CD流水线中自动运行测试,实现代码提交即测试。 - * **前端**:学习并引入`Vitest`进行单元测试,使用`Cypress`或`Playwright`进行端到端(E2E)测试,实现对关键用户流程的自动化回归验证。 - -* **拥抱云原生与DevOps**:当前项目采用的是传统的手动部署模式,效率低下且容易出错。我渴望掌握云原生技术栈: - * **容器化**:学习`Docker`,将前后端应用及其依赖环境打包成独立的、可移植的容器镜像。 - * **容器编排**:学习`Kubernetes`,实现容器化应用的自动化部署、弹性伸缩和高可用运维。 - * **CI/CD**:学习`Jenkins`或`GitHub Actions`,搭建一条完整的自动化流水线,实现从代码提交到测试、构建、部署的全流程自动化。 - -* **深化业务与技术的融合创新**: - * **实时化改造**:引入`WebSocket`或`Server-Sent Events (SSE)`,将任务的分配、状态流转等关键信息实时推送到前端,极大地提升系统的即时性和用户的沉浸式体验。 - * **数据智能升级**:当数据量增长后,引入`Elasticsearch`,提供强大的全文检索和聚合分析能力。长远来看,可以结合机器学习算法,对环境问题数据进行深度挖掘,实现如"区域环境问题热点预测"、"污染源智能追溯"等更高阶的智能应用。 - -总而言之,这次课程设计是我从一名编程学习者向一名准软件工程师转变的里程碑。它不仅系统性地检验了我过往的知识积累,更重要的是,它点燃了我对软件架构、代码匠艺和工程卓越的无限热情。在这次实践中收获的宝贵经验和暴露的不足,都将化为我未来职业道路上最坚实的基石和最清晰的路标,激励我不断学习、持续精进。 - -## 内容完成情况 - -在本次环境监督系统(EMS)的开发实践中,我完成了以下主要内容: - -1. **系统设计文档**:完成了详细的系统设计文档,包括整体架构设计、功能模块划分、数据库设计和API接口设计等方面。 - -2. **后端核心功能实现**: - - 实现了基于JSON文件的泛型存储服务 - - 开发了A*寻路算法用于网格员路径规划 - - 完成了反馈管理模块的全流程实现 - - 设计并实现了任务智能分配算法 - -3. **前端界面设计与实现**: - - 开发了主管工作台、网格员工作台和决策支持看板等核心界面 - - 实现了响应式布局和组件化开发 - -4. **系统实现文档**:编写了详细的系统实现文档,包括开发环境与技术栈、核心功能模块实现和界面展示等内容。 - -## 创新点 - -### 1. 泛型JSON存储服务 - -设计并实现了一个创新的数据持久化解决方案,使用JSON文件代替传统数据库进行数据存储。这种方案具有以下创新点: - -- **类型安全的泛型设计**:通过Java泛型和TypeReference,实现了类型安全的数据读写,支持任意模型类。 -- **类数据库接口**:提供了类似于JPA的操作接口,使得业务层代码无需关心底层存储细节。 -- **部署简化**:无需配置和维护数据库,大大简化了系统部署过程。 -- **线程安全机制**:使用synchronized关键字确保写操作的线程安全,防止并发写入导致的数据损坏。 - -### 2. A*寻路算法的动态地图适配 - -在网格与地图模块中,实现的A*寻路算法具有以下创新特点: - -- **动态地图加载**:算法从数据库动态加载地图数据,包括障碍物信息和地图尺寸,使得路径规划能够适应地图的变化。 -- **优先队列优化**:使用优先队列(PriorityQueue)存储开放列表,确保每次都能高效地选择F值最小的节点。 -- **适应性启发函数**:选择曼哈顿距离作为启发函数,适合网格化的城市环境移动模式。 - -### 3. 多因素加权的任务智能分配算法 - -任务分配算法综合考虑了多种因素,具有较高的智能性: - -- **多维度评分机制**:算法综合考虑了地理距离、当前负载、专业匹配度和历史表现四个因素。 -- **动态技能匹配**:根据任务的污染类型和严重程度,动态确定所需的技能列表,然后与网格员的技能进行匹配。 -- **可配置权重系统**:各因素的权重可以根据实际需求进行调整,使得算法更加灵活和适应性强。 - -### 4. 事件驱动的业务流程设计 - -在反馈管理模块中,采用了事件驱动的架构设计: - -- **解耦的业务流程**:通过Spring的事件机制,实现了反馈提交后的异步处理,如AI审核和任务创建。 -- **状态机模式**:使用状态机模式管理反馈的生命周期,确保状态转换的合法性,防止非法操作。 -- **可扩展的事件处理**:新的事件处理器可以轻松添加,无需修改现有代码,符合开闭原则。 - -### 5. 文件上传与存储服务 - -在环境问题反馈和任务处理过程中,实现了高效且安全的文件上传与存储机制: - -- **多媒体支持**:支持图片、视频和音频等多种媒体格式的上传,为环境问题提供直观的证据支持。 -- **安全性控制**:实现了文件类型验证、大小限制和内容扫描,防止恶意文件上传,保障系统安全。 -- **分层存储架构**:采用物理路径与逻辑路径分离的设计,文件实际存储在安全目录,而数据库中仅保存引用路径,增强了系统的安全性和灵活性。 -- **按需加载策略**:对于大型媒体文件,实现了流式传输和分块下载,优化了带宽使用和用户体验。 - -## 不足与可改进之处 - -尽管系统已经实现了核心功能,但由于时间限制,仍有一些方面可以进一步完善: - -### 1. 数据持久化机制的优化 - -当前的JSON文件存储方案虽然简化了部署,但也存在一些局限性: - -- **性能瓶颈**:随着数据量增大,JSON文件的读写性能可能成为瓶颈。可以考虑实现分片存储或引入缓存机制。 -- **事务支持**:当前实现缺乏事务支持,无法保证跨文件操作的原子性。可以设计一个简单的事务管理器来解决这个问题。 -- **索引机制**:缺乏高效的索引机制,导致复杂查询性能较低。可以实现内存索引或考虑集成轻量级数据库如SQLite。 - -### 2. A*算法的进一步优化 - -A*寻路算法还可以在以下方面进行优化: - -- **双向搜索**:实现双向A*搜索,从起点和终点同时开始搜索,可以显著减少搜索空间。 -- **层次化路径规划**:对于大型地图,可以实现层次化的路径规划,先在抽象层次上找到大致路径,再在细节层次上优化。 -- **动态权重调整**:根据不同的地形特征动态调整边的权重,更好地模拟现实世界的移动成本。 - -### 3. 任务分配算法的增强 - -任务分配算法可以通过以下方式进一步增强: - -- **机器学习集成**:引入机器学习模型,根据历史分配数据学习最优的权重配置,实现自适应的任务分配。 -- **群体智能优化**:考虑整个网格员团队的工作负载均衡,而不仅仅是单个任务的最优分配。 -- **预测性分配**:基于历史数据预测未来一段时间内可能出现的任务,提前做好人力资源规划。 - -### 4. 前端用户体验优化 - -前端界面还可以在以下方面进行改进: - -- **离线支持**:实现Progressive Web App (PWA),使网格员在网络不稳定的野外环境也能使用系统。 -- **实时通知**:集成WebSocket或服务器发送事件(SSE),实现实时任务通知和状态更新。 -- **移动端适配**:进一步优化移动端体验,考虑开发原生移动应用,提供更好的定位和拍照体验。 - -### 5. 系统安全性增强 - -安全方面还可以进一步加强: - -- **细粒度权限控制**:实现基于资源的访问控制(RBAC),对不同资源设置更细粒度的权限控制。 -- **API限流与防护**:实现API限流机制,防止DoS攻击;添加CSRF保护和更完善的输入验证。 -- **审计日志**:实现全面的审计日志系统,记录所有关键操作,便于安全审计和问题追踪。 - -## 结论 - -通过本次实践,我成功地设计并实现了环境监督系统的核心功能,在泛型JSON存储服务、A*寻路算法、任务智能分配算法和事件驱动架构等方面进行了创新。虽然由于时间限制,一些高级特性和优化未能实现,但这些都已经被识别为未来可以改进的方向。总体而言,系统达到了预期的设计目标,提供了一个功能完整、架构合理的环境监督解决方案。 + +# 4. 实践总结与展望 + +本次"东软环保公众监督系统"的课程设计,对我而言,远不止是一次课程任务的完成,更是一场从理论到实践、从单一技能到综合工程能力的深度淬炼。通过从零开始,亲历一个完整软件项目的生命周期——从模糊的需求雏形到清晰的系统蓝图,从优雅的架构设计到严谨的代码实现,再到全面的测试交付——我不仅高质量地达成了所有预设目标,更在思想认知、技术栈深度和工程素养上实现了跨越式的成长。 + +## 4.1 收获与体会:从"会用"到"精通"的蜕变 + +### 1. 宏观架构观的树立与深化 + +本次实践最大的收获,莫过于在真实场景中主导并落地了**前后端分离架构**。这让我对现代Web应用架构的理解,从"知道其然"深入到了"知其所以然"。 + +* **深刻理解"解耦"的价值**:我不再将"解耦"视为一个抽象概念,而是亲身体会到它带来的巨大工程优势:后端可以专注于业务逻辑与数据服务,不受前端界面迭代的影响;前端则可以自由选择技术栈(Vue 3全家桶),聚焦于用户体验的打磨。这种并行的开发模式极大地提升了团队协作的效率。 +* **掌握RESTful API设计精髓**:我学习并实践了一套规范的RESTful API设计原则,包括使用HTTP动词(GET, POST, PUT, DELETE)表达操作,通过URI定位资源,以及设计统一的响应数据结构(包含状态码、消息和数据体)。这使得前后端的数据交互变得清晰、可预测且易于调试。 +* **拥抱"无状态服务"理念**:通过引入JWT进行认证授权,我设计的后端服务实现了无状态化。服务器不再需要存储用户的Session信息,每一次请求都包含了完整的认证信息,这为未来系统的水平扩展和负载均衡奠定了坚实的基础。 + +### 2. 设计能力与代码匠艺的磨练 + +面对"基于文件存储"这一核心约束,我没有选择简单的、面向过程的文件I/O操作,而是进行了一次富有创造性的技术探索,设计并实现了一套**模拟JPA思想的泛型JSON仓储层(Repository Layer)**。这成为我本次实践中最具价值的技术沉淀。 + +* **设计模式的实战应用**:该仓储层的实现,是对**SOLID原则**的一次综合演练。通过定义泛型接口`JsonRepository`,我实践了**依赖倒置原则**;通过`JsonStorageService`的封装,实现了数据读写逻辑的单一职责;通过为不同实体提供具体的Repository实现,遵循了**接口隔离原则**。这让我真正领会到"面向接口编程"如何带来代码的灵活性、可测试性和可扩展性。 +* **并发编程的初探**:为了解决多用户并发写文件可能导致的数据覆盖或错乱问题,我深入研究了Java的并发控制机制,并最终选用`synchronized`关键字对核心的写入方法进行加锁。通过压力测试,我验证了该机制的有效性,这让我对线程安全问题有了具体而深刻的认识,也为未来学习更高级的并发技术(如`ReentrantLock`、`CAS`)打下了基础。 + +### 3. 解决复杂技术难题的信心与方法论 + +项目开发过程并非一帆风顺,我遭遇并攻克了多个技术难点,这个过程极大地锻炼了我独立解决问题的能力。 + +* **攻坚`Spring Security`**:配置`Spring Security`与JWT的整合是一大挑战。我通过深入阅读官方文档和优秀开源项目的源码,理解了其Filter链的工作机制,并成功自定义了`JwtAuthenticationFilter`,实现了从请求头解析Token、验证签名、加载用户权限,最终将其置入`SecurityContextHolder`的完整流程。我还学会了使用`@PreAuthorize`注解,实现了精确到方法级别的声明式权限控制。 +* **构建优雅的全局异常处理**:为了避免在每个Controller方法中都充斥着大量的`try-catch`块,我利用Spring MVC提供的`@ControllerAdvice`和`@ExceptionHandler`注解,构建了一个统一的全局异常处理器。它可以捕获不同类型的业务异常(如`ResourceNotFoundException`)和系统异常,并将其转换为标准化的API错误响应返回给前端。这不仅净化了业务代码,也提升了API的健壮性和用户体验。 + +### 4. 软件工程全景视野的拓展 + +这次经历让我彻底摆脱了"代码工人"的思维定式,开始以一名"软件工程师"的视角来审视整个项目。我认识到,高质量的软件交付,远不止代码本身。 + +* **文档的价值**:我投入了大量精力编写清晰、规范的Markdown项目文档,包括需求文档、设计文档、API文档(Swagger)和测试报告。我发现,高质量的文档是团队沟通的基石,是项目知识传承的载体,更是保证项目长期可维护性的关键。 +* **测试的左移**:我主动引入了单元测试,并坚持在开发阶段就为核心模块编写测试用例。这让我体会到"测试左移"的重要性——越早发现Bug,修复的成本越低。全面的API测试和集成测试则构成了产品质量的最后一道防线。 + +## 4.2 不足与展望:迈向更高阶的工程师之路 + +在收获满满的同时,我也清醒地看到了当前项目的局限性和个人能力的待提升之处,这为我规划了清晰的未来学习路径。 + +* **构建成熟的自动化测试体系**:目前项目的自动化测试还处于初级阶段。我的下一步计划是: + * **后端**:深入学习`Mockito`的高级用法(如`ArgumentCaptor`),并引入集成测试框架(如`Testcontainers`),在CI/CD流水线中自动运行测试,实现代码提交即测试。 + * **前端**:学习并引入`Vitest`进行单元测试,使用`Cypress`或`Playwright`进行端到端(E2E)测试,实现对关键用户流程的自动化回归验证。 + +* **拥抱云原生与DevOps**:当前项目采用的是传统的手动部署模式,效率低下且容易出错。我渴望掌握云原生技术栈: + * **容器化**:学习`Docker`,将前后端应用及其依赖环境打包成独立的、可移植的容器镜像。 + * **容器编排**:学习`Kubernetes`,实现容器化应用的自动化部署、弹性伸缩和高可用运维。 + * **CI/CD**:学习`Jenkins`或`GitHub Actions`,搭建一条完整的自动化流水线,实现从代码提交到测试、构建、部署的全流程自动化。 + +* **深化业务与技术的融合创新**: + * **实时化改造**:引入`WebSocket`或`Server-Sent Events (SSE)`,将任务的分配、状态流转等关键信息实时推送到前端,极大地提升系统的即时性和用户的沉浸式体验。 + * **数据智能升级**:当数据量增长后,引入`Elasticsearch`,提供强大的全文检索和聚合分析能力。长远来看,可以结合机器学习算法,对环境问题数据进行深度挖掘,实现如"区域环境问题热点预测"、"污染源智能追溯"等更高阶的智能应用。 + +总而言之,这次课程设计是我从一名编程学习者向一名准软件工程师转变的里程碑。它不仅系统性地检验了我过往的知识积累,更重要的是,它点燃了我对软件架构、代码匠艺和工程卓越的无限热情。在这次实践中收获的宝贵经验和暴露的不足,都将化为我未来职业道路上最坚实的基石和最清晰的路标,激励我不断学习、持续精进。 + +## 内容完成情况 + +在本次环境监督系统(EMS)的开发实践中,我完成了以下主要内容: + +1. **系统设计文档**:完成了详细的系统设计文档,包括整体架构设计、功能模块划分、数据库设计和API接口设计等方面。 + +2. **后端核心功能实现**: + - 实现了基于JSON文件的泛型存储服务 + - 开发了A*寻路算法用于网格员路径规划 + - 完成了反馈管理模块的全流程实现 + - 设计并实现了任务智能分配算法 + +3. **前端界面设计与实现**: + - 开发了主管工作台、网格员工作台和决策支持看板等核心界面 + - 实现了响应式布局和组件化开发 + +4. **系统实现文档**:编写了详细的系统实现文档,包括开发环境与技术栈、核心功能模块实现和界面展示等内容。 + +## 创新点 + +### 1. 泛型JSON存储服务 + +设计并实现了一个创新的数据持久化解决方案,使用JSON文件代替传统数据库进行数据存储。这种方案具有以下创新点: + +- **类型安全的泛型设计**:通过Java泛型和TypeReference,实现了类型安全的数据读写,支持任意模型类。 +- **类数据库接口**:提供了类似于JPA的操作接口,使得业务层代码无需关心底层存储细节。 +- **部署简化**:无需配置和维护数据库,大大简化了系统部署过程。 +- **线程安全机制**:使用synchronized关键字确保写操作的线程安全,防止并发写入导致的数据损坏。 + +### 2. A*寻路算法的动态地图适配 + +在网格与地图模块中,实现的A*寻路算法具有以下创新特点: + +- **动态地图加载**:算法从数据库动态加载地图数据,包括障碍物信息和地图尺寸,使得路径规划能够适应地图的变化。 +- **优先队列优化**:使用优先队列(PriorityQueue)存储开放列表,确保每次都能高效地选择F值最小的节点。 +- **适应性启发函数**:选择曼哈顿距离作为启发函数,适合网格化的城市环境移动模式。 + +### 3. 多因素加权的任务智能分配算法 + +任务分配算法综合考虑了多种因素,具有较高的智能性: + +- **多维度评分机制**:算法综合考虑了地理距离、当前负载、专业匹配度和历史表现四个因素。 +- **动态技能匹配**:根据任务的污染类型和严重程度,动态确定所需的技能列表,然后与网格员的技能进行匹配。 +- **可配置权重系统**:各因素的权重可以根据实际需求进行调整,使得算法更加灵活和适应性强。 + +### 4. 事件驱动的业务流程设计 + +在反馈管理模块中,采用了事件驱动的架构设计: + +- **解耦的业务流程**:通过Spring的事件机制,实现了反馈提交后的异步处理,如AI审核和任务创建。 +- **状态机模式**:使用状态机模式管理反馈的生命周期,确保状态转换的合法性,防止非法操作。 +- **可扩展的事件处理**:新的事件处理器可以轻松添加,无需修改现有代码,符合开闭原则。 + +### 5. 文件上传与存储服务 + +在环境问题反馈和任务处理过程中,实现了高效且安全的文件上传与存储机制: + +- **多媒体支持**:支持图片、视频和音频等多种媒体格式的上传,为环境问题提供直观的证据支持。 +- **安全性控制**:实现了文件类型验证、大小限制和内容扫描,防止恶意文件上传,保障系统安全。 +- **分层存储架构**:采用物理路径与逻辑路径分离的设计,文件实际存储在安全目录,而数据库中仅保存引用路径,增强了系统的安全性和灵活性。 +- **按需加载策略**:对于大型媒体文件,实现了流式传输和分块下载,优化了带宽使用和用户体验。 + +## 不足与可改进之处 + +尽管系统已经实现了核心功能,但由于时间限制,仍有一些方面可以进一步完善: + +### 1. 数据持久化机制的优化 + +当前的JSON文件存储方案虽然简化了部署,但也存在一些局限性: + +- **性能瓶颈**:随着数据量增大,JSON文件的读写性能可能成为瓶颈。可以考虑实现分片存储或引入缓存机制。 +- **事务支持**:当前实现缺乏事务支持,无法保证跨文件操作的原子性。可以设计一个简单的事务管理器来解决这个问题。 +- **索引机制**:缺乏高效的索引机制,导致复杂查询性能较低。可以实现内存索引或考虑集成轻量级数据库如SQLite。 + +### 2. A*算法的进一步优化 + +A*寻路算法还可以在以下方面进行优化: + +- **双向搜索**:实现双向A*搜索,从起点和终点同时开始搜索,可以显著减少搜索空间。 +- **层次化路径规划**:对于大型地图,可以实现层次化的路径规划,先在抽象层次上找到大致路径,再在细节层次上优化。 +- **动态权重调整**:根据不同的地形特征动态调整边的权重,更好地模拟现实世界的移动成本。 + +### 3. 任务分配算法的增强 + +任务分配算法可以通过以下方式进一步增强: + +- **机器学习集成**:引入机器学习模型,根据历史分配数据学习最优的权重配置,实现自适应的任务分配。 +- **群体智能优化**:考虑整个网格员团队的工作负载均衡,而不仅仅是单个任务的最优分配。 +- **预测性分配**:基于历史数据预测未来一段时间内可能出现的任务,提前做好人力资源规划。 + +### 4. 前端用户体验优化 + +前端界面还可以在以下方面进行改进: + +- **离线支持**:实现Progressive Web App (PWA),使网格员在网络不稳定的野外环境也能使用系统。 +- **实时通知**:集成WebSocket或服务器发送事件(SSE),实现实时任务通知和状态更新。 +- **移动端适配**:进一步优化移动端体验,考虑开发原生移动应用,提供更好的定位和拍照体验。 + +### 5. 系统安全性增强 + +安全方面还可以进一步加强: + +- **细粒度权限控制**:实现基于资源的访问控制(RBAC),对不同资源设置更细粒度的权限控制。 +- **API限流与防护**:实现API限流机制,防止DoS攻击;添加CSRF保护和更完善的输入验证。 +- **审计日志**:实现全面的审计日志系统,记录所有关键操作,便于安全审计和问题追踪。 + +## 结论 + +通过本次实践,我成功地设计并实现了环境监督系统的核心功能,在泛型JSON存储服务、A*寻路算法、任务智能分配算法和事件驱动架构等方面进行了创新。虽然由于时间限制,一些高级特性和优化未能实现,但这些都已经被识别为未来可以改进的方向。总体而言,系统达到了预期的设计目标,提供了一个功能完整、架构合理的环境监督解决方案。 --- - + ### Report/完整依赖关系列表.md - -# EMS系统完整依赖关系列表 - -## 1. 控制器层依赖 (Controller Dependencies) - -### 控制器 → 服务层 -- `AuthController` → `AuthService`, `VerificationCodeService`, `OperationLogService` -- `DashboardController` → `DashboardService` -- `FeedbackController` → `FeedbackService` -- `FileController` → `FileStorageService` -- `GridController` → `GridService`, `GridRepository`, `UserAccountRepository`, `OperationLogService` -- `GridWorkerTaskController` → `GridWorkerTaskService` -- `MapController` → `MapGridRepository` -- `OperationLogController` → `OperationLogService` -- `PathfindingController` → `AStarService` -- `PersonnelController` → `PersonnelService`, `UserAccountService` -- `ProfileController` → `UserFeedbackService` -- `PublicController` → `FeedbackService` -- `SupervisorController` → `SupervisorService` -- `TaskAssignmentController` → `TaskAssignmentService` -- `TaskManagementController` → `TaskManagementService` - -### 控制器 → DTO -- `AuthController` → `LoginRequest` -- `FeedbackController` → `FeedbackSubmissionRequest` -- `TaskManagementController` → `TaskCreationRequest` -- `PersonnelController` → `UserCreationRequest` - -## 2. 服务层依赖 (Service Dependencies) - -### 服务 → 仓库层 -- `AiReviewService` → `FeedbackRepository` -- `AuthService` → `UserAccountRepository`, `PasswordResetTokenRepository` -- `DashboardService` → `FeedbackRepository`, `UserAccountRepository`, `AqiDataRepository`, `AqiRecordRepository`, `GridRepository`, `TaskRepository`, `PollutantThresholdRepository` -- `FeedbackService` → `FeedbackRepository`, `UserAccountRepository`, `TaskRepository` -- `FileStorageService` → `AttachmentRepository` -- `GridService` → `GridRepository`, `UserAccountRepository`, `MapGridRepository` -- `GridWorkerTaskService` → `TaskRepository`, `TaskHistoryRepository`, `TaskSubmissionRepository`, `AttachmentRepository` -- `OperationLogService` → `OperationLogRepository`, `UserAccountRepository` -- `PersonnelService` → `UserAccountRepository` -- `SupervisorService` → `FeedbackRepository` -- `TaskAssignmentService` → `FeedbackRepository`, `UserAccountRepository`, `AssignmentRepository`, `TaskRepository` -- `TaskManagementService` → `TaskRepository`, `UserAccountRepository`, `TaskHistoryRepository`, `FeedbackRepository`, `TaskSubmissionRepository`, `AttachmentRepository` -- `UserAccountService` → `UserAccountRepository` -- `UserFeedbackService` → `FeedbackRepository` -- `AStarService` → `MapGridRepository` - -### 服务 → 服务层 -- `AuthService` → `JwtService`, `VerificationCodeService`, `OperationLogService` -- `FeedbackService` → `FileStorageService`, `TaskManagementService`, `OperationLogService` -- `GridService` → `OperationLogService` -- `GridWorkerTaskService` → `FileStorageService`, `OperationLogService` -- `PersonnelService` → `OperationLogService` -- `TaskAssignmentService` → `OperationLogService` -- `TaskManagementService` → `OperationLogService`, `AStarService` -- `VerificationCodeService` → `MailService` - -## 3. 仓库层依赖 (Repository Dependencies) - -### 仓库实现 → 存储服务 -- `JsonAssignmentRecordRepository` → `JsonStorageService` -- `JsonAssignmentRepository` → `JsonStorageService` -- `JsonAqiDataRepository` → `JsonStorageService` -- `JsonAqiRecordRepository` → `JsonStorageService` -- `JsonAttachmentRepository` → `JsonStorageService` -- `JsonFeedbackRepository` → `JsonStorageService` -- `JsonGridRepository` → `JsonStorageService` -- `JsonMapGridRepository` → `JsonStorageService` -- `JsonOperationLogRepository` → `JsonStorageService` -- `JsonPasswordResetTokenRepository` → `JsonStorageService` -- `JsonPollutantThresholdRepository` → `JsonStorageService` -- `JsonTaskHistoryRepository` → `JsonStorageService` -- `JsonTaskRepository` → `JsonStorageService` -- `JsonTaskSubmissionRepository` → `JsonStorageService` -- `JsonUserAccountRepository` → `JsonStorageService` - -### 仓库 → 模型层 -- `AqiDataRepository` → `AqiData` -- `AqiRecordRepository` → `AqiRecord` -- `AssignmentRepository` → `Assignment` -- `AssignmentRecordRepository` → `AssignmentRecord` -- `AttachmentRepository` → `Attachment` -- `FeedbackRepository` → `Feedback` -- `GridRepository` → `Grid` -- `MapGridRepository` → `MapGrid` -- `OperationLogRepository` → `OperationLog` -- `PasswordResetTokenRepository` → `PasswordResetToken` -- `PollutantThresholdRepository` → `PollutantThreshold` -- `TaskRepository` → `Task` -- `TaskHistoryRepository` → `TaskHistory` -- `TaskSubmissionRepository` → `TaskSubmission` -- `UserAccountRepository` → `UserAccount` - -## 4. 模型层关系 (Model Relationships) - -### 实体关联 -- `UserAccount` (1) ↔ `Feedback` (*) : 用户提交反馈 -- `UserAccount` (1) ↔ `Task` (*) : 用户被分配任务 -- `UserAccount` (1) ↔ `Role` (1) : 用户拥有角色 -- `UserAccount` (1) ↔ `Assignment` (*) : 用户有分配记录 -- `Feedback` (1) ↔ `Task` (1) : 反馈生成任务 -- `Feedback` (*) ↔ `Attachment` (*) : 反馈包含附件 -- `Task` (1) ↔ `Assignment` (*) : 任务有分配记录 -- `Task` (*) ↔ `Attachment` (*) : 任务包含附件 -- `Task` (1) ↔ `TaskHistory` (*) : 任务有历史记录 -- `Task` (1) ↔ `TaskSubmission` (*) : 任务有提交记录 -- `Grid` (1) ↔ `AqiRecord` (*) : 网格有空气质量记录 -- `Assignment` → `Task`, `UserAccount` : 分配关联任务和用户 -- `AssignmentRecord` → `Feedback`, `UserAccount` : 分配记录关联反馈和用户 -- `Attachment` → `Feedback`, `TaskSubmission` : 附件关联反馈或任务提交 -- `AqiData` → `Grid` : 空气质量数据关联网格 -- `AqiRecord` → `Grid` : 空气质量记录关联网格 - -## 5. 服务实现依赖 (Service Implementation Dependencies) - -### 接口实现关系 -- `AiReviewServiceImpl` → `AiReviewService` -- `AuthServiceImpl` → `AuthService` -- `DashboardServiceImpl` → `DashboardService` -- `FeedbackServiceImpl` → `FeedbackService` -- `FileStorageServiceImpl` → `FileStorageService` -- `GridServiceImpl` → `GridService` -- `GridWorkerTaskServiceImpl` → `GridWorkerTaskService` -- `JsonStorageServiceImpl` → `JsonStorageService` -- `JwtServiceImpl` → `JwtService` -- `LoginAttemptServiceImpl` → `LoginAttemptService` -- `MailServiceImpl` → `MailService` -- `OperationLogServiceImpl` → `OperationLogService` -- `PersonnelServiceImpl` → `PersonnelService` -- `SupervisorServiceImpl` → `SupervisorService` -- `TaskAssignmentServiceImpl` → `TaskAssignmentService` -- `TaskManagementServiceImpl` → `TaskManagementService` -- `UserAccountServiceImpl` → `UserAccountService` -- `UserFeedbackServiceImpl` → `UserFeedbackService` -- `VerificationCodeServiceImpl` → `VerificationCodeService` -- `AStarServiceImpl` → `AStarService` - -## 6. 安全层依赖 (Security Dependencies) - -### 安全配置 -- `SecurityConfig` → `JwtAuthenticationFilter`, `UserAccountService` -- `JwtAuthenticationFilter` → `UserDetailsServiceImpl`, `JwtService` -- `UserDetailsServiceImpl` → `UserAccountService` -- `CustomUserDetails` → `UserAccount` -- `JwtService` → `UserAccountRepository` - -## 7. 配置层依赖 (Configuration Dependencies) - -### 配置类 -- `WebConfig` : Web配置 -- `SecurityConfig` : 安全配置 -- `JacksonConfig` : JSON序列化配置 - -## 8. 异常处理依赖 (Exception Dependencies) - -### 异常类 -- `GlobalExceptionHandler` : 全局异常处理器 -- `FileStorageException` : 文件存储异常 -- `ResourceNotFoundException` : 资源未找到异常 -- `UnauthorizedException` : 未授权异常 -- `ValidationException` : 验证异常 - -## 9. 事件处理依赖 (Event Dependencies) - -### 事件监听器 -- `TaskEventListener` : 任务事件监听器 -- `FeedbackEventListener` : 反馈事件监听器 - -## 10. 枚举依赖 (Enum Dependencies) - -### 枚举类型 -- `Role` : 用户角色枚举 -- `TaskStatus` : 任务状态枚举 -- `FeedbackStatus` : 反馈状态枚举 -- `OperationType` : 操作类型枚举 -- `PollutantType` : 污染物类型枚举 -- `GridStatus` : 网格状态枚举 - -## 总结 - -本系统采用分层架构设计,包含: -- **控制器层 (Controller)**: 15个控制器类 -- **服务层 (Service)**: 20个服务接口及其实现 -- **仓库层 (Repository)**: 15个仓库接口及其JSON实现 -- **模型层 (Model)**: 15个实体类 -- **安全层 (Security)**: 5个安全相关类 -- **配置层 (Configuration)**: 3个配置类 -- **异常处理**: 5个异常类 -- **事件处理**: 2个事件监听器 -- **枚举类型**: 6个枚举 - -总计约 **86个主要组件** 及其相互依赖关系,形成了一个完整的环境监测系统架构。 + +# EMS系统完整依赖关系列表 + +## 1. 控制器层依赖 (Controller Dependencies) + +### 控制器 → 服务层 +- `AuthController` → `AuthService`, `VerificationCodeService`, `OperationLogService` +- `DashboardController` → `DashboardService` +- `FeedbackController` → `FeedbackService` +- `FileController` → `FileStorageService` +- `GridController` → `GridService`, `GridRepository`, `UserAccountRepository`, `OperationLogService` +- `GridWorkerTaskController` → `GridWorkerTaskService` +- `MapController` → `MapGridRepository` +- `OperationLogController` → `OperationLogService` +- `PathfindingController` → `AStarService` +- `PersonnelController` → `PersonnelService`, `UserAccountService` +- `ProfileController` → `UserFeedbackService` +- `PublicController` → `FeedbackService` +- `SupervisorController` → `SupervisorService` +- `TaskAssignmentController` → `TaskAssignmentService` +- `TaskManagementController` → `TaskManagementService` + +### 控制器 → DTO +- `AuthController` → `LoginRequest` +- `FeedbackController` → `FeedbackSubmissionRequest` +- `TaskManagementController` → `TaskCreationRequest` +- `PersonnelController` → `UserCreationRequest` + +## 2. 服务层依赖 (Service Dependencies) + +### 服务 → 仓库层 +- `AiReviewService` → `FeedbackRepository` +- `AuthService` → `UserAccountRepository`, `PasswordResetTokenRepository` +- `DashboardService` → `FeedbackRepository`, `UserAccountRepository`, `AqiDataRepository`, `AqiRecordRepository`, `GridRepository`, `TaskRepository`, `PollutantThresholdRepository` +- `FeedbackService` → `FeedbackRepository`, `UserAccountRepository`, `TaskRepository` +- `FileStorageService` → `AttachmentRepository` +- `GridService` → `GridRepository`, `UserAccountRepository`, `MapGridRepository` +- `GridWorkerTaskService` → `TaskRepository`, `TaskHistoryRepository`, `TaskSubmissionRepository`, `AttachmentRepository` +- `OperationLogService` → `OperationLogRepository`, `UserAccountRepository` +- `PersonnelService` → `UserAccountRepository` +- `SupervisorService` → `FeedbackRepository` +- `TaskAssignmentService` → `FeedbackRepository`, `UserAccountRepository`, `AssignmentRepository`, `TaskRepository` +- `TaskManagementService` → `TaskRepository`, `UserAccountRepository`, `TaskHistoryRepository`, `FeedbackRepository`, `TaskSubmissionRepository`, `AttachmentRepository` +- `UserAccountService` → `UserAccountRepository` +- `UserFeedbackService` → `FeedbackRepository` +- `AStarService` → `MapGridRepository` + +### 服务 → 服务层 +- `AuthService` → `JwtService`, `VerificationCodeService`, `OperationLogService` +- `FeedbackService` → `FileStorageService`, `TaskManagementService`, `OperationLogService` +- `GridService` → `OperationLogService` +- `GridWorkerTaskService` → `FileStorageService`, `OperationLogService` +- `PersonnelService` → `OperationLogService` +- `TaskAssignmentService` → `OperationLogService` +- `TaskManagementService` → `OperationLogService`, `AStarService` +- `VerificationCodeService` → `MailService` + +## 3. 仓库层依赖 (Repository Dependencies) + +### 仓库实现 → 存储服务 +- `JsonAssignmentRecordRepository` → `JsonStorageService` +- `JsonAssignmentRepository` → `JsonStorageService` +- `JsonAqiDataRepository` → `JsonStorageService` +- `JsonAqiRecordRepository` → `JsonStorageService` +- `JsonAttachmentRepository` → `JsonStorageService` +- `JsonFeedbackRepository` → `JsonStorageService` +- `JsonGridRepository` → `JsonStorageService` +- `JsonMapGridRepository` → `JsonStorageService` +- `JsonOperationLogRepository` → `JsonStorageService` +- `JsonPasswordResetTokenRepository` → `JsonStorageService` +- `JsonPollutantThresholdRepository` → `JsonStorageService` +- `JsonTaskHistoryRepository` → `JsonStorageService` +- `JsonTaskRepository` → `JsonStorageService` +- `JsonTaskSubmissionRepository` → `JsonStorageService` +- `JsonUserAccountRepository` → `JsonStorageService` + +### 仓库 → 模型层 +- `AqiDataRepository` → `AqiData` +- `AqiRecordRepository` → `AqiRecord` +- `AssignmentRepository` → `Assignment` +- `AssignmentRecordRepository` → `AssignmentRecord` +- `AttachmentRepository` → `Attachment` +- `FeedbackRepository` → `Feedback` +- `GridRepository` → `Grid` +- `MapGridRepository` → `MapGrid` +- `OperationLogRepository` → `OperationLog` +- `PasswordResetTokenRepository` → `PasswordResetToken` +- `PollutantThresholdRepository` → `PollutantThreshold` +- `TaskRepository` → `Task` +- `TaskHistoryRepository` → `TaskHistory` +- `TaskSubmissionRepository` → `TaskSubmission` +- `UserAccountRepository` → `UserAccount` + +## 4. 模型层关系 (Model Relationships) + +### 实体关联 +- `UserAccount` (1) ↔ `Feedback` (*) : 用户提交反馈 +- `UserAccount` (1) ↔ `Task` (*) : 用户被分配任务 +- `UserAccount` (1) ↔ `Role` (1) : 用户拥有角色 +- `UserAccount` (1) ↔ `Assignment` (*) : 用户有分配记录 +- `Feedback` (1) ↔ `Task` (1) : 反馈生成任务 +- `Feedback` (*) ↔ `Attachment` (*) : 反馈包含附件 +- `Task` (1) ↔ `Assignment` (*) : 任务有分配记录 +- `Task` (*) ↔ `Attachment` (*) : 任务包含附件 +- `Task` (1) ↔ `TaskHistory` (*) : 任务有历史记录 +- `Task` (1) ↔ `TaskSubmission` (*) : 任务有提交记录 +- `Grid` (1) ↔ `AqiRecord` (*) : 网格有空气质量记录 +- `Assignment` → `Task`, `UserAccount` : 分配关联任务和用户 +- `AssignmentRecord` → `Feedback`, `UserAccount` : 分配记录关联反馈和用户 +- `Attachment` → `Feedback`, `TaskSubmission` : 附件关联反馈或任务提交 +- `AqiData` → `Grid` : 空气质量数据关联网格 +- `AqiRecord` → `Grid` : 空气质量记录关联网格 + +## 5. 服务实现依赖 (Service Implementation Dependencies) + +### 接口实现关系 +- `AiReviewServiceImpl` → `AiReviewService` +- `AuthServiceImpl` → `AuthService` +- `DashboardServiceImpl` → `DashboardService` +- `FeedbackServiceImpl` → `FeedbackService` +- `FileStorageServiceImpl` → `FileStorageService` +- `GridServiceImpl` → `GridService` +- `GridWorkerTaskServiceImpl` → `GridWorkerTaskService` +- `JsonStorageServiceImpl` → `JsonStorageService` +- `JwtServiceImpl` → `JwtService` +- `LoginAttemptServiceImpl` → `LoginAttemptService` +- `MailServiceImpl` → `MailService` +- `OperationLogServiceImpl` → `OperationLogService` +- `PersonnelServiceImpl` → `PersonnelService` +- `SupervisorServiceImpl` → `SupervisorService` +- `TaskAssignmentServiceImpl` → `TaskAssignmentService` +- `TaskManagementServiceImpl` → `TaskManagementService` +- `UserAccountServiceImpl` → `UserAccountService` +- `UserFeedbackServiceImpl` → `UserFeedbackService` +- `VerificationCodeServiceImpl` → `VerificationCodeService` +- `AStarServiceImpl` → `AStarService` + +## 6. 安全层依赖 (Security Dependencies) + +### 安全配置 +- `SecurityConfig` → `JwtAuthenticationFilter`, `UserAccountService` +- `JwtAuthenticationFilter` → `UserDetailsServiceImpl`, `JwtService` +- `UserDetailsServiceImpl` → `UserAccountService` +- `CustomUserDetails` → `UserAccount` +- `JwtService` → `UserAccountRepository` + +## 7. 配置层依赖 (Configuration Dependencies) + +### 配置类 +- `WebConfig` : Web配置 +- `SecurityConfig` : 安全配置 +- `JacksonConfig` : JSON序列化配置 + +## 8. 异常处理依赖 (Exception Dependencies) + +### 异常类 +- `GlobalExceptionHandler` : 全局异常处理器 +- `FileStorageException` : 文件存储异常 +- `ResourceNotFoundException` : 资源未找到异常 +- `UnauthorizedException` : 未授权异常 +- `ValidationException` : 验证异常 + +## 9. 事件处理依赖 (Event Dependencies) + +### 事件监听器 +- `TaskEventListener` : 任务事件监听器 +- `FeedbackEventListener` : 反馈事件监听器 + +## 10. 枚举依赖 (Enum Dependencies) + +### 枚举类型 +- `Role` : 用户角色枚举 +- `TaskStatus` : 任务状态枚举 +- `FeedbackStatus` : 反馈状态枚举 +- `OperationType` : 操作类型枚举 +- `PollutantType` : 污染物类型枚举 +- `GridStatus` : 网格状态枚举 + +## 总结 + +本系统采用分层架构设计,包含: +- **控制器层 (Controller)**: 15个控制器类 +- **服务层 (Service)**: 20个服务接口及其实现 +- **仓库层 (Repository)**: 15个仓库接口及其JSON实现 +- **模型层 (Model)**: 15个实体类 +- **安全层 (Security)**: 5个安全相关类 +- **配置层 (Configuration)**: 3个配置类 +- **异常处理**: 5个异常类 +- **事件处理**: 2个事件监听器 +- **枚举类型**: 6个枚举 + +总计约 **86个主要组件** 及其相互依赖关系,形成了一个完整的环境监测系统架构。 --- - + ### Report/系统测试.md - -# 3.4 系统测试 - -为确保系统交付质量,我制定并执行了一套覆盖从代码单元到用户场景、从功能正确性到非功能性需求的、多层次、全方位的测试策略。 - -## 3.4.1 后端测试 - -我主要负责后端的单元测试、集成测试和API接口测试,旨在从源头保证服务的稳定、可靠与安全。 - -### 1. 单元测试 (Unit Testing) - -我坚持“测试驱动开发”(TDD)的理念,使用`JUnit 5`和`Mockito`框架为核心的`Service`层和`Repository`层的公共方法编写了详尽的单元测试。单元测试的目标是隔离验证最小代码单元(一个方法或一个类)的逻辑正确性。 - -**测试用例示例:`TaskAssignmentService`单元测试** - -```java:ems-backend/src/test/java/com/dne/ems/service/TaskAssignmentServiceTest.java -@ExtendWith(MockitoExtension.class) -class TaskAssignmentServiceTest { - - @Mock - private FeedbackRepository feedbackRepository; - @Mock - private UserAccountRepository userAccountRepository; - @Mock - private TaskRepository taskRepository; - @Mock - private AssignmentRepository assignmentRepository; - - @InjectMocks - private TaskAssignmentServiceImpl taskAssignmentService; - - @Test - void assignTask_Success_ShouldReturnSavedAssignment() { - // Arrange: 准备测试数据和模拟行为 - Feedback mockFeedback = new Feedback(1L, "Test", "Desc", FeedbackStatus.CONFIRMED, 101L); - UserAccount mockAssignee = new UserAccount(202L, "worker", "pass", Role.GRID_WORKER); - when(feedbackRepository.findById(1L)).thenReturn(Optional.of(mockFeedback)); - when(userAccountRepository.findById(202L)).thenReturn(Optional.of(mockAssignee)); - when(taskRepository.save(any(Task.class))).thenAnswer(i -> i.getArgument(0)); - when(assignmentRepository.save(any(Assignment.class))).thenAnswer(i -> i.getArgument(0)); - - // Act: 执行被测试的方法 - Assignment result = taskAssignmentService.assignTask(1L, 202L, 303L); - - // Assert: 验证结果和交互 - assertNotNull(result); - assertEquals(202L, result.getAssigneeId()); - verify(feedbackRepository, times(1)).save(any(Feedback.class)); // 验证Feedback状态是否被更新并保存 - assertEquals(FeedbackStatus.ASSIGNED, mockFeedback.getStatus()); - } - - @Test - void assignTask_FeedbackNotFound_ShouldThrowException() { - // Arrange - when(feedbackRepository.findById(99L)).thenReturn(Optional.empty()); - - // Act & Assert - assertThrows(IllegalArgumentException.class, () -> { - taskAssignmentService.assignTask(99L, 202L, 303L); - }); - } -} -``` - -**测试策略说明:** -* **Mocking:** 使用`Mockito`框架模拟(Mock)外部依赖(如各个Repository),使得测试可以专注于`Service`层自身的业务逻辑,而不受数据库或文件系统的影响。 -* **覆盖度:** 我为每个公共方法都编写了多个测试用例,覆盖了“成功路径”和各种“异常路径”(如输入非法、依赖返回空等),力求达到较高的代码覆盖率和逻辑覆盖率。 - -### 2. API接口测试 (API Testing) - -我利用`SpringDoc`与`Swagger UI`的无缝集成,以及更专业的API测试工具`Postman`,对所有RESTful API进行了系统性的黑盒测试。 - -* **测试范围:** 覆盖了用户认证、权限管理、反馈生命周期、任务生命周期等所有核心模块的每一个API端点。 -* **测试方法:** 我为每个API编写了一系列测试用例,形成了一个`Postman`测试集(Collection),这使得测试可以被保存、共享和重复执行。 - -**API测试用例表示例 (扩展):** - -| 用例ID | 模块 | 接口 | 场景描述 | 输入数据 | 预期HTTP状态 | 预期响应体关键内容 | 结果 | -| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | -| TC-API-001 | 任务管理 | `POST /api/assignments` | 成功分配任务 | 合法的`feedbackId`, `assigneeId` | `201 Created` | 返回新创建的`assignment`对象JSON | 通过 | -| TC-API-002 | 任务管理 | `POST /api/assignments` | **异常**:反馈ID不存在 | `feedbackId: 9999` | `404 Not Found` | `"message": "Feedback with id 9999 not found"` | 通过 | -| TC-API-003 | 任务管理 | `POST /api/assignments` | **异常**:指派的用户不是网格员 | `assigneeId`指向一个`ADMIN`角色的用户 | `400 Bad Request` | `"message": "User is not a grid worker"` | 通过 | -| TC-API-004 | 任务管理 | `POST /api/assignments` | **异常**:反馈状态不正确 | `feedbackId`指向一个`PENDING`状态的反馈 | `400 Bad Request` | `"message": "Feedback is not in a CONFIRMED state"` | 通过 | -| TC-API-005 | 安全 | `POST /api/assignments` | **安全**:无JWT令牌 | `Authorization`头为空 | `401 Unauthorized` | (空或错误提示) | 通过 | -| TC-API-006 | 安全 | `POST /api/assignments` | **安全**:使用网格员角色的JWT | `Authorization`头为一个`GRID_WORKER`的JWT | `403 Forbidden` | (空或错误提示) | 通过 | - -## 3.4.2 前端测试 - -我与负责前端的组员紧密协作,进行了全面的前端功能、UI/UX和兼容性测试。 - -* **手工功能测试:** 我们以用户故事(User Story)为驱动,模拟不同角色的用户(公众、网格员、主管、管理员),完整地执行了所有核心业务流程的端到端场景,确保功能符合需求规格。 -* **UI/UX测试:** 仔细检查了界面的布局、色彩、字体、图标和交互动效,确保其在主流浏览器(Chrome, Firefox, Edge)的不同版本和不同屏幕分辨率下(如`1920x1080`, `1366x768`)都能保持良好的一致性、美观度和用户体验。 - -## 3.4.3 集成与系统测试 - -在前后端分别完成各自的测试后,我们将整个系统部署到一台独立的测试服务器上,进行了端到端的集成测试和系统测试。 - -* **目的:** 验证前后端数据接口的正确性、API调用的顺畅性、以及在真实网络环境下整个系统业务流程的闭环。 -* **过程:** 我们共同执行了一套预先设计的系统级测试用例,这些用例模拟了真实世界中的复杂操作序列。例如: - 1. 公众用户A提交反馈 -> 主管B审核通过 -> 主管B将任务分配给网格员C -> 网格员C完成任务并提交数据 -> 主管B查看已完成的任务和数据。 - 2. 并发测试:多个用户同时提交反馈或更新任务,验证后端`synchronized`机制是否有效防止了数据冲突。 -* **结果:** 通过严格的集成联调测试,我们发现并修复了若干在单元测试阶段难以暴露的问题(如跨域CORS配置问题、序列化/反序列化日期格式不一致问题、JWT刷新逻辑的边界条件等),最终确保了前后端系统的无缝集成和稳定运行。 - -## 3.4.4 非功能性测试 - -除了功能测试,我还对系统的部分非功能性需求进行了初步的探索性测试。 - -* **性能测试:** 使用`Apache JMeter`工具,对核心的登录和查询类API进行了简单的压力测试。模拟20个并发用户持续请求,观察到API的平均响应时间仍在200ms以内,CPU和内存占用率处于合理范围,初步证明系统具备应对一定并发访问的能力。 -* **安全测试:** - * **权限测试:** 严格测试了不同角色的用户访问未授权API的情况,验证系统是否能正确返回`403 Forbidden`。 - * **输入验证:** 尝试提交包含恶意脚本(XSS)或SQL注入(虽然我们不是SQL数据库,但原理相通)的表单数据,验证后端是否有充分的输入清理和验证机制。 + +# 3.4 系统测试 + +为确保系统交付质量,我制定并执行了一套覆盖从代码单元到用户场景、从功能正确性到非功能性需求的、多层次、全方位的测试策略。 + +## 3.4.1 后端测试 + +我主要负责后端的单元测试、集成测试和API接口测试,旨在从源头保证服务的稳定、可靠与安全。 + +### 1. 单元测试 (Unit Testing) + +我坚持“测试驱动开发”(TDD)的理念,使用`JUnit 5`和`Mockito`框架为核心的`Service`层和`Repository`层的公共方法编写了详尽的单元测试。单元测试的目标是隔离验证最小代码单元(一个方法或一个类)的逻辑正确性。 + +**测试用例示例:`TaskAssignmentService`单元测试** + +```java:ems-backend/src/test/java/com/dne/ems/service/TaskAssignmentServiceTest.java +@ExtendWith(MockitoExtension.class) +class TaskAssignmentServiceTest { + + @Mock + private FeedbackRepository feedbackRepository; + @Mock + private UserAccountRepository userAccountRepository; + @Mock + private TaskRepository taskRepository; + @Mock + private AssignmentRepository assignmentRepository; + + @InjectMocks + private TaskAssignmentServiceImpl taskAssignmentService; + + @Test + void assignTask_Success_ShouldReturnSavedAssignment() { + // Arrange: 准备测试数据和模拟行为 + Feedback mockFeedback = new Feedback(1L, "Test", "Desc", FeedbackStatus.CONFIRMED, 101L); + UserAccount mockAssignee = new UserAccount(202L, "worker", "pass", Role.GRID_WORKER); + when(feedbackRepository.findById(1L)).thenReturn(Optional.of(mockFeedback)); + when(userAccountRepository.findById(202L)).thenReturn(Optional.of(mockAssignee)); + when(taskRepository.save(any(Task.class))).thenAnswer(i -> i.getArgument(0)); + when(assignmentRepository.save(any(Assignment.class))).thenAnswer(i -> i.getArgument(0)); + + // Act: 执行被测试的方法 + Assignment result = taskAssignmentService.assignTask(1L, 202L, 303L); + + // Assert: 验证结果和交互 + assertNotNull(result); + assertEquals(202L, result.getAssigneeId()); + verify(feedbackRepository, times(1)).save(any(Feedback.class)); // 验证Feedback状态是否被更新并保存 + assertEquals(FeedbackStatus.ASSIGNED, mockFeedback.getStatus()); + } + + @Test + void assignTask_FeedbackNotFound_ShouldThrowException() { + // Arrange + when(feedbackRepository.findById(99L)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(IllegalArgumentException.class, () -> { + taskAssignmentService.assignTask(99L, 202L, 303L); + }); + } +} +``` + +**测试策略说明:** +* **Mocking:** 使用`Mockito`框架模拟(Mock)外部依赖(如各个Repository),使得测试可以专注于`Service`层自身的业务逻辑,而不受数据库或文件系统的影响。 +* **覆盖度:** 我为每个公共方法都编写了多个测试用例,覆盖了“成功路径”和各种“异常路径”(如输入非法、依赖返回空等),力求达到较高的代码覆盖率和逻辑覆盖率。 + +### 2. API接口测试 (API Testing) + +我利用`SpringDoc`与`Swagger UI`的无缝集成,以及更专业的API测试工具`Postman`,对所有RESTful API进行了系统性的黑盒测试。 + +* **测试范围:** 覆盖了用户认证、权限管理、反馈生命周期、任务生命周期等所有核心模块的每一个API端点。 +* **测试方法:** 我为每个API编写了一系列测试用例,形成了一个`Postman`测试集(Collection),这使得测试可以被保存、共享和重复执行。 + +**API测试用例表示例 (扩展):** + +| 用例ID | 模块 | 接口 | 场景描述 | 输入数据 | 预期HTTP状态 | 预期响应体关键内容 | 结果 | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| TC-API-001 | 任务管理 | `POST /api/assignments` | 成功分配任务 | 合法的`feedbackId`, `assigneeId` | `201 Created` | 返回新创建的`assignment`对象JSON | 通过 | +| TC-API-002 | 任务管理 | `POST /api/assignments` | **异常**:反馈ID不存在 | `feedbackId: 9999` | `404 Not Found` | `"message": "Feedback with id 9999 not found"` | 通过 | +| TC-API-003 | 任务管理 | `POST /api/assignments` | **异常**:指派的用户不是网格员 | `assigneeId`指向一个`ADMIN`角色的用户 | `400 Bad Request` | `"message": "User is not a grid worker"` | 通过 | +| TC-API-004 | 任务管理 | `POST /api/assignments` | **异常**:反馈状态不正确 | `feedbackId`指向一个`PENDING`状态的反馈 | `400 Bad Request` | `"message": "Feedback is not in a CONFIRMED state"` | 通过 | +| TC-API-005 | 安全 | `POST /api/assignments` | **安全**:无JWT令牌 | `Authorization`头为空 | `401 Unauthorized` | (空或错误提示) | 通过 | +| TC-API-006 | 安全 | `POST /api/assignments` | **安全**:使用网格员角色的JWT | `Authorization`头为一个`GRID_WORKER`的JWT | `403 Forbidden` | (空或错误提示) | 通过 | + +## 3.4.2 前端测试 + +我与负责前端的组员紧密协作,进行了全面的前端功能、UI/UX和兼容性测试。 + +* **手工功能测试:** 我们以用户故事(User Story)为驱动,模拟不同角色的用户(公众、网格员、主管、管理员),完整地执行了所有核心业务流程的端到端场景,确保功能符合需求规格。 +* **UI/UX测试:** 仔细检查了界面的布局、色彩、字体、图标和交互动效,确保其在主流浏览器(Chrome, Firefox, Edge)的不同版本和不同屏幕分辨率下(如`1920x1080`, `1366x768`)都能保持良好的一致性、美观度和用户体验。 + +## 3.4.3 集成与系统测试 + +在前后端分别完成各自的测试后,我们将整个系统部署到一台独立的测试服务器上,进行了端到端的集成测试和系统测试。 + +* **目的:** 验证前后端数据接口的正确性、API调用的顺畅性、以及在真实网络环境下整个系统业务流程的闭环。 +* **过程:** 我们共同执行了一套预先设计的系统级测试用例,这些用例模拟了真实世界中的复杂操作序列。例如: + 1. 公众用户A提交反馈 -> 主管B审核通过 -> 主管B将任务分配给网格员C -> 网格员C完成任务并提交数据 -> 主管B查看已完成的任务和数据。 + 2. 并发测试:多个用户同时提交反馈或更新任务,验证后端`synchronized`机制是否有效防止了数据冲突。 +* **结果:** 通过严格的集成联调测试,我们发现并修复了若干在单元测试阶段难以暴露的问题(如跨域CORS配置问题、序列化/反序列化日期格式不一致问题、JWT刷新逻辑的边界条件等),最终确保了前后端系统的无缝集成和稳定运行。 + +## 3.4.4 非功能性测试 + +除了功能测试,我还对系统的部分非功能性需求进行了初步的探索性测试。 + +* **性能测试:** 使用`Apache JMeter`工具,对核心的登录和查询类API进行了简单的压力测试。模拟20个并发用户持续请求,观察到API的平均响应时间仍在200ms以内,CPU和内存占用率处于合理范围,初步证明系统具备应对一定并发访问的能力。 +* **安全测试:** + * **权限测试:** 严格测试了不同角色的用户访问未授权API的情况,验证系统是否能正确返回`403 Forbidden`。 + * **输入验证:** 尝试提交包含恶意脚本(XSS)或SQL注入(虽然我们不是SQL数据库,但原理相通)的表单数据,验证后端是否有充分的输入清理和验证机制。 --- - + ### Report/系统设计_第二部分.md - -## 2. 详细设计 - -### 2.1 功能模块详细设计 - -本章节将对核心功能模块的内部设计进行更深入的阐述。 - -#### 2.1.1 反馈管理模块 - -反馈管理模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。 - -**核心组件**: -- **主要控制器**: `FeedbackController`, `PublicController` -- **核心服务**: `FeedbackService`, `FeedbackAiReviewService` -- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest` - -**设计要点**: -- 采用 `@RequestPart` 同时接收JSON数据和文件上传,实现了富文本内容的反馈提交。 -- 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件,将AI审核流程解耦并异步化,提高了API的响应速度。 -- 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。 - -**反馈状态流转**: -``` -SUBMITTED → AI_REVIEWED → PENDING_REVIEW → APPROVED/REJECTED → PROCESSED → CLOSED -``` - -**主要业务流程**: -1. 公众用户提交反馈,系统生成唯一事件ID -2. AI自动审核内容,识别垃圾信息和重复提交 -3. 主管进行人工审核,确认反馈有效性 -4. 有效反馈转化为任务,进入任务管理模块 -5. 任务完成后,反馈状态更新为已关闭 - -#### 2.1.2 任务管理模块 - -任务管理模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。 - -**核心组件**: -- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController` -- **核心服务**: `TaskService`, `TaskAssignmentService` -- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO` - -**设计要点**: -- 清晰的状态机管理任务的生命周期(CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED)。 -- 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。 -- 为主管和网格员提供了完全独立的API端点,严格分离了不同角色的操作权限。 - -**任务分配算法**: -任务分配采用了一种多因素加权评分机制,综合考虑以下因素: -1. **地理距离**: 计算网格员当前位置到任务位置的距离 -2. **当前负载**: 评估网格员手头的任务数量和优先级 -3. **专业匹配度**: 根据任务类型和网格员的专业技能进行匹配 -4. **历史表现**: 考虑网格员的历史完成率和质量评分 - -这些因素通过加权计算得出每个候选网格员的综合得分,系统推荐得分最高的网格员。主管可以接受系统建议或手动选择其他网格员。 - -**主要业务流程**: -1. 从审核通过的反馈自动创建任务,或由主管手动创建临时任务 -2. 系统推荐最适合的处理人员,或由主管手动指定 -3. 任务分配给网格员,并发送通知 -4. 网格员接受任务,更新状态为进行中 -5. 网格员处理任务,提交处理结果 -6. 主管审核处理结果,确认任务完成或要求重新处理 - -#### 2.1.3 用户与人员管理模块 - -用户与人员管理模块为系统的权限管理和组织架构提供了基础。 - -**核心组件**: -- **主要控制器**: `PersonnelController`, `UserProfileController` -- **核心服务**: `UserAccountService`, `OperationLogService` -- **关键DTO**: `UserCreationRequest`, `UserUpdateRequest`, `UserRoleUpdateRequest` - -**设计要点**: -- 提供了对用户账户的完整CRUD操作。 -- 实现了用户角色和状态的精细化管理。 -- 所有关键操作均通过`OperationLogService`记录日志,便于审计和追踪。 - -**用户角色体系**: -系统定义了五种核心角色,每种角色具有不同的权限范围: -1. **ADMIN**: 系统管理员,拥有所有功能的访问权限 -2. **DECISION_MAKER**: 决策者,主要访问统计数据和决策支持功能 -3. **SUPERVISOR**: 主管,负责审核反馈和任务分配 -4. **GRID_WORKER**: 网格员,负责执行具体任务 -5. **PUBLIC_USER**: 公众用户,仅能提交反馈和查询自己的反馈历史 - -**主要业务流程**: -1. 管理员创建和管理系统用户 -2. 用户登录系统,获取JWT令牌 -3. 用户访问系统功能,系统根据角色进行权限控制 -4. 用户可以更新个人信息和密码 -5. 管理员可以禁用或重新激活用户账号 - -#### 2.1.4 网格与地图模块 - -网格与地图模块是系统实现区域化、网格化管理的核心。 - -**核心组件**: -- **主要控制器**: `GridController`, `MapController` -- **核心服务**: `GridService`, `PathfindingService` -- **关键算法**: `AStarService` - -**设计要点**: -- 支持对地理区域进行灵活的网格化定义,并可将网格标记为障碍。 -- 实现了网格与网格员的关联管理。 -- 核心亮点是集成了`AStarService`,该服务封装了A*寻路算法,为任务路径规划提供支持。 - -**网格数据结构**: -每个网格单元包含以下关键信息: -- 网格坐标(x, y) -- 所属城市和区域 -- 是否为障碍物 -- 关联的网格员(可选) -- 描述信息 - -**A*寻路算法实现**: -系统实现了高效的A*寻路算法,用于为网格员规划从当前位置到任务地点的最优路径: -1. 使用优先队列管理开放列表,确保每次选择F值最小的节点 -2. F值计算为从起点到当前节点的实际代价(G值)与从当前节点到目标的估计代价(H值)之和 -3. 使用曼哈顿距离作为启发函数,适合网格化的移动模式 -4. 动态加载地图障碍物信息,确保路径规划避开不可通行区域 - -**主要业务流程**: -1. 管理员定义城市网格系统 -2. 管理员将网格员分配到特定网格 -3. 用户提交反馈时,系统自动关联到对应网格 -4. 网格员接收任务后,系统提供从当前位置到任务地点的最优路径 -5. 管理层可查看基于网格的统计数据,识别问题高发区域 - -#### 2.1.5 决策支持模块 - -决策支持模块为管理层提供数据洞察和可视化报告,辅助决策制定。 - -**核心组件**: -- **主要控制器**: `DashboardController`, `ReportController` -- **核心服务**: `DashboardService`, `StatisticsService` -- **关键DTO**: `DashboardStatsDTO`, `AqiDistributionDTO`, `HeatmapPointDTO` - -**设计要点**: -- 采用数据聚合和统计分析技术,从系统运行数据中提取有价值的信息。 -- 提供多维度的数据可视化,包括趋势图、分布图和热力图。 -- 支持数据导出功能,便于进一步分析和报告生成。 - -**核心统计指标**: -1. **反馈处理效率**: 平均响应时间、处理时长分布 -2. **任务完成情况**: 按时完成率、任务状态分布 -3. **区域问题分布**: 按网格统计的问题密度和类型分布 -4. **人员绩效**: 网格员的任务完成量和质量评分 - -**主要业务流程**: -1. 决策者登录系统,访问决策支持模块 -2. 系统实时计算和展示核心业务指标 -3. 决策者可选择不同维度进行数据分析 -4. 系统生成可视化图表,展示数据趋势和分布 -5. 决策者可导出分析结果,用于报告和决策支持 + +## 2. 详细设计 + +### 2.1 功能模块详细设计 + +本章节将对核心功能模块的内部设计进行更深入的阐述。 + +#### 2.1.1 反馈管理模块 + +反馈管理模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。 + +**核心组件**: +- **主要控制器**: `FeedbackController`, `PublicController` +- **核心服务**: `FeedbackService`, `FeedbackAiReviewService` +- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest` + +**设计要点**: +- 采用 `@RequestPart` 同时接收JSON数据和文件上传,实现了富文本内容的反馈提交。 +- 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件,将AI审核流程解耦并异步化,提高了API的响应速度。 +- 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。 + +**反馈状态流转**: +``` +SUBMITTED → AI_REVIEWED → PENDING_REVIEW → APPROVED/REJECTED → PROCESSED → CLOSED +``` + +**主要业务流程**: +1. 公众用户提交反馈,系统生成唯一事件ID +2. AI自动审核内容,识别垃圾信息和重复提交 +3. 主管进行人工审核,确认反馈有效性 +4. 有效反馈转化为任务,进入任务管理模块 +5. 任务完成后,反馈状态更新为已关闭 + +#### 2.1.2 任务管理模块 + +任务管理模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。 + +**核心组件**: +- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController` +- **核心服务**: `TaskService`, `TaskAssignmentService` +- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO` + +**设计要点**: +- 清晰的状态机管理任务的生命周期(CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED)。 +- 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。 +- 为主管和网格员提供了完全独立的API端点,严格分离了不同角色的操作权限。 + +**任务分配算法**: +任务分配采用了一种多因素加权评分机制,综合考虑以下因素: +1. **地理距离**: 计算网格员当前位置到任务位置的距离 +2. **当前负载**: 评估网格员手头的任务数量和优先级 +3. **专业匹配度**: 根据任务类型和网格员的专业技能进行匹配 +4. **历史表现**: 考虑网格员的历史完成率和质量评分 + +这些因素通过加权计算得出每个候选网格员的综合得分,系统推荐得分最高的网格员。主管可以接受系统建议或手动选择其他网格员。 + +**主要业务流程**: +1. 从审核通过的反馈自动创建任务,或由主管手动创建临时任务 +2. 系统推荐最适合的处理人员,或由主管手动指定 +3. 任务分配给网格员,并发送通知 +4. 网格员接受任务,更新状态为进行中 +5. 网格员处理任务,提交处理结果 +6. 主管审核处理结果,确认任务完成或要求重新处理 + +#### 2.1.3 用户与人员管理模块 + +用户与人员管理模块为系统的权限管理和组织架构提供了基础。 + +**核心组件**: +- **主要控制器**: `PersonnelController`, `UserProfileController` +- **核心服务**: `UserAccountService`, `OperationLogService` +- **关键DTO**: `UserCreationRequest`, `UserUpdateRequest`, `UserRoleUpdateRequest` + +**设计要点**: +- 提供了对用户账户的完整CRUD操作。 +- 实现了用户角色和状态的精细化管理。 +- 所有关键操作均通过`OperationLogService`记录日志,便于审计和追踪。 + +**用户角色体系**: +系统定义了五种核心角色,每种角色具有不同的权限范围: +1. **ADMIN**: 系统管理员,拥有所有功能的访问权限 +2. **DECISION_MAKER**: 决策者,主要访问统计数据和决策支持功能 +3. **SUPERVISOR**: 主管,负责审核反馈和任务分配 +4. **GRID_WORKER**: 网格员,负责执行具体任务 +5. **PUBLIC_USER**: 公众用户,仅能提交反馈和查询自己的反馈历史 + +**主要业务流程**: +1. 管理员创建和管理系统用户 +2. 用户登录系统,获取JWT令牌 +3. 用户访问系统功能,系统根据角色进行权限控制 +4. 用户可以更新个人信息和密码 +5. 管理员可以禁用或重新激活用户账号 + +#### 2.1.4 网格与地图模块 + +网格与地图模块是系统实现区域化、网格化管理的核心。 + +**核心组件**: +- **主要控制器**: `GridController`, `MapController` +- **核心服务**: `GridService`, `PathfindingService` +- **关键算法**: `AStarService` + +**设计要点**: +- 支持对地理区域进行灵活的网格化定义,并可将网格标记为障碍。 +- 实现了网格与网格员的关联管理。 +- 核心亮点是集成了`AStarService`,该服务封装了A*寻路算法,为任务路径规划提供支持。 + +**网格数据结构**: +每个网格单元包含以下关键信息: +- 网格坐标(x, y) +- 所属城市和区域 +- 是否为障碍物 +- 关联的网格员(可选) +- 描述信息 + +**A*寻路算法实现**: +系统实现了高效的A*寻路算法,用于为网格员规划从当前位置到任务地点的最优路径: +1. 使用优先队列管理开放列表,确保每次选择F值最小的节点 +2. F值计算为从起点到当前节点的实际代价(G值)与从当前节点到目标的估计代价(H值)之和 +3. 使用曼哈顿距离作为启发函数,适合网格化的移动模式 +4. 动态加载地图障碍物信息,确保路径规划避开不可通行区域 + +**主要业务流程**: +1. 管理员定义城市网格系统 +2. 管理员将网格员分配到特定网格 +3. 用户提交反馈时,系统自动关联到对应网格 +4. 网格员接收任务后,系统提供从当前位置到任务地点的最优路径 +5. 管理层可查看基于网格的统计数据,识别问题高发区域 + +#### 2.1.5 决策支持模块 + +决策支持模块为管理层提供数据洞察和可视化报告,辅助决策制定。 + +**核心组件**: +- **主要控制器**: `DashboardController`, `ReportController` +- **核心服务**: `DashboardService`, `StatisticsService` +- **关键DTO**: `DashboardStatsDTO`, `AqiDistributionDTO`, `HeatmapPointDTO` + +**设计要点**: +- 采用数据聚合和统计分析技术,从系统运行数据中提取有价值的信息。 +- 提供多维度的数据可视化,包括趋势图、分布图和热力图。 +- 支持数据导出功能,便于进一步分析和报告生成。 + +**核心统计指标**: +1. **反馈处理效率**: 平均响应时间、处理时长分布 +2. **任务完成情况**: 按时完成率、任务状态分布 +3. **区域问题分布**: 按网格统计的问题密度和类型分布 +4. **人员绩效**: 网格员的任务完成量和质量评分 + +**主要业务流程**: +1. 决策者登录系统,访问决策支持模块 +2. 系统实时计算和展示核心业务指标 +3. 决策者可选择不同维度进行数据分析 +4. 系统生成可视化图表,展示数据趋势和分布 +5. 决策者可导出分析结果,用于报告和决策支持 --- - + ### Report/系统设计_第三部分.md - -### 2.2 类和对象的设计 - -本节定义了系统核心业务对象的静态结构,即类图。这些类图展示了系统中的主要实体、它们的属性和方法,以及它们之间的关系。 - -#### 2.2.1 `UserAccount` 类图 - -`UserAccount`类是系统中用户账户的核心模型,包含用户的基本信息、认证信息和角色信息。 - -```mermaid -classDiagram - class UserAccount { - -Long id - -String name - -String phone - -String email - -String password - -Gender gender - -UserRole role - -UserStatus status - -Integer gridX - -Integer gridY - -String region - -String level - -List~String~ skills - -Boolean enabled - -Double currentLatitude - -Double currentLongitude - -Integer failedLoginAttempts - -LocalDateTime lockoutEndTime - -LocalDateTime createdAt - -LocalDateTime updatedAt - +getId() Long - +getName() String - +getPhone() String - +getEmail() String - +getPassword() String - +getRole() UserRole - +getStatus() UserStatus - +isEnabled() Boolean - +setPassword(String) void - +updateProfile(UserUpdateRequest) void - +isAccountNonLocked() Boolean - } - - class UserRole { - <> - ADMIN - SUPERVISOR - GRID_WORKER - DECISION_MAKER - PUBLIC_USER - } - - class UserStatus { - <> - ACTIVE - INACTIVE - SUSPENDED - } - - class Gender { - <> - MALE - FEMALE - OTHER - } - - UserAccount --> UserRole - UserAccount --> UserStatus - UserAccount --> Gender -``` - -#### 2.2.2 `Feedback` 类图 - -`Feedback`类表示用户提交的环境问题反馈,是系统的核心业务对象之一。 - -```mermaid -classDiagram - class Feedback { - -Long id - -String eventId - -String title - -String description - -PollutionType pollutionType - -SeverityLevel severityLevel - -FeedbackStatus status - -String textAddress - -Integer gridX - -Integer gridY - -Double latitude - -Double longitude - -UserAccount submitter - -LocalDateTime createdAt - -LocalDateTime updatedAt - -List~Attachment~ attachments - -Task relatedTask - +getId() Long - +getEventId() String - +getTitle() String - +getDescription() String - +getPollutionType() PollutionType - +getSeverityLevel() SeverityLevel - +getStatus() FeedbackStatus - +updateStatus(FeedbackStatus) void - +addAttachment(Attachment) void - } - - class PollutionType { - <> - AIR - WATER - SOIL - NOISE - LIGHT - OTHER - } - - class SeverityLevel { - <> - LOW - MEDIUM - HIGH - CRITICAL - } - - class FeedbackStatus { - <> - SUBMITTED - AI_REVIEWED - PENDING_REVIEW - APPROVED - REJECTED - PROCESSED - CLOSED - } - - class Attachment { - -Long id - -String fileName - -String filePath - -String fileType - -Long fileSize - -LocalDateTime uploadedAt - +getId() Long - +getFileName() String - +getFilePath() String - } - - Feedback --> PollutionType - Feedback --> SeverityLevel - Feedback --> FeedbackStatus - Feedback --> UserAccount : submitter - Feedback "1" --> "*" Attachment - Feedback "1" --> "0..1" Task -``` - -#### 2.2.3 `Task` 类图 - -`Task`类表示网格员需要执行的工作任务,通常由反馈生成。 - -```mermaid -classDiagram - class Task { - -Long id - -Feedback feedback - -UserAccount assignee - -UserAccount createdBy - -TaskStatus status - -LocalDateTime assignedAt - -LocalDateTime completedAt - -LocalDateTime createdAt - -LocalDateTime updatedAt - -String title - -String description - -PollutionType pollutionType - -SeverityLevel severityLevel - -String textAddress - -Integer gridX - -Integer gridY - -Double latitude - -Double longitude - -List~TaskHistory~ history - -Assignment assignment - -List~TaskSubmission~ submissions - +getId() Long - +getStatus() TaskStatus - +getAssignee() UserAccount - +updateStatus(TaskStatus) void - +assign(UserAccount) void - +addSubmission(TaskSubmission) void - } - - class TaskStatus { - <> - CREATED - ASSIGNED - ACCEPTED - IN_PROGRESS - SUBMITTED - APPROVED - REJECTED - COMPLETED - } - - class TaskHistory { - -Long id - -Task task - -UserAccount actor - -TaskStatus oldStatus - -TaskStatus newStatus - -String remarks - -LocalDateTime timestamp - +getId() Long - +getTask() Task - +getOldStatus() TaskStatus - +getNewStatus() TaskStatus - } - - class TaskSubmission { - -Long id - -Task task - -UserAccount submitter - -String description - -LocalDateTime submittedAt - -List~Attachment~ attachments - +getId() Long - +getTask() Task - +getSubmitter() UserAccount - +addAttachment(Attachment) void - } - - Task --> TaskStatus - Task --> UserAccount : assignee - Task --> UserAccount : createdBy - Task "1" --> "*" TaskHistory - Task "1" --> "0..1" Assignment - Task "1" --> "*" TaskSubmission - Task "0..1" --> "1" Feedback - TaskHistory --> Task - TaskHistory --> UserAccount : actor - TaskSubmission --> Task - TaskSubmission --> UserAccount : submitter - TaskSubmission "1" --> "*" Attachment -``` - -#### 2.2.4 `Grid` 和 `Assignment` 类图 - -`Grid`类表示地理空间的网格单元,`Assignment`类表示任务分配记录。 - -```mermaid -classDiagram - class Grid { - -Long id - -Integer gridX - -Integer gridY - -String cityName - -String districtName - -String description - -Boolean isObstacle - +getId() Long - +getGridX() Integer - +getGridY() Integer - +isObstacle() Boolean - } - - class Assignment { - -Long id - -Task task - -UserAccount assigner - -LocalDateTime assignmentTime - -LocalDateTime deadline - -AssignmentStatus status - -String remarks - +getId() Long - +getTask() Task - +getAssigner() UserAccount - +getStatus() AssignmentStatus - +updateStatus(AssignmentStatus) void - } - - class AssignmentStatus { - <> - PENDING - ACCEPTED - REJECTED - COMPLETED - } - - Assignment --> Task - Assignment --> UserAccount : assigner - Assignment --> AssignmentStatus -``` - -### 2.3 动态模型设计 - -本节描述了系统在运行时,对象之间的交互行为。通过时序图和状态图,展示了系统的动态行为和状态转换。 - -#### 2.3.1 核心时序图 - -时序图展示了系统中对象之间的交互序列,清晰地表达了业务流程的执行顺序和对象间的消息传递。 - -**用户登录认证时序图** -```mermaid -sequenceDiagram - participant User as 用户 - participant AuthController as 认证控制器 - participant AuthService as 认证服务 - participant JwtUtil as JWT工具 - - User->>AuthController: POST /api/auth/login (email, password) - AuthController->>AuthService: signIn(LoginRequest) - AuthService->>AuthService: 验证凭证 - alt 验证成功 - AuthService->>JwtUtil: generateToken(user) - JwtUtil-->>AuthService: 返回JWT令牌 - AuthService-->>AuthController: 返回JwtAuthenticationResponse - AuthController-->>User: 200 OK (token) - else 验证失败 - AuthService-->>AuthController: 抛出AuthenticationException - AuthController-->>User: 401 Unauthorized - end -``` - -**反馈提交与处理时序图** -```mermaid -sequenceDiagram - participant User as 用户 - participant FeedbackController as 反馈控制器 - participant FeedbackService as 反馈服务 - participant EventPublisher as 事件发布器 - participant FeedbackAiReviewService as AI审核服务 - participant TaskService as 任务服务 - - User->>FeedbackController: POST /api/feedback/submit - FeedbackController->>FeedbackService: submitFeedback(request, files) - FeedbackService->>FeedbackService: 生成事件ID - FeedbackService->>FeedbackService: 保存反馈和附件 - FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent) - FeedbackService-->>FeedbackController: 返回反馈信息 - FeedbackController-->>User: 201 Created - - EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核 - FeedbackAiReviewService->>FeedbackAiReviewService: 分析反馈内容 - FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED) - - Note over User,TaskService: 主管审核流程 - - alt 主管批准反馈 - FeedbackService->>TaskService: createTaskFromFeedback(feedback) - TaskService->>TaskService: 创建任务 - TaskService-->>FeedbackService: 返回创建的任务 - FeedbackService->>FeedbackService: updateFeedbackStatus(PROCESSED) - else 主管拒绝反馈 - FeedbackService->>FeedbackService: updateFeedbackStatus(REJECTED) - end -``` - -**主管分配任务时序图** -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant TaskMgmtController as 任务管理控制器 - participant TaskService as 任务服务 - participant AssignmentService as 分配服务 - participant NotificationService as 通知服务 - participant GridWorker as 网格员 - - Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId) - TaskMgmtController->>TaskService: assignTask(taskId, workerId) - TaskService->>TaskService: 验证任务状态 - TaskService->>AssignmentService: createAssignment(task, worker) - AssignmentService->>AssignmentService: 创建分配记录 - AssignmentService-->>TaskService: 返回Assignment - TaskService->>TaskService: 更新任务状态为ASSIGNED - TaskService->>NotificationService: notifyWorker(workerId, taskId) - NotificationService->>NotificationService: 创建通知 - TaskService-->>TaskMgmtController: 分配成功 - TaskMgmtController-->>Supervisor: 200 OK - - NotificationService-->>GridWorker: 发送任务通知 -``` - -**网格员处理任务时序图** -```mermaid -sequenceDiagram - participant GridWorker as 网格员 - participant WorkerTaskController as 网格员任务控制器 - participant TaskService as 任务服务 - participant PathfindingService as 寻路服务 - participant Supervisor as 主管 - - GridWorker->>WorkerTaskController: POST /api/worker/tasks/{taskId}/accept - WorkerTaskController->>TaskService: acceptTask(taskId, workerId) - TaskService->>TaskService: 更新任务状态为ACCEPTED - TaskService-->>WorkerTaskController: 返回更新后的任务 - WorkerTaskController-->>GridWorker: 200 OK - - GridWorker->>WorkerTaskController: GET /api/pathfinding/find?start=x,y&end=a,b - WorkerTaskController->>PathfindingService: findPath(start, end) - PathfindingService->>PathfindingService: 执行A*算法 - PathfindingService-->>WorkerTaskController: 返回路径点列表 - WorkerTaskController-->>GridWorker: 200 OK (路径数据) - - GridWorker->>WorkerTaskController: POST /api/worker/tasks/{taskId}/submit - WorkerTaskController->>TaskService: submitTaskCompletion(taskId, workerId, request, files) - TaskService->>TaskService: 验证并更新任务状态为SUBMITTED - TaskService->>TaskService: 保存处理结果和附件 - TaskService-->>WorkerTaskController: 返回更新后的任务 - WorkerTaskController-->>GridWorker: 200 OK - - Note over GridWorker,Supervisor: 主管审核流程 - - alt 主管批准任务完成 - TaskService->>TaskService: 更新任务状态为COMPLETED - else 主管拒绝任务完成 - TaskService->>TaskService: 更新任务状态为REJECTED - TaskService->>NotificationService: 通知网格员重新处理 - end -``` - -#### 2.3.2 核心状态图 - -状态图展示了系统中关键对象的状态变化和转换条件,帮助理解对象的生命周期。 - -**反馈状态机 (Feedback Status)** -```mermaid -stateDiagram-v2 - [*] --> SUBMITTED: 用户提交反馈 - SUBMITTED --> AI_REVIEWED: AI自动审核 - AI_REVIEWED --> PENDING_REVIEW: 进入人工审核队列 - PENDING_REVIEW --> APPROVED: 主管批准 - PENDING_REVIEW --> REJECTED: 主管拒绝 - APPROVED --> PROCESSED: 创建关联任务 - PROCESSED --> CLOSED: 任务完成 - REJECTED --> [*] - CLOSED --> [*] -``` - -**任务状态机 (Task Status)** -```mermaid -stateDiagram-v2 - [*] --> CREATED: 创建任务 - CREATED --> ASSIGNED: 分配给网格员 - ASSIGNED --> ACCEPTED: 网格员接受 - ASSIGNED --> CREATED: 网格员拒绝 - ACCEPTED --> IN_PROGRESS: 开始处理 - IN_PROGRESS --> SUBMITTED: 提交处理结果 - SUBMITTED --> APPROVED: 主管批准 - SUBMITTED --> REJECTED: 主管拒绝 - REJECTED --> IN_PROGRESS: 重新处理 - APPROVED --> COMPLETED: 完成归档 - COMPLETED --> [*] -``` - -**用户状态机 (User Status)** -```mermaid -stateDiagram-v2 - [*] --> ACTIVE: 创建账户 - ACTIVE --> SUSPENDED: 管理员暂停 - SUSPENDED --> ACTIVE: 管理员重新激活 - ACTIVE --> INACTIVE: 长期未使用 - INACTIVE --> ACTIVE: 用户登录 - INACTIVE --> [*]: 账户删除 - SUSPENDED --> [*]: 账户删除 -``` + +### 2.2 类和对象的设计 + +本节定义了系统核心业务对象的静态结构,即类图。这些类图展示了系统中的主要实体、它们的属性和方法,以及它们之间的关系。 + +#### 2.2.1 `UserAccount` 类图 + +`UserAccount`类是系统中用户账户的核心模型,包含用户的基本信息、认证信息和角色信息。 + +```mermaid +classDiagram + class UserAccount { + -Long id + -String name + -String phone + -String email + -String password + -Gender gender + -UserRole role + -UserStatus status + -Integer gridX + -Integer gridY + -String region + -String level + -List~String~ skills + -Boolean enabled + -Double currentLatitude + -Double currentLongitude + -Integer failedLoginAttempts + -LocalDateTime lockoutEndTime + -LocalDateTime createdAt + -LocalDateTime updatedAt + +getId() Long + +getName() String + +getPhone() String + +getEmail() String + +getPassword() String + +getRole() UserRole + +getStatus() UserStatus + +isEnabled() Boolean + +setPassword(String) void + +updateProfile(UserUpdateRequest) void + +isAccountNonLocked() Boolean + } + + class UserRole { + <> + ADMIN + SUPERVISOR + GRID_WORKER + DECISION_MAKER + PUBLIC_USER + } + + class UserStatus { + <> + ACTIVE + INACTIVE + SUSPENDED + } + + class Gender { + <> + MALE + FEMALE + OTHER + } + + UserAccount --> UserRole + UserAccount --> UserStatus + UserAccount --> Gender +``` + +#### 2.2.2 `Feedback` 类图 + +`Feedback`类表示用户提交的环境问题反馈,是系统的核心业务对象之一。 + +```mermaid +classDiagram + class Feedback { + -Long id + -String eventId + -String title + -String description + -PollutionType pollutionType + -SeverityLevel severityLevel + -FeedbackStatus status + -String textAddress + -Integer gridX + -Integer gridY + -Double latitude + -Double longitude + -UserAccount submitter + -LocalDateTime createdAt + -LocalDateTime updatedAt + -List~Attachment~ attachments + -Task relatedTask + +getId() Long + +getEventId() String + +getTitle() String + +getDescription() String + +getPollutionType() PollutionType + +getSeverityLevel() SeverityLevel + +getStatus() FeedbackStatus + +updateStatus(FeedbackStatus) void + +addAttachment(Attachment) void + } + + class PollutionType { + <> + AIR + WATER + SOIL + NOISE + LIGHT + OTHER + } + + class SeverityLevel { + <> + LOW + MEDIUM + HIGH + CRITICAL + } + + class FeedbackStatus { + <> + SUBMITTED + AI_REVIEWED + PENDING_REVIEW + APPROVED + REJECTED + PROCESSED + CLOSED + } + + class Attachment { + -Long id + -String fileName + -String filePath + -String fileType + -Long fileSize + -LocalDateTime uploadedAt + +getId() Long + +getFileName() String + +getFilePath() String + } + + Feedback --> PollutionType + Feedback --> SeverityLevel + Feedback --> FeedbackStatus + Feedback --> UserAccount : submitter + Feedback "1" --> "*" Attachment + Feedback "1" --> "0..1" Task +``` + +#### 2.2.3 `Task` 类图 + +`Task`类表示网格员需要执行的工作任务,通常由反馈生成。 + +```mermaid +classDiagram + class Task { + -Long id + -Feedback feedback + -UserAccount assignee + -UserAccount createdBy + -TaskStatus status + -LocalDateTime assignedAt + -LocalDateTime completedAt + -LocalDateTime createdAt + -LocalDateTime updatedAt + -String title + -String description + -PollutionType pollutionType + -SeverityLevel severityLevel + -String textAddress + -Integer gridX + -Integer gridY + -Double latitude + -Double longitude + -List~TaskHistory~ history + -Assignment assignment + -List~TaskSubmission~ submissions + +getId() Long + +getStatus() TaskStatus + +getAssignee() UserAccount + +updateStatus(TaskStatus) void + +assign(UserAccount) void + +addSubmission(TaskSubmission) void + } + + class TaskStatus { + <> + CREATED + ASSIGNED + ACCEPTED + IN_PROGRESS + SUBMITTED + APPROVED + REJECTED + COMPLETED + } + + class TaskHistory { + -Long id + -Task task + -UserAccount actor + -TaskStatus oldStatus + -TaskStatus newStatus + -String remarks + -LocalDateTime timestamp + +getId() Long + +getTask() Task + +getOldStatus() TaskStatus + +getNewStatus() TaskStatus + } + + class TaskSubmission { + -Long id + -Task task + -UserAccount submitter + -String description + -LocalDateTime submittedAt + -List~Attachment~ attachments + +getId() Long + +getTask() Task + +getSubmitter() UserAccount + +addAttachment(Attachment) void + } + + Task --> TaskStatus + Task --> UserAccount : assignee + Task --> UserAccount : createdBy + Task "1" --> "*" TaskHistory + Task "1" --> "0..1" Assignment + Task "1" --> "*" TaskSubmission + Task "0..1" --> "1" Feedback + TaskHistory --> Task + TaskHistory --> UserAccount : actor + TaskSubmission --> Task + TaskSubmission --> UserAccount : submitter + TaskSubmission "1" --> "*" Attachment +``` + +#### 2.2.4 `Grid` 和 `Assignment` 类图 + +`Grid`类表示地理空间的网格单元,`Assignment`类表示任务分配记录。 + +```mermaid +classDiagram + class Grid { + -Long id + -Integer gridX + -Integer gridY + -String cityName + -String districtName + -String description + -Boolean isObstacle + +getId() Long + +getGridX() Integer + +getGridY() Integer + +isObstacle() Boolean + } + + class Assignment { + -Long id + -Task task + -UserAccount assigner + -LocalDateTime assignmentTime + -LocalDateTime deadline + -AssignmentStatus status + -String remarks + +getId() Long + +getTask() Task + +getAssigner() UserAccount + +getStatus() AssignmentStatus + +updateStatus(AssignmentStatus) void + } + + class AssignmentStatus { + <> + PENDING + ACCEPTED + REJECTED + COMPLETED + } + + Assignment --> Task + Assignment --> UserAccount : assigner + Assignment --> AssignmentStatus +``` + +### 2.3 动态模型设计 + +本节描述了系统在运行时,对象之间的交互行为。通过时序图和状态图,展示了系统的动态行为和状态转换。 + +#### 2.3.1 核心时序图 + +时序图展示了系统中对象之间的交互序列,清晰地表达了业务流程的执行顺序和对象间的消息传递。 + +**用户登录认证时序图** +```mermaid +sequenceDiagram + participant User as 用户 + participant AuthController as 认证控制器 + participant AuthService as 认证服务 + participant JwtUtil as JWT工具 + + User->>AuthController: POST /api/auth/login (email, password) + AuthController->>AuthService: signIn(LoginRequest) + AuthService->>AuthService: 验证凭证 + alt 验证成功 + AuthService->>JwtUtil: generateToken(user) + JwtUtil-->>AuthService: 返回JWT令牌 + AuthService-->>AuthController: 返回JwtAuthenticationResponse + AuthController-->>User: 200 OK (token) + else 验证失败 + AuthService-->>AuthController: 抛出AuthenticationException + AuthController-->>User: 401 Unauthorized + end +``` + +**反馈提交与处理时序图** +```mermaid +sequenceDiagram + participant User as 用户 + participant FeedbackController as 反馈控制器 + participant FeedbackService as 反馈服务 + participant EventPublisher as 事件发布器 + participant FeedbackAiReviewService as AI审核服务 + participant TaskService as 任务服务 + + User->>FeedbackController: POST /api/feedback/submit + FeedbackController->>FeedbackService: submitFeedback(request, files) + FeedbackService->>FeedbackService: 生成事件ID + FeedbackService->>FeedbackService: 保存反馈和附件 + FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent) + FeedbackService-->>FeedbackController: 返回反馈信息 + FeedbackController-->>User: 201 Created + + EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核 + FeedbackAiReviewService->>FeedbackAiReviewService: 分析反馈内容 + FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED) + + Note over User,TaskService: 主管审核流程 + + alt 主管批准反馈 + FeedbackService->>TaskService: createTaskFromFeedback(feedback) + TaskService->>TaskService: 创建任务 + TaskService-->>FeedbackService: 返回创建的任务 + FeedbackService->>FeedbackService: updateFeedbackStatus(PROCESSED) + else 主管拒绝反馈 + FeedbackService->>FeedbackService: updateFeedbackStatus(REJECTED) + end +``` + +**主管分配任务时序图** +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant TaskMgmtController as 任务管理控制器 + participant TaskService as 任务服务 + participant AssignmentService as 分配服务 + participant NotificationService as 通知服务 + participant GridWorker as 网格员 + + Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId) + TaskMgmtController->>TaskService: assignTask(taskId, workerId) + TaskService->>TaskService: 验证任务状态 + TaskService->>AssignmentService: createAssignment(task, worker) + AssignmentService->>AssignmentService: 创建分配记录 + AssignmentService-->>TaskService: 返回Assignment + TaskService->>TaskService: 更新任务状态为ASSIGNED + TaskService->>NotificationService: notifyWorker(workerId, taskId) + NotificationService->>NotificationService: 创建通知 + TaskService-->>TaskMgmtController: 分配成功 + TaskMgmtController-->>Supervisor: 200 OK + + NotificationService-->>GridWorker: 发送任务通知 +``` + +**网格员处理任务时序图** +```mermaid +sequenceDiagram + participant GridWorker as 网格员 + participant WorkerTaskController as 网格员任务控制器 + participant TaskService as 任务服务 + participant PathfindingService as 寻路服务 + participant Supervisor as 主管 + + GridWorker->>WorkerTaskController: POST /api/worker/tasks/{taskId}/accept + WorkerTaskController->>TaskService: acceptTask(taskId, workerId) + TaskService->>TaskService: 更新任务状态为ACCEPTED + TaskService-->>WorkerTaskController: 返回更新后的任务 + WorkerTaskController-->>GridWorker: 200 OK + + GridWorker->>WorkerTaskController: GET /api/pathfinding/find?start=x,y&end=a,b + WorkerTaskController->>PathfindingService: findPath(start, end) + PathfindingService->>PathfindingService: 执行A*算法 + PathfindingService-->>WorkerTaskController: 返回路径点列表 + WorkerTaskController-->>GridWorker: 200 OK (路径数据) + + GridWorker->>WorkerTaskController: POST /api/worker/tasks/{taskId}/submit + WorkerTaskController->>TaskService: submitTaskCompletion(taskId, workerId, request, files) + TaskService->>TaskService: 验证并更新任务状态为SUBMITTED + TaskService->>TaskService: 保存处理结果和附件 + TaskService-->>WorkerTaskController: 返回更新后的任务 + WorkerTaskController-->>GridWorker: 200 OK + + Note over GridWorker,Supervisor: 主管审核流程 + + alt 主管批准任务完成 + TaskService->>TaskService: 更新任务状态为COMPLETED + else 主管拒绝任务完成 + TaskService->>TaskService: 更新任务状态为REJECTED + TaskService->>NotificationService: 通知网格员重新处理 + end +``` + +#### 2.3.2 核心状态图 + +状态图展示了系统中关键对象的状态变化和转换条件,帮助理解对象的生命周期。 + +**反馈状态机 (Feedback Status)** +```mermaid +stateDiagram-v2 + [*] --> SUBMITTED: 用户提交反馈 + SUBMITTED --> AI_REVIEWED: AI自动审核 + AI_REVIEWED --> PENDING_REVIEW: 进入人工审核队列 + PENDING_REVIEW --> APPROVED: 主管批准 + PENDING_REVIEW --> REJECTED: 主管拒绝 + APPROVED --> PROCESSED: 创建关联任务 + PROCESSED --> CLOSED: 任务完成 + REJECTED --> [*] + CLOSED --> [*] +``` + +**任务状态机 (Task Status)** +```mermaid +stateDiagram-v2 + [*] --> CREATED: 创建任务 + CREATED --> ASSIGNED: 分配给网格员 + ASSIGNED --> ACCEPTED: 网格员接受 + ASSIGNED --> CREATED: 网格员拒绝 + ACCEPTED --> IN_PROGRESS: 开始处理 + IN_PROGRESS --> SUBMITTED: 提交处理结果 + SUBMITTED --> APPROVED: 主管批准 + SUBMITTED --> REJECTED: 主管拒绝 + REJECTED --> IN_PROGRESS: 重新处理 + APPROVED --> COMPLETED: 完成归档 + COMPLETED --> [*] +``` + +**用户状态机 (User Status)** +```mermaid +stateDiagram-v2 + [*] --> ACTIVE: 创建账户 + ACTIVE --> SUSPENDED: 管理员暂停 + SUSPENDED --> ACTIVE: 管理员重新激活 + ACTIVE --> INACTIVE: 长期未使用 + INACTIVE --> ACTIVE: 用户登录 + INACTIVE --> [*]: 账户删除 + SUSPENDED --> [*]: 账户删除 +``` --- - + ### Report/系统设计_第四部分.md - -### 2.4 算法设计 - -本节详细描述了系统中的核心算法设计,这些算法是系统智能化和高效运行的关键。 - -#### 2.4.1 A* 寻路算法 - -A*寻路算法是系统中的核心算法之一,用于为网格员规划从当前位置到任务目标点的最优路径。该算法在网格与地图模块中发挥着重要作用。 - -**算法目的**: -为网格员提供从当前位置到任务地点的最优(最短或最快)路径,避开障碍物。 - -**算法输入**: -- `startNode`: 起始点坐标 (x, y)。 -- `endNode`: 目标点坐标 (x, y)。 -- `grid`: 包含障碍物信息的地图网格数据。 - -**核心逻辑**: -1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。 - - `openList`: 存储待探索的节点,使用优先队列实现,按F值排序。 - - `closedList`: 存储已探索过的节点。 -2. 从 `openList` 中选取F值(G值+H值)最小的节点作为当前节点。 - - G值: 从起点到当前节点的实际代价。 - - H值: 从当前节点到终点的预估代价(启发函数)。 - - F值: G值 + H值,表示经过当前节点到达终点的总代价估计。 -3. 遍历当前节点的相邻节点(上、下、左、右四个方向)。 -4. 对于每个相邻节点: - - 如果是障碍物或已在`closedList`中,则跳过。 - - 如果不在`openList`中,计算其G值、H值和F值,将其加入`openList`,并记录其父节点为当前节点。 - - 如果已在`openList`中,检查经由当前节点到达该相邻节点的路径是否更优(G值更小)。如果更优,则更新其G值、F值和父节点。 -5. 将当前节点从`openList`移除,加入`closedList`。 -6. 重复步骤2-5,直到: - - 找到目标节点(当前节点为终点)。 - - 或`openList`为空(无法找到路径)。 -7. 如果找到目标节点,通过回溯父节点构建从起点到终点的路径。 - -**启发函数选择**: -系统采用曼哈顿距离(Manhattan Distance)作为启发函数,计算公式为: -``` -h(n) = |n.x - goal.x| + |n.y - goal.y| -``` -这种启发函数适合网格化的移动模式,只允许上、下、左、右四个方向的移动。 - -**算法实现**: -```java -public List findPath(Point start, Point end) { - // 加载地图数据和障碍物信息 - List mapGrids = mapGridRepository.findAll(); - Set obstacles = new HashSet<>(); - for (MapGrid gridCell : mapGrids) { - if (gridCell.isObstacle()) { - obstacles.add(new Point(gridCell.getX(), gridCell.getY())); - } - } - - // 初始化开放列表和所有节点映射 - PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f)); - Map allNodes = new HashMap<>(); - - // 创建起始节点 - Node startNode = new Node(start, null, 0, calculateHeuristic(start, end)); - openSet.add(startNode); - allNodes.put(start, startNode); - - // 开始A*算法主循环 - while (!openSet.isEmpty()) { - Node currentNode = openSet.poll(); - - // 找到目标 - if (currentNode.point.equals(end)) { - return reconstructPath(currentNode); - } - - // 探索相邻节点 - for (Point neighborPoint : getNeighbors(currentNode.point)) { - if (obstacles.contains(neighborPoint)) { - continue; // 跳过障碍物 - } - - int tentativeG = currentNode.g + 1; // 相邻节点间距离为1 - Node neighborNode = allNodes.get(neighborPoint); - - if (neighborNode == null) { - // 新节点 - int h = calculateHeuristic(neighborPoint, end); - neighborNode = new Node(neighborPoint, currentNode, tentativeG, h); - allNodes.put(neighborPoint, neighborNode); - openSet.add(neighborNode); - } else if (tentativeG < neighborNode.g) { - // 找到更好的路径 - neighborNode.parent = currentNode; - neighborNode.g = tentativeG; - neighborNode.f = tentativeG + neighborNode.h; - // 更新优先队列 - openSet.remove(neighborNode); - openSet.add(neighborNode); - } - } - } - - return Collections.emptyList(); // 没有找到路径 -} - -// 计算启发式函数值(曼哈顿距离) -private int calculateHeuristic(Point a, Point b) { - return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()); -} - -// 获取相邻节点(上、下、左、右) -private List getNeighbors(Point point) { - List neighbors = new ArrayList<>(4); - int[] dx = {0, 0, 1, -1}; // 右、左、下、上 - int[] dy = {1, -1, 0, 0}; - - for (int i = 0; i < 4; i++) { - int newX = point.x() + dx[i]; - int newY = point.y() + dy[i]; - - // 检查边界 - if (newX >= 0 && newX < mapWidth && newY >= 0 && newY < mapHeight) { - neighbors.add(new Point(newX, newY)); - } - } - return neighbors; -} - -// 重建路径 -private List reconstructPath(Node node) { - LinkedList path = new LinkedList<>(); - while (node != null) { - path.addFirst(node.point); - node = node.parent; - } - return path; -} -``` - -**算法输出**: -从起点到终点的一系列坐标点列表,表示规划好的路径。如果无法找到路径,则返回空列表。 - -**算法应用**: -在`PathfindingController`中通过`/api/pathfinding/find`接口暴露,网格员可以通过该接口获取从当前位置到任务地点的最优路径。 - -#### 2.4.2 任务智能分配算法 - -任务智能分配算法是系统的另一个核心算法,用于在多个可用网格员中,为新任务选择最合适的执行者。 - -**算法目的**: -在多个可用网格员中,为新任务选择最合适的执行者,优化任务分配效率和完成质量。 - -**算法输入**: -- `task`: 需要分配的任务对象。 -- `availableWorkers`: 可用的网格员列表。 - -**核心逻辑**: -这是一个复合策略算法,综合考虑多个因素,通过加权评分机制选择最合适的网格员: - -1. **地理距离评分**: - - 计算每个网格员当前位置到任务位置的距离。 - - 距离越近,评分越高。 - - 使用公式: `distanceScore = maxDistance - distance`,其中`maxDistance`是一个预设的最大考虑距离。 - -2. **当前负载评分**: - - 评估每个网格员当前正在处理的任务数量。 - - 任务数量越少,评分越高。 - - 使用公式: `loadScore = maxTasks - currentTasks`,其中`maxTasks`是一个预设的最大任务数。 - -3. **专业匹配度评分**: - - 根据任务类型和网格员的专业技能进行匹配。 - - 匹配度越高,评分越高。 - - 使用公式: `skillScore = matchedSkills / requiredSkills.size()`。 - -4. **历史表现评分**: - - 考虑网格员的历史完成率和质量评分。 - - 表现越好,评分越高。 - - 使用公式: `performanceScore = (completionRate * 0.7) + (qualityRating * 0.3)`。 - -5. **综合评分计算**: - - 对上述各项评分进行加权求和。 - - 使用公式: `totalScore = (distanceScore * w1) + (loadScore * w2) + (skillScore * w3) + (performanceScore * w4)`。 - - 其中w1、w2、w3、w4是各项评分的权重,且满足`w1 + w2 + w3 + w4 = 1`。 - -6. **选择最高评分的网格员**: - - 根据综合评分对网格员进行排序。 - - 选择评分最高的网格员作为推荐人选。 - -**算法实现**: -```java -public UserAccount recommendWorkerForTask(Task task, List availableWorkers) { - if (availableWorkers.isEmpty()) { - return null; - } - - // 权重配置 - final double DISTANCE_WEIGHT = 0.4; - final double LOAD_WEIGHT = 0.3; - final double SKILL_WEIGHT = 0.2; - final double PERFORMANCE_WEIGHT = 0.1; - - // 任务位置 - Point taskLocation = new Point(task.getGridX(), task.getGridY()); - - // 计算每个网格员的评分 - Map workerScores = new HashMap<>(); - - for (UserAccount worker : availableWorkers) { - // 1. 地理距离评分 - Point workerLocation = new Point(worker.getGridX(), worker.getGridY()); - double distance = calculateDistance(workerLocation, taskLocation); - double maxDistance = 10.0; // 最大考虑距离 - double distanceScore = Math.max(0, maxDistance - distance) / maxDistance; - - // 2. 当前负载评分 - int currentTasks = taskRepository.countByAssigneeIdAndStatusIn( - worker.getId(), Arrays.asList(TaskStatus.ASSIGNED, TaskStatus.IN_PROGRESS)); - int maxTasks = 5; // 最大任务数 - double loadScore = (double)(maxTasks - currentTasks) / maxTasks; - - // 3. 专业匹配度评分 - double skillScore = calculateSkillMatch(worker, task); - - // 4. 历史表现评分 - double completionRate = workerStatsService.getCompletionRate(worker.getId()); - double qualityRating = workerStatsService.getAverageQualityRating(worker.getId()); - double performanceScore = (completionRate * 0.7) + (qualityRating * 0.3); - - // 5. 综合评分 - double totalScore = (distanceScore * DISTANCE_WEIGHT) + - (loadScore * LOAD_WEIGHT) + - (skillScore * SKILL_WEIGHT) + - (performanceScore * PERFORMANCE_WEIGHT); - - workerScores.put(worker, totalScore); - } - - // 选择评分最高的网格员 - return workerScores.entrySet().stream() - .max(Map.Entry.comparingByValue()) - .map(Map.Entry::getKey) - .orElse(null); -} -``` - -**算法输出**: -推荐的最佳网格员对象。如果没有合适的网格员,则返回null。 - -**算法应用**: -在`TaskAssignmentService`中实现,当主管触发自动分配或系统基于反馈自动创建任务时调用。 - -### 2.5 数据持久化设计 - -系统采用了一种独特的数据持久化方案,不使用传统的关系型数据库,而是以JSON文件的形式存储所有数据。这种设计简化了部署和配置,特别适合快速迭代和中小型应用场景。 - -#### 2.5.1 JSON文件存储架构 - -系统的数据持久化层基于以下架构: - -1. **核心存储服务**: - - `JsonStorageService`: 提供对JSON文件的读写操作,包括序列化和反序列化。 - - `FileSystemService`: 处理文件系统操作,如创建、读取、更新和删除文件。 - -2. **Repository层**: - - 为每种核心模型提供专门的Repository类,如`UserRepository`、`FeedbackRepository`等。 - - 这些Repository类模拟了类似JPA的接口,提供CRUD操作和查询功能。 - - 内部使用`JsonStorageService`进行实际的文件操作。 - -3. **并发控制**: - - 使用文件锁机制确保在并发环境下的数据一致性。 - - 实现了简单的乐观锁定策略,通过版本号检测冲突。 - -4. **索引和查询优化**: - - 在内存中维护索引结构,加速常见查询操作。 - - 支持基于字段值的过滤和排序。 - -#### 2.5.2 核心JSON文件结构 - -系统中的每个核心模型对应一个JSON文件,下面详细描述了这些文件的结构。 - -##### `users.json` - 用户账户数据 - -存储系统中所有用户的账户信息,包括认证信息和角色权限。 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | -| `name` | `String` | 非空 | 用户姓名 | -| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | -| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | -| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | -| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | -| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | -| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | -| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | -| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | -| `region` | `String` | | 所属区域或地区 | -| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | -| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | -| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | -| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | -| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | -| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | -| `lockout_end_time` | `String` | | 账户锁定截止时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -##### `feedback.json` - 环境问题反馈数据 - -存储用户提交的所有环境问题反馈信息。 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | -| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | -| `title` | `String` | 非空 | 反馈标题 | -| `description` | `String` | | 问题详细描述 | -| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | -| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | -| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | -| `text_address` | `String` | | 文字描述的地址 | -| `grid_x` | `Number` | | 事发地网格X坐标 | -| `grid_y` | `Number` | | 事发地网格Y坐标 | -| `latitude` | `Number` | | 事发地纬度 | -| `longitude` | `Number` | | 事发地经度 | -| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | -| `attachments` | `Array` | | 附件列表 (包含文件路径等信息) | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -##### `tasks.json` - 任务数据 - -存储系统中所有工作任务的信息。 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | -| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | -| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | -| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | -| `status` | `String` | 非空, (ENUM) | 任务状态 (CREATED, ASSIGNED, IN_PROGRESS等) | -| `title` | `String` | | 任务标题 | -| `description` | `String` | | 任务详细描述 | -| `pollution_type` | `String` | (ENUM) | 污染类型 | -| `severity_level` | `String` | (ENUM) | 严重程度 | -| `text_address` | `String` | | 任务地点文字描述 | -| `grid_x` | `Number` | | 任务地点网格X坐标 | -| `grid_y` | `Number` | | 任务地点网格Y坐标 | -| `latitude` | `Number` | | 任务地点纬度 | -| `longitude` | `Number` | | 任务地点经度 | -| `assigned_at` | `String` | | 任务分配时间 | -| `completed_at` | `String` | | 任务完成时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | -| `deadline` | `String` | | 任务截止日期 | -| `history` | `Array` | | 任务状态历史记录 | -| `submissions` | `Array` | | 任务提交记录 | - -##### `grids.json` - 业务网格数据 - -存储系统中定义的地理网格信息。 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------- | --------------- | ------------------- | ---------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | -| `grid_x` | `Number` | | 网格X坐标 | -| `grid_y` | `Number` | | 网格Y坐标 | -| `city_name` | `String` | | 所属城市 | -| `district_name` | `String` | | 所属区县 | -| `description` | `String` | | 网格描述信息 | -| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -##### `assignments.json` - 任务分配记录数据 - -存储任务分配的详细记录。 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | -------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | -| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | -| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | -| `status` | `String` | 非空, (ENUM) | 分配状态 | -| `remarks` | `String` | | 分配备注 | -| `assignment_time`| `String` | 非空 | 分配时间 | -| `deadline` | `String` | | 任务截止日期 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +### 2.4 算法设计 + +本节详细描述了系统中的核心算法设计,这些算法是系统智能化和高效运行的关键。 + +#### 2.4.1 A* 寻路算法 + +A*寻路算法是系统中的核心算法之一,用于为网格员规划从当前位置到任务目标点的最优路径。该算法在网格与地图模块中发挥着重要作用。 + +**算法目的**: +为网格员提供从当前位置到任务地点的最优(最短或最快)路径,避开障碍物。 + +**算法输入**: +- `startNode`: 起始点坐标 (x, y)。 +- `endNode`: 目标点坐标 (x, y)。 +- `grid`: 包含障碍物信息的地图网格数据。 + +**核心逻辑**: +1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。 + - `openList`: 存储待探索的节点,使用优先队列实现,按F值排序。 + - `closedList`: 存储已探索过的节点。 +2. 从 `openList` 中选取F值(G值+H值)最小的节点作为当前节点。 + - G值: 从起点到当前节点的实际代价。 + - H值: 从当前节点到终点的预估代价(启发函数)。 + - F值: G值 + H值,表示经过当前节点到达终点的总代价估计。 +3. 遍历当前节点的相邻节点(上、下、左、右四个方向)。 +4. 对于每个相邻节点: + - 如果是障碍物或已在`closedList`中,则跳过。 + - 如果不在`openList`中,计算其G值、H值和F值,将其加入`openList`,并记录其父节点为当前节点。 + - 如果已在`openList`中,检查经由当前节点到达该相邻节点的路径是否更优(G值更小)。如果更优,则更新其G值、F值和父节点。 +5. 将当前节点从`openList`移除,加入`closedList`。 +6. 重复步骤2-5,直到: + - 找到目标节点(当前节点为终点)。 + - 或`openList`为空(无法找到路径)。 +7. 如果找到目标节点,通过回溯父节点构建从起点到终点的路径。 + +**启发函数选择**: +系统采用曼哈顿距离(Manhattan Distance)作为启发函数,计算公式为: +``` +h(n) = |n.x - goal.x| + |n.y - goal.y| +``` +这种启发函数适合网格化的移动模式,只允许上、下、左、右四个方向的移动。 + +**算法实现**: +```java +public List findPath(Point start, Point end) { + // 加载地图数据和障碍物信息 + List mapGrids = mapGridRepository.findAll(); + Set obstacles = new HashSet<>(); + for (MapGrid gridCell : mapGrids) { + if (gridCell.isObstacle()) { + obstacles.add(new Point(gridCell.getX(), gridCell.getY())); + } + } + + // 初始化开放列表和所有节点映射 + PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f)); + Map allNodes = new HashMap<>(); + + // 创建起始节点 + Node startNode = new Node(start, null, 0, calculateHeuristic(start, end)); + openSet.add(startNode); + allNodes.put(start, startNode); + + // 开始A*算法主循环 + while (!openSet.isEmpty()) { + Node currentNode = openSet.poll(); + + // 找到目标 + if (currentNode.point.equals(end)) { + return reconstructPath(currentNode); + } + + // 探索相邻节点 + for (Point neighborPoint : getNeighbors(currentNode.point)) { + if (obstacles.contains(neighborPoint)) { + continue; // 跳过障碍物 + } + + int tentativeG = currentNode.g + 1; // 相邻节点间距离为1 + Node neighborNode = allNodes.get(neighborPoint); + + if (neighborNode == null) { + // 新节点 + int h = calculateHeuristic(neighborPoint, end); + neighborNode = new Node(neighborPoint, currentNode, tentativeG, h); + allNodes.put(neighborPoint, neighborNode); + openSet.add(neighborNode); + } else if (tentativeG < neighborNode.g) { + // 找到更好的路径 + neighborNode.parent = currentNode; + neighborNode.g = tentativeG; + neighborNode.f = tentativeG + neighborNode.h; + // 更新优先队列 + openSet.remove(neighborNode); + openSet.add(neighborNode); + } + } + } + + return Collections.emptyList(); // 没有找到路径 +} + +// 计算启发式函数值(曼哈顿距离) +private int calculateHeuristic(Point a, Point b) { + return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()); +} + +// 获取相邻节点(上、下、左、右) +private List getNeighbors(Point point) { + List neighbors = new ArrayList<>(4); + int[] dx = {0, 0, 1, -1}; // 右、左、下、上 + int[] dy = {1, -1, 0, 0}; + + for (int i = 0; i < 4; i++) { + int newX = point.x() + dx[i]; + int newY = point.y() + dy[i]; + + // 检查边界 + if (newX >= 0 && newX < mapWidth && newY >= 0 && newY < mapHeight) { + neighbors.add(new Point(newX, newY)); + } + } + return neighbors; +} + +// 重建路径 +private List reconstructPath(Node node) { + LinkedList path = new LinkedList<>(); + while (node != null) { + path.addFirst(node.point); + node = node.parent; + } + return path; +} +``` + +**算法输出**: +从起点到终点的一系列坐标点列表,表示规划好的路径。如果无法找到路径,则返回空列表。 + +**算法应用**: +在`PathfindingController`中通过`/api/pathfinding/find`接口暴露,网格员可以通过该接口获取从当前位置到任务地点的最优路径。 + +#### 2.4.2 任务智能分配算法 + +任务智能分配算法是系统的另一个核心算法,用于在多个可用网格员中,为新任务选择最合适的执行者。 + +**算法目的**: +在多个可用网格员中,为新任务选择最合适的执行者,优化任务分配效率和完成质量。 + +**算法输入**: +- `task`: 需要分配的任务对象。 +- `availableWorkers`: 可用的网格员列表。 + +**核心逻辑**: +这是一个复合策略算法,综合考虑多个因素,通过加权评分机制选择最合适的网格员: + +1. **地理距离评分**: + - 计算每个网格员当前位置到任务位置的距离。 + - 距离越近,评分越高。 + - 使用公式: `distanceScore = maxDistance - distance`,其中`maxDistance`是一个预设的最大考虑距离。 + +2. **当前负载评分**: + - 评估每个网格员当前正在处理的任务数量。 + - 任务数量越少,评分越高。 + - 使用公式: `loadScore = maxTasks - currentTasks`,其中`maxTasks`是一个预设的最大任务数。 + +3. **专业匹配度评分**: + - 根据任务类型和网格员的专业技能进行匹配。 + - 匹配度越高,评分越高。 + - 使用公式: `skillScore = matchedSkills / requiredSkills.size()`。 + +4. **历史表现评分**: + - 考虑网格员的历史完成率和质量评分。 + - 表现越好,评分越高。 + - 使用公式: `performanceScore = (completionRate * 0.7) + (qualityRating * 0.3)`。 + +5. **综合评分计算**: + - 对上述各项评分进行加权求和。 + - 使用公式: `totalScore = (distanceScore * w1) + (loadScore * w2) + (skillScore * w3) + (performanceScore * w4)`。 + - 其中w1、w2、w3、w4是各项评分的权重,且满足`w1 + w2 + w3 + w4 = 1`。 + +6. **选择最高评分的网格员**: + - 根据综合评分对网格员进行排序。 + - 选择评分最高的网格员作为推荐人选。 + +**算法实现**: +```java +public UserAccount recommendWorkerForTask(Task task, List availableWorkers) { + if (availableWorkers.isEmpty()) { + return null; + } + + // 权重配置 + final double DISTANCE_WEIGHT = 0.4; + final double LOAD_WEIGHT = 0.3; + final double SKILL_WEIGHT = 0.2; + final double PERFORMANCE_WEIGHT = 0.1; + + // 任务位置 + Point taskLocation = new Point(task.getGridX(), task.getGridY()); + + // 计算每个网格员的评分 + Map workerScores = new HashMap<>(); + + for (UserAccount worker : availableWorkers) { + // 1. 地理距离评分 + Point workerLocation = new Point(worker.getGridX(), worker.getGridY()); + double distance = calculateDistance(workerLocation, taskLocation); + double maxDistance = 10.0; // 最大考虑距离 + double distanceScore = Math.max(0, maxDistance - distance) / maxDistance; + + // 2. 当前负载评分 + int currentTasks = taskRepository.countByAssigneeIdAndStatusIn( + worker.getId(), Arrays.asList(TaskStatus.ASSIGNED, TaskStatus.IN_PROGRESS)); + int maxTasks = 5; // 最大任务数 + double loadScore = (double)(maxTasks - currentTasks) / maxTasks; + + // 3. 专业匹配度评分 + double skillScore = calculateSkillMatch(worker, task); + + // 4. 历史表现评分 + double completionRate = workerStatsService.getCompletionRate(worker.getId()); + double qualityRating = workerStatsService.getAverageQualityRating(worker.getId()); + double performanceScore = (completionRate * 0.7) + (qualityRating * 0.3); + + // 5. 综合评分 + double totalScore = (distanceScore * DISTANCE_WEIGHT) + + (loadScore * LOAD_WEIGHT) + + (skillScore * SKILL_WEIGHT) + + (performanceScore * PERFORMANCE_WEIGHT); + + workerScores.put(worker, totalScore); + } + + // 选择评分最高的网格员 + return workerScores.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse(null); +} +``` + +**算法输出**: +推荐的最佳网格员对象。如果没有合适的网格员,则返回null。 + +**算法应用**: +在`TaskAssignmentService`中实现,当主管触发自动分配或系统基于反馈自动创建任务时调用。 + +### 2.5 数据持久化设计 + +系统采用了一种独特的数据持久化方案,不使用传统的关系型数据库,而是以JSON文件的形式存储所有数据。这种设计简化了部署和配置,特别适合快速迭代和中小型应用场景。 + +#### 2.5.1 JSON文件存储架构 + +系统的数据持久化层基于以下架构: + +1. **核心存储服务**: + - `JsonStorageService`: 提供对JSON文件的读写操作,包括序列化和反序列化。 + - `FileSystemService`: 处理文件系统操作,如创建、读取、更新和删除文件。 + +2. **Repository层**: + - 为每种核心模型提供专门的Repository类,如`UserRepository`、`FeedbackRepository`等。 + - 这些Repository类模拟了类似JPA的接口,提供CRUD操作和查询功能。 + - 内部使用`JsonStorageService`进行实际的文件操作。 + +3. **并发控制**: + - 使用文件锁机制确保在并发环境下的数据一致性。 + - 实现了简单的乐观锁定策略,通过版本号检测冲突。 + +4. **索引和查询优化**: + - 在内存中维护索引结构,加速常见查询操作。 + - 支持基于字段值的过滤和排序。 + +#### 2.5.2 核心JSON文件结构 + +系统中的每个核心模型对应一个JSON文件,下面详细描述了这些文件的结构。 + +##### `users.json` - 用户账户数据 + +存储系统中所有用户的账户信息,包括认证信息和角色权限。 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | +| `name` | `String` | 非空 | 用户姓名 | +| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | +| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | +| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | +| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | +| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | +| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | +| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | +| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | +| `region` | `String` | | 所属区域或地区 | +| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | +| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | +| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | +| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | +| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | +| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | +| `lockout_end_time` | `String` | | 账户锁定截止时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +##### `feedback.json` - 环境问题反馈数据 + +存储用户提交的所有环境问题反馈信息。 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | +| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | +| `title` | `String` | 非空 | 反馈标题 | +| `description` | `String` | | 问题详细描述 | +| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | +| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | +| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | +| `text_address` | `String` | | 文字描述的地址 | +| `grid_x` | `Number` | | 事发地网格X坐标 | +| `grid_y` | `Number` | | 事发地网格Y坐标 | +| `latitude` | `Number` | | 事发地纬度 | +| `longitude` | `Number` | | 事发地经度 | +| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | +| `attachments` | `Array` | | 附件列表 (包含文件路径等信息) | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +##### `tasks.json` - 任务数据 + +存储系统中所有工作任务的信息。 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | +| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | +| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | +| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | +| `status` | `String` | 非空, (ENUM) | 任务状态 (CREATED, ASSIGNED, IN_PROGRESS等) | +| `title` | `String` | | 任务标题 | +| `description` | `String` | | 任务详细描述 | +| `pollution_type` | `String` | (ENUM) | 污染类型 | +| `severity_level` | `String` | (ENUM) | 严重程度 | +| `text_address` | `String` | | 任务地点文字描述 | +| `grid_x` | `Number` | | 任务地点网格X坐标 | +| `grid_y` | `Number` | | 任务地点网格Y坐标 | +| `latitude` | `Number` | | 任务地点纬度 | +| `longitude` | `Number` | | 任务地点经度 | +| `assigned_at` | `String` | | 任务分配时间 | +| `completed_at` | `String` | | 任务完成时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | +| `deadline` | `String` | | 任务截止日期 | +| `history` | `Array` | | 任务状态历史记录 | +| `submissions` | `Array` | | 任务提交记录 | + +##### `grids.json` - 业务网格数据 + +存储系统中定义的地理网格信息。 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------- | --------------- | ------------------- | ---------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | +| `grid_x` | `Number` | | 网格X坐标 | +| `grid_y` | `Number` | | 网格Y坐标 | +| `city_name` | `String` | | 所属城市 | +| `district_name` | `String` | | 所属区县 | +| `description` | `String` | | 网格描述信息 | +| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +##### `assignments.json` - 任务分配记录数据 + +存储任务分配的详细记录。 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | -------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | +| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | +| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | +| `status` | `String` | 非空, (ENUM) | 分配状态 | +| `remarks` | `String` | | 分配备注 | +| `assignment_time`| `String` | 非空 | 分配时间 | +| `deadline` | `String` | | 任务截止日期 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | --- - + ### Report/系统设计_第一部分.md - -# 系统设计文档 - -本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 - -## 1. 总体设计 - -总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 - -### 1.1 系统架构设计 - -#### 1.1.1 架构选型与原则 - -本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: -- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 -- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 -- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 - -在架构设计中,我们遵循了以下核心原则: -- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。 -- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。 -- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。 -- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 - -#### 1.1.2 后端架构 - -后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: - -- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 -- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 -- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 - -此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 - -```plantuml -@startuml 后端分层架构图 -package "EMS后端架构" { - [前端应用] --> [Controller层] - [Controller层] --> [Service层] - [Service层] --> [Repository层] - [Repository层] --> [JSON文件存储] - - note right of [Controller层] - 负责接收HTTP请求 - 参数验证 - 权限检查 - end note - - note right of [Service层] - 业务逻辑核心 - 事务管理 - 事件发布 - end note - - note right of [Repository层] - 数据访问 - 查询构建 - 持久化逻辑 - end note -} -@enduml -``` - -#### 1.1.3 前端架构 - -前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 -- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 -- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 -- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 - -### 1.2 功能模块划分 - -系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 - -| 核心模块 | 主要职责 | 关键功能点 | -| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | -| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | -| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | -| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | -| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | -| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | -| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 | - -```plantuml -@startuml 功能模块图 -!define RECTANGLE class - -RECTANGLE "认证与授权模块" as auth #LightBlue -RECTANGLE "用户与人员管理模块" as user #LightGreen -RECTANGLE "反馈管理模块" as feedback #LightYellow -RECTANGLE "任务管理模块" as task #LightPink -RECTANGLE "网格与地图模块" as grid #LightCyan -RECTANGLE "决策支持模块" as decision #LightGray -RECTANGLE "个人中心模块" as profile #LightSalmon - -auth -- user : 提供身份信息 -feedback -- task : 生成任务 -task -- grid : 使用地理信息 -user -- task : 分配任务 -task -- decision : 提供数据 -feedback -- decision : 提供数据 -user -- profile : 个人信息 -@enduml -``` + +# 系统设计文档 + +本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 + +## 1. 总体设计 + +总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 + +### 1.1 系统架构设计 + +#### 1.1.1 架构选型与原则 + +本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: +- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 +- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 +- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 + +在架构设计中,我们遵循了以下核心原则: +- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。 +- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。 +- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。 +- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 + +#### 1.1.2 后端架构 + +后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: + +- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 +- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 +- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 + +此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 + +```plantuml +@startuml 后端分层架构图 +package "EMS后端架构" { + [前端应用] --> [Controller层] + [Controller层] --> [Service层] + [Service层] --> [Repository层] + [Repository层] --> [JSON文件存储] + + note right of [Controller层] + 负责接收HTTP请求 + 参数验证 + 权限检查 + end note + + note right of [Service层] + 业务逻辑核心 + 事务管理 + 事件发布 + end note + + note right of [Repository层] + 数据访问 + 查询构建 + 持久化逻辑 + end note +} +@enduml +``` + +#### 1.1.3 前端架构 + +前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 +- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 +- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 +- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 + +### 1.2 功能模块划分 + +系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 + +| 核心模块 | 主要职责 | 关键功能点 | +| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | +| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | +| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | +| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | +| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | +| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | +| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 | + +```plantuml +@startuml 功能模块图 +!define RECTANGLE class + +RECTANGLE "认证与授权模块" as auth #LightBlue +RECTANGLE "用户与人员管理模块" as user #LightGreen +RECTANGLE "反馈管理模块" as feedback #LightYellow +RECTANGLE "任务管理模块" as task #LightPink +RECTANGLE "网格与地图模块" as grid #LightCyan +RECTANGLE "决策支持模块" as decision #LightGray +RECTANGLE "个人中心模块" as profile #LightSalmon + +auth -- user : 提供身份信息 +feedback -- task : 生成任务 +task -- grid : 使用地理信息 +user -- task : 分配任务 +task -- decision : 提供数据 +feedback -- decision : 提供数据 +user -- profile : 个人信息 +@enduml +``` --- - + ### Report/系统设计_v2.md - -# 系统设计文档 - -本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 - -## 1. 总体设计 - -总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 - -### 1.1 系统架构设计 - -#### 1.1.1 架构选型与原则 - -本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: -- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 -- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 -- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 - -在架构设计中,我们遵循了以下核心原则: -- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。 -- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。 -- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。 -- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 - -#### 1.1.2 后端架构 - -后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: - -- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 -- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 -- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 - -此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 - -![后端分层架构图](https://www.plantuml.com/plantuml/svg/SoWkIImgAStDuG8oX1An24dCoKnELT2gKiX8p-L8AawncdNa5A2gY2pDoN8200000) - -**技术选型**: -- **核心框架**: Spring Boot 3 -- **开发语言**: Java 17 -- **身份认证**: Spring Security + JWT (JSON Web Tokens) -- **数据持久化**: 自定义JSON文件存储 -- **异步处理**: Spring Async & Events, Spring WebFlux -- **API文档**: SpringDoc (OpenAPI 3 / Swagger) - -#### 1.1.3 前端架构 - -前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 -- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 -- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 -- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 - -### 1.2 功能模块划分 - -系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 - -| 核心模块 | 主要职责 | 关键功能点 | -| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | -| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | -| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | -| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | -| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | -| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | -| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 | - -![功能模块图](https://www.plantuml.com/plantuml/svg/XLD1Jy5358xXF-S5wGgNkgRD1s2aD2gbzEd-Lgy_8QduTqb2lC_A5gYXaIikIeylC-yS339vj2vj-1e9z9oY-Oq9kGSpT8T5yPiU5sO1rKqpwXWkGZJ9G8P7k9y5Zt9B1pZ2b7h93e0b8B8pX78sYcQ6G2vE_uHlGgC0) - -## 2. 详细设计 - -### 2.1 数据持久化设计 (JSON文件) - -系统不使用传统数据库,所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署,但也对数据一致性和并发控制提出了更高的要求。 - -#### 2.1.1 `users.json` - 用户账户数据 - -存储所有系统用户的信息,包括登录凭证、角色和个人资料。 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | -| `name` | `String` | 非空 | 用户姓名 | -| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | -| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | -| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | -| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | -| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | -| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | -| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | -| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | -| `region` | `String` | | 所属区域或地区 | -| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | -| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | -| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | -| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | -| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | -| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | -| `lockout_end_time` | `String` | | 账户锁定截止时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -#### 2.1.2 `feedback.json` - 环境问题反馈数据 - -存储所有由用户(包括公众)提交的环境问题反馈。 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | -| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | -| `title` | `String` | 非空 | 反馈标题 | -| `description` | `String` | | 问题详细描述 | -| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | -| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | -| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | -| `text_address` | `String` | | 文字描述的地址 | -| `grid_x` | `Number` | | 事发地网格X坐标 | -| `grid_y` | `Number` | | 事发地网格Y坐标 | -| `latitude` | `Number` | | 事发地纬度 | -| `longitude` | `Number` | | 事发地经度 | -| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -#### 2.1.3 `tasks.json` - 任务数据 - -存储由反馈转化而来或手动创建的具体处理任务。 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | -| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | -| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | -| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | -| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) | -| `title` | `String` | | 任务标题 | -| `description` | `String` | | 任务详细描述 | -| `pollution_type` | `String` | (ENUM) | 污染类型 | -| `severity_level` | `String` | (ENUM) | 严重程度 | -| `text_address` | `String` | | 任务地点文字描述 | -| `grid_x` | `Number` | | 任务地点网格X坐标 | -| `grid_y` | `Number` | | 任务地点网格Y坐标 | -| `latitude` | `Number` | | 任务地点纬度 | -| `longitude` | `Number` | | 任务地点经度 | -| `assigned_at` | `String` | | 任务分配时间 | -| `completed_at` | `String` | | 任务完成时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | -| `deadline` | `String` | | 任务截止日期 | - -#### 2.1.4 `grids.json` - 业务网格数据 - -存储业务逻辑上的地理网格信息。 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------- | --------------- | ------------------- | ---------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | -| `gridx` | `Number` | | 网格X坐标 | -| `gridy` | `Number` | | 网格Y坐标 | -| `city_name` | `String` | | 所属城市 | -| `district_name` | `String` | | 所属区县 | -| `description` | `String` | | 网格描述信息 | -| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | - -#### 2.1.5 `assignments.json` - 任务分配记录数据 - -记录任务分配的详细信息,连接任务、分配者和执行者。 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | -------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | -| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | -| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | -| `status` | `String` | 非空, (ENUM) | 分配状态 | -| `remarks` | `String` | | 分配备注 | -| `assignment_time`| `String` | 非空 | 分配时间 | -| `deadline` | `String` | | 任务截止日期 | - ---- -*(其他辅助表如 `attachment.json`, `operation_log.json`, `map_grid.json` 等的设计将遵循类似结构)* - -### 2.2 核心业务流程 - -本章节将结合时序图和业务规则,详细描述系统的主要业务工作流。 - -#### 2.2.1 反馈处理与任务生成流程 - -这是系统的核心业务闭环,涵盖了从问题发现到任务完成的全过程。其状态流转严格遵循预设的规则。 - -**反馈处理流程**: - -1. **提交反馈**: 公众用户或认证用户通过API提交环境问题反馈。 -2. **进入AI审核**: 新提交的反馈初始状态为 `PENDING`,系统发布 `FeedbackSubmittedForAiReviewEvent` 事件,触发AI服务进行异步审核。反馈状态变为 `AI_REVIEWING`。 -3. **主管人工审核**: AI审核后,无论结果如何,反馈都将进入人工审核阶段。主管(Supervisor)在系统中查看待处理的反馈。 -4. **决策与流转**: - * **批准 (APPROVED)**: 如果反馈有效,主管将其批准。系统据此发布 `TaskReadyForAssignmentEvent` 事件,表明可以基于此反馈创建任务。原反馈的状态更新为 `TASK_CREATED`。 - * **拒绝 (REJECTED)**: 如果反馈无效或重复,主管可以拒绝该反馈,并填写原因。 - -**任务处理流程**: - -1. **创建任务**: 基于已批准的反馈,系统创建一条新任务,初始状态为 `CREATED`。 -2. **分配任务**: 主管根据网格区域、网格员负载等规则,将任务指派给特定的网格员(Grid Worker)。任务状态更新为 `ASSIGNED`。 -3. **执行任务**: 网格员接收到任务通知,接受任务后,状态变为 `IN_PROGRESS`。网格员前往现场处理。 -4. **提交结果**: 网格员完成任务后,在系统中提交工作报告。任务状态更新为 `SUBMITTED`(待审核)。 -5. **审核任务**: 主管审核已完成的任务。 - * **批准 (APPROVED)**: 任务审核通过,流程结束。 - * **拒绝 (REJECTED)**: 任务不符合要求,可将任务打回给网格员重新处理。 - -#### 2.2.2 用户认证与授权流程 - -1. **用户登录**: 用户使用邮箱/手机号和密码请求登录。 -2. **凭证验证**: 系统验证用户信息和密码的正确性。 -3. **令牌生成**: 验证通过后,系统使用JWT生成一个有时效性的 `access_token`。 -4. **访问授权**: 用户在后续的请求中,需在请求头携带此 `token`。后端通过一个安全过滤器(Filter)来解析和验证 `token`,确认用户的身份和权限,从而决定是否能访问受保护的API资源。 - -#### 2.2.3 密码重置流程 - -1. **请求重置**: 用户在登录页面点击"忘记密码",输入注册时使用的邮箱地址。 -2. **发送验证码**: 系统向该邮箱发送一封包含随机验证码的邮件。 -3. **验证与重置**: 用户在指定时间内输入收到的验证码和新密码。系统校验验证码的正确性后,更新用户在 `users.json` 文件中的密码。 - -#### 2.2.4 主要业务规则 - -- **权限控制规则**: - 1. **ADMIN**: 拥有全系统所有权限,是系统的最高管理员。 - 2. **SUPERVISOR**: 负责管理网格员、审核反馈和任务,是区域的管理者。 - 3. **DECISION_MAKER**: 拥有数据查看和分析权限,通常是决策层用户。 - 4. **GRID_WORKER**: 负责执行被分配的任务,是系统的主要执行者。 - 5. **PUBLIC**: 只能提交反馈,是系统的外部参与者。 - -- **任务分配规则**: - 1. 优先基于网格区域进行自动分配。 - 2. 分配时会综合考虑网格员的当前工作负载,避免分配不均。 - 3. 标记为"紧急"的反馈所生成的任务,拥有更高的分配优先级。 - 4. 主管可以对已分配的任务进行手动干预和重新分配。 - -- **反馈处理规则**: - 1. 所有反馈提交后,首先由AI服务进行预审核和内容分析。 - 2. 系统会检测可能重复的反馈,并提示审核人员进行合并处理。 - 3. 紧急反馈(如严重等级为`CRITICAL`)会被优先展示给审核人员。 - -### 2.3 API接口设计 - -本章节详细定义了系统各模块的API接口,包括认证、任务管理、数据上报等。 - -#### 2.3.1 认证接口 (`/api/auth`) - -处理用户注册、登录、登出和密码管理等功能。 - -| 序号 | 接口路径 | HTTP方法 | 功能描述 | 请求参数 (Body/Query) | 成功响应 (Body) | -|----|-------------------------------|----------|------------------------|-----------------------------------------------------------|---------------------------------| -| 1 | `/api/auth/signup` | `POST` | 用户注册 | `SignUpRequest` (name, phone, email, password, role, etc.) | `201 CREATED` | -| 2 | `/api/auth/login` | `POST` | 用户登录 | `LoginRequest` (email/phone, password) | `JwtAuthenticationResponse` (token) | -| 3 | `/api/auth/logout` | `POST` | 用户登出 | 无 | `200 OK` | -| 4 | `/api/auth/send-verification-code` | `POST` | 发送注册验证码 | `email` (Query Param) | `200 OK` | -| 5 | `/api/auth/send-password-reset-code` | `POST` | 发送密码重置验证码 | `email` (Query Param) | `200 OK` | -| 6 | `/api/auth/reset-password-with-code` | `POST` | 使用验证码重置密码 | `PasswordResetWithCodeDto` (email, code, newPassword) | `200 OK` | - -#### 2.3.2 公共接口 (`/api/public`) - -提供给外部系统或公众使用的无需认证的接口。 - -| 序号 | 接口路径 | HTTP方法 | 功能描述 | 请求参数 (Body/Form-Data) | 成功响应 (Body) | -|----|-----------------------|----------|--------------------|--------------------------------------------------------------|---------------------------------| -| 1 | `/api/public/feedback` | `POST` | 提交公众反馈(带文件) | `feedback`: `PublicFeedbackRequest` (JSON), `files`: `MultipartFile[]` | `201 CREATED` (返回 `Feedback` 对象) | - -#### 2.3.3 反馈管理接口 (`/api/feedback`) - -认证用户(如主管、管理员)对环境问题反馈进行查询、统计和处理。 - -| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | -|----|-----------------------------|----------|----------------------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------|---------------------------------------| -| 1 | `/api/feedback/submit` | `POST` | 认证用户提交反馈(带文件) | `isAuthenticated()` | `feedback`: `FeedbackSubmissionRequest` (JSON), `files`: `MultipartFile[]` | `201 CREATED` (返回 `Feedback` 对象) | -| 2 | `/api/feedback` | `GET` | 获取所有反馈(分页+多条件过滤) | `isAuthenticated()` | `status`, `pollutionType`, `severityLevel`, `cityName`, `districtName`, `startDate`, `endDate`, `keyword`, `pageable` | `Page` | -| 3 | `/api/feedback/{id}` | `GET` | 根据ID获取反馈详情 | `ADMIN`, `SUPERVISOR`, `DECISION_MAKER` | `id` (Path Var) | `FeedbackResponseDTO` | -| 4 | `/api/feedback/stats` | `GET` | 获取反馈统计数据 | `isAuthenticated()` | 无 | `FeedbackStatsResponse` | -| 5 | `/api/feedback/{id}/process`| `POST` | 处理反馈(如审核通过) | `ADMIN`, `SUPERVISOR` | `id` (Path Var), `ProcessFeedbackRequest` (Body) | `FeedbackResponseDTO` | - -#### 2.3.4 任务管理接口 (`/api/management/tasks`) - -主管和管理员用于任务的全生命周期管理,包括创建、分配、查询和审批。 - -| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | -|----|-----------------------------------------|----------|----------------------------------|------------------------------------|--------------------------------------------------------------------------------------------|---------------------------------| -| 1 | `/api/management/tasks` | `GET` | 获取任务列表(分页+多条件过滤) | `ADMIN`, `SUPERVISOR`, `DECISION_MAKER` | `status`, `assigneeId`, `severity`, `pollutionType`, `startDate`, `endDate`, `pageable` | `List` | -| 2 | `/api/management/tasks/{taskId}` | `GET` | 获取任务详情 | `SUPERVISOR` | `taskId` (Path Var) | `TaskDetailDTO` | -| 3 | `/api/management/tasks` | `POST` | 手动创建新任务 | `SUPERVISOR` | `TaskCreationRequest` (Body) | `201 CREATED` (返回 `TaskDetailDTO`) | -| 4 | `/api/management/tasks/{taskId}/assign` | `POST` | 分配任务给网格员 | `SUPERVISOR` | `taskId` (Path Var), `TaskAssignmentRequest` (Body) | `TaskSummaryDTO` | -| 5 | `/api/management/tasks/{taskId}/review` | `POST` | 审核任务 | `SUPERVISOR` | `taskId` (Path Var), `TaskApprovalRequest` (Body) | `TaskDetailDTO` | -| 6 | `/api/management/tasks/{taskId}/cancel` | `POST` | 取消任务 | `SUPERVISOR` | `taskId` (Path Var) | `TaskDetailDTO` | -| 7 | `/api/management/tasks/feedback` | `GET` | 获取待处理的反馈列表 | `SUPERVISOR`, `ADMIN` | 无 | `List` | -| 8 | `/api/management/tasks/feedback/{feedbackId}/create-task` | `POST` | 从反馈创建任务 | `SUPERVISOR` | `feedbackId` (Path Var), `TaskFromFeedbackRequest` (Body) | `201 CREATED` (返回 `TaskDetailDTO`) | -| 9 | `/api/management/tasks/{taskId}/approve`| `POST` | 批准任务(完成) | `ADMIN` | `taskId` (Path Var) | `TaskDetailDTO` | -| 10 | `/api/management/tasks/{taskId}/reject` | `POST` | 拒绝任务(打回) | `ADMIN` | `taskId` (Path Var), `TaskRejectionRequest` (Body) | `TaskDetailDTO` | - -#### 2.3.5 网格员任务接口 (`/api/worker`) - -网格员用于查看和处理自己被分配的任务。 - -| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | -|----|---------------------------------|----------|------------------------|---------------|-----------------------------------------------------------|----------------------------| -| 1 | `/api/worker` | `GET` | 获取我被分配的任务列表 | `GRID_WORKER` | `status` (Query Param), `pageable` | `Page` | -| 2 | `/api/worker/{taskId}` | `GET` | 获取任务详情 | `GRID_WORKER` | `taskId` (Path Var) | `TaskDetailDTO` | -| 3 | `/api/worker/{taskId}/accept` | `POST` | 接受任务 | `GRID_WORKER` | `taskId` (Path Var) | `TaskSummaryDTO` | -| 4 | `/api/worker/{taskId}/submit` | `POST` | 提交任务完成情况(带文件) | `GRID_WORKER` | `taskId` (Path Var), `comments` (Form Part), `files` (Form Part) | `TaskSummaryDTO` | - -#### 2.3.6 人员管理接口 (`/api/personnel`) - -管理员用于管理系统中的所有用户账户。 - -| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | -|----|---------------------------------|------------|------------------------------|---------|-----------------------------------------------------------|---------------------------------| -| 1 | `/api/personnel/users` | `POST` | 创建新用户 | `ADMIN` | `UserCreationRequest` (Body) | `201 CREATED` (返回 `UserAccount` 对象) | -| 2 | `/api/personnel/users` | `GET` | 获取用户列表(分页+过滤) | `ADMIN`, `GRID_WORKER` | `role`, `name` (Query Params), `pageable` | `PageDTO` | -| 3 | `/api/personnel/users/{userId}` | `GET` | 根据ID获取用户详情 | `ADMIN` | `userId` (Path Var) | `UserAccount` | -| 4 | `/api/personnel/users/{userId}` | `PATCH` | 更新用户信息(部分更新) | `ADMIN` | `userId` (Path Var), `UserUpdateRequest` (Body) | `UserAccount` | -| 5 | `/api/personnel/users/{userId}/role` | `PUT` | 更新用户角色 | `ADMIN` | `userId` (Path Var), `UserRoleUpdateRequest` (Body) | `UserAccount` | -| 6 | `/api/personnel/users/{userId}` | `DELETE` | 删除用户 | `ADMIN` | `userId` (Path Var) | `204 NO_CONTENT` | - -#### 2.3.7 网格管理接口 (`/api/grids`) - -管理员用于管理地理网格、分配网格员和查看统计数据。 - -| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | -|----|-----------------------------------------------|----------|----------------------------------|----------------------------------------|-------------------------------------------------|---------------------------------| -| 1 | `/api/grids` | `GET` | 获取所有网格列表 | `ADMIN`, `DECISION_MAKER`, `GRID_WORKER` | 无 | `List` | -| 2 | `/api/grids/{id}` | `PATCH` | 更新网格信息(如设为障碍物) | `ADMIN` | `id` (Path Var), `GridUpdateRequest` (Body) | `Grid` | -| 3 | `/api/grids/coverage` | `GET` | 获取网格覆盖率统计 | `ADMIN` | 无 | `List` | -| 4 | `/api/grids/{gridId}/assign` | `POST` | 分配网格员到指定网格(通过ID) | `ADMIN` | `gridId` (Path Var), `userId` (Body) | `200 OK` | -| 5 | `/api/grids/{gridId}/unassign` | `POST` | 从网格中移除网格员(通过ID) | `ADMIN` | `gridId` (Path Var) | `200 OK` | -| 6 | `/api/grids/coordinates/{gridX}/{gridY}/assign` | `POST` | 分配网格员到指定网格(通过坐标) | `ADMIN`, `GRID_WORKER` | `gridX`, `gridY` (Path Vars), `userId` (Body) | `200 OK` | -| 7 | `/api/grids/coordinates/{gridX}/{gridY}/unassign`| `POST`| 从网格中移除网格员(通过坐标) | `ADMIN`, `GRID_WORKER` | `gridX`, `gridY` (Path Vars) | `200 OK` | - -#### 2.3.8 仪表盘数据接口 (`/api/dashboard`) - -为前端仪表盘提供各种聚合和统计数据,用于决策支持和系统状态监控。 - -| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | -|----|------------------------------------------|----------|----------------------------------|---------------------------|---------------------------|---------------------------------------| -| 1 | `/api/dashboard/stats` | `GET` | 获取仪表盘核心统计数据 | `DECISION_MAKER`, `ADMIN` | 无 | `DashboardStatsDTO` | -| 2 | `/api/dashboard/reports/aqi-distribution`| `GET` | 获取AQI等级分布数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | -| 3 | `/api/dashboard/reports/monthly-exceedance-trend` | `GET` | 获取月度超标趋势数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | -| 4 | `/api/dashboard/reports/grid-coverage` | `GET` | 获取网格覆盖情况数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | -| 5 | `/api/dashboard/map/heatmap` | `GET` | 获取反馈热力图数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | -| 6 | `/api/dashboard/reports/pollution-stats` | `GET` | 获取污染物统计报告数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | -| 7 | `/api/dashboard/reports/task-completion-stats` | `GET` | 获取任务完成情况统计数据 | `DECISION_MAKER`, `ADMIN` | 无 | `TaskStatsDTO` | -| 8 | `/api/dashboard/map/aqi-heatmap` | `GET` | 获取AQI热力图数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | -| 9 | `/api/dashboard/thresholds` | `GET` | 获取所有污染物阈值设置 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | -| 10 | `/api/dashboard/thresholds/{pollutantName}` | `GET` | 获取指定污染物的阈值设置 | `DECISION_MAKER`, `ADMIN` | `pollutantName` (Path Var)| `PollutantThresholdDTO` | -| 11 | `/api/dashboard/thresholds` | `POST` | 保存污染物阈值设置 | `ADMIN` | `PollutantThresholdDTO` (Body) | `PollutantThresholdDTO` | -| 12 | `/api/dashboard/reports/pollutant-monthly-trends` | `GET` | 获取各污染物月度趋势数据 | `DECISION_MAKER`, `ADMIN` | 无 | `Map>` | - -#### 2.3.9 其他接口 - -包含文件处理、操作日志、个人资料等辅助功能的接口。 - -| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | -|----|--------------------------|----------|------------------------|------|---------------------------|---------------------------------| -| 1 | `/api/files/{filename}` | `GET` | 下载/预览文件(下载) | `isAuthenticated()` | `filename` (Path Var) | `Resource` (文件流) | -| 2 | `/api/view/{filename}` | `GET` | 下载/预览文件(内联预览) | `isAuthenticated()` | `filename` (Path Var) | `Resource` (文件流) | -| 3 | `/api/logs/my-logs` | `GET` | 获取当前用户的操作日志 | `isAuthenticated()` | 无 | `List` | -| 4 | `/api/logs` | `GET` | 获取所有操作日志(可过滤) | `ADMIN` | `operationType`, `userId`, `startTime`, `endTime` | `List` | -| 5 | `/api/me/feedback` | `GET` | 获取当前用户的反馈历史 | `isAuthenticated()` | `pageable` | `List` | -| 6 | `/api/supervisor/reviews`| `GET` | 获取待审核的反馈列表 | `SUPERVISOR`, `ADMIN` | 无 | `List` | -| 7 | `/api/supervisor/reviews/{feedbackId}/approve` | `POST` | 审核通过反馈 | `SUPERVISOR`, `ADMIN` | `feedbackId` (Path Var) | `200 OK` | -| 8 | `/api/supervisor/reviews/{feedbackId}/reject` | `POST` | 审核拒绝反馈 | `SUPERVISOR`, `ADMIN` | `feedbackId` (Path Var), `RejectFeedbackRequest` (Body) | `200 OK` | -| 9 | `/api/map/grid` | `GET` | 获取完整地图网格数据 | `isAuthenticated()` | 无 | `List` | -| 10 | `/api/map/grid` | `POST` | 创建/更新单个网格单元 | `ADMIN` | `MapGrid` (Body) | `MapGrid` | -| 11 | `/api/map/initialize` | `POST` | 初始化地图网格 | `ADMIN` | `width`, `height` (Query) | `String` (Message) | -| 12 | `/api/pathfinding/find` | `POST` | A*寻路算法查找路径 | `isAuthenticated()` | `PathfindingRequest` (Body) | `List` | -| 13 | `/api/tasks/unassigned` | `GET` | 获取未分配的任务列表 | `ADMIN`, `SUPERVISOR` | 无 | `List` | -| 14 | `/api/tasks/grid-workers`| `GET` | 获取可用的网格员列表 | `ADMIN`, `SUPERVISOR` | 无 | `List` | -| 15 | `/api/tasks/assign` | `POST` | 分配任务给网格员 | `ADMIN`, `SUPERVISOR` | `AssignmentRequest` (Body)| `Assignment` | - -#### 2.3.10 API 设计原则与最佳实践 - -为保证API的规范性、安全性和易用性,系统在设计和实现中遵循了以下原则: - -1. **统一响应格式**: 所有API响应都封装在一个标准结构中,包含成功/失败状态、数据负载、消息和时间戳,便于前端统一处理。分页查询则返回包含分页信息(总数、总页数等)的标准化分页对象。 -2. **细粒度权限控制**: 系统利用Spring Security的 `@PreAuthorize` 注解在方法级别上实现了细粒度的权限控制,确保即使用户通过了认证,也只能访问其角色被授权的特定资源和操作。 -3. **严格的参数验证**: 所有接收输入的API端点(特别是写操作)都使用了JSR-303验证注解(如 `@Valid`, `@NotBlank`, `@Email`)对请求体和参数进行严格校验,从入口处保证了数据的合法性和完整性。 -4. **清晰的API文档**: 通过集成SpringDoc,为所有公共API生成了遵循OpenAPI 3.0规范的交互式文档(Swagger UI),包含了清晰的描述、参数说明和响应示例。 -5. **全面的日志记录**: 使用 `@Slf4j` 在关键业务流程、成功操作及异常情况处记录了详细日志,便于系统监控、问题排查和安全审计。 - ---- - -## 2.4 架构特性与外部集成 - -除了核心的业务功能,系统在架构层面采用了一些现代化的设计来实现高性能和高可扩展性,并集成了一些外部服务来增强功能。 - -### 2.4.1 事件驱动架构 (EDA) - -系统内部采用轻量级的事件驱动模型(基于Spring Events)来实现关键业务逻辑的解耦。 - -- **核心事件**: - 1. `FeedbackSubmittedForAiReviewEvent`: 当一个新反馈被提交后发布,用于触发AI服务的异步分析。 - 2. `TaskReadyForAssignmentEvent`: 当一个反馈被批准并可以转化为任务时发布。 - 3. `AuthenticationSuccessEvent`: 用户成功登录时发布,可用于记录日志或更新用户状态。 - 4. `AuthenticationFailureEvent`: 用户登录失败时发布,用于监控恶意尝试。 - -- **优势**: - - **解耦**: 事件的发布者和消费者之间没有直接依赖,易于扩展新功能。 - - **异步化**: 许多耗时的操作(如AI分析、邮件发送)可以通过异步监听事件来完成,避免阻塞主线程,提升用户体验。 - -### 2.4.2 外部服务集成 - -- **AI服务**: - - **服务商**: 火山引擎 (Volcano Engine) - - **核心功能**: 用于反馈内容的智能分析,包括自动提取关键词、评估严重等级和进行初步分类,为人工审核提供决策支持。 - -- **邮件服务**: - - **服务商**: 163 SMTP - - **核心功能**: 用于发送系统通知类邮件,如用户注册、密码重置验证码、任务分配通知等。 - -### 2.4.3 性能与缓存 - -为了提升系统响应速度和处理高并发请求的能力,系统内置了基于内存的缓存机制。 - -- **缓存技术**: Google Guava Cache -- **缓存内容**: - - **用户数据**: 缓存频繁访问的用户信息。 - - **配置数据**: 如污染物阈值等不经常变动的系统配置。 - - **热点数据**: 如热门网格的统计信息。 -- **策略**: 缓存设置了合理的过期时间和大小限制,以保证数据的一致性和内存的有效利用。 + +# 系统设计文档 + +本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 + +## 1. 总体设计 + +总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 + +### 1.1 系统架构设计 + +#### 1.1.1 架构选型与原则 + +本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: +- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 +- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 +- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 + +在架构设计中,我们遵循了以下核心原则: +- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。 +- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。 +- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。 +- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 + +#### 1.1.2 后端架构 + +后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: + +- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 +- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 +- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 + +此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 + +![后端分层架构图](https://www.plantuml.com/plantuml/svg/SoWkIImgAStDuG8oX1An24dCoKnELT2gKiX8p-L8AawncdNa5A2gY2pDoN8200000) + +**技术选型**: +- **核心框架**: Spring Boot 3 +- **开发语言**: Java 17 +- **身份认证**: Spring Security + JWT (JSON Web Tokens) +- **数据持久化**: 自定义JSON文件存储 +- **异步处理**: Spring Async & Events, Spring WebFlux +- **API文档**: SpringDoc (OpenAPI 3 / Swagger) + +#### 1.1.3 前端架构 + +前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 +- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 +- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 +- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 + +### 1.2 功能模块划分 + +系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 + +| 核心模块 | 主要职责 | 关键功能点 | +| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | +| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | +| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | +| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | +| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | +| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | +| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 | + +![功能模块图](https://www.plantuml.com/plantuml/svg/XLD1Jy5358xXF-S5wGgNkgRD1s2aD2gbzEd-Lgy_8QduTqb2lC_A5gYXaIikIeylC-yS339vj2vj-1e9z9oY-Oq9kGSpT8T5yPiU5sO1rKqpwXWkGZJ9G8P7k9y5Zt9B1pZ2b7h93e0b8B8pX78sYcQ6G2vE_uHlGgC0) + +## 2. 详细设计 + +### 2.1 数据持久化设计 (JSON文件) + +系统不使用传统数据库,所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署,但也对数据一致性和并发控制提出了更高的要求。 + +#### 2.1.1 `users.json` - 用户账户数据 + +存储所有系统用户的信息,包括登录凭证、角色和个人资料。 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | +| `name` | `String` | 非空 | 用户姓名 | +| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | +| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | +| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | +| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | +| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | +| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | +| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | +| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | +| `region` | `String` | | 所属区域或地区 | +| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | +| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | +| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | +| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | +| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | +| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | +| `lockout_end_time` | `String` | | 账户锁定截止时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +#### 2.1.2 `feedback.json` - 环境问题反馈数据 + +存储所有由用户(包括公众)提交的环境问题反馈。 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | +| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | +| `title` | `String` | 非空 | 反馈标题 | +| `description` | `String` | | 问题详细描述 | +| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | +| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | +| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | +| `text_address` | `String` | | 文字描述的地址 | +| `grid_x` | `Number` | | 事发地网格X坐标 | +| `grid_y` | `Number` | | 事发地网格Y坐标 | +| `latitude` | `Number` | | 事发地纬度 | +| `longitude` | `Number` | | 事发地经度 | +| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +#### 2.1.3 `tasks.json` - 任务数据 + +存储由反馈转化而来或手动创建的具体处理任务。 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | +| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | +| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | +| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | +| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) | +| `title` | `String` | | 任务标题 | +| `description` | `String` | | 任务详细描述 | +| `pollution_type` | `String` | (ENUM) | 污染类型 | +| `severity_level` | `String` | (ENUM) | 严重程度 | +| `text_address` | `String` | | 任务地点文字描述 | +| `grid_x` | `Number` | | 任务地点网格X坐标 | +| `grid_y` | `Number` | | 任务地点网格Y坐标 | +| `latitude` | `Number` | | 任务地点纬度 | +| `longitude` | `Number` | | 任务地点经度 | +| `assigned_at` | `String` | | 任务分配时间 | +| `completed_at` | `String` | | 任务完成时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | +| `deadline` | `String` | | 任务截止日期 | + +#### 2.1.4 `grids.json` - 业务网格数据 + +存储业务逻辑上的地理网格信息。 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------- | --------------- | ------------------- | ---------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | +| `gridx` | `Number` | | 网格X坐标 | +| `gridy` | `Number` | | 网格Y坐标 | +| `city_name` | `String` | | 所属城市 | +| `district_name` | `String` | | 所属区县 | +| `description` | `String` | | 网格描述信息 | +| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | + +#### 2.1.5 `assignments.json` - 任务分配记录数据 + +记录任务分配的详细信息,连接任务、分配者和执行者。 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | -------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | +| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | +| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | +| `status` | `String` | 非空, (ENUM) | 分配状态 | +| `remarks` | `String` | | 分配备注 | +| `assignment_time`| `String` | 非空 | 分配时间 | +| `deadline` | `String` | | 任务截止日期 | + +--- +*(其他辅助表如 `attachment.json`, `operation_log.json`, `map_grid.json` 等的设计将遵循类似结构)* + +### 2.2 核心业务流程 + +本章节将结合时序图和业务规则,详细描述系统的主要业务工作流。 + +#### 2.2.1 反馈处理与任务生成流程 + +这是系统的核心业务闭环,涵盖了从问题发现到任务完成的全过程。其状态流转严格遵循预设的规则。 + +**反馈处理流程**: + +1. **提交反馈**: 公众用户或认证用户通过API提交环境问题反馈。 +2. **进入AI审核**: 新提交的反馈初始状态为 `PENDING`,系统发布 `FeedbackSubmittedForAiReviewEvent` 事件,触发AI服务进行异步审核。反馈状态变为 `AI_REVIEWING`。 +3. **主管人工审核**: AI审核后,无论结果如何,反馈都将进入人工审核阶段。主管(Supervisor)在系统中查看待处理的反馈。 +4. **决策与流转**: + * **批准 (APPROVED)**: 如果反馈有效,主管将其批准。系统据此发布 `TaskReadyForAssignmentEvent` 事件,表明可以基于此反馈创建任务。原反馈的状态更新为 `TASK_CREATED`。 + * **拒绝 (REJECTED)**: 如果反馈无效或重复,主管可以拒绝该反馈,并填写原因。 + +**任务处理流程**: + +1. **创建任务**: 基于已批准的反馈,系统创建一条新任务,初始状态为 `CREATED`。 +2. **分配任务**: 主管根据网格区域、网格员负载等规则,将任务指派给特定的网格员(Grid Worker)。任务状态更新为 `ASSIGNED`。 +3. **执行任务**: 网格员接收到任务通知,接受任务后,状态变为 `IN_PROGRESS`。网格员前往现场处理。 +4. **提交结果**: 网格员完成任务后,在系统中提交工作报告。任务状态更新为 `SUBMITTED`(待审核)。 +5. **审核任务**: 主管审核已完成的任务。 + * **批准 (APPROVED)**: 任务审核通过,流程结束。 + * **拒绝 (REJECTED)**: 任务不符合要求,可将任务打回给网格员重新处理。 + +#### 2.2.2 用户认证与授权流程 + +1. **用户登录**: 用户使用邮箱/手机号和密码请求登录。 +2. **凭证验证**: 系统验证用户信息和密码的正确性。 +3. **令牌生成**: 验证通过后,系统使用JWT生成一个有时效性的 `access_token`。 +4. **访问授权**: 用户在后续的请求中,需在请求头携带此 `token`。后端通过一个安全过滤器(Filter)来解析和验证 `token`,确认用户的身份和权限,从而决定是否能访问受保护的API资源。 + +#### 2.2.3 密码重置流程 + +1. **请求重置**: 用户在登录页面点击"忘记密码",输入注册时使用的邮箱地址。 +2. **发送验证码**: 系统向该邮箱发送一封包含随机验证码的邮件。 +3. **验证与重置**: 用户在指定时间内输入收到的验证码和新密码。系统校验验证码的正确性后,更新用户在 `users.json` 文件中的密码。 + +#### 2.2.4 主要业务规则 + +- **权限控制规则**: + 1. **ADMIN**: 拥有全系统所有权限,是系统的最高管理员。 + 2. **SUPERVISOR**: 负责管理网格员、审核反馈和任务,是区域的管理者。 + 3. **DECISION_MAKER**: 拥有数据查看和分析权限,通常是决策层用户。 + 4. **GRID_WORKER**: 负责执行被分配的任务,是系统的主要执行者。 + 5. **PUBLIC**: 只能提交反馈,是系统的外部参与者。 + +- **任务分配规则**: + 1. 优先基于网格区域进行自动分配。 + 2. 分配时会综合考虑网格员的当前工作负载,避免分配不均。 + 3. 标记为"紧急"的反馈所生成的任务,拥有更高的分配优先级。 + 4. 主管可以对已分配的任务进行手动干预和重新分配。 + +- **反馈处理规则**: + 1. 所有反馈提交后,首先由AI服务进行预审核和内容分析。 + 2. 系统会检测可能重复的反馈,并提示审核人员进行合并处理。 + 3. 紧急反馈(如严重等级为`CRITICAL`)会被优先展示给审核人员。 + +### 2.3 API接口设计 + +本章节详细定义了系统各模块的API接口,包括认证、任务管理、数据上报等。 + +#### 2.3.1 认证接口 (`/api/auth`) + +处理用户注册、登录、登出和密码管理等功能。 + +| 序号 | 接口路径 | HTTP方法 | 功能描述 | 请求参数 (Body/Query) | 成功响应 (Body) | +|----|-------------------------------|----------|------------------------|-----------------------------------------------------------|---------------------------------| +| 1 | `/api/auth/signup` | `POST` | 用户注册 | `SignUpRequest` (name, phone, email, password, role, etc.) | `201 CREATED` | +| 2 | `/api/auth/login` | `POST` | 用户登录 | `LoginRequest` (email/phone, password) | `JwtAuthenticationResponse` (token) | +| 3 | `/api/auth/logout` | `POST` | 用户登出 | 无 | `200 OK` | +| 4 | `/api/auth/send-verification-code` | `POST` | 发送注册验证码 | `email` (Query Param) | `200 OK` | +| 5 | `/api/auth/send-password-reset-code` | `POST` | 发送密码重置验证码 | `email` (Query Param) | `200 OK` | +| 6 | `/api/auth/reset-password-with-code` | `POST` | 使用验证码重置密码 | `PasswordResetWithCodeDto` (email, code, newPassword) | `200 OK` | + +#### 2.3.2 公共接口 (`/api/public`) + +提供给外部系统或公众使用的无需认证的接口。 + +| 序号 | 接口路径 | HTTP方法 | 功能描述 | 请求参数 (Body/Form-Data) | 成功响应 (Body) | +|----|-----------------------|----------|--------------------|--------------------------------------------------------------|---------------------------------| +| 1 | `/api/public/feedback` | `POST` | 提交公众反馈(带文件) | `feedback`: `PublicFeedbackRequest` (JSON), `files`: `MultipartFile[]` | `201 CREATED` (返回 `Feedback` 对象) | + +#### 2.3.3 反馈管理接口 (`/api/feedback`) + +认证用户(如主管、管理员)对环境问题反馈进行查询、统计和处理。 + +| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | +|----|-----------------------------|----------|----------------------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------|---------------------------------------| +| 1 | `/api/feedback/submit` | `POST` | 认证用户提交反馈(带文件) | `isAuthenticated()` | `feedback`: `FeedbackSubmissionRequest` (JSON), `files`: `MultipartFile[]` | `201 CREATED` (返回 `Feedback` 对象) | +| 2 | `/api/feedback` | `GET` | 获取所有反馈(分页+多条件过滤) | `isAuthenticated()` | `status`, `pollutionType`, `severityLevel`, `cityName`, `districtName`, `startDate`, `endDate`, `keyword`, `pageable` | `Page` | +| 3 | `/api/feedback/{id}` | `GET` | 根据ID获取反馈详情 | `ADMIN`, `SUPERVISOR`, `DECISION_MAKER` | `id` (Path Var) | `FeedbackResponseDTO` | +| 4 | `/api/feedback/stats` | `GET` | 获取反馈统计数据 | `isAuthenticated()` | 无 | `FeedbackStatsResponse` | +| 5 | `/api/feedback/{id}/process`| `POST` | 处理反馈(如审核通过) | `ADMIN`, `SUPERVISOR` | `id` (Path Var), `ProcessFeedbackRequest` (Body) | `FeedbackResponseDTO` | + +#### 2.3.4 任务管理接口 (`/api/management/tasks`) + +主管和管理员用于任务的全生命周期管理,包括创建、分配、查询和审批。 + +| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | +|----|-----------------------------------------|----------|----------------------------------|------------------------------------|--------------------------------------------------------------------------------------------|---------------------------------| +| 1 | `/api/management/tasks` | `GET` | 获取任务列表(分页+多条件过滤) | `ADMIN`, `SUPERVISOR`, `DECISION_MAKER` | `status`, `assigneeId`, `severity`, `pollutionType`, `startDate`, `endDate`, `pageable` | `List` | +| 2 | `/api/management/tasks/{taskId}` | `GET` | 获取任务详情 | `SUPERVISOR` | `taskId` (Path Var) | `TaskDetailDTO` | +| 3 | `/api/management/tasks` | `POST` | 手动创建新任务 | `SUPERVISOR` | `TaskCreationRequest` (Body) | `201 CREATED` (返回 `TaskDetailDTO`) | +| 4 | `/api/management/tasks/{taskId}/assign` | `POST` | 分配任务给网格员 | `SUPERVISOR` | `taskId` (Path Var), `TaskAssignmentRequest` (Body) | `TaskSummaryDTO` | +| 5 | `/api/management/tasks/{taskId}/review` | `POST` | 审核任务 | `SUPERVISOR` | `taskId` (Path Var), `TaskApprovalRequest` (Body) | `TaskDetailDTO` | +| 6 | `/api/management/tasks/{taskId}/cancel` | `POST` | 取消任务 | `SUPERVISOR` | `taskId` (Path Var) | `TaskDetailDTO` | +| 7 | `/api/management/tasks/feedback` | `GET` | 获取待处理的反馈列表 | `SUPERVISOR`, `ADMIN` | 无 | `List` | +| 8 | `/api/management/tasks/feedback/{feedbackId}/create-task` | `POST` | 从反馈创建任务 | `SUPERVISOR` | `feedbackId` (Path Var), `TaskFromFeedbackRequest` (Body) | `201 CREATED` (返回 `TaskDetailDTO`) | +| 9 | `/api/management/tasks/{taskId}/approve`| `POST` | 批准任务(完成) | `ADMIN` | `taskId` (Path Var) | `TaskDetailDTO` | +| 10 | `/api/management/tasks/{taskId}/reject` | `POST` | 拒绝任务(打回) | `ADMIN` | `taskId` (Path Var), `TaskRejectionRequest` (Body) | `TaskDetailDTO` | + +#### 2.3.5 网格员任务接口 (`/api/worker`) + +网格员用于查看和处理自己被分配的任务。 + +| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | +|----|---------------------------------|----------|------------------------|---------------|-----------------------------------------------------------|----------------------------| +| 1 | `/api/worker` | `GET` | 获取我被分配的任务列表 | `GRID_WORKER` | `status` (Query Param), `pageable` | `Page` | +| 2 | `/api/worker/{taskId}` | `GET` | 获取任务详情 | `GRID_WORKER` | `taskId` (Path Var) | `TaskDetailDTO` | +| 3 | `/api/worker/{taskId}/accept` | `POST` | 接受任务 | `GRID_WORKER` | `taskId` (Path Var) | `TaskSummaryDTO` | +| 4 | `/api/worker/{taskId}/submit` | `POST` | 提交任务完成情况(带文件) | `GRID_WORKER` | `taskId` (Path Var), `comments` (Form Part), `files` (Form Part) | `TaskSummaryDTO` | + +#### 2.3.6 人员管理接口 (`/api/personnel`) + +管理员用于管理系统中的所有用户账户。 + +| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | +|----|---------------------------------|------------|------------------------------|---------|-----------------------------------------------------------|---------------------------------| +| 1 | `/api/personnel/users` | `POST` | 创建新用户 | `ADMIN` | `UserCreationRequest` (Body) | `201 CREATED` (返回 `UserAccount` 对象) | +| 2 | `/api/personnel/users` | `GET` | 获取用户列表(分页+过滤) | `ADMIN`, `GRID_WORKER` | `role`, `name` (Query Params), `pageable` | `PageDTO` | +| 3 | `/api/personnel/users/{userId}` | `GET` | 根据ID获取用户详情 | `ADMIN` | `userId` (Path Var) | `UserAccount` | +| 4 | `/api/personnel/users/{userId}` | `PATCH` | 更新用户信息(部分更新) | `ADMIN` | `userId` (Path Var), `UserUpdateRequest` (Body) | `UserAccount` | +| 5 | `/api/personnel/users/{userId}/role` | `PUT` | 更新用户角色 | `ADMIN` | `userId` (Path Var), `UserRoleUpdateRequest` (Body) | `UserAccount` | +| 6 | `/api/personnel/users/{userId}` | `DELETE` | 删除用户 | `ADMIN` | `userId` (Path Var) | `204 NO_CONTENT` | + +#### 2.3.7 网格管理接口 (`/api/grids`) + +管理员用于管理地理网格、分配网格员和查看统计数据。 + +| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | +|----|-----------------------------------------------|----------|----------------------------------|----------------------------------------|-------------------------------------------------|---------------------------------| +| 1 | `/api/grids` | `GET` | 获取所有网格列表 | `ADMIN`, `DECISION_MAKER`, `GRID_WORKER` | 无 | `List` | +| 2 | `/api/grids/{id}` | `PATCH` | 更新网格信息(如设为障碍物) | `ADMIN` | `id` (Path Var), `GridUpdateRequest` (Body) | `Grid` | +| 3 | `/api/grids/coverage` | `GET` | 获取网格覆盖率统计 | `ADMIN` | 无 | `List` | +| 4 | `/api/grids/{gridId}/assign` | `POST` | 分配网格员到指定网格(通过ID) | `ADMIN` | `gridId` (Path Var), `userId` (Body) | `200 OK` | +| 5 | `/api/grids/{gridId}/unassign` | `POST` | 从网格中移除网格员(通过ID) | `ADMIN` | `gridId` (Path Var) | `200 OK` | +| 6 | `/api/grids/coordinates/{gridX}/{gridY}/assign` | `POST` | 分配网格员到指定网格(通过坐标) | `ADMIN`, `GRID_WORKER` | `gridX`, `gridY` (Path Vars), `userId` (Body) | `200 OK` | +| 7 | `/api/grids/coordinates/{gridX}/{gridY}/unassign`| `POST`| 从网格中移除网格员(通过坐标) | `ADMIN`, `GRID_WORKER` | `gridX`, `gridY` (Path Vars) | `200 OK` | + +#### 2.3.8 仪表盘数据接口 (`/api/dashboard`) + +为前端仪表盘提供各种聚合和统计数据,用于决策支持和系统状态监控。 + +| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | +|----|------------------------------------------|----------|----------------------------------|---------------------------|---------------------------|---------------------------------------| +| 1 | `/api/dashboard/stats` | `GET` | 获取仪表盘核心统计数据 | `DECISION_MAKER`, `ADMIN` | 无 | `DashboardStatsDTO` | +| 2 | `/api/dashboard/reports/aqi-distribution`| `GET` | 获取AQI等级分布数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | +| 3 | `/api/dashboard/reports/monthly-exceedance-trend` | `GET` | 获取月度超标趋势数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | +| 4 | `/api/dashboard/reports/grid-coverage` | `GET` | 获取网格覆盖情况数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | +| 5 | `/api/dashboard/map/heatmap` | `GET` | 获取反馈热力图数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | +| 6 | `/api/dashboard/reports/pollution-stats` | `GET` | 获取污染物统计报告数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | +| 7 | `/api/dashboard/reports/task-completion-stats` | `GET` | 获取任务完成情况统计数据 | `DECISION_MAKER`, `ADMIN` | 无 | `TaskStatsDTO` | +| 8 | `/api/dashboard/map/aqi-heatmap` | `GET` | 获取AQI热力图数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | +| 9 | `/api/dashboard/thresholds` | `GET` | 获取所有污染物阈值设置 | `DECISION_MAKER`, `ADMIN` | 无 | `List` | +| 10 | `/api/dashboard/thresholds/{pollutantName}` | `GET` | 获取指定污染物的阈值设置 | `DECISION_MAKER`, `ADMIN` | `pollutantName` (Path Var)| `PollutantThresholdDTO` | +| 11 | `/api/dashboard/thresholds` | `POST` | 保存污染物阈值设置 | `ADMIN` | `PollutantThresholdDTO` (Body) | `PollutantThresholdDTO` | +| 12 | `/api/dashboard/reports/pollutant-monthly-trends` | `GET` | 获取各污染物月度趋势数据 | `DECISION_MAKER`, `ADMIN` | 无 | `Map>` | + +#### 2.3.9 其他接口 + +包含文件处理、操作日志、个人资料等辅助功能的接口。 + +| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) | +|----|--------------------------|----------|------------------------|------|---------------------------|---------------------------------| +| 1 | `/api/files/{filename}` | `GET` | 下载/预览文件(下载) | `isAuthenticated()` | `filename` (Path Var) | `Resource` (文件流) | +| 2 | `/api/view/{filename}` | `GET` | 下载/预览文件(内联预览) | `isAuthenticated()` | `filename` (Path Var) | `Resource` (文件流) | +| 3 | `/api/logs/my-logs` | `GET` | 获取当前用户的操作日志 | `isAuthenticated()` | 无 | `List` | +| 4 | `/api/logs` | `GET` | 获取所有操作日志(可过滤) | `ADMIN` | `operationType`, `userId`, `startTime`, `endTime` | `List` | +| 5 | `/api/me/feedback` | `GET` | 获取当前用户的反馈历史 | `isAuthenticated()` | `pageable` | `List` | +| 6 | `/api/supervisor/reviews`| `GET` | 获取待审核的反馈列表 | `SUPERVISOR`, `ADMIN` | 无 | `List` | +| 7 | `/api/supervisor/reviews/{feedbackId}/approve` | `POST` | 审核通过反馈 | `SUPERVISOR`, `ADMIN` | `feedbackId` (Path Var) | `200 OK` | +| 8 | `/api/supervisor/reviews/{feedbackId}/reject` | `POST` | 审核拒绝反馈 | `SUPERVISOR`, `ADMIN` | `feedbackId` (Path Var), `RejectFeedbackRequest` (Body) | `200 OK` | +| 9 | `/api/map/grid` | `GET` | 获取完整地图网格数据 | `isAuthenticated()` | 无 | `List` | +| 10 | `/api/map/grid` | `POST` | 创建/更新单个网格单元 | `ADMIN` | `MapGrid` (Body) | `MapGrid` | +| 11 | `/api/map/initialize` | `POST` | 初始化地图网格 | `ADMIN` | `width`, `height` (Query) | `String` (Message) | +| 12 | `/api/pathfinding/find` | `POST` | A*寻路算法查找路径 | `isAuthenticated()` | `PathfindingRequest` (Body) | `List` | +| 13 | `/api/tasks/unassigned` | `GET` | 获取未分配的任务列表 | `ADMIN`, `SUPERVISOR` | 无 | `List` | +| 14 | `/api/tasks/grid-workers`| `GET` | 获取可用的网格员列表 | `ADMIN`, `SUPERVISOR` | 无 | `List` | +| 15 | `/api/tasks/assign` | `POST` | 分配任务给网格员 | `ADMIN`, `SUPERVISOR` | `AssignmentRequest` (Body)| `Assignment` | + +#### 2.3.10 API 设计原则与最佳实践 + +为保证API的规范性、安全性和易用性,系统在设计和实现中遵循了以下原则: + +1. **统一响应格式**: 所有API响应都封装在一个标准结构中,包含成功/失败状态、数据负载、消息和时间戳,便于前端统一处理。分页查询则返回包含分页信息(总数、总页数等)的标准化分页对象。 +2. **细粒度权限控制**: 系统利用Spring Security的 `@PreAuthorize` 注解在方法级别上实现了细粒度的权限控制,确保即使用户通过了认证,也只能访问其角色被授权的特定资源和操作。 +3. **严格的参数验证**: 所有接收输入的API端点(特别是写操作)都使用了JSR-303验证注解(如 `@Valid`, `@NotBlank`, `@Email`)对请求体和参数进行严格校验,从入口处保证了数据的合法性和完整性。 +4. **清晰的API文档**: 通过集成SpringDoc,为所有公共API生成了遵循OpenAPI 3.0规范的交互式文档(Swagger UI),包含了清晰的描述、参数说明和响应示例。 +5. **全面的日志记录**: 使用 `@Slf4j` 在关键业务流程、成功操作及异常情况处记录了详细日志,便于系统监控、问题排查和安全审计。 + +--- + +## 2.4 架构特性与外部集成 + +除了核心的业务功能,系统在架构层面采用了一些现代化的设计来实现高性能和高可扩展性,并集成了一些外部服务来增强功能。 + +### 2.4.1 事件驱动架构 (EDA) + +系统内部采用轻量级的事件驱动模型(基于Spring Events)来实现关键业务逻辑的解耦。 + +- **核心事件**: + 1. `FeedbackSubmittedForAiReviewEvent`: 当一个新反馈被提交后发布,用于触发AI服务的异步分析。 + 2. `TaskReadyForAssignmentEvent`: 当一个反馈被批准并可以转化为任务时发布。 + 3. `AuthenticationSuccessEvent`: 用户成功登录时发布,可用于记录日志或更新用户状态。 + 4. `AuthenticationFailureEvent`: 用户登录失败时发布,用于监控恶意尝试。 + +- **优势**: + - **解耦**: 事件的发布者和消费者之间没有直接依赖,易于扩展新功能。 + - **异步化**: 许多耗时的操作(如AI分析、邮件发送)可以通过异步监听事件来完成,避免阻塞主线程,提升用户体验。 + +### 2.4.2 外部服务集成 + +- **AI服务**: + - **服务商**: 火山引擎 (Volcano Engine) + - **核心功能**: 用于反馈内容的智能分析,包括自动提取关键词、评估严重等级和进行初步分类,为人工审核提供决策支持。 + +- **邮件服务**: + - **服务商**: 163 SMTP + - **核心功能**: 用于发送系统通知类邮件,如用户注册、密码重置验证码、任务分配通知等。 + +### 2.4.3 性能与缓存 + +为了提升系统响应速度和处理高并发请求的能力,系统内置了基于内存的缓存机制。 + +- **缓存技术**: Google Guava Cache +- **缓存内容**: + - **用户数据**: 缓存频繁访问的用户信息。 + - **配置数据**: 如污染物阈值等不经常变动的系统配置。 + - **热点数据**: 如热门网格的统计信息。 +- **策略**: 缓存设置了合理的过期时间和大小限制,以保证数据的一致性和内存的有效利用。 --- - + ### Report/系统设计_v3.md - -# 系统设计文档 - -本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 - -## 1. 总体设计 - -总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 - -### 1.1 系统架构设计 - -#### 1.1.1 架构选型与原则 - -本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: -- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 -- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 -- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 - -在架构设计中,我们遵循了以下核心原则: -- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。 -- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。 -- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。 -- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 - -#### 1.1.2 后端架构 - -后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: - -- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 -- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 -- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 - -此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 - -![后端分层架构图](https://www.plantuml.com/plantuml/svg/SoWkIImgAStDuG8oX1An24dCoKnELT2gKiX8p-L8AawncdNa5A2gY2pDoN820000) - -#### 1.1.3 前端架构 - -前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 -- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 -- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 -- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 - -### 1.2 功能模块划分 - -系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 - -| 核心模块 | 主要职责 | 关键功能点 | -| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | -| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | -| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | -| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | -| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | -| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | -| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 | - -![功能模块图](https://www.plantuml.com/plantuml/svg/XLD1Jy5358xXF-S5wGgNkgRD1s2aD2gbzEd-Lgy_8QduTqb2lC_A5gYXaIikIeylC-yS339vj2vj-1e9z9oY-Oq9kGSpT8T5yPiU5sO1rKqpwXWkGZJ9G8P7k9y5Zt9B1pZ2b7h93e0b8B8pX78sYcQ6G2vE_uHlGgC0) - -## 2. 详细设计 - -### 2.1 功能模块详细设计 - -本章节将对核心功能模块的内部设计进行更深入的阐述。 - -#### 2.1.1 反馈管理模块 - -该模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。 -- **主要控制器**: `FeedbackController`, `PublicController` -- **核心服务**: `FeedbackService`, `FeedbackAiReviewService` -- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest` -- **设计要点**: - - 采用 `@RequestPart` 同时接收JSON数据和文件上传,实现了富文本内容的反馈提交。 - - 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件,将AI审核流程解耦并异步化,提高了API的响应速度。 - - 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。 - -#### 2.1.2 任务管理模块 - -该模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。 -- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController` -- **核心服务**: `TaskService`, `TaskAssignmentService` -- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO` -- **设计要点**: - - 清晰的状态机管理任务的生命周期(CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED)。 - - 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。 - - 为主管和网格员提供了完全独立的API端点,严格分离了不同角色的操作权限。 - -### 2.2 类和对象的设计 - -本节定义了系统核心业务对象的静态结构,即类图。 - -#### 2.2.1 `Feedback` 类图 - -`Feedback` 对象是系统中最核心的数据模型之一,代表一次环境问题反馈。 - -![Feedback Class Diagram](https://www.plantuml.com/plantuml/svg/VP1DJy8n48Rl-HH5s9Y2dAnAEoiT3qajR_GAnWmaGZDIyB9n-Oa9e6iY2p9oPQuAU1y8jAY8576d1gtP7Z6sZY8DkTGVKgI9gL1pCBP_O2Cudn-ex3lVgoqHhq9wHlDMXe8pY5pT5VdqzImeD-eXoVxuFOyO1jD4dfhWh6uiofWKbv0GgXg9T8nK0gWzYlBv4AAYk2f7uVoqioIp2kXyG-uTQ2tD_oT_Eeyc2hXv8ALa2iYtHAg4g5KzGv-pB2c_X9vR-y9o5m00) - -### 2.3 动态模型设计 - -本节描述了系统在运行时,对象之间的交互行为。 - -#### 2.3.1 核心时序图 - -**反馈提交与处理时序图** - -该图展示了从用户提交反馈到系统创建任务的完整交互流程。 - -```mermaid -sequenceDiagram - participant User as 用户 - participant FeedbackController as 反馈控制器 - participant FeedbackService as 反馈服务 - participant EventPublisher as 事件发布器 - participant FeedbackAiReviewService as AI审核服务 - participant TaskService as 任务服务 - - User->>FeedbackController: POST /api/feedback/submit - FeedbackController->>FeedbackService: submitFeedback(request, files) - FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent) - FeedbackService-->>FeedbackController: 返回初步响应 - FeedbackController-->>User: 201 Created - - EventPublisher-->>FeedbackAiReviewService: 异步触发 - FeedbackAiReviewService->>FeedbackAiReviewService: 调用火山引擎AI分析 - FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED) - - Supervisor->>FeedbackService: approveFeedback(feedbackId) - FeedbackService->>TaskService: createTaskFromFeedback(feedback) - TaskService-->>FeedbackService: 任务创建成功 -``` - -#### 2.3.2 核心状态图 - -**反馈状态机 (Feedback Status)** - -![Feedback Status Diagram](https://www.plantuml.com/plantuml/svg/LOrDIy5058xXF-S5wGgNkgRD1s2aD2oXzE9-Lgy_8kduTqbWlC_A5gYvaWMLR-yY_YoHqC1f9YpT9opT9ooHqC1f-HpX-A_p9t9oY-Gq9kGSpTgEBAoC1IayWjMDaIqjLMa2b7h95t9h93t0b8B8p_DoYcQ6G2vE_uHlGgC0) - -### 2.4 算法设计 - -#### 2.4.1 A* 寻路算法 - -- **目的**: 为网格员规划从当前位置到任务目标点的最优(最短或最快)路径。 -- **输入**: - - `startNode`: 起始点坐标。 - - `endNode`: 目标点坐标。 - - `grid`: 包含障碍物信息的地图网格数据。 -- **核心逻辑**: - 1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。 - 2. 从 `openList` 中选取F值(G值+H值)最小的节点作为当前节点。 - - G值: 从起点到当前节点的实际代价。 - - H值: 从当前节点到终点的预估代价(启发函数,通常使用曼哈顿距离或欧氏距离)。 - 3. 遍历当前节点的相邻节点,如果邻居不在`closedList`中且不是障碍物,则计算其G值和H值,并将其加入`openList`。 - 4. 重复此过程,直到当前节点为目标节点,或`openList`为空。 -- **输出**: 从起点到终点的一系列坐标点,即规划好的路径。 -- **应用**: 在`PathfindingController`中通过`/api/pathfinding/find`接口暴露。 - -### 2.5 数据持久化设计 - -系统不使用传统数据库,所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署,但也对数据一致性和并发控制提出了更高的要求。 - -#### 2.5.1 `users.json` - 用户账户数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | -| `name` | `String` | 非空 | 用户姓名 | -| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | -| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | -| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | -| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | -| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | -| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | -| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | -| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | -| `region` | `String` | | 所属区域或地区 | -| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | -| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | -| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | -| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | -| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | -| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | -| `lockout_end_time` | `String` | | 账户锁定截止时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -#### 2.5.2 `feedback.json` - 环境问题反馈数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | -| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | -| `title` | `String` | 非空 | 反馈标题 | -| `description` | `String` | | 问题详细描述 | -| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | -| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | -| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | -| `text_address` | `String` | | 文字描述的地址 | -| `grid_x` | `Number` | | 事发地网格X坐标 | -| `grid_y` | `Number` | | 事发地网格Y坐标 | -| `latitude` | `Number` | | 事发地纬度 | -| `longitude` | `Number` | | 事发地经度 | -| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -#### 2.5.3 `tasks.json` - 任务数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | -| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | -| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | -| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | -| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) | -| `title` | `String` | | 任务标题 | -| `description` | `String` | | 任务详细描述 | -| `pollution_type` | `String` | (ENUM) | 污染类型 | -| `severity_level` | `String` | (ENUM) | 严重程度 | -| `text_address` | `String` | | 任务地点文字描述 | -| `grid_x` | `Number` | | 任务地点网格X坐标 | -| `grid_y` | `Number` | | 任务地点网格Y坐标 | -| `latitude` | `Number` | | 任务地点纬度 | -| `longitude` | `Number` | | 任务地点经度 | -| `assigned_at` | `String` | | 任务分配时间 | -| `completed_at` | `String` | | 任务完成时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | -| `deadline` | `String` | | 任务截止日期 | - -#### 2.5.4 `grids.json` - 业务网格数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------- | --------------- | ------------------- | ---------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | -| `gridx` | `Number` | | 网格X坐标 | -| `gridy` | `Number` | | 网格Y坐标 | -| `city_name` | `String` | | 所属城市 | -| `district_name` | `String` | | 所属区县 | -| `description` | `String` | | 网格描述信息 | -| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | - -#### 2.5.5 `assignments.json` - 任务分配记录数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | -------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | -| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | -| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | -| `status` | `String` | 非空, (ENUM) | 分配状态 | -| `remarks` | `String` | | 分配备注 | -| `assignment_time`| `String` | 非空 | 分配时间 | -| `deadline` | `String` | | 任务截止日期 | -``` + +# 系统设计文档 + +本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 + +## 1. 总体设计 + +总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 + +### 1.1 系统架构设计 + +#### 1.1.1 架构选型与原则 + +本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: +- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 +- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 +- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 + +在架构设计中,我们遵循了以下核心原则: +- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。 +- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。 +- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。 +- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 + +#### 1.1.2 后端架构 + +后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: + +- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 +- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 +- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 + +此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 + +![后端分层架构图](https://www.plantuml.com/plantuml/svg/SoWkIImgAStDuG8oX1An24dCoKnELT2gKiX8p-L8AawncdNa5A2gY2pDoN820000) + +#### 1.1.3 前端架构 + +前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 +- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 +- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 +- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 + +### 1.2 功能模块划分 + +系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 + +| 核心模块 | 主要职责 | 关键功能点 | +| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | +| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | +| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | +| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | +| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | +| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | +| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 | + +![功能模块图](https://www.plantuml.com/plantuml/svg/XLD1Jy5358xXF-S5wGgNkgRD1s2aD2gbzEd-Lgy_8QduTqb2lC_A5gYXaIikIeylC-yS339vj2vj-1e9z9oY-Oq9kGSpT8T5yPiU5sO1rKqpwXWkGZJ9G8P7k9y5Zt9B1pZ2b7h93e0b8B8pX78sYcQ6G2vE_uHlGgC0) + +## 2. 详细设计 + +### 2.1 功能模块详细设计 + +本章节将对核心功能模块的内部设计进行更深入的阐述。 + +#### 2.1.1 反馈管理模块 + +该模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。 +- **主要控制器**: `FeedbackController`, `PublicController` +- **核心服务**: `FeedbackService`, `FeedbackAiReviewService` +- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest` +- **设计要点**: + - 采用 `@RequestPart` 同时接收JSON数据和文件上传,实现了富文本内容的反馈提交。 + - 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件,将AI审核流程解耦并异步化,提高了API的响应速度。 + - 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。 + +#### 2.1.2 任务管理模块 + +该模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。 +- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController` +- **核心服务**: `TaskService`, `TaskAssignmentService` +- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO` +- **设计要点**: + - 清晰的状态机管理任务的生命周期(CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED)。 + - 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。 + - 为主管和网格员提供了完全独立的API端点,严格分离了不同角色的操作权限。 + +### 2.2 类和对象的设计 + +本节定义了系统核心业务对象的静态结构,即类图。 + +#### 2.2.1 `Feedback` 类图 + +`Feedback` 对象是系统中最核心的数据模型之一,代表一次环境问题反馈。 + +![Feedback Class Diagram](https://www.plantuml.com/plantuml/svg/VP1DJy8n48Rl-HH5s9Y2dAnAEoiT3qajR_GAnWmaGZDIyB9n-Oa9e6iY2p9oPQuAU1y8jAY8576d1gtP7Z6sZY8DkTGVKgI9gL1pCBP_O2Cudn-ex3lVgoqHhq9wHlDMXe8pY5pT5VdqzImeD-eXoVxuFOyO1jD4dfhWh6uiofWKbv0GgXg9T8nK0gWzYlBv4AAYk2f7uVoqioIp2kXyG-uTQ2tD_oT_Eeyc2hXv8ALa2iYtHAg4g5KzGv-pB2c_X9vR-y9o5m00) + +### 2.3 动态模型设计 + +本节描述了系统在运行时,对象之间的交互行为。 + +#### 2.3.1 核心时序图 + +**反馈提交与处理时序图** + +该图展示了从用户提交反馈到系统创建任务的完整交互流程。 + +```mermaid +sequenceDiagram + participant User as 用户 + participant FeedbackController as 反馈控制器 + participant FeedbackService as 反馈服务 + participant EventPublisher as 事件发布器 + participant FeedbackAiReviewService as AI审核服务 + participant TaskService as 任务服务 + + User->>FeedbackController: POST /api/feedback/submit + FeedbackController->>FeedbackService: submitFeedback(request, files) + FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent) + FeedbackService-->>FeedbackController: 返回初步响应 + FeedbackController-->>User: 201 Created + + EventPublisher-->>FeedbackAiReviewService: 异步触发 + FeedbackAiReviewService->>FeedbackAiReviewService: 调用火山引擎AI分析 + FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED) + + Supervisor->>FeedbackService: approveFeedback(feedbackId) + FeedbackService->>TaskService: createTaskFromFeedback(feedback) + TaskService-->>FeedbackService: 任务创建成功 +``` + +#### 2.3.2 核心状态图 + +**反馈状态机 (Feedback Status)** + +![Feedback Status Diagram](https://www.plantuml.com/plantuml/svg/LOrDIy5058xXF-S5wGgNkgRD1s2aD2oXzE9-Lgy_8kduTqbWlC_A5gYvaWMLR-yY_YoHqC1f9YpT9opT9ooHqC1f-HpX-A_p9t9oY-Gq9kGSpTgEBAoC1IayWjMDaIqjLMa2b7h95t9h93t0b8B8p_DoYcQ6G2vE_uHlGgC0) + +### 2.4 算法设计 + +#### 2.4.1 A* 寻路算法 + +- **目的**: 为网格员规划从当前位置到任务目标点的最优(最短或最快)路径。 +- **输入**: + - `startNode`: 起始点坐标。 + - `endNode`: 目标点坐标。 + - `grid`: 包含障碍物信息的地图网格数据。 +- **核心逻辑**: + 1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。 + 2. 从 `openList` 中选取F值(G值+H值)最小的节点作为当前节点。 + - G值: 从起点到当前节点的实际代价。 + - H值: 从当前节点到终点的预估代价(启发函数,通常使用曼哈顿距离或欧氏距离)。 + 3. 遍历当前节点的相邻节点,如果邻居不在`closedList`中且不是障碍物,则计算其G值和H值,并将其加入`openList`。 + 4. 重复此过程,直到当前节点为目标节点,或`openList`为空。 +- **输出**: 从起点到终点的一系列坐标点,即规划好的路径。 +- **应用**: 在`PathfindingController`中通过`/api/pathfinding/find`接口暴露。 + +### 2.5 数据持久化设计 + +系统不使用传统数据库,所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署,但也对数据一致性和并发控制提出了更高的要求。 + +#### 2.5.1 `users.json` - 用户账户数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | +| `name` | `String` | 非空 | 用户姓名 | +| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | +| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | +| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | +| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | +| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | +| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | +| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | +| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | +| `region` | `String` | | 所属区域或地区 | +| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | +| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | +| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | +| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | +| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | +| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | +| `lockout_end_time` | `String` | | 账户锁定截止时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +#### 2.5.2 `feedback.json` - 环境问题反馈数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | +| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | +| `title` | `String` | 非空 | 反馈标题 | +| `description` | `String` | | 问题详细描述 | +| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | +| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | +| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | +| `text_address` | `String` | | 文字描述的地址 | +| `grid_x` | `Number` | | 事发地网格X坐标 | +| `grid_y` | `Number` | | 事发地网格Y坐标 | +| `latitude` | `Number` | | 事发地纬度 | +| `longitude` | `Number` | | 事发地经度 | +| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +#### 2.5.3 `tasks.json` - 任务数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | +| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | +| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | +| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | +| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) | +| `title` | `String` | | 任务标题 | +| `description` | `String` | | 任务详细描述 | +| `pollution_type` | `String` | (ENUM) | 污染类型 | +| `severity_level` | `String` | (ENUM) | 严重程度 | +| `text_address` | `String` | | 任务地点文字描述 | +| `grid_x` | `Number` | | 任务地点网格X坐标 | +| `grid_y` | `Number` | | 任务地点网格Y坐标 | +| `latitude` | `Number` | | 任务地点纬度 | +| `longitude` | `Number` | | 任务地点经度 | +| `assigned_at` | `String` | | 任务分配时间 | +| `completed_at` | `String` | | 任务完成时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | +| `deadline` | `String` | | 任务截止日期 | + +#### 2.5.4 `grids.json` - 业务网格数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------- | --------------- | ------------------- | ---------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | +| `gridx` | `Number` | | 网格X坐标 | +| `gridy` | `Number` | | 网格Y坐标 | +| `city_name` | `String` | | 所属城市 | +| `district_name` | `String` | | 所属区县 | +| `description` | `String` | | 网格描述信息 | +| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | + +#### 2.5.5 `assignments.json` - 任务分配记录数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | -------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | +| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | +| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | +| `status` | `String` | 非空, (ENUM) | 分配状态 | +| `remarks` | `String` | | 分配备注 | +| `assignment_time`| `String` | 非空 | 分配时间 | +| `deadline` | `String` | | 任务截止日期 | +``` --- - + ### Report/系统设计_v4.md - -# 系统设计文档 - -本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 - -## 1. 总体设计 - -总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 - -### 1.1 系统架构设计 - -#### 1.1.1 架构选型与原则 - -本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: -- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 -- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 -- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 - -在架构设计中,我们遵循了以下核心原则: -- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。 -- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。 -- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。 -- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 - -#### 1.1.2 后端架构 - -后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: - -- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 -- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 -- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 - -此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 - -![后端分层架构图](https://www.plantuml.com/plantuml/svg/SoWkIImgAStDuG8oX1An24dCoKnELT2gKiX8p-L8AawncdNa5A2gY2pDoN820000) - -#### 1.1.3 前端架构 - -前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 -- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 -- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 -- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 - -### 1.2 功能模块划分 - -系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 - -| 核心模块 | 主要职责 | 关键功能点 | -| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | -| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | -| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | -| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | -| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | -| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | -| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 |真实吧。妈 - -![功能模块图](https://www.plantuml.com/plantuml/svg/XLD1Jy5358xXF-S5wGgNkgRD1s2aD2gbzEd-Lgy_8QduTqb2lC_A5gYXaIikIeylC-yS339vj2vj-1e9z9oY-Oq9kGSpT8T5yPiU5sO1rKqpwXWkGZJ9G8P7k9y5Zt9B1pZ2b7h93e0b8B8pX78sYcQ6G2vE_uHlGgC0) - -## 2. 详细设计 - -### 2.1 功能模块详细设计 - -本章节将对核心功能模块的内部设计进行更深入的阐述。 - -#### 2.1.1 反馈管理模块 - -该模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。 -- **主要控制器**: `FeedbackController`, `PublicController` -- **核心服务**: `FeedbackService`, `FeedbackAiReviewService` -- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest` -- **设计要点**: - - 采用 `@RequestPart` 同时接收JSON数据和文件上传,实现了富文本内容的反馈提交。 - - 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件,将AI审核流程解耦并异步化,提高了API的响应速度。 - - 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。 - -#### 2.1.2 任务管理模块 - -该模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。 -- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController` -- **核心服务**: `TaskService`, `TaskAssignmentService` -- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO` -- **设计要点**: - - 清晰的状态机管理任务的生命周期(CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED)。 - - 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。 - - 为主管和网格员提供了完全独立的API端点,严格分离了不同角色的操作权限。 - -#### 2.1.3 用户与人员管理模块 - -该模块为系统的权限管理和组织架构提供了基础。 -- **主要控制器**: `PersonnelController` -- **核心服务**: `UserAccountService`, `OperationLogService` -- **关键DTO**: `UserCreationRequest`, `UserUpdateRequest`, `UserRoleUpdateRequest` -- **设计要点**: - - 提供了对用户账户的完整CRUD操作。 - - 实现了用户角色和状态的精细化管理。 - - 所有关键操作均通过`OperationLogService`记录日志,便于审计和追踪。 - -#### 2.1.4 网格与地图模块 - -该模块是系统实现区域化、网格化管理的核心。 -- **主要控制器**: `GridController`, `MapController` -- **核心服务**: `GridService`, `PathfindingService` -- **设计要点**: - - 支持对地理区域进行灵活的网格化定义,并可将网格标记为障碍。 - - 实现了网格与网格员的关联管理。 - - 核心亮点是集成了`PathfindingService`,该服务封装了A*寻路算法,为任务路径规划提供支持。 - - -### 2.2 类和对象的设计 - -本节定义了系统核心业务对象的静态结构,即类图。 - -#### 2.2.1 `UserAccount` 类图 -![UserAccount Class Diagram](https://www.plantuml.com/plantuml/svg/ZLJRSzD24BtxAovE_-bL2XAn2aYgY2pDoN820000_5-AY4TQb1y7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000) - -#### 2.2.2 `Feedback` 类图 -![Feedback Class Diagram](https://www.plantuml.com/plantuml/svg/VP1DJy8n48Rl-HH5s9Y2dAnAEoiT3qajR_GAnWmaGZDIyB9n-Oa9e6iY2p9oPQuAU1y8jAY8576d1gtP7Z6sZY8DkTGVKgI9gL1pCBP_O2Cudn-ex3lVgoqHhq9wHlDMXe8pY5pT5VdqzImeD-eXoVxuFOyO1jD4dfhWh6uiofWKbv0GgXg9T8nK0gWzYlBv4AAYk2f7uVoqioIp2kXyG-uTQ2tD_oT_Eeyc2hXv8ALa2iYtHAg4g5KzGv-pB2c_X9vR-y9o5m00) - -#### 2.2.3 `Task` 类图 -![Task Class Diagram](https://www.plantuml.com/plantuml/svg/RLJDRT8n3BpxAovE_-dL2Imga2iXgY2pDoN820000_59AYoTQb1S7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000_5-9o4TQb1A7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000) - -#### 2.2.4 `Grid` 和 `Assignment` 类图 -![Grid and Assignment Class Diagram](https://www.plantuml.com/plantuml/svg/RLJDRT8n3BpxAovE_-dL2Imga2iXgY2pDoN820000_5-AooTQb1y7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000) - -### 2.3 动态模型设计 - -本节描述了系统在运行时,对象之间的交互行为。 - -#### 2.3.1 核心时序图 - -**用户登录认证时序图** -```mermaid -sequenceDiagram - participant User as 用户 - participant AuthController as 认证控制器 - participant AuthService as 认证服务 - participant JwtUtil as JWT工具 - - User->>AuthController: POST /api/auth/login (email, password) - AuthController->>AuthService: signIn(LoginRequest) - AuthService->>AuthService: 验证凭证 - alt 验证成功 - AuthService->>JwtUtil: generateToken(user) - JwtUtil-->>AuthService: 返回JWT令牌 - AuthService-->>AuthController: 返回JwtAuthenticationResponse - AuthController-->>User: 200 OK (token) - else 验证失败 - AuthService-->>AuthController: 抛出AuthenticationException - AuthController-->>User: 401 Unauthorized - end -``` - -**反馈提交与处理时序图** -```mermaid -sequenceDiagram - participant User as 用户 - participant FeedbackController as 反馈控制器 - participant FeedbackService as 反馈服务 - participant EventPublisher as 事件发布器 - participant FeedbackAiReviewService as AI审核服务 - - User->>FeedbackController: POST /api/feedback/submit - FeedbackController->>FeedbackService: submitFeedback(request, files) - FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent) - FeedbackService-->>FeedbackController: 返回初步响应 - Controller-->>User: 201 Created - - EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核 - FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED) -``` - -**主管分配任务时序图** -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant TaskMgmtController as 任务管理控制器 - participant TaskService as 任务服务 - participant AssignmentService as 分配服务 - participant NotificationService as 通知服务 - - Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId) - TaskMgmtController->>TaskService: assignTask(taskId, workerId) - TaskService->>AssignmentService: createAssignment(task, worker) - AssignmentService-->>TaskService: 返回Assignment - TaskService->>TaskService: 更新任务状态为ASSIGNED - TaskService->>NotificationService: notifyWorker(workerId, taskId) - TaskService-->>TaskMgmtController: 分配成功 - TaskMgmtController-->>Supervisor: 200 OK -``` - -#### 2.3.2 核心状态图 - -**反馈状态机 (Feedback Status)** -![Feedback Status Diagram](https://www.plantuml.com/plantuml/svg/LOrDIy5058xXF-S5wGgNkgRD1s2aD2oXzE9-Lgy_8kduTqbWlC_A5gYvaWMLR-yY_YoHqC1f9YpT9opT9ooHqC1f-HpX-A_p9t9oY-Gq9kGSpTgEBAoC1IayWjMDaIqjLMa2b7h95t9h93t0b8B8p_DoYcQ6G2vE_uHlGgC0) - -**任务状态机 (Task Status)** -![Task Status Diagram](https://www.plantuml.com/plantuml/svg/TOpDIy5058xXF-S5wGgNkgRD1s2aD2oXzE9-Lgy_8kduTqbWlC_A5gYvaWMLR-yY_YoHqC1f9YpT9opT9ooHqC1f-HpX-A_p9t9oY-Gq9kGSpTgEBAoC1IayWjMDaIqjLMa2b7h95t9h93t0b8B8p_DoYcQ6G2vE_uHlGgC0) - - -### 2.4 算法与策略设计 - -#### 2.4.1 A* 寻路算法 - -- **目的**: 为网格员规划从当前位置到任务目标点的最优(最短或最快)路径。 -- **输入**: - - `startNode`: 起始点坐标。 - - `endNode`: 目标点坐标。 - - `grid`: 包含障碍物信息的地图网格数据。 -- **核心逻辑**: - 1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。 - 2. 从 `openList` 中选取F值(G值+H值)最小的节点作为当前节点。 - - G值: 从起点到当前节点的实际代价。 - - H值: 从当前节点到终点的预估代价(启发函数,通常使用曼哈顿距离或欧氏距离)。 - 3. 遍历当前节点的相邻节点,如果邻居不在`closedList`中且不是障碍物,则计算其G值和H值,并将其加入`openList`。 - 4. 重复此过程,直到当前节点为目标节点,或`openList`为空。 -- **输出**: 从起点到终点的一系列坐标点,即规划好的路径。 -- **应用**: 在`PathfindingController`中通过`/api/pathfinding/find`接口暴露。 - -#### 2.4.2 任务分配策略 - -- **目的**: 在多个可用网格员中,为新任务选择最合适的执行者。 -- **核心逻辑**: 这是一个复合策略,综合考虑以下因素,并计算加权得分: - 1. **地理位置优先**: 优先选择任务所在网格或邻近网格的网格员。 - 2. **负载均衡**: 优先选择当前进行中任务数量最少的网格员。 - 3. **技能匹配**: (未来扩展)可根据任务需要的技能(`skills`字段)与网格员的技能进行匹配。 - 4. **任务优先级**: 对于高优先级任务,可以适当放宽地理位置限制,确保有可用人员处理。 -- **应用**: 在`TaskAssignmentService`中实现,当主管触发自动分配或系统基于反馈自动创建任务时调用。 - -### 2.5 数据持久化设计 - -系统不使用传统数据库,所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署,但也对数据一致性和并发控制提出了更高的要求。 - -#### 2.5.1 `users.json` - 用户账户数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | -| `name` | `String` | 非空 | 用户姓名 | -| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | -| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | -| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | -| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | -| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | -| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | -| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | -| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | -| `region` | `String` | | 所属区域或地区 | -| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | -| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | -| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | -| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | -| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | -| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | -| `lockout_end_time` | `String` | | 账户锁定截止时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -#### 2.5.2 `feedback.json` - 环境问题反馈数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | -| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | -| `title` | `String` | 非空 | 反馈标题 | -| `description` | `String` | | 问题详细描述 | -| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | -| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | -| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | -| `text_address` | `String` | | 文字描述的地址 | -| `grid_x` | `Number` | | 事发地网格X坐标 | -| `grid_y` | `Number` | | 事发地网格Y坐标 | -| `latitude` | `Number` | | 事发地纬度 | -| `longitude` | `Number` | | 事发地经度 | -| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -#### 2.5.3 `tasks.json` - 任务数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | -| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | -| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | -| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | -| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) | -| `title` | `String` | | 任务标题 | -| `description` | `String` | | 任务详细描述 | -| `pollution_type` | `String` | (ENUM) | 污染类型 | -| `severity_level` | `String` | (ENUM) | 严重程度 | -| `text_address` | `String` | | 任务地点文字描述 | -| `grid_x` | `Number` | | 任务地点网格X坐标 | -| `grid_y` | `Number` | | 任务地点网格Y坐标 | -| `latitude` | `Number` | | 任务地点纬度 | -| `longitude` | `Number` | | 任务地点经度 | -| `assigned_at` | `String` | | 任务分配时间 | -| `completed_at` | `String` | | 任务完成时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | -| `deadline` | `String` | | 任务截止日期 | - -#### 2.5.4 `grids.json` - 业务网格数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------- | --------------- | ------------------- | ---------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | -| `gridx` | `Number` | | 网格X坐标 | -| `gridy` | `Number` | | 网格Y坐标 | -| `city_name` | `String` | | 所属城市 | -| `district_name` | `String` | | 所属区县 | -| `description` | `String` | | 网格描述信息 | -| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | - -#### 2.5.5 `assignments.json` - 任务分配记录数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | -------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | -| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | -| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | -| `status` | `String` | 非空, (ENUM) | 分配状态 | -| `remarks` | `String` | | 分配备注 | -| `assignment_time`| `String` | 非空 | 分配时间 | -| `deadline` | `String` | | 任务截止日期 | -``` + +# 系统设计文档 + +本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 + +## 1. 总体设计 + +总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 + +### 1.1 系统架构设计 + +#### 1.1.1 架构选型与原则 + +本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: +- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 +- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 +- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 + +在架构设计中,我们遵循了以下核心原则: +- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。 +- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。 +- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。 +- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 + +#### 1.1.2 后端架构 + +后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: + +- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 +- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 +- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 + +此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 + +![后端分层架构图](https://www.plantuml.com/plantuml/svg/SoWkIImgAStDuG8oX1An24dCoKnELT2gKiX8p-L8AawncdNa5A2gY2pDoN820000) + +#### 1.1.3 前端架构 + +前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 +- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 +- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 +- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 + +### 1.2 功能模块划分 + +系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 + +| 核心模块 | 主要职责 | 关键功能点 | +| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | +| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | +| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | +| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | +| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | +| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | +| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 |真实吧。妈 + +![功能模块图](https://www.plantuml.com/plantuml/svg/XLD1Jy5358xXF-S5wGgNkgRD1s2aD2gbzEd-Lgy_8QduTqb2lC_A5gYXaIikIeylC-yS339vj2vj-1e9z9oY-Oq9kGSpT8T5yPiU5sO1rKqpwXWkGZJ9G8P7k9y5Zt9B1pZ2b7h93e0b8B8pX78sYcQ6G2vE_uHlGgC0) + +## 2. 详细设计 + +### 2.1 功能模块详细设计 + +本章节将对核心功能模块的内部设计进行更深入的阐述。 + +#### 2.1.1 反馈管理模块 + +该模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。 +- **主要控制器**: `FeedbackController`, `PublicController` +- **核心服务**: `FeedbackService`, `FeedbackAiReviewService` +- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest` +- **设计要点**: + - 采用 `@RequestPart` 同时接收JSON数据和文件上传,实现了富文本内容的反馈提交。 + - 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件,将AI审核流程解耦并异步化,提高了API的响应速度。 + - 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。 + +#### 2.1.2 任务管理模块 + +该模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。 +- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController` +- **核心服务**: `TaskService`, `TaskAssignmentService` +- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO` +- **设计要点**: + - 清晰的状态机管理任务的生命周期(CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED)。 + - 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。 + - 为主管和网格员提供了完全独立的API端点,严格分离了不同角色的操作权限。 + +#### 2.1.3 用户与人员管理模块 + +该模块为系统的权限管理和组织架构提供了基础。 +- **主要控制器**: `PersonnelController` +- **核心服务**: `UserAccountService`, `OperationLogService` +- **关键DTO**: `UserCreationRequest`, `UserUpdateRequest`, `UserRoleUpdateRequest` +- **设计要点**: + - 提供了对用户账户的完整CRUD操作。 + - 实现了用户角色和状态的精细化管理。 + - 所有关键操作均通过`OperationLogService`记录日志,便于审计和追踪。 + +#### 2.1.4 网格与地图模块 + +该模块是系统实现区域化、网格化管理的核心。 +- **主要控制器**: `GridController`, `MapController` +- **核心服务**: `GridService`, `PathfindingService` +- **设计要点**: + - 支持对地理区域进行灵活的网格化定义,并可将网格标记为障碍。 + - 实现了网格与网格员的关联管理。 + - 核心亮点是集成了`PathfindingService`,该服务封装了A*寻路算法,为任务路径规划提供支持。 + + +### 2.2 类和对象的设计 + +本节定义了系统核心业务对象的静态结构,即类图。 + +#### 2.2.1 `UserAccount` 类图 +![UserAccount Class Diagram](https://www.plantuml.com/plantuml/svg/ZLJRSzD24BtxAovE_-bL2XAn2aYgY2pDoN820000_5-AY4TQb1y7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000) + +#### 2.2.2 `Feedback` 类图 +![Feedback Class Diagram](https://www.plantuml.com/plantuml/svg/VP1DJy8n48Rl-HH5s9Y2dAnAEoiT3qajR_GAnWmaGZDIyB9n-Oa9e6iY2p9oPQuAU1y8jAY8576d1gtP7Z6sZY8DkTGVKgI9gL1pCBP_O2Cudn-ex3lVgoqHhq9wHlDMXe8pY5pT5VdqzImeD-eXoVxuFOyO1jD4dfhWh6uiofWKbv0GgXg9T8nK0gWzYlBv4AAYk2f7uVoqioIp2kXyG-uTQ2tD_oT_Eeyc2hXv8ALa2iYtHAg4g5KzGv-pB2c_X9vR-y9o5m00) + +#### 2.2.3 `Task` 类图 +![Task Class Diagram](https://www.plantuml.com/plantuml/svg/RLJDRT8n3BpxAovE_-dL2Imga2iXgY2pDoN820000_59AYoTQb1S7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000_5-9o4TQb1A7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000) + +#### 2.2.4 `Grid` 和 `Assignment` 类图 +![Grid and Assignment Class Diagram](https://www.plantuml.com/plantuml/svg/RLJDRT8n3BpxAovE_-dL2Imga2iXgY2pDoN820000_5-AooTQb1y7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000) + +### 2.3 动态模型设计 + +本节描述了系统在运行时,对象之间的交互行为。 + +#### 2.3.1 核心时序图 + +**用户登录认证时序图** +```mermaid +sequenceDiagram + participant User as 用户 + participant AuthController as 认证控制器 + participant AuthService as 认证服务 + participant JwtUtil as JWT工具 + + User->>AuthController: POST /api/auth/login (email, password) + AuthController->>AuthService: signIn(LoginRequest) + AuthService->>AuthService: 验证凭证 + alt 验证成功 + AuthService->>JwtUtil: generateToken(user) + JwtUtil-->>AuthService: 返回JWT令牌 + AuthService-->>AuthController: 返回JwtAuthenticationResponse + AuthController-->>User: 200 OK (token) + else 验证失败 + AuthService-->>AuthController: 抛出AuthenticationException + AuthController-->>User: 401 Unauthorized + end +``` + +**反馈提交与处理时序图** +```mermaid +sequenceDiagram + participant User as 用户 + participant FeedbackController as 反馈控制器 + participant FeedbackService as 反馈服务 + participant EventPublisher as 事件发布器 + participant FeedbackAiReviewService as AI审核服务 + + User->>FeedbackController: POST /api/feedback/submit + FeedbackController->>FeedbackService: submitFeedback(request, files) + FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent) + FeedbackService-->>FeedbackController: 返回初步响应 + Controller-->>User: 201 Created + + EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核 + FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED) +``` + +**主管分配任务时序图** +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant TaskMgmtController as 任务管理控制器 + participant TaskService as 任务服务 + participant AssignmentService as 分配服务 + participant NotificationService as 通知服务 + + Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId) + TaskMgmtController->>TaskService: assignTask(taskId, workerId) + TaskService->>AssignmentService: createAssignment(task, worker) + AssignmentService-->>TaskService: 返回Assignment + TaskService->>TaskService: 更新任务状态为ASSIGNED + TaskService->>NotificationService: notifyWorker(workerId, taskId) + TaskService-->>TaskMgmtController: 分配成功 + TaskMgmtController-->>Supervisor: 200 OK +``` + +#### 2.3.2 核心状态图 + +**反馈状态机 (Feedback Status)** +![Feedback Status Diagram](https://www.plantuml.com/plantuml/svg/LOrDIy5058xXF-S5wGgNkgRD1s2aD2oXzE9-Lgy_8kduTqbWlC_A5gYvaWMLR-yY_YoHqC1f9YpT9opT9ooHqC1f-HpX-A_p9t9oY-Gq9kGSpTgEBAoC1IayWjMDaIqjLMa2b7h95t9h93t0b8B8p_DoYcQ6G2vE_uHlGgC0) + +**任务状态机 (Task Status)** +![Task Status Diagram](https://www.plantuml.com/plantuml/svg/TOpDIy5058xXF-S5wGgNkgRD1s2aD2oXzE9-Lgy_8kduTqbWlC_A5gYvaWMLR-yY_YoHqC1f9YpT9opT9ooHqC1f-HpX-A_p9t9oY-Gq9kGSpTgEBAoC1IayWjMDaIqjLMa2b7h95t9h93t0b8B8p_DoYcQ6G2vE_uHlGgC0) + + +### 2.4 算法与策略设计 + +#### 2.4.1 A* 寻路算法 + +- **目的**: 为网格员规划从当前位置到任务目标点的最优(最短或最快)路径。 +- **输入**: + - `startNode`: 起始点坐标。 + - `endNode`: 目标点坐标。 + - `grid`: 包含障碍物信息的地图网格数据。 +- **核心逻辑**: + 1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。 + 2. 从 `openList` 中选取F值(G值+H值)最小的节点作为当前节点。 + - G值: 从起点到当前节点的实际代价。 + - H值: 从当前节点到终点的预估代价(启发函数,通常使用曼哈顿距离或欧氏距离)。 + 3. 遍历当前节点的相邻节点,如果邻居不在`closedList`中且不是障碍物,则计算其G值和H值,并将其加入`openList`。 + 4. 重复此过程,直到当前节点为目标节点,或`openList`为空。 +- **输出**: 从起点到终点的一系列坐标点,即规划好的路径。 +- **应用**: 在`PathfindingController`中通过`/api/pathfinding/find`接口暴露。 + +#### 2.4.2 任务分配策略 + +- **目的**: 在多个可用网格员中,为新任务选择最合适的执行者。 +- **核心逻辑**: 这是一个复合策略,综合考虑以下因素,并计算加权得分: + 1. **地理位置优先**: 优先选择任务所在网格或邻近网格的网格员。 + 2. **负载均衡**: 优先选择当前进行中任务数量最少的网格员。 + 3. **技能匹配**: (未来扩展)可根据任务需要的技能(`skills`字段)与网格员的技能进行匹配。 + 4. **任务优先级**: 对于高优先级任务,可以适当放宽地理位置限制,确保有可用人员处理。 +- **应用**: 在`TaskAssignmentService`中实现,当主管触发自动分配或系统基于反馈自动创建任务时调用。 + +### 2.5 数据持久化设计 + +系统不使用传统数据库,所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署,但也对数据一致性和并发控制提出了更高的要求。 + +#### 2.5.1 `users.json` - 用户账户数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | +| `name` | `String` | 非空 | 用户姓名 | +| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | +| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | +| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | +| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | +| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | +| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | +| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | +| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | +| `region` | `String` | | 所属区域或地区 | +| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | +| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | +| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | +| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | +| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | +| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | +| `lockout_end_time` | `String` | | 账户锁定截止时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +#### 2.5.2 `feedback.json` - 环境问题反馈数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | +| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | +| `title` | `String` | 非空 | 反馈标题 | +| `description` | `String` | | 问题详细描述 | +| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | +| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | +| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | +| `text_address` | `String` | | 文字描述的地址 | +| `grid_x` | `Number` | | 事发地网格X坐标 | +| `grid_y` | `Number` | | 事发地网格Y坐标 | +| `latitude` | `Number` | | 事发地纬度 | +| `longitude` | `Number` | | 事发地经度 | +| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +#### 2.5.3 `tasks.json` - 任务数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | +| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | +| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | +| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | +| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) | +| `title` | `String` | | 任务标题 | +| `description` | `String` | | 任务详细描述 | +| `pollution_type` | `String` | (ENUM) | 污染类型 | +| `severity_level` | `String` | (ENUM) | 严重程度 | +| `text_address` | `String` | | 任务地点文字描述 | +| `grid_x` | `Number` | | 任务地点网格X坐标 | +| `grid_y` | `Number` | | 任务地点网格Y坐标 | +| `latitude` | `Number` | | 任务地点纬度 | +| `longitude` | `Number` | | 任务地点经度 | +| `assigned_at` | `String` | | 任务分配时间 | +| `completed_at` | `String` | | 任务完成时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | +| `deadline` | `String` | | 任务截止日期 | + +#### 2.5.4 `grids.json` - 业务网格数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------- | --------------- | ------------------- | ---------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | +| `gridx` | `Number` | | 网格X坐标 | +| `gridy` | `Number` | | 网格Y坐标 | +| `city_name` | `String` | | 所属城市 | +| `district_name` | `String` | | 所属区县 | +| `description` | `String` | | 网格描述信息 | +| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | + +#### 2.5.5 `assignments.json` - 任务分配记录数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | -------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | +| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | +| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | +| `status` | `String` | 非空, (ENUM) | 分配状态 | +| `remarks` | `String` | | 分配备注 | +| `assignment_time`| `String` | 非空 | 分配时间 | +| `deadline` | `String` | | 任务截止日期 | +``` --- - + ### Report/系统设计_v5.md - -# 系统设计文档 - -本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 - -## 1. 总体设计 - -总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 - -### 1.1 系统架构设计 - -#### 1.1.1 架构选型与原则 - -本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: -- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 -- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 -- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 - -在架构设计中,我们遵循了以下核心原则: -- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。 -- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。 -- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。 -- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 - -#### 1.1.2 后端架构 - -后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: - -- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 -- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 -- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 - -此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 - -**后端分层架构图** -```plantuml -@startuml -package "Backend Architecture" { - [Controller Layer] <> - [Service Layer] <> - [Repository Layer] <> - - [Controller Layer] --> [Service Layer] - [Service Layer] --> [Repository Layer] -} -@enduml -``` - -#### 1.1.3 前端架构 - -前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 -- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 -- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 -- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 - -### 1.2 功能模块划分 - -系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 - -| 核心模块 | 主要职责 | 关键功能点 | -| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | -| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | -| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | -| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | -| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | -| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | -| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 | - -**功能模块图** -```plantuml -@startuml -left to right direction -actor User - -rectangle "环境监督系统 (EMS)" { - User -- (认证与授权模块) - (认证与授权模块) ..> (用户与人员管理模块) : 依赖 - - User -- (反馈管理模块) - (反馈管理模块) ..> (任务管理模块) : 创建任务 - (任务管理模块) ..> (网格与地图模块) : 路径规划 - (任务管理模块) ..> (用户与人员管理模块) : 分配任务 - - (决策支持模块) .up.> (反馈管理模块) : 数据分析 - (决策支持模块) .up.> (任务管理模块) : 数据分析 - - User -- (个人中心模块) -} -@enduml -``` - -## 2. 详细设计 - -### 2.1 功能模块详细设计 - -本章节将对核心功能模块的内部设计进行更深入的阐述。 - -#### 2.1.1 反馈管理模块 - -该模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。 -- **主要控制器**: `FeedbackController`, `PublicController` -- **核心服务**: `FeedbackService`, `FeedbackAiReviewService` -- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest` -- **设计要点**: - - 采用 `@RequestPart` 同时接收JSON数据和文件上传,实现了富文本内容的反馈提交。 - - 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件,将AI审核流程解耦并异步化,提高了API的响应速度。 - - 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。 - -#### 2.1.2 任务管理模块 - -该模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。 -- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController` -- **核心服务**: `TaskService`, `TaskAssignmentService` -- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO` -- **设计要点**: - - 清晰的状态机管理任务的生命周期(CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED)。 - - 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。 - - 为主管和网格员提供了完全独立的API端点,严格分离了不同角色的操作权限。 - -#### 2.1.3 用户与人员管理模块 - -该模块为系统的权限管理和组织架构提供了基础。 -- **主要控制器**: `PersonnelController` -- **核心服务**: `UserAccountService`, `OperationLogService` -- **关键DTO**: `UserCreationRequest`, `UserUpdateRequest`, `UserRoleUpdateRequest` -- **设计要点**: - - 提供了对用户账户的完整CRUD操作。 - - 实现了用户角色和状态的精细化管理。 - - 所有关键操作均通过`OperationLogService`记录日志,便于审计和追踪。 - -#### 2.1.4 网格与地图模块 - -该模块是系统实现区域化、网格化管理的核心。 -- **主要控制器**: `GridController`, `MapController` -- **核心服务**: `GridService`, `PathfindingService` -- **设计要点**: - - 支持对地理区域进行灵活的网格化定义,并可将网格标记为障碍。 - - 实现了网格与网格员的关联管理。 - - 核心亮点是集成了`PathfindingService`,该服务封装了A*寻路算法,为任务路径规划提供支持。 - - -### 2.2 类和对象的设计 - -本节定义了系统核心业务对象的静态结构,即类图。 - -#### 2.2.1 `UserAccount` 类图 -```plantuml -@startuml -class UserAccount { - - Long id - - String name - - String phone - - String email - - String password - - Gender gender - - Role role - - UserStatus status - - Integer gridX - - Integer gridY - + isValid() -} - -enum Gender { - MALE - FEMALE - OTHER -} - -enum Role { - ADMIN - SUPERVISOR - GRID_WORKER -} - -enum UserStatus { - ACTIVE - INACTIVE -} - -UserAccount *-- Gender -UserAccount *-- Role -UserAccount *-- UserStatus -@enduml -``` - -#### 2.2.2 `Feedback` 类图 -```plantuml -@startuml -class Feedback { - - Long id - - String eventId - - String title - - PollutionType pollutionType - - SeverityLevel severityLevel - - FeedbackStatus status - - Long submitterId - + process() - + approve() -} - -enum PollutionType { - AIR - WATER - SOIL - NOISE -} - -enum SeverityLevel { - LOW - MEDIUM - HIGH - CRITICAL -} - -enum FeedbackStatus { - PENDING_REVIEW - AI_REJECTED - PROCESSED -} - -Feedback *-- PollutionType -Feedback *-- SeverityLevel -Feedback *-- FeedbackStatus -@enduml -``` - -#### 2.2.3 `Task` 类图 -```plantuml -@startuml -class Task { - - Long id - - Long feedbackId - - Long assigneeId - - Long createdBy - - TaskStatus status - - String title - + assignTo(workerId) - + complete() - + approve() -} - -enum TaskStatus { - CREATED - ASSIGNED - IN_PROGRESS - SUBMITTED - APPROVED - REJECTED -} - -Task *-- TaskStatus -@enduml -``` - -#### 2.2.4 `Grid` 和 `Assignment` 类图 -```plantuml -@startuml -class Grid { - - Long id - - Integer gridX - - Integer gridY - - String cityName - - boolean isObstacle -} - -class Assignment { - - Long id - - Long taskId - - Long assignerId - - AssignmentStatus status - - String remarks - - LocalDateTime assignmentTime -} - -enum AssignmentStatus { - PENDING - ACCEPTED - REJECTED -} - -Assignment *-- AssignmentStatus -@enduml -``` - -### 2.3 动态模型设计 - -本节描述了系统在运行时,对象之间的交互行为。 - -#### 2.3.1 核心时序图 - -**用户登录认证时序图** -```mermaid -sequenceDiagram - participant User as 用户 - participant AuthController as 认证控制器 - participant AuthService as 认证服务 - participant JwtUtil as JWT工具 - - User->>AuthController: POST /api/auth/login (email, password) - AuthController->>AuthService: signIn(LoginRequest) - AuthService->>AuthService: 验证凭证 - alt 验证成功 - AuthService->>JwtUtil: generateToken(user) - JwtUtil-->>AuthService: 返回JWT令牌 - AuthService-->>AuthController: 返回JwtAuthenticationResponse - AuthController-->>User: 200 OK (token) - else 验证失败 - AuthService-->>AuthController: 抛出AuthenticationException - AuthController-->>User: 401 Unauthorized - end -``` - -**反馈提交与处理时序图** -```mermaid -sequenceDiagram - participant User as 用户 - participant FeedbackController as 反馈控制器 - participant FeedbackService as 反馈服务 - participant EventPublisher as 事件发布器 - participant FeedbackAiReviewService as AI审核服务 - - User->>FeedbackController: POST /api/feedback/submit - FeedbackController->>FeedbackService: submitFeedback(request, files) - FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent) - FeedbackService-->>FeedbackController: 返回初步响应 - Controller-->>User: 201 Created - - EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核 - FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED) -``` - -**主管分配任务时序图** -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant TaskMgmtController as 任务管理控制器 - participant TaskService as 任务服务 - participant AssignmentService as 分配服务 - participant NotificationService as 通知服务 - - Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId) - TaskMgmtController->>TaskService: assignTask(taskId, workerId) - TaskService->>AssignmentService: createAssignment(task, worker) - AssignmentService-->>TaskService: 返回Assignment - TaskService->>TaskService: 更新任务状态为ASSIGNED - TaskService->>NotificationService: notifyWorker(workerId, taskId) - TaskService-->>TaskMgmtController: 分配成功 - TaskMgmtController-->>Supervisor: 200 OK -``` - -#### 2.3.2 核心状态图 - -**反馈状态机 (Feedback Status)** -```plantuml -@startuml -state "待审核" as PENDING_REVIEW -state "AI审核通过" as AI_APPROVED -state "AI拒绝" as AI_REJECTED -state "已处理" as PROCESSED -state "已关闭" as CLOSED - -[*] --> PENDING_REVIEW : 提交反馈 -PENDING_REVIEW --> AI_APPROVED : AI审核通过 -PENDING_REVIEW --> AI_REJECTED : AI审核拒绝 -AI_APPROVED --> PROCESSED : 主管创建任务 -PROCESSED --> CLOSED : 任务完成 -AI_REJECTED --> CLOSED : 归档 -@enduml -``` - -**任务状态机 (Task Status)** -```plantuml -@startuml -state "已创建" as CREATED -state "已分配" as ASSIGNED -state "进行中" as IN_PROGRESS -state "已提交" as SUBMITTED -state "已批准" as APPROVED -state "已拒绝" as REJECTED - -[*] --> CREATED : 创建任务 -CREATED --> ASSIGNED : 分配给网格员 -ASSIGNED --> IN_PROGRESS : 网格员接受任务 -IN_PROGRESS --> SUBMITTED : 网格员完成并提交 -SUBMITTED --> APPROVED : 主管审核通过 -SUBMITTED --> REJECTED : 主管审核拒绝 -REJECTED --> ASSIGNED : 重新分配 -APPROVED --> [*] -@enduml -``` - - -### 2.4 算法与策略设计 - -#### 2.4.1 A* 寻路算法 - -- **目的**: 为网格员规划从当前位置到任务目标点的最优(最短或最快)路径。 -- **输入**: - - `startNode`: 起始点坐标。 - - `endNode`: 目标点坐标。 - - `grid`: 包含障碍物信息的地图网格数据。 -- **核心逻辑**: - 1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。 - 2. 从 `openList` 中选取F值(G值+H值)最小的节点作为当前节点。 - - G值: 从起点到当前节点的实际代价。 - - H值: 从当前节点到终点的预估代价(启发函数,通常使用曼哈顿距离或欧氏距离)。 - 3. 遍历当前节点的相邻节点,如果邻居不在`closedList`中且不是障碍物,则计算其G值和H值,并将其加入`openList`。 - 4. 重复此过程,直到当前节点为目标节点,或`openList`为空。 -- **输出**: 从起点到终点的一系列坐标点,即规划好的路径。 -- **应用**: 在`PathfindingController`中通过`/api/pathfinding/find`接口暴露。 - -#### 2.4.2 任务分配策略 - -- **目的**: 在多个可用网格员中,为新任务选择最合适的执行者。 -- **核心逻辑**: 这是一个复合策略,综合考虑以下因素,并计算加权得分: - 1. **地理位置优先**: 优先选择任务所在网格或邻近网格的网格员。 - 2. **负载均衡**: 优先选择当前进行中任务数量最少的网格员。 - 3. **技能匹配**: (未来扩展)可根据任务需要的技能(`skills`字段)与网格员的技能进行匹配。 - 4. **任务优先级**: 对于高优先级任务,可以适当放宽地理位置限制,确保有可用人员处理。 -- **应用**: 在`TaskAssignmentService`中实现,当主管触发自动分配或系统基于反馈自动创建任务时调用。 - -### 2.5 数据持久化设计 - -系统不使用传统数据库,所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署,但也对数据一致性和并发控制提出了更高的要求。 - -#### 2.5.1 `users.json` - 用户账户数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | -| `name` | `String` | 非空 | 用户姓名 | -| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | -| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | -| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | -| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | -| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | -| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | -| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | -| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | -| `region` | `String` | | 所属区域或地区 | -| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | -| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | -| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | -| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | -| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | -| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | -| `lockout_end_time` | `String` | | 账户锁定截止时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -#### 2.5.2 `feedback.json` - 环境问题反馈数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | -| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | -| `title` | `String` | 非空 | 反馈标题 | -| `description` | `String` | | 问题详细描述 | -| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | -| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | -| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | -| `text_address` | `String` | | 文字描述的地址 | -| `grid_x` | `Number` | | 事发地网格X坐标 | -| `grid_y` | `Number` | | 事发地网格Y坐标 | -| `latitude` | `Number` | | 事发地纬度 | -| `longitude` | `Number` | | 事发地经度 | -| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -#### 2.5.3 `tasks.json` - 任务数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | -| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | -| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | -| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | -| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) | -| `title` | `String` | | 任务标题 | -| `description` | `String` | | 任务详细描述 | -| `pollution_type` | `String` | (ENUM) | 污染类型 | -| `severity_level` | `String` | (ENUM) | 严重程度 | -| `text_address` | `String` | | 任务地点文字描述 | -| `grid_x` | `Number` | | 任务地点网格X坐标 | -| `grid_y` | `Number` | | 任务地点网格Y坐标 | -| `latitude` | `Number` | | 任务地点纬度 | -| `longitude` | `Number` | | 任务地点经度 | -| `assigned_at` | `String` | | 任务分配时间 | -| `completed_at` | `String` | | 任务完成时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | -| `deadline` | `String` | | 任务截止日期 | - -#### 2.5.4 `grids.json` - 业务网格数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------- | --------------- | ------------------- | ---------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | -| `gridx` | `Number` | | 网格X坐标 | -| `gridy` | `Number` | | 网格Y坐标 | -| `city_name` | `String` | | 所属城市 | -| `district_name` | `String` | | 所属区县 | -| `description` | `String` | | 网格描述信息 | -| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | - -#### 2.5.5 `assignments.json` - 任务分配记录数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | -------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | -| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | -| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | -| `status` | `String` | 非空, (ENUM) | 分配状态 | -| `remarks` | `String` | | 分配备注 | -| `assignment_time`| `String` | 非空 | 分配时间 | -| `deadline` | `String` | | 任务截止日期 | -``` + +# 系统设计文档 + +本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 + +## 1. 总体设计 + +总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 + +### 1.1 系统架构设计 + +#### 1.1.1 架构选型与原则 + +本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: +- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 +- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 +- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 + +在架构设计中,我们遵循了以下核心原则: +- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。 +- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。 +- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。 +- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 + +#### 1.1.2 后端架构 + +后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: + +- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 +- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 +- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 + +此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 + +**后端分层架构图** +```plantuml +@startuml +package "Backend Architecture" { + [Controller Layer] <> + [Service Layer] <> + [Repository Layer] <> + + [Controller Layer] --> [Service Layer] + [Service Layer] --> [Repository Layer] +} +@enduml +``` + +#### 1.1.3 前端架构 + +前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 +- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 +- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 +- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 + +### 1.2 功能模块划分 + +系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 + +| 核心模块 | 主要职责 | 关键功能点 | +| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | +| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | +| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | +| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | +| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | +| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | +| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 | + +**功能模块图** +```plantuml +@startuml +left to right direction +actor User + +rectangle "环境监督系统 (EMS)" { + User -- (认证与授权模块) + (认证与授权模块) ..> (用户与人员管理模块) : 依赖 + + User -- (反馈管理模块) + (反馈管理模块) ..> (任务管理模块) : 创建任务 + (任务管理模块) ..> (网格与地图模块) : 路径规划 + (任务管理模块) ..> (用户与人员管理模块) : 分配任务 + + (决策支持模块) .up.> (反馈管理模块) : 数据分析 + (决策支持模块) .up.> (任务管理模块) : 数据分析 + + User -- (个人中心模块) +} +@enduml +``` + +## 2. 详细设计 + +### 2.1 功能模块详细设计 + +本章节将对核心功能模块的内部设计进行更深入的阐述。 + +#### 2.1.1 反馈管理模块 + +该模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。 +- **主要控制器**: `FeedbackController`, `PublicController` +- **核心服务**: `FeedbackService`, `FeedbackAiReviewService` +- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest` +- **设计要点**: + - 采用 `@RequestPart` 同时接收JSON数据和文件上传,实现了富文本内容的反馈提交。 + - 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件,将AI审核流程解耦并异步化,提高了API的响应速度。 + - 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。 + +#### 2.1.2 任务管理模块 + +该模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。 +- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController` +- **核心服务**: `TaskService`, `TaskAssignmentService` +- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO` +- **设计要点**: + - 清晰的状态机管理任务的生命周期(CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED)。 + - 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。 + - 为主管和网格员提供了完全独立的API端点,严格分离了不同角色的操作权限。 + +#### 2.1.3 用户与人员管理模块 + +该模块为系统的权限管理和组织架构提供了基础。 +- **主要控制器**: `PersonnelController` +- **核心服务**: `UserAccountService`, `OperationLogService` +- **关键DTO**: `UserCreationRequest`, `UserUpdateRequest`, `UserRoleUpdateRequest` +- **设计要点**: + - 提供了对用户账户的完整CRUD操作。 + - 实现了用户角色和状态的精细化管理。 + - 所有关键操作均通过`OperationLogService`记录日志,便于审计和追踪。 + +#### 2.1.4 网格与地图模块 + +该模块是系统实现区域化、网格化管理的核心。 +- **主要控制器**: `GridController`, `MapController` +- **核心服务**: `GridService`, `PathfindingService` +- **设计要点**: + - 支持对地理区域进行灵活的网格化定义,并可将网格标记为障碍。 + - 实现了网格与网格员的关联管理。 + - 核心亮点是集成了`PathfindingService`,该服务封装了A*寻路算法,为任务路径规划提供支持。 + + +### 2.2 类和对象的设计 + +本节定义了系统核心业务对象的静态结构,即类图。 + +#### 2.2.1 `UserAccount` 类图 +```plantuml +@startuml +class UserAccount { + - Long id + - String name + - String phone + - String email + - String password + - Gender gender + - Role role + - UserStatus status + - Integer gridX + - Integer gridY + + isValid() +} + +enum Gender { + MALE + FEMALE + OTHER +} + +enum Role { + ADMIN + SUPERVISOR + GRID_WORKER +} + +enum UserStatus { + ACTIVE + INACTIVE +} + +UserAccount *-- Gender +UserAccount *-- Role +UserAccount *-- UserStatus +@enduml +``` + +#### 2.2.2 `Feedback` 类图 +```plantuml +@startuml +class Feedback { + - Long id + - String eventId + - String title + - PollutionType pollutionType + - SeverityLevel severityLevel + - FeedbackStatus status + - Long submitterId + + process() + + approve() +} + +enum PollutionType { + AIR + WATER + SOIL + NOISE +} + +enum SeverityLevel { + LOW + MEDIUM + HIGH + CRITICAL +} + +enum FeedbackStatus { + PENDING_REVIEW + AI_REJECTED + PROCESSED +} + +Feedback *-- PollutionType +Feedback *-- SeverityLevel +Feedback *-- FeedbackStatus +@enduml +``` + +#### 2.2.3 `Task` 类图 +```plantuml +@startuml +class Task { + - Long id + - Long feedbackId + - Long assigneeId + - Long createdBy + - TaskStatus status + - String title + + assignTo(workerId) + + complete() + + approve() +} + +enum TaskStatus { + CREATED + ASSIGNED + IN_PROGRESS + SUBMITTED + APPROVED + REJECTED +} + +Task *-- TaskStatus +@enduml +``` + +#### 2.2.4 `Grid` 和 `Assignment` 类图 +```plantuml +@startuml +class Grid { + - Long id + - Integer gridX + - Integer gridY + - String cityName + - boolean isObstacle +} + +class Assignment { + - Long id + - Long taskId + - Long assignerId + - AssignmentStatus status + - String remarks + - LocalDateTime assignmentTime +} + +enum AssignmentStatus { + PENDING + ACCEPTED + REJECTED +} + +Assignment *-- AssignmentStatus +@enduml +``` + +### 2.3 动态模型设计 + +本节描述了系统在运行时,对象之间的交互行为。 + +#### 2.3.1 核心时序图 + +**用户登录认证时序图** +```mermaid +sequenceDiagram + participant User as 用户 + participant AuthController as 认证控制器 + participant AuthService as 认证服务 + participant JwtUtil as JWT工具 + + User->>AuthController: POST /api/auth/login (email, password) + AuthController->>AuthService: signIn(LoginRequest) + AuthService->>AuthService: 验证凭证 + alt 验证成功 + AuthService->>JwtUtil: generateToken(user) + JwtUtil-->>AuthService: 返回JWT令牌 + AuthService-->>AuthController: 返回JwtAuthenticationResponse + AuthController-->>User: 200 OK (token) + else 验证失败 + AuthService-->>AuthController: 抛出AuthenticationException + AuthController-->>User: 401 Unauthorized + end +``` + +**反馈提交与处理时序图** +```mermaid +sequenceDiagram + participant User as 用户 + participant FeedbackController as 反馈控制器 + participant FeedbackService as 反馈服务 + participant EventPublisher as 事件发布器 + participant FeedbackAiReviewService as AI审核服务 + + User->>FeedbackController: POST /api/feedback/submit + FeedbackController->>FeedbackService: submitFeedback(request, files) + FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent) + FeedbackService-->>FeedbackController: 返回初步响应 + Controller-->>User: 201 Created + + EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核 + FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED) +``` + +**主管分配任务时序图** +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant TaskMgmtController as 任务管理控制器 + participant TaskService as 任务服务 + participant AssignmentService as 分配服务 + participant NotificationService as 通知服务 + + Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId) + TaskMgmtController->>TaskService: assignTask(taskId, workerId) + TaskService->>AssignmentService: createAssignment(task, worker) + AssignmentService-->>TaskService: 返回Assignment + TaskService->>TaskService: 更新任务状态为ASSIGNED + TaskService->>NotificationService: notifyWorker(workerId, taskId) + TaskService-->>TaskMgmtController: 分配成功 + TaskMgmtController-->>Supervisor: 200 OK +``` + +#### 2.3.2 核心状态图 + +**反馈状态机 (Feedback Status)** +```plantuml +@startuml +state "待审核" as PENDING_REVIEW +state "AI审核通过" as AI_APPROVED +state "AI拒绝" as AI_REJECTED +state "已处理" as PROCESSED +state "已关闭" as CLOSED + +[*] --> PENDING_REVIEW : 提交反馈 +PENDING_REVIEW --> AI_APPROVED : AI审核通过 +PENDING_REVIEW --> AI_REJECTED : AI审核拒绝 +AI_APPROVED --> PROCESSED : 主管创建任务 +PROCESSED --> CLOSED : 任务完成 +AI_REJECTED --> CLOSED : 归档 +@enduml +``` + +**任务状态机 (Task Status)** +```plantuml +@startuml +state "已创建" as CREATED +state "已分配" as ASSIGNED +state "进行中" as IN_PROGRESS +state "已提交" as SUBMITTED +state "已批准" as APPROVED +state "已拒绝" as REJECTED + +[*] --> CREATED : 创建任务 +CREATED --> ASSIGNED : 分配给网格员 +ASSIGNED --> IN_PROGRESS : 网格员接受任务 +IN_PROGRESS --> SUBMITTED : 网格员完成并提交 +SUBMITTED --> APPROVED : 主管审核通过 +SUBMITTED --> REJECTED : 主管审核拒绝 +REJECTED --> ASSIGNED : 重新分配 +APPROVED --> [*] +@enduml +``` + + +### 2.4 算法与策略设计 + +#### 2.4.1 A* 寻路算法 + +- **目的**: 为网格员规划从当前位置到任务目标点的最优(最短或最快)路径。 +- **输入**: + - `startNode`: 起始点坐标。 + - `endNode`: 目标点坐标。 + - `grid`: 包含障碍物信息的地图网格数据。 +- **核心逻辑**: + 1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。 + 2. 从 `openList` 中选取F值(G值+H值)最小的节点作为当前节点。 + - G值: 从起点到当前节点的实际代价。 + - H值: 从当前节点到终点的预估代价(启发函数,通常使用曼哈顿距离或欧氏距离)。 + 3. 遍历当前节点的相邻节点,如果邻居不在`closedList`中且不是障碍物,则计算其G值和H值,并将其加入`openList`。 + 4. 重复此过程,直到当前节点为目标节点,或`openList`为空。 +- **输出**: 从起点到终点的一系列坐标点,即规划好的路径。 +- **应用**: 在`PathfindingController`中通过`/api/pathfinding/find`接口暴露。 + +#### 2.4.2 任务分配策略 + +- **目的**: 在多个可用网格员中,为新任务选择最合适的执行者。 +- **核心逻辑**: 这是一个复合策略,综合考虑以下因素,并计算加权得分: + 1. **地理位置优先**: 优先选择任务所在网格或邻近网格的网格员。 + 2. **负载均衡**: 优先选择当前进行中任务数量最少的网格员。 + 3. **技能匹配**: (未来扩展)可根据任务需要的技能(`skills`字段)与网格员的技能进行匹配。 + 4. **任务优先级**: 对于高优先级任务,可以适当放宽地理位置限制,确保有可用人员处理。 +- **应用**: 在`TaskAssignmentService`中实现,当主管触发自动分配或系统基于反馈自动创建任务时调用。 + +### 2.5 数据持久化设计 + +系统不使用传统数据库,所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署,但也对数据一致性和并发控制提出了更高的要求。 + +#### 2.5.1 `users.json` - 用户账户数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | +| `name` | `String` | 非空 | 用户姓名 | +| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | +| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | +| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | +| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | +| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | +| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | +| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | +| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | +| `region` | `String` | | 所属区域或地区 | +| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | +| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | +| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | +| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | +| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | +| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | +| `lockout_end_time` | `String` | | 账户锁定截止时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +#### 2.5.2 `feedback.json` - 环境问题反馈数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | +| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | +| `title` | `String` | 非空 | 反馈标题 | +| `description` | `String` | | 问题详细描述 | +| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | +| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | +| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | +| `text_address` | `String` | | 文字描述的地址 | +| `grid_x` | `Number` | | 事发地网格X坐标 | +| `grid_y` | `Number` | | 事发地网格Y坐标 | +| `latitude` | `Number` | | 事发地纬度 | +| `longitude` | `Number` | | 事发地经度 | +| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +#### 2.5.3 `tasks.json` - 任务数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | +| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | +| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | +| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | +| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) | +| `title` | `String` | | 任务标题 | +| `description` | `String` | | 任务详细描述 | +| `pollution_type` | `String` | (ENUM) | 污染类型 | +| `severity_level` | `String` | (ENUM) | 严重程度 | +| `text_address` | `String` | | 任务地点文字描述 | +| `grid_x` | `Number` | | 任务地点网格X坐标 | +| `grid_y` | `Number` | | 任务地点网格Y坐标 | +| `latitude` | `Number` | | 任务地点纬度 | +| `longitude` | `Number` | | 任务地点经度 | +| `assigned_at` | `String` | | 任务分配时间 | +| `completed_at` | `String` | | 任务完成时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | +| `deadline` | `String` | | 任务截止日期 | + +#### 2.5.4 `grids.json` - 业务网格数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------- | --------------- | ------------------- | ---------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | +| `gridx` | `Number` | | 网格X坐标 | +| `gridy` | `Number` | | 网格Y坐标 | +| `city_name` | `String` | | 所属城市 | +| `district_name` | `String` | | 所属区县 | +| `description` | `String` | | 网格描述信息 | +| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | + +#### 2.5.5 `assignments.json` - 任务分配记录数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | -------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | +| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | +| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | +| `status` | `String` | 非空, (ENUM) | 分配状态 | +| `remarks` | `String` | | 分配备注 | +| `assignment_time`| `String` | 非空 | 分配时间 | +| `deadline` | `String` | | 任务截止日期 | +``` --- - + ### Report/系统设计_v6.md - -# 系统设计文档 V6 - -本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 - -## 1. 总体设计 - -总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 - -### 1.1 系统架构设计 - -#### 1.1.1 架构选型与原则 - -本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: -- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 -- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 -- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 - -在架构设计中,我们遵循了以下核心原则: -- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。这是通过清晰的模块划分和基于事件的通信(如AI审核)实现的。 -- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。模块化的设计使得添加新功能时,对现有系统的影响降到最低。 -- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。采用JWT保障API安全,并通过角色权限(RBAC)控制对不同资源的访问。 -- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 - -#### 1.1.2 后端架构 - -后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: - -- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 -- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 -- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 - -此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 - -**后端分层架构图** -```plantuml -@startuml -package "Backend Architecture" { - [Controller Layer] <> - [Service Layer] <> - [Repository Layer] <> - - [Controller Layer] --> [Service Layer] - [Service Layer] --> [Repository Layer] -} -@enduml -``` - -#### 1.1.3 前端架构 - -前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 -- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 -- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 -- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 - -### 1.2 功能模块划分 - -系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 - -| 核心模块 | 主要职责 | 关键功能点 | -| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | -| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | -| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | -| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | -| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | -| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | -| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 | - -**功能模块图** -```plantuml -@startuml -left to right direction -actor User - -rectangle "环境监督系统 (EMS)" { - User -- (认证与授权模块) - (认证与授权模块) ..> (用户与人员管理模块) : 依赖 - - User -- (反馈管理模块) - (反馈管理模块) ..> (任务管理模块) : 创建任务 - (任务管理模块) ..> (网格与地图模块) : 路径规划 - (任务管理模块) ..> (用户与人员管理模块) : 分配任务 - - (决策支持模块) .up.> (反馈管理模块) : 数据分析 - (决策支持模块) .up.> (任务管理模块) : 数据分析 - - User -- (个人中心模块) -} -@enduml -``` - -## 2. 详细设计 - -### 2.1 功能模块详细设计 - -本章节将对系统的核心功能模块进行详细的拆解和说明,阐述每个模块的职责、主要功能和关键参与者。 - -#### 2.1.1 用户与认证模块 (User & Authentication) - -* **核心职责**: 负责管理所有用户账户、角色、权限,并提供安全可靠的身份认证和授权服务。是整个系统安全访问的基石。 -* **关键参与者**: 系统管理员 (Admin), 主管 (Supervisor), 网格员 (Grid Worker), 注册用户。 -* **主要功能点**: - * **用户注册**: 提供新用户账户的创建接口。 - * **身份认证**: - * 通过用户名(邮箱)和密码进行登录验证。 - * 成功登录后,生成符合JWT (JSON Web Token) 标准的访问令牌 (`access_token`) 和刷新令牌 (`refresh_token`)。 - * **权限控制**: - * 基于角色的访问控制 (RBAC)。不同角色(如 `ADMIN`, `SUPERVISOR`, `GRID_WORKER`)拥有不同的API访问权限。 - * 通过在API请求的Header中携带JWT令牌来验证用户身份和权限。 - * **账户管理 (管理员)**: - * 创建、查看、更新、删除系统内的所有用户账户。 - * 分配和修改用户角色。 - * 管理用户状态(激活、禁用、暂停)。 - * **密码重置**: 提供忘记密码后的安全重置流程(例如,通过邮件发送重置链接)。 -* **相关服务**: `AuthService`, `UserAccountService`, `SupervisorService`。 - -#### 2.1.2 反馈管理模块 (Feedback Management) - -* **核心职责**: 统一处理所有来源(公众、系统用户)的环境问题反馈,确保每一条反馈都得到有效记录、审查和跟进。 -* **关键参与者**: 公众用户 (匿名/注册), 主管, 系统。 -* **主要功能点**: - * **反馈提交**: 允许用户通过API提交带有详细描述、位置信息、严重等级和附件(图片/视频)的反馈。 - * **AI自动审查**: - * 新提交的反馈会触发一个异步事件。 - * AI服务对反馈内容进行初步分析,评估其有效性、严重性和污染类型。 - * 根据AI审查结果,反馈状态可能被更新为 `AI_REJECTED` 或触发任务创建流程。 - * **主管人工处理**: - * 主管可以查看所有待处理的反馈。 - * 对于AI无法处理或需要人工判断的反馈,主管可以进行审核、批准或拒绝。 - * 批准后的反馈将转化为一个明确的、可执行的任务。 - * **状态跟踪**: 反馈的整个生命周期(`PENDING_REVIEW` -> `PROCESSED` -> `CLOSED`)都被完整记录和追踪。 -* **相关服务**: `FeedbackService`, `FeedbackAiReviewService`。 - -#### 2.1.3 任务管理模块 (Task Management) - -* **核心职责**: 负责将经过审核的反馈转化为具体的工作任务,并对任务的全生命周期(创建、分配、执行、审核、归档)进行管理。 -* **关键参与者**: 主管, 网格员。 -* **主要功能点**: - * **任务创建**: - * 可由反馈自动转化而来。 - * 主管也可以根据需要手动创建新任务。 - * **任务分配**: - * **手动分配**: 主管可以直接将任务指派给特定的网格员。 - * **智能分配 (设计中)**: 系统在任务创建后,自动调用分配算法,综合考虑网格员的位置、负载等因素,推荐或直接分配最合适的人选。 - * **任务执行 (网格员)**: - * 网格员可以查看分配给自己的任务列表。 - * 接受、拒绝或开始执行任务,并更新任务状态。 - * 完成任务后,提交包含处理说明和附件的工作报告。 - * **任务审核 (主管)**: - * 主管审查网格员提交的任务结果。 - * 可以选择"批准"(任务完成)或"驳回"(任务需要返工)。 - * **历史追溯**: 完整的任务状态变更历史、操作记录和处理意见都会被存档,方便审计和复盘。 -* **相关服务**: `TaskManagementService`, `GridWorkerTaskService`, `TaskAssignmentService`。 - -#### 2.1.4 网格与地图模块 (Grid & Map) - -* **核心职责**: 负责定义和管理地理网格系统,并提供基于地理位置的辅助功能,如路径规划。 -* **关键参与者**: 系统管理员, 网格员。 -* **主要功能点**: - * **网格定义**: 管理员可以定义城市地图的网格系统,包括每个网格的坐标、属性以及是否为障碍物。 - * **人员-网格关联**: 管理员可将特定的网格员分配到指定的责任网格中。 - * **路径规划**: - * 集成 A* 寻路算法。 - * 为网格员提供从当前位置到任务地点的最优路径规划,并能在地图上进行可视化展示。 -* **相关服务**: `GridService`, `PathfindingService`。 - -#### 2.1.5 决策支持模块 (Decision Support) - -* **核心职责**: 汇集和分析各类环境数据,为管理人员提供数据洞察和决策依据。 -* **关键参与者**: 主管, 系统管理员。 -* **主要功能点**: - * **数据看板 (Dashboard)**: - * 实时展示关键指标(KPI),如待处理反馈数、进行中任务数、平均处理时长等。 - * 以图表形式展示污染类型、区域分布、严重程度的统计分析。 - * **历史数据查询**: 提供对历史反馈和任务数据的多维度查询和筛选功能。 - * **报告生成**: (未来扩展) 定期自动生成环境状况分析报告。 -* **相关服务**: `AqiService`, (未来可能引入的`AnalyticsService`)。 - -#### 2.1.6 个人中心模块 (Personal Center) - -* **核心职责**: 为登录用户提供管理个人账户信息和偏好的专属空间。 -* **关键参与者**: 所有登录用户 (主管, 网格员)。 -* **主要功能点**: - * **个人资料查看与编辑**: 用户可以查看和修改自己的基本信息,如联系电话、头像等。 - * **密码修改**: 提供安全的旧密码验证和新密码设置功能。 - * **我的任务/反馈**: 快速访问与自己相关的任务或反馈列表。 -* **相关服务**: `UserProfileService`。 - -#### 2.1.7 日志与审计模块 (Logging & Auditing) - -* **核心职责**: 记录系统中发生的所有重要操作和事件,确保系统的可追溯性和安全性。 -* **关键参与者**: 系统管理员, 系统。 -* **主要功能点**: - * **操作日志**: 自动记录关键的用户操作,如谁在什么时间创建了任务、修改了反馈状态等。 - * **系统日志**: 记录应用运行时的错误、警告和重要信息,用于问题排查和性能监控。 - * **日志查询**: 提供接口供管理员查询和审计操作日志。 -* **相关服务**: `OperationLogService`。 - -### 2.2 类和对象的设计 - -本节定义了系统核心业务对象的静态结构,即类图。 - -#### 2.2.1 `UserAccount` 类图 -```plantuml -@startuml -class UserAccount { - - Long id - - String name - - String phone - - String email - - String password - - Gender gender - - Role role - - UserStatus status - - Integer gridX - - Integer gridY - + isValid() -} - -enum Gender { - MALE - FEMALE - OTHER -} - -enum Role { - ADMIN - SUPERVISOR - GRID_WORKER -} - -enum UserStatus { - ACTIVE - INACTIVE -} - -UserAccount *-- Gender -UserAccount *-- Role -UserAccount *-- UserStatus -@enduml -``` - -#### 2.2.2 `Feedback` 类图 -```plantuml -@startuml -class Feedback { - - Long id - - String eventId - - String title - - PollutionType pollutionType - - SeverityLevel severityLevel - - FeedbackStatus status - - Long submitterId - + process() - + approve() -} - -enum PollutionType { - AIR - WATER - SOIL - NOISE -} - -enum SeverityLevel { - LOW - MEDIUM - HIGH - CRITICAL -} - -enum FeedbackStatus { - PENDING_REVIEW - AI_REJECTED - PROCESSED -} - -Feedback *-- PollutionType -Feedback *-- SeverityLevel -Feedback *-- FeedbackStatus -@enduml -``` - -#### 2.2.3 `Task` 类图 -```plantuml -@startuml -class Task { - - Long id - - Long feedbackId - - Long assigneeId - - Long createdBy - - TaskStatus status - - String title - + assignTo(workerId) - + complete() - + approve() -} - -enum TaskStatus { - CREATED - ASSIGNED - IN_PROGRESS - SUBMITTED - APPROVED - REJECTED -} - -Task *-- TaskStatus -@enduml -``` - -#### 2.2.4 `Grid` 和 `Assignment` 类图 -```plantuml -@startuml -class Grid { - - Long id - - Integer gridX - - Integer gridY - - String cityName - - boolean isObstacle -} - -class Assignment { - - Long id - - Long taskId - - Long assignerId - - AssignmentStatus status - - String remarks - - LocalDateTime assignmentTime -} - -enum AssignmentStatus { - PENDING - ACCEPTED - REJECTED -} - -Assignment *-- AssignmentStatus -@enduml -``` - -### 2.3 动态模型设计 - -本节描述了系统在运行时,对象之间的交互行为。 - -#### 2.3.1 核心时序图 - -**用户登录认证时序图** -```mermaid -sequenceDiagram - participant User as 用户 - participant AuthController as 认证控制器 - participant AuthService as 认证服务 - participant JwtUtil as JWT工具 - - User->>AuthController: POST /api/auth/login (email, password) - AuthController->>AuthService: signIn(LoginRequest) - AuthService->>AuthService: 验证凭证 - alt 验证成功 - AuthService->>JwtUtil: generateToken(user) - JwtUtil-->>AuthService: 返回JWT令牌 - AuthService-->>AuthController: 返回JwtAuthenticationResponse - AuthController-->>User: 200 OK (token) - else 验证失败 - AuthService-->>AuthController: 抛出AuthenticationException - AuthController-->>User: 401 Unauthorized - end -``` - -**反馈提交与处理时序图** -```mermaid -sequenceDiagram - participant User as 用户 - participant FeedbackController as 反馈控制器 - participant FeedbackService as 反馈服务 - participant EventPublisher as 事件发布器 - participant FeedbackAiReviewService as AI审核服务 - - User->>FeedbackController: POST /api/feedback/submit - FeedbackController->>FeedbackService: submitFeedback(request, files) - FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent) - FeedbackService-->>FeedbackController: 返回初步响应 - Controller-->>User: 201 Created - - EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核 - FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED) -``` - -**主管分配任务时序图** -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant TaskMgmtController as 任务管理控制器 - participant TaskService as 任务服务 - participant AssignmentService as 分配服务 - participant NotificationService as 通知服务 - - Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId) - TaskMgmtController->>TaskService: assignTask(taskId, workerId) - TaskService->>AssignmentService: createAssignment(task, worker) - AssignmentService-->>TaskService: 返回Assignment - TaskService->>TaskService: 更新任务状态为ASSIGNED - TaskService->>NotificationService: notifyWorker(workerId, taskId) - TaskService-->>TaskMgmtController: 分配成功 - TaskMgmtController-->>Supervisor: 200 OK -``` - -**主管审核反馈并创建任务** -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant FeedbackController as 反馈控制器 - participant FeedbackService as 反馈服务 - participant TaskService as 任务服务 - participant TaskRepository as 任务仓库 - participant Database as JSON持久化存储 - - Supervisor->>FeedbackController: POST /api/feedback/{id}/process - FeedbackController->>FeedbackService: processFeedback(feedbackId, request) - FeedbackService->>FeedbackService: 验证反馈状态和主管权限 - alt 同意反馈并创建任务 - FeedbackService->>TaskService: createTaskFromFeedback(feedback) - TaskService->>TaskRepository: save(task) - TaskRepository->>Database: 保存新任务 - Database-->>TaskRepository: 返回保存的任务 - TaskRepository-->>TaskService: 返回Task实体 - TaskService->>FeedbackService: 更新反馈状态为PENDING_ASSIGNMENT - FeedbackService-->>FeedbackController: 返回处理结果 - else 拒绝反馈 - FeedbackService->>FeedbackService: 更新反馈状态为REJECTED - FeedbackService-->>FeedbackController: 返回处理结果 - end - FeedbackController-->>Supervisor: 200 OK -``` - -**网格员获取和管理任务** -```mermaid -sequenceDiagram - participant GridWorker as 网格员 - participant GridWorkerTaskController as 网格员任务控制器 - participant GridWorkerTaskService as 网格员任务服务 - participant TaskRepository as 任务仓库 - - alt 获取任务列表 - GridWorker->>GridWorkerTaskController: GET /api/worker/tasks - GridWorkerTaskController->>GridWorkerTaskService: getAssignedTasks(workerId, status) - GridWorkerTaskService->>TaskRepository: findByAssigneeIdAndStatus(workerId, status) - TaskRepository-->>GridWorkerTaskService: 返回Page - GridWorkerTaskService-->>GridWorkerTaskController: 返回Page - GridWorkerTaskController-->>GridWorker: 返回任务列表 - end - - alt 接受任务 - GridWorker->>GridWorkerTaskController: POST /api/worker/tasks/{taskId}/accept - GridWorkerTaskController->>GridWorkerTaskService: acceptTask(taskId, workerId) - GridWorkerTaskService->>TaskRepository: findById(taskId) - TaskRepository-->>GridWorkerTaskService: 返回Task - GridWorkerTaskService->>GridWorkerTaskService: 更新任务状态为ACCEPTED - GridWorkerTaskService->>TaskRepository: save(task) - TaskRepository-->>GridWorkerTaskService: 返回更新后的Task - GridWorkerTaskService-->>GridWorkerTaskController: 返回TaskSummaryDTO - GridWorkerTaskController-->>GridWorker: 返回更新后的任务 - end -``` - -#### 2.3.2 核心状态图 - -**反馈状态机 (Feedback Status)** -```plantuml -@startuml -state "待审核" as PENDING_REVIEW -state "AI审核通过" as AI_APPROVED -state "AI拒绝" as AI_REJECTED -state "已处理" as PROCESSED -state "已关闭" as CLOSED - -[*] --> PENDING_REVIEW : 提交反馈 -PENDING_REVIEW --> AI_APPROVED : AI审核通过 -PENDING_REVIEW --> AI_REJECTED : AI审核拒绝 -AI_APPROVED --> PROCESSED : 主管创建任务 -PROCESSED --> CLOSED : 任务完成 -AI_REJECTED --> CLOSED : 归档 -@enduml -``` - -**任务状态机 (Task Status)** -```plantuml -@startuml -state "已创建" as CREATED -state "已分配" as ASSIGNED -state "进行中" as IN_PROGRESS -state "已提交" as SUBMITTED -state "已批准" as APPROVED -state "已拒绝" as REJECTED - -[*] --> CREATED : 创建任务 -CREATED --> ASSIGNED : 分配给网格员 -ASSIGNED --> IN_PROGRESS : 网格员接受任务 -IN_PROGRESS --> SUBMITTED : 网格员完成并提交 -SUBMITTED --> APPROVED : 主管审核通过 -SUBMITTED --> REJECTED : 主管审核拒绝 -REJECTED --> ASSIGNED : 重新分配 -APPROVED --> [*] -@enduml -``` - - -### 2.4 算法与策略设计 - -本章节详细阐述了系统运行过程中依赖的核心算法和业务决策策略。 - -#### 2.4.1 A* 路径规划算法 - -* **目标**: 为网格员在复杂的城市网格环境中,规划出从当前位置到任务地点的最优(最短)路径,同时能够有效规避障碍物。 -* **输入**: - * `startNode`: 起始点网格坐标。 - * `endNode`: 目标点网格坐标。 - * `gridMap`: 一个二维数组或Map,包含了所有网格节点的信息,特别是标识了哪些节点是不可通行的障碍物。 -* **核心逻辑**: - 1. 维护一个"开放列表"(Open List)用于存放待考察的节点,以及一个"关闭列表"(Closed List)用于存放已考察过的节点。 - 2. 对每个节点,计算两个核心成本: - * `gCost`: 从起点到当前节点的实际移动成本。 - * `hCost` (启发式成本): 从当前节点到终点的预估移动成本(通常使用曼哈顿距离或欧几里得距离)。 - 3. 总成本 `fCost = gCost + hCost`。 - 4. 算法从开放列表中选取 `fCost` 最低的节点进行探索,直到找到终点。 -* **输出**: 一个包含了从起点到终点所有节点坐标的列表,即规划出的路径。 -* **伪代码**: - ``` - function AStar(start, end, grid): - openSet = {start} - closedSet = {} - cameFrom = new Map() - - gScore = new Map().setDefault(infinity) - gScore[start] = 0 - - fScore = new Map().setDefault(infinity) - fScore[start] = heuristic_cost(start, end) - - while openSet is not empty: - current = node in openSet with the lowest fScore - if current == end: - return reconstruct_path(cameFrom, current) - - remove current from openSet - add current to closedSet - - for each neighbor of current: - if neighbor in closedSet: - continue - - tentative_gScore = gScore[current] + dist_between(current, neighbor) - - if neighbor not in openSet: - add neighbor to openSet - else if tentative_gScore >= gScore[neighbor]: - continue - - cameFrom[neighbor] = current - gScore[neighbor] = tentative_gScore - fScore[neighbor] = gScore[neighbor] + heuristic_cost(neighbor, end) - - return failure // No path found - ``` - -#### 2.4.2 智能任务分配算法 (设计与提案) - -> **现状说明**: 根据对 `TaskManagementServiceImpl.java` 的代码审查,当前的 `intelligentAssignTask` 方法为一个虚拟实现,系统暂无自动化的任务分配逻辑,依赖于主管手动分配。本节内容是为该功能的未来实现所做的**设计提案**。 - -* **目标**: 当新任务创建后,系统能够自动、快速、合理地将其分配给最合适的网格员,以提升响应效率和资源利用率。 -* **核心思想**: 算法通过计算每位可用网格员的"适任度分数"(Suitability Score)来决定任务归属。分数最高的网格员将被分配该任务。 -* **评分因子**: - 1. **地理邻近度 (Proximity)**: 网格员当前位置与任务发生地的距离。这是最重要的决定因素。距离越近,分数越高。 - 2. **当前工作负载 (Workload)**: 网格员手上正在处理的 (`IN_PROGRESS`状态) 任务数量。负载越低,分数越高。 - 3. **技能匹配度 (Skill Match)**: (未来扩展) 可为网格员和任务定义所需技能标签(如"水质检测","大气采样")。技能匹配度越高,得分越高。 -* **评分模型 (Scoring Model)**: - 适任度分数 `S` 由各因子加权计算得出: - \[ S = (W_p \times \text{Score}_p) + (W_w \times \text{Score}_w) \] - * **权重 (Weights)**: - * `W_p`: 邻近度权重 (推荐值: `0.7`) - * `W_w`: 工作负载权重 (推荐值: `0.3`) - * 权重总和应为 1。这些值应在配置文件中可配置,便于调整策略。 - * **因子分数计算**: - * 邻近度分数 \(\text{Score}_p = \frac{1}{D + 1}\) - * `D` 是网格员与任务之间的曼哈顿距离 `(|x1-x2| + |y1-y2|)`。`+1` 是为了防止除以零。 - * 工作负载分数 \(\text{Score}_w = \frac{1}{N + 1}\) - * `N` 是网格员当前正在处理的任务数。 -* **算法流程伪代码**: - ``` - function intelligentAssignTask(task): - // 1. 获取所有符合条件的网格员 - availableWorkers = userRepo.findAllByRole(GRID_WORKER) and status(ACTIVE) - - if availableWorkers is empty: - log("No available workers to assign task " + task.id) - return - - bestWorker = null - highestScore = -1 - - // 2. 遍历所有可用网格员,计算得分 - for each worker in availableWorkers: - // 2a. 计算距离 - distance = manhattan_distance(worker.gridPos, task.gridPos) - proximityScore = 1 / (distance + 1) - - // 2b. 计算工作负载 - currentTasks = taskRepo.countByAssignee(worker) and status(IN_PROGRESS) - workloadScore = 1 / (currentTasks + 1) - - // 2c. 计算最终加权总分 - finalScore = (W_p * proximityScore) + (W_w * workloadScore) - - // 2d. 记录并更新最高分和最佳人选 - if finalScore > highestScore: - highestScore = finalScore - bestWorker = worker - - // 3. 分配任务给最佳人选 - if bestWorker is not null: - task.setAssignee(bestWorker) - task.setStatus(ASSIGNED) - taskRepo.save(task) - notificationService.notify(bestWorker, "You have a new task.") - log("Task " + task.id + " assigned to " + bestWorker.name) - else: - log("Could not determine a best worker for task " + task.id) - ``` - -### 2.5 数据持久化设计 - -系统不使用传统数据库,所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署,但也对数据一致性和并发控制提出了更高的要求。 - -#### 2.5.1 `users.json` - 用户账户数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | -| `name` | `String` | 非空 | 用户姓名 | -| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | -| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | -| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | -| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | -| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | -| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | -| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | -| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | -| `region` | `String` | | 所属区域或地区 | -| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | -| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | -| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | -| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | -| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | -| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | -| `lockout_end_time` | `String` | | 账户锁定截止时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -#### 2.5.2 `feedback.json` - 环境问题反馈数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | -| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | -| `title` | `String` | 非空 | 反馈标题 | -| `description` | `String` | | 问题详细描述 | -| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | -| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | -| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | -| `text_address` | `String` | | 文字描述的地址 | -| `grid_x` | `Number` | | 事发地网格X坐标 | -| `grid_y` | `Number` | | 事发地网格Y坐标 | -| `latitude` | `Number` | | 事发地纬度 | -| `longitude` | `Number` | | 事发地经度 | -| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | - -#### 2.5.3 `tasks.json` - 任务数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | ---------------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | -| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | -| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | -| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | -| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) | -| `title` | `String` | | 任务标题 | -| `description` | `String` | | 任务详细描述 | -| `pollution_type` | `String` | (ENUM) | 污染类型 | -| `severity_level` | `String` | (ENUM) | 严重程度 | -| `text_address` | `String` | | 任务地点文字描述 | -| `grid_x` | `Number` | | 任务地点网格X坐标 | -| `grid_y` | `Number` | | 任务地点网格Y坐标 | -| `latitude` | `Number` | | 任务地点纬度 | -| `longitude` | `Number` | | 任务地点经度 | -| `assigned_at` | `String` | | 任务分配时间 | -| `completed_at` | `String` | | 任务完成时间 | -| `created_at` | `String` | 非空 | 记录创建时间 | -| `updated_at` | `String` | 非空 | 记录最后更新时间 | -| `deadline` | `String` | | 任务截止日期 | - -#### 2.5.4 `grids.json` - 业务网格数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| --------------- | --------------- | ------------------- | ---------------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | -| `gridx` | `Number` | | 网格X坐标 | -| `gridy` | `Number` | | 网格Y坐标 | -| `city_name` | `String` | | 所属城市 | -| `district_name` | `String` | | 所属区县 | -| `description` | `String` | | 网格描述信息 | -| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | - -#### 2.5.5 `assignments.json` - 任务分配记录数据 - -| 字段名 | JSON数据类型 | 约束/说明 | 描述 | -| ---------------- | --------------- | ------------------------ | -------------------------- | -| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | -| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | -| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | -| `status` | `String` | 非空, (ENUM) | 分配状态 | -| `remarks` | `String` | | 分配备注 | -| `assignment_time`| `String` | 非空 | 分配时间 | -| `deadline` | `String` | | 任务截止日期 | -``` + +# 系统设计文档 V6 + +本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 + +## 1. 总体设计 + +总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。 + +### 1.1 系统架构设计 + +#### 1.1.1 架构选型与原则 + +本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于: +- **并行开发**: 前后端团队可以并行开发、测试和部署,只需遵守统一的API约定,从而显著提升开发效率。 +- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。 +- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。 + +在架构设计中,我们遵循了以下核心原则: +- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。这是通过清晰的模块划分和基于事件的通信(如AI审核)实现的。 +- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。模块化的设计使得添加新功能时,对现有系统的影响降到最低。 +- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。采用JWT保障API安全,并通过角色权限(RBAC)控制对不同资源的访问。 +- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。 + +#### 1.1.2 后端架构 + +后端服务基于 **Spring Boot 3** 和 **Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式: + +- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求,使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数(使用JSR-303注解),并调用业务逻辑层处理请求,但不包含任何业务逻辑。 +- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。 +- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类,模拟了类似JPA的接口,实现了对`users.json`, `tasks.json`等核心数据文件的增删改查(CRUD)操作。选择JSON文件存储简化了项目的部署和配置,特别适合快速迭代和中小型应用场景。 + +此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。 + +**后端分层架构图** +```plantuml +@startuml +package "Backend Architecture" { + [Controller Layer] <> + [Service Layer] <> + [Repository Layer] <> + + [Controller Layer] --> [Service Layer] + [Service Layer] --> [Repository Layer] +} +@enduml +``` + +#### 1.1.3 前端架构 + +前端应用是一个基于 **Vue 3** 的单页面应用(SPA),使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。 +- **UI组件库**: **Element Plus**,提供了一套高质量、符合设计规范的UI组件,加速了界面的开发。 +- **状态管理**: **Pinia**,作为Vue 3官方推荐的状态管理库,它提供了极简的API和强大的类型推断支持,能有效管理复杂的应用状态。 +- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。 + +### 1.2 功能模块划分 + +系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。 + +| 核心模块 | 主要职责 | 关键功能点 | +| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出
- JWT令牌生成与验证
- 密码重置
- 基于角色的访问控制(RBAC) | +| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查
- 用户角色分配与变更
- 用户状态管理(激活/禁用) | +| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈
- 集成AI进行内容预审核
- 人工审核与处理
- 反馈状态跟踪与统计 | +| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务
- 任务分配给网格员
- 任务状态(分配、执行、完成)跟踪
- 任务审核与结果归档 | +| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分
- 网格员与网格的关联
- **A\*寻路算法**服务,优化任务路径 | +| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标(KPI)统计
- AQI、任务完成率等数据的可视化
- 生成反馈热力图 | +| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料
- 查询个人提交历史
- 查看个人操作日志 | + +**功能模块图** +```plantuml +@startuml +left to right direction +actor User + +rectangle "环境监督系统 (EMS)" { + User -- (认证与授权模块) + (认证与授权模块) ..> (用户与人员管理模块) : 依赖 + + User -- (反馈管理模块) + (反馈管理模块) ..> (任务管理模块) : 创建任务 + (任务管理模块) ..> (网格与地图模块) : 路径规划 + (任务管理模块) ..> (用户与人员管理模块) : 分配任务 + + (决策支持模块) .up.> (反馈管理模块) : 数据分析 + (决策支持模块) .up.> (任务管理模块) : 数据分析 + + User -- (个人中心模块) +} +@enduml +``` + +## 2. 详细设计 + +### 2.1 功能模块详细设计 + +本章节将对系统的核心功能模块进行详细的拆解和说明,阐述每个模块的职责、主要功能和关键参与者。 + +#### 2.1.1 用户与认证模块 (User & Authentication) + +* **核心职责**: 负责管理所有用户账户、角色、权限,并提供安全可靠的身份认证和授权服务。是整个系统安全访问的基石。 +* **关键参与者**: 系统管理员 (Admin), 主管 (Supervisor), 网格员 (Grid Worker), 注册用户。 +* **主要功能点**: + * **用户注册**: 提供新用户账户的创建接口。 + * **身份认证**: + * 通过用户名(邮箱)和密码进行登录验证。 + * 成功登录后,生成符合JWT (JSON Web Token) 标准的访问令牌 (`access_token`) 和刷新令牌 (`refresh_token`)。 + * **权限控制**: + * 基于角色的访问控制 (RBAC)。不同角色(如 `ADMIN`, `SUPERVISOR`, `GRID_WORKER`)拥有不同的API访问权限。 + * 通过在API请求的Header中携带JWT令牌来验证用户身份和权限。 + * **账户管理 (管理员)**: + * 创建、查看、更新、删除系统内的所有用户账户。 + * 分配和修改用户角色。 + * 管理用户状态(激活、禁用、暂停)。 + * **密码重置**: 提供忘记密码后的安全重置流程(例如,通过邮件发送重置链接)。 +* **相关服务**: `AuthService`, `UserAccountService`, `SupervisorService`。 + +#### 2.1.2 反馈管理模块 (Feedback Management) + +* **核心职责**: 统一处理所有来源(公众、系统用户)的环境问题反馈,确保每一条反馈都得到有效记录、审查和跟进。 +* **关键参与者**: 公众用户 (匿名/注册), 主管, 系统。 +* **主要功能点**: + * **反馈提交**: 允许用户通过API提交带有详细描述、位置信息、严重等级和附件(图片/视频)的反馈。 + * **AI自动审查**: + * 新提交的反馈会触发一个异步事件。 + * AI服务对反馈内容进行初步分析,评估其有效性、严重性和污染类型。 + * 根据AI审查结果,反馈状态可能被更新为 `AI_REJECTED` 或触发任务创建流程。 + * **主管人工处理**: + * 主管可以查看所有待处理的反馈。 + * 对于AI无法处理或需要人工判断的反馈,主管可以进行审核、批准或拒绝。 + * 批准后的反馈将转化为一个明确的、可执行的任务。 + * **状态跟踪**: 反馈的整个生命周期(`PENDING_REVIEW` -> `PROCESSED` -> `CLOSED`)都被完整记录和追踪。 +* **相关服务**: `FeedbackService`, `FeedbackAiReviewService`。 + +#### 2.1.3 任务管理模块 (Task Management) + +* **核心职责**: 负责将经过审核的反馈转化为具体的工作任务,并对任务的全生命周期(创建、分配、执行、审核、归档)进行管理。 +* **关键参与者**: 主管, 网格员。 +* **主要功能点**: + * **任务创建**: + * 可由反馈自动转化而来。 + * 主管也可以根据需要手动创建新任务。 + * **任务分配**: + * **手动分配**: 主管可以直接将任务指派给特定的网格员。 + * **智能分配 (设计中)**: 系统在任务创建后,自动调用分配算法,综合考虑网格员的位置、负载等因素,推荐或直接分配最合适的人选。 + * **任务执行 (网格员)**: + * 网格员可以查看分配给自己的任务列表。 + * 接受、拒绝或开始执行任务,并更新任务状态。 + * 完成任务后,提交包含处理说明和附件的工作报告。 + * **任务审核 (主管)**: + * 主管审查网格员提交的任务结果。 + * 可以选择"批准"(任务完成)或"驳回"(任务需要返工)。 + * **历史追溯**: 完整的任务状态变更历史、操作记录和处理意见都会被存档,方便审计和复盘。 +* **相关服务**: `TaskManagementService`, `GridWorkerTaskService`, `TaskAssignmentService`。 + +#### 2.1.4 网格与地图模块 (Grid & Map) + +* **核心职责**: 负责定义和管理地理网格系统,并提供基于地理位置的辅助功能,如路径规划。 +* **关键参与者**: 系统管理员, 网格员。 +* **主要功能点**: + * **网格定义**: 管理员可以定义城市地图的网格系统,包括每个网格的坐标、属性以及是否为障碍物。 + * **人员-网格关联**: 管理员可将特定的网格员分配到指定的责任网格中。 + * **路径规划**: + * 集成 A* 寻路算法。 + * 为网格员提供从当前位置到任务地点的最优路径规划,并能在地图上进行可视化展示。 +* **相关服务**: `GridService`, `PathfindingService`。 + +#### 2.1.5 决策支持模块 (Decision Support) + +* **核心职责**: 汇集和分析各类环境数据,为管理人员提供数据洞察和决策依据。 +* **关键参与者**: 主管, 系统管理员。 +* **主要功能点**: + * **数据看板 (Dashboard)**: + * 实时展示关键指标(KPI),如待处理反馈数、进行中任务数、平均处理时长等。 + * 以图表形式展示污染类型、区域分布、严重程度的统计分析。 + * **历史数据查询**: 提供对历史反馈和任务数据的多维度查询和筛选功能。 + * **报告生成**: (未来扩展) 定期自动生成环境状况分析报告。 +* **相关服务**: `AqiService`, (未来可能引入的`AnalyticsService`)。 + +#### 2.1.6 个人中心模块 (Personal Center) + +* **核心职责**: 为登录用户提供管理个人账户信息和偏好的专属空间。 +* **关键参与者**: 所有登录用户 (主管, 网格员)。 +* **主要功能点**: + * **个人资料查看与编辑**: 用户可以查看和修改自己的基本信息,如联系电话、头像等。 + * **密码修改**: 提供安全的旧密码验证和新密码设置功能。 + * **我的任务/反馈**: 快速访问与自己相关的任务或反馈列表。 +* **相关服务**: `UserProfileService`。 + +#### 2.1.7 日志与审计模块 (Logging & Auditing) + +* **核心职责**: 记录系统中发生的所有重要操作和事件,确保系统的可追溯性和安全性。 +* **关键参与者**: 系统管理员, 系统。 +* **主要功能点**: + * **操作日志**: 自动记录关键的用户操作,如谁在什么时间创建了任务、修改了反馈状态等。 + * **系统日志**: 记录应用运行时的错误、警告和重要信息,用于问题排查和性能监控。 + * **日志查询**: 提供接口供管理员查询和审计操作日志。 +* **相关服务**: `OperationLogService`。 + +### 2.2 类和对象的设计 + +本节定义了系统核心业务对象的静态结构,即类图。 + +#### 2.2.1 `UserAccount` 类图 +```plantuml +@startuml +class UserAccount { + - Long id + - String name + - String phone + - String email + - String password + - Gender gender + - Role role + - UserStatus status + - Integer gridX + - Integer gridY + + isValid() +} + +enum Gender { + MALE + FEMALE + OTHER +} + +enum Role { + ADMIN + SUPERVISOR + GRID_WORKER +} + +enum UserStatus { + ACTIVE + INACTIVE +} + +UserAccount *-- Gender +UserAccount *-- Role +UserAccount *-- UserStatus +@enduml +``` + +#### 2.2.2 `Feedback` 类图 +```plantuml +@startuml +class Feedback { + - Long id + - String eventId + - String title + - PollutionType pollutionType + - SeverityLevel severityLevel + - FeedbackStatus status + - Long submitterId + + process() + + approve() +} + +enum PollutionType { + AIR + WATER + SOIL + NOISE +} + +enum SeverityLevel { + LOW + MEDIUM + HIGH + CRITICAL +} + +enum FeedbackStatus { + PENDING_REVIEW + AI_REJECTED + PROCESSED +} + +Feedback *-- PollutionType +Feedback *-- SeverityLevel +Feedback *-- FeedbackStatus +@enduml +``` + +#### 2.2.3 `Task` 类图 +```plantuml +@startuml +class Task { + - Long id + - Long feedbackId + - Long assigneeId + - Long createdBy + - TaskStatus status + - String title + + assignTo(workerId) + + complete() + + approve() +} + +enum TaskStatus { + CREATED + ASSIGNED + IN_PROGRESS + SUBMITTED + APPROVED + REJECTED +} + +Task *-- TaskStatus +@enduml +``` + +#### 2.2.4 `Grid` 和 `Assignment` 类图 +```plantuml +@startuml +class Grid { + - Long id + - Integer gridX + - Integer gridY + - String cityName + - boolean isObstacle +} + +class Assignment { + - Long id + - Long taskId + - Long assignerId + - AssignmentStatus status + - String remarks + - LocalDateTime assignmentTime +} + +enum AssignmentStatus { + PENDING + ACCEPTED + REJECTED +} + +Assignment *-- AssignmentStatus +@enduml +``` + +### 2.3 动态模型设计 + +本节描述了系统在运行时,对象之间的交互行为。 + +#### 2.3.1 核心时序图 + +**用户登录认证时序图** +```mermaid +sequenceDiagram + participant User as 用户 + participant AuthController as 认证控制器 + participant AuthService as 认证服务 + participant JwtUtil as JWT工具 + + User->>AuthController: POST /api/auth/login (email, password) + AuthController->>AuthService: signIn(LoginRequest) + AuthService->>AuthService: 验证凭证 + alt 验证成功 + AuthService->>JwtUtil: generateToken(user) + JwtUtil-->>AuthService: 返回JWT令牌 + AuthService-->>AuthController: 返回JwtAuthenticationResponse + AuthController-->>User: 200 OK (token) + else 验证失败 + AuthService-->>AuthController: 抛出AuthenticationException + AuthController-->>User: 401 Unauthorized + end +``` + +**反馈提交与处理时序图** +```mermaid +sequenceDiagram + participant User as 用户 + participant FeedbackController as 反馈控制器 + participant FeedbackService as 反馈服务 + participant EventPublisher as 事件发布器 + participant FeedbackAiReviewService as AI审核服务 + + User->>FeedbackController: POST /api/feedback/submit + FeedbackController->>FeedbackService: submitFeedback(request, files) + FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent) + FeedbackService-->>FeedbackController: 返回初步响应 + Controller-->>User: 201 Created + + EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核 + FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED) +``` + +**主管分配任务时序图** +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant TaskMgmtController as 任务管理控制器 + participant TaskService as 任务服务 + participant AssignmentService as 分配服务 + participant NotificationService as 通知服务 + + Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId) + TaskMgmtController->>TaskService: assignTask(taskId, workerId) + TaskService->>AssignmentService: createAssignment(task, worker) + AssignmentService-->>TaskService: 返回Assignment + TaskService->>TaskService: 更新任务状态为ASSIGNED + TaskService->>NotificationService: notifyWorker(workerId, taskId) + TaskService-->>TaskMgmtController: 分配成功 + TaskMgmtController-->>Supervisor: 200 OK +``` + +**主管审核反馈并创建任务** +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant FeedbackController as 反馈控制器 + participant FeedbackService as 反馈服务 + participant TaskService as 任务服务 + participant TaskRepository as 任务仓库 + participant Database as JSON持久化存储 + + Supervisor->>FeedbackController: POST /api/feedback/{id}/process + FeedbackController->>FeedbackService: processFeedback(feedbackId, request) + FeedbackService->>FeedbackService: 验证反馈状态和主管权限 + alt 同意反馈并创建任务 + FeedbackService->>TaskService: createTaskFromFeedback(feedback) + TaskService->>TaskRepository: save(task) + TaskRepository->>Database: 保存新任务 + Database-->>TaskRepository: 返回保存的任务 + TaskRepository-->>TaskService: 返回Task实体 + TaskService->>FeedbackService: 更新反馈状态为PENDING_ASSIGNMENT + FeedbackService-->>FeedbackController: 返回处理结果 + else 拒绝反馈 + FeedbackService->>FeedbackService: 更新反馈状态为REJECTED + FeedbackService-->>FeedbackController: 返回处理结果 + end + FeedbackController-->>Supervisor: 200 OK +``` + +**网格员获取和管理任务** +```mermaid +sequenceDiagram + participant GridWorker as 网格员 + participant GridWorkerTaskController as 网格员任务控制器 + participant GridWorkerTaskService as 网格员任务服务 + participant TaskRepository as 任务仓库 + + alt 获取任务列表 + GridWorker->>GridWorkerTaskController: GET /api/worker/tasks + GridWorkerTaskController->>GridWorkerTaskService: getAssignedTasks(workerId, status) + GridWorkerTaskService->>TaskRepository: findByAssigneeIdAndStatus(workerId, status) + TaskRepository-->>GridWorkerTaskService: 返回Page + GridWorkerTaskService-->>GridWorkerTaskController: 返回Page + GridWorkerTaskController-->>GridWorker: 返回任务列表 + end + + alt 接受任务 + GridWorker->>GridWorkerTaskController: POST /api/worker/tasks/{taskId}/accept + GridWorkerTaskController->>GridWorkerTaskService: acceptTask(taskId, workerId) + GridWorkerTaskService->>TaskRepository: findById(taskId) + TaskRepository-->>GridWorkerTaskService: 返回Task + GridWorkerTaskService->>GridWorkerTaskService: 更新任务状态为ACCEPTED + GridWorkerTaskService->>TaskRepository: save(task) + TaskRepository-->>GridWorkerTaskService: 返回更新后的Task + GridWorkerTaskService-->>GridWorkerTaskController: 返回TaskSummaryDTO + GridWorkerTaskController-->>GridWorker: 返回更新后的任务 + end +``` + +#### 2.3.2 核心状态图 + +**反馈状态机 (Feedback Status)** +```plantuml +@startuml +state "待审核" as PENDING_REVIEW +state "AI审核通过" as AI_APPROVED +state "AI拒绝" as AI_REJECTED +state "已处理" as PROCESSED +state "已关闭" as CLOSED + +[*] --> PENDING_REVIEW : 提交反馈 +PENDING_REVIEW --> AI_APPROVED : AI审核通过 +PENDING_REVIEW --> AI_REJECTED : AI审核拒绝 +AI_APPROVED --> PROCESSED : 主管创建任务 +PROCESSED --> CLOSED : 任务完成 +AI_REJECTED --> CLOSED : 归档 +@enduml +``` + +**任务状态机 (Task Status)** +```plantuml +@startuml +state "已创建" as CREATED +state "已分配" as ASSIGNED +state "进行中" as IN_PROGRESS +state "已提交" as SUBMITTED +state "已批准" as APPROVED +state "已拒绝" as REJECTED + +[*] --> CREATED : 创建任务 +CREATED --> ASSIGNED : 分配给网格员 +ASSIGNED --> IN_PROGRESS : 网格员接受任务 +IN_PROGRESS --> SUBMITTED : 网格员完成并提交 +SUBMITTED --> APPROVED : 主管审核通过 +SUBMITTED --> REJECTED : 主管审核拒绝 +REJECTED --> ASSIGNED : 重新分配 +APPROVED --> [*] +@enduml +``` + + +### 2.4 算法与策略设计 + +本章节详细阐述了系统运行过程中依赖的核心算法和业务决策策略。 + +#### 2.4.1 A* 路径规划算法 + +* **目标**: 为网格员在复杂的城市网格环境中,规划出从当前位置到任务地点的最优(最短)路径,同时能够有效规避障碍物。 +* **输入**: + * `startNode`: 起始点网格坐标。 + * `endNode`: 目标点网格坐标。 + * `gridMap`: 一个二维数组或Map,包含了所有网格节点的信息,特别是标识了哪些节点是不可通行的障碍物。 +* **核心逻辑**: + 1. 维护一个"开放列表"(Open List)用于存放待考察的节点,以及一个"关闭列表"(Closed List)用于存放已考察过的节点。 + 2. 对每个节点,计算两个核心成本: + * `gCost`: 从起点到当前节点的实际移动成本。 + * `hCost` (启发式成本): 从当前节点到终点的预估移动成本(通常使用曼哈顿距离或欧几里得距离)。 + 3. 总成本 `fCost = gCost + hCost`。 + 4. 算法从开放列表中选取 `fCost` 最低的节点进行探索,直到找到终点。 +* **输出**: 一个包含了从起点到终点所有节点坐标的列表,即规划出的路径。 +* **伪代码**: + ``` + function AStar(start, end, grid): + openSet = {start} + closedSet = {} + cameFrom = new Map() + + gScore = new Map().setDefault(infinity) + gScore[start] = 0 + + fScore = new Map().setDefault(infinity) + fScore[start] = heuristic_cost(start, end) + + while openSet is not empty: + current = node in openSet with the lowest fScore + if current == end: + return reconstruct_path(cameFrom, current) + + remove current from openSet + add current to closedSet + + for each neighbor of current: + if neighbor in closedSet: + continue + + tentative_gScore = gScore[current] + dist_between(current, neighbor) + + if neighbor not in openSet: + add neighbor to openSet + else if tentative_gScore >= gScore[neighbor]: + continue + + cameFrom[neighbor] = current + gScore[neighbor] = tentative_gScore + fScore[neighbor] = gScore[neighbor] + heuristic_cost(neighbor, end) + + return failure // No path found + ``` + +#### 2.4.2 智能任务分配算法 (设计与提案) + +> **现状说明**: 根据对 `TaskManagementServiceImpl.java` 的代码审查,当前的 `intelligentAssignTask` 方法为一个虚拟实现,系统暂无自动化的任务分配逻辑,依赖于主管手动分配。本节内容是为该功能的未来实现所做的**设计提案**。 + +* **目标**: 当新任务创建后,系统能够自动、快速、合理地将其分配给最合适的网格员,以提升响应效率和资源利用率。 +* **核心思想**: 算法通过计算每位可用网格员的"适任度分数"(Suitability Score)来决定任务归属。分数最高的网格员将被分配该任务。 +* **评分因子**: + 1. **地理邻近度 (Proximity)**: 网格员当前位置与任务发生地的距离。这是最重要的决定因素。距离越近,分数越高。 + 2. **当前工作负载 (Workload)**: 网格员手上正在处理的 (`IN_PROGRESS`状态) 任务数量。负载越低,分数越高。 + 3. **技能匹配度 (Skill Match)**: (未来扩展) 可为网格员和任务定义所需技能标签(如"水质检测","大气采样")。技能匹配度越高,得分越高。 +* **评分模型 (Scoring Model)**: + 适任度分数 `S` 由各因子加权计算得出: + \[ S = (W_p \times \text{Score}_p) + (W_w \times \text{Score}_w) \] + * **权重 (Weights)**: + * `W_p`: 邻近度权重 (推荐值: `0.7`) + * `W_w`: 工作负载权重 (推荐值: `0.3`) + * 权重总和应为 1。这些值应在配置文件中可配置,便于调整策略。 + * **因子分数计算**: + * 邻近度分数 \(\text{Score}_p = \frac{1}{D + 1}\) + * `D` 是网格员与任务之间的曼哈顿距离 `(|x1-x2| + |y1-y2|)`。`+1` 是为了防止除以零。 + * 工作负载分数 \(\text{Score}_w = \frac{1}{N + 1}\) + * `N` 是网格员当前正在处理的任务数。 +* **算法流程伪代码**: + ``` + function intelligentAssignTask(task): + // 1. 获取所有符合条件的网格员 + availableWorkers = userRepo.findAllByRole(GRID_WORKER) and status(ACTIVE) + + if availableWorkers is empty: + log("No available workers to assign task " + task.id) + return + + bestWorker = null + highestScore = -1 + + // 2. 遍历所有可用网格员,计算得分 + for each worker in availableWorkers: + // 2a. 计算距离 + distance = manhattan_distance(worker.gridPos, task.gridPos) + proximityScore = 1 / (distance + 1) + + // 2b. 计算工作负载 + currentTasks = taskRepo.countByAssignee(worker) and status(IN_PROGRESS) + workloadScore = 1 / (currentTasks + 1) + + // 2c. 计算最终加权总分 + finalScore = (W_p * proximityScore) + (W_w * workloadScore) + + // 2d. 记录并更新最高分和最佳人选 + if finalScore > highestScore: + highestScore = finalScore + bestWorker = worker + + // 3. 分配任务给最佳人选 + if bestWorker is not null: + task.setAssignee(bestWorker) + task.setStatus(ASSIGNED) + taskRepo.save(task) + notificationService.notify(bestWorker, "You have a new task.") + log("Task " + task.id + " assigned to " + bestWorker.name) + else: + log("Could not determine a best worker for task " + task.id) + ``` + +### 2.5 数据持久化设计 + +系统不使用传统数据库,所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署,但也对数据一致性和并发控制提出了更高的要求。 + +#### 2.5.1 `users.json` - 用户账户数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------------- | ----------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 | +| `name` | `String` | 非空 | 用户姓名 | +| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 | +| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 | +| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 | +| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) | +| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) | +| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) | +| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) | +| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) | +| `region` | `String` | | 所属区域或地区 | +| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) | +| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) | +| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 | +| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) | +| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) | +| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 | +| `lockout_end_time` | `String` | | 账户锁定截止时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +#### 2.5.2 `feedback.json` - 环境问题反馈数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 | +| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID | +| `title` | `String` | 非空 | 反馈标题 | +| `description` | `String` | | 问题详细描述 | +| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) | +| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) | +| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) | +| `text_address` | `String` | | 文字描述的地址 | +| `grid_x` | `Number` | | 事发地网格X坐标 | +| `grid_y` | `Number` | | 事发地网格Y坐标 | +| `latitude` | `Number` | | 事发地纬度 | +| `longitude` | `Number` | | 事发地经度 | +| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | + +#### 2.5.3 `tasks.json` - 任务数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | ---------------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 | +| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID | +| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人(网格员)ID | +| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人(主管)ID | +| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) | +| `title` | `String` | | 任务标题 | +| `description` | `String` | | 任务详细描述 | +| `pollution_type` | `String` | (ENUM) | 污染类型 | +| `severity_level` | `String` | (ENUM) | 严重程度 | +| `text_address` | `String` | | 任务地点文字描述 | +| `grid_x` | `Number` | | 任务地点网格X坐标 | +| `grid_y` | `Number` | | 任务地点网格Y坐标 | +| `latitude` | `Number` | | 任务地点纬度 | +| `longitude` | `Number` | | 任务地点经度 | +| `assigned_at` | `String` | | 任务分配时间 | +| `completed_at` | `String` | | 任务完成时间 | +| `created_at` | `String` | 非空 | 记录创建时间 | +| `updated_at` | `String` | 非空 | 记录最后更新时间 | +| `deadline` | `String` | | 任务截止日期 | + +#### 2.5.4 `grids.json` - 业务网格数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| --------------- | --------------- | ------------------- | ---------------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 | +| `gridx` | `Number` | | 网格X坐标 | +| `gridy` | `Number` | | 网格Y坐标 | +| `city_name` | `String` | | 所属城市 | +| `district_name` | `String` | | 所属区县 | +| `description` | `String` | | 网格描述信息 | +| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) | + +#### 2.5.5 `assignments.json` - 任务分配记录数据 + +| 字段名 | JSON数据类型 | 约束/说明 | 描述 | +| ---------------- | --------------- | ------------------------ | -------------------------- | +| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 | +| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID | +| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者(主管)ID | +| `status` | `String` | 非空, (ENUM) | 分配状态 | +| `remarks` | `String` | | 分配备注 | +| `assignment_time`| `String` | 非空 | 分配时间 | +| `deadline` | `String` | | 任务截止日期 | +``` --- - + ### Report/系统设计.md - -# 系统设计文档 - -本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 - -由于文档较长,为了便于阅读和维护,内容已被分割为以下几个部分: - -## 文档目录 - -1. [总体设计](系统设计_第一部分.md) - - 系统架构设计 - - 架构选型与原则 - - 后端架构 - - 前端架构 - - 功能模块划分 - -2. [功能模块详细设计](系统设计_第二部分.md) - - 反馈管理模块 - - 任务管理模块 - - 用户与人员管理模块 - - 网格与地图模块 - - 决策支持模块 - -3. [类和对象的设计与动态模型设计](系统设计_第三部分.md) - - UserAccount 类图 - - Feedback 类图 - - Task 类图 - - Grid 和 Assignment 类图 - - 核心时序图 - - 核心状态图 - -4. [算法设计与数据持久化设计](系统设计_第四部分.md) - - A* 寻路算法 - - 任务智能分配算法 - - JSON文件存储架构 - - 核心JSON文件结构 - -**注意:** 所有图表均使用本地渲染的方式,无需在线渲染服务。 - - + +# 系统设计文档 + +本文档旨在详细阐述环境监督系统(EMS)的系统架构、功能模块和实现细节,为开发、测试和维护提供指导。 + +由于文档较长,为了便于阅读和维护,内容已被分割为以下几个部分: + +## 文档目录 + +1. [总体设计](系统设计_第一部分.md) + - 系统架构设计 + - 架构选型与原则 + - 后端架构 + - 前端架构 + - 功能模块划分 + +2. [功能模块详细设计](系统设计_第二部分.md) + - 反馈管理模块 + - 任务管理模块 + - 用户与人员管理模块 + - 网格与地图模块 + - 决策支持模块 + +3. [类和对象的设计与动态模型设计](系统设计_第三部分.md) + - UserAccount 类图 + - Feedback 类图 + - Task 类图 + - Grid 和 Assignment 类图 + - 核心时序图 + - 核心状态图 + +4. [算法设计与数据持久化设计](系统设计_第四部分.md) + - A* 寻路算法 + - 任务智能分配算法 + - JSON文件存储架构 + - 核心JSON文件结构 + +**注意:** 所有图表均使用本地渲染的方式,无需在线渲染服务。 + + --- - + ### Report/系统实现_第二部分.md - -**实现要点分析**: - -1. **动态地图加载**: 算法从数据库动态加载地图数据,包括障碍物信息和地图尺寸,使得路径规划能够适应地图的变化。 -2. **优先队列优化**: 使用优先队列(PriorityQueue)存储开放列表,确保每次都能高效地选择F值最小的节点,大大提高了算法效率。 -3. **曼哈顿距离启发函数**: 选择曼哈顿距离作为启发函数,适合网格化的移动模式,能够准确估计网格间的距离。 -4. **路径重建**: 通过记录每个节点的父节点,实现了从终点回溯到起点的路径重建,返回完整的路径点列表。 - -**程序流程图**: - -```mermaid -flowchart TD - A[开始] --> B[加载地图数据] - B --> C[初始化开放列表和关闭列表] - C --> D[将起点加入开放列表] - D --> E{开放列表为空?} - E -->|是| F[返回空路径] - E -->|否| G[从开放列表取出F值最小的节点] - G --> H{是终点?} - H -->|是| I[重建并返回路径] - H -->|否| J[处理相邻节点] - J --> E -``` - -**API接口**: - -```java -@RestController -@RequestMapping("/api/pathfinding") -@RequiredArgsConstructor -public class PathfindingController { - - private final AStarService aStarService; - - @GetMapping("/find") - @PreAuthorize("hasRole('GRID_WORKER')") - public ResponseEntity> findPath( - @RequestParam int startX, @RequestParam int startY, - @RequestParam int endX, @RequestParam int endY) { - - Point start = new Point(startX, startY); - Point end = new Point(endX, endY); - - List path = aStarService.findPath(start, end); - return ResponseEntity.ok(path); - } -} -``` - -### 3. 反馈管理模块 - -反馈管理模块是系统的核心业务模块之一,负责处理用户提交的环境问题反馈。我实现了完整的反馈提交、审核和处理流程,包括多条件查询、状态流转和文件上传等功能。 - -**关键代码展示**: - -```java -@RestController -@RequestMapping("/api/feedback") -@RequiredArgsConstructor -public class FeedbackController { - - private final FeedbackService feedbackService; - - /** - * 提交反馈(正式接口) - * 使用multipart/form-data格式提交反馈,支持附件上传 - */ - @PostMapping(value = "/submit", consumes = {"multipart/form-data"}) - @PreAuthorize("isAuthenticated()") - public ResponseEntity submitFeedback( - @Valid @RequestPart("feedback") FeedbackSubmissionRequest request, - @RequestPart(value = "files", required = false) MultipartFile[] files, - @AuthenticationPrincipal CustomUserDetails userDetails) { - - Feedback createdFeedback = feedbackService.submitFeedback(request, files); - return new ResponseEntity<>(createdFeedback, HttpStatus.CREATED); - } - - /** - * 获取所有反馈(分页+多条件过滤) - * 支持按状态、污染类型、严重程度、地理位置、时间范围和关键词进行组合查询 - */ - @GetMapping - @PreAuthorize("isAuthenticated()") - public ResponseEntity> getAllFeedback( - @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestParam(required = false) FeedbackStatus status, - @RequestParam(required = false) PollutionType pollutionType, - @RequestParam(required = false) SeverityLevel severityLevel, - @RequestParam(required = false) String cityName, - @RequestParam(required = false) String districtName, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, - @RequestParam(required = false) String keyword, - Pageable pageable) { - - Page feedbackPage = feedbackService.getFeedback( - userDetails, status, pollutionType, severityLevel, cityName, districtName, - startDate, endDate, keyword, pageable); - - return ResponseEntity.ok(feedbackPage); - } - - /** - * 处理反馈 (例如, 批准) - */ - @PostMapping("/{id}/process") - @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") - public ResponseEntity processFeedback( - @PathVariable Long id, - @Valid @RequestBody ProcessFeedbackRequest request) { - FeedbackResponseDTO updatedFeedback = feedbackService.processFeedback(id, request); - return ResponseEntity.ok(updatedFeedback); - } -} -``` - -**服务层实现**: - -```java -@Service -@RequiredArgsConstructor -public class FeedbackServiceImpl implements FeedbackService { - - private final FeedbackRepository feedbackRepository; - private final UserAccountRepository userAccountRepository; - private final FileStorageService fileStorageService; - private final ApplicationEventPublisher eventPublisher; - - @Override - public Feedback submitFeedback(FeedbackSubmissionRequest request, MultipartFile[] files) { - // 1. 获取当前用户 - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String email = authentication.getName(); - UserAccount user = userAccountRepository.findByEmail(email) - .orElseThrow(() -> new ResourceNotFoundException("User", "email", email)); - - // 2. 创建反馈实体 - Feedback feedback = new Feedback(); - feedback.setTitle(request.getTitle()); - feedback.setDescription(request.getDescription()); - feedback.setPollutionType(request.getPollutionType()); - feedback.setSeverityLevel(request.getSeverityLevel()); - feedback.setLatitude(request.getLatitude()); - feedback.setLongitude(request.getLongitude()); - feedback.setTextAddress(request.getTextAddress()); - feedback.setGridX(request.getGridX()); - feedback.setGridY(request.getGridY()); - feedback.setStatus(FeedbackStatus.SUBMITTED); - feedback.setSubmitter(user); - feedback.setEventId(generateEventId()); - - // 3. 保存反馈 - Feedback savedFeedback = feedbackRepository.save(feedback); - - // 4. 处理附件 - if (files != null && files.length > 0) { - List attachments = new ArrayList<>(); - for (MultipartFile file : files) { - String fileName = fileStorageService.storeFile(file); - Attachment attachment = new Attachment(); - attachment.setFileName(fileName); - attachment.setFilePath("/uploads/" + fileName); - attachment.setFileType(file.getContentType()); - attachment.setFileSize(file.getSize()); - attachments.add(attachment); - } - savedFeedback.setAttachments(attachments); - feedbackRepository.save(savedFeedback); - } - - // 5. 发布事件,触发AI审核 - eventPublisher.publishEvent(new FeedbackSubmittedEvent(savedFeedback)); - - return savedFeedback; - } - - @Override - public FeedbackResponseDTO processFeedback(Long id, ProcessFeedbackRequest request) { - Feedback feedback = feedbackRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException("Feedback", "id", id)); - - // 验证状态转换是否有效 - if (!isValidStatusTransition(feedback.getStatus(), request.getStatus())) { - throw new IllegalStateException("Invalid status transition from " + - feedback.getStatus() + " to " + request.getStatus()); - } - - // 更新反馈状态 - feedback.setStatus(request.getStatus()); - - // 如果批准反馈,则更新为PENDING_ASSIGNMENT状态 - if (request.getStatus() == FeedbackStatus.APPROVED) { - feedback.setStatus(FeedbackStatus.PENDING_ASSIGNMENT); - } - - Feedback updatedFeedback = feedbackRepository.save(feedback); - - // 如果反馈被批准,发布事件通知任务管理模块 - if (request.getStatus() == FeedbackStatus.APPROVED) { - eventPublisher.publishEvent(new FeedbackApprovedEvent(updatedFeedback)); - } - - return mapToDTO(updatedFeedback); - } - - // 生成唯一的事件ID - private String generateEventId() { - return "EMS-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-" - + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); - } - - // 验证状态转换是否有效 - private boolean isValidStatusTransition(FeedbackStatus current, FeedbackStatus next) { - // 实现状态机逻辑 - switch (current) { - case SUBMITTED: - return next == FeedbackStatus.AI_REVIEWED; - case AI_REVIEWED: - return next == FeedbackStatus.PENDING_REVIEW; - case PENDING_REVIEW: - return next == FeedbackStatus.APPROVED || next == FeedbackStatus.REJECTED; - case APPROVED: - return next == FeedbackStatus.PENDING_ASSIGNMENT; - case PENDING_ASSIGNMENT: - return next == FeedbackStatus.ASSIGNED; - case ASSIGNED: - return next == FeedbackStatus.PROCESSED; - case PROCESSED: - return next == FeedbackStatus.CLOSED; - default: - return false; - } - } -} -``` - -**实现要点分析**: - -1. **多部分表单处理**: 使用`@RequestPart`注解同时处理JSON数据和文件上传,实现了富媒体反馈提交。 -2. **事件驱动架构**: 通过Spring的事件机制,实现了反馈提交后的异步处理,如AI审核和任务创建,提高了系统响应速度。 -3. **状态机模式**: 使用状态机模式管理反馈的生命周期,确保状态转换的合法性,防止非法操作。 -4. **多条件动态查询**: 实现了灵活的多条件组合查询,支持按状态、类型、严重程度、地理位置和时间范围等进行过滤。 + +**实现要点分析**: + +1. **动态地图加载**: 算法从数据库动态加载地图数据,包括障碍物信息和地图尺寸,使得路径规划能够适应地图的变化。 +2. **优先队列优化**: 使用优先队列(PriorityQueue)存储开放列表,确保每次都能高效地选择F值最小的节点,大大提高了算法效率。 +3. **曼哈顿距离启发函数**: 选择曼哈顿距离作为启发函数,适合网格化的移动模式,能够准确估计网格间的距离。 +4. **路径重建**: 通过记录每个节点的父节点,实现了从终点回溯到起点的路径重建,返回完整的路径点列表。 + +**程序流程图**: + +```mermaid +flowchart TD + A[开始] --> B[加载地图数据] + B --> C[初始化开放列表和关闭列表] + C --> D[将起点加入开放列表] + D --> E{开放列表为空?} + E -->|是| F[返回空路径] + E -->|否| G[从开放列表取出F值最小的节点] + G --> H{是终点?} + H -->|是| I[重建并返回路径] + H -->|否| J[处理相邻节点] + J --> E +``` + +**API接口**: + +```java +@RestController +@RequestMapping("/api/pathfinding") +@RequiredArgsConstructor +public class PathfindingController { + + private final AStarService aStarService; + + @GetMapping("/find") + @PreAuthorize("hasRole('GRID_WORKER')") + public ResponseEntity> findPath( + @RequestParam int startX, @RequestParam int startY, + @RequestParam int endX, @RequestParam int endY) { + + Point start = new Point(startX, startY); + Point end = new Point(endX, endY); + + List path = aStarService.findPath(start, end); + return ResponseEntity.ok(path); + } +} +``` + +### 3. 反馈管理模块 + +反馈管理模块是系统的核心业务模块之一,负责处理用户提交的环境问题反馈。我实现了完整的反馈提交、审核和处理流程,包括多条件查询、状态流转和文件上传等功能。 + +**关键代码展示**: + +```java +@RestController +@RequestMapping("/api/feedback") +@RequiredArgsConstructor +public class FeedbackController { + + private final FeedbackService feedbackService; + + /** + * 提交反馈(正式接口) + * 使用multipart/form-data格式提交反馈,支持附件上传 + */ + @PostMapping(value = "/submit", consumes = {"multipart/form-data"}) + @PreAuthorize("isAuthenticated()") + public ResponseEntity submitFeedback( + @Valid @RequestPart("feedback") FeedbackSubmissionRequest request, + @RequestPart(value = "files", required = false) MultipartFile[] files, + @AuthenticationPrincipal CustomUserDetails userDetails) { + + Feedback createdFeedback = feedbackService.submitFeedback(request, files); + return new ResponseEntity<>(createdFeedback, HttpStatus.CREATED); + } + + /** + * 获取所有反馈(分页+多条件过滤) + * 支持按状态、污染类型、严重程度、地理位置、时间范围和关键词进行组合查询 + */ + @GetMapping + @PreAuthorize("isAuthenticated()") + public ResponseEntity> getAllFeedback( + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestParam(required = false) FeedbackStatus status, + @RequestParam(required = false) PollutionType pollutionType, + @RequestParam(required = false) SeverityLevel severityLevel, + @RequestParam(required = false) String cityName, + @RequestParam(required = false) String districtName, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + @RequestParam(required = false) String keyword, + Pageable pageable) { + + Page feedbackPage = feedbackService.getFeedback( + userDetails, status, pollutionType, severityLevel, cityName, districtName, + startDate, endDate, keyword, pageable); + + return ResponseEntity.ok(feedbackPage); + } + + /** + * 处理反馈 (例如, 批准) + */ + @PostMapping("/{id}/process") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") + public ResponseEntity processFeedback( + @PathVariable Long id, + @Valid @RequestBody ProcessFeedbackRequest request) { + FeedbackResponseDTO updatedFeedback = feedbackService.processFeedback(id, request); + return ResponseEntity.ok(updatedFeedback); + } +} +``` + +**服务层实现**: + +```java +@Service +@RequiredArgsConstructor +public class FeedbackServiceImpl implements FeedbackService { + + private final FeedbackRepository feedbackRepository; + private final UserAccountRepository userAccountRepository; + private final FileStorageService fileStorageService; + private final ApplicationEventPublisher eventPublisher; + + @Override + public Feedback submitFeedback(FeedbackSubmissionRequest request, MultipartFile[] files) { + // 1. 获取当前用户 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getName(); + UserAccount user = userAccountRepository.findByEmail(email) + .orElseThrow(() -> new ResourceNotFoundException("User", "email", email)); + + // 2. 创建反馈实体 + Feedback feedback = new Feedback(); + feedback.setTitle(request.getTitle()); + feedback.setDescription(request.getDescription()); + feedback.setPollutionType(request.getPollutionType()); + feedback.setSeverityLevel(request.getSeverityLevel()); + feedback.setLatitude(request.getLatitude()); + feedback.setLongitude(request.getLongitude()); + feedback.setTextAddress(request.getTextAddress()); + feedback.setGridX(request.getGridX()); + feedback.setGridY(request.getGridY()); + feedback.setStatus(FeedbackStatus.SUBMITTED); + feedback.setSubmitter(user); + feedback.setEventId(generateEventId()); + + // 3. 保存反馈 + Feedback savedFeedback = feedbackRepository.save(feedback); + + // 4. 处理附件 + if (files != null && files.length > 0) { + List attachments = new ArrayList<>(); + for (MultipartFile file : files) { + String fileName = fileStorageService.storeFile(file); + Attachment attachment = new Attachment(); + attachment.setFileName(fileName); + attachment.setFilePath("/uploads/" + fileName); + attachment.setFileType(file.getContentType()); + attachment.setFileSize(file.getSize()); + attachments.add(attachment); + } + savedFeedback.setAttachments(attachments); + feedbackRepository.save(savedFeedback); + } + + // 5. 发布事件,触发AI审核 + eventPublisher.publishEvent(new FeedbackSubmittedEvent(savedFeedback)); + + return savedFeedback; + } + + @Override + public FeedbackResponseDTO processFeedback(Long id, ProcessFeedbackRequest request) { + Feedback feedback = feedbackRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Feedback", "id", id)); + + // 验证状态转换是否有效 + if (!isValidStatusTransition(feedback.getStatus(), request.getStatus())) { + throw new IllegalStateException("Invalid status transition from " + + feedback.getStatus() + " to " + request.getStatus()); + } + + // 更新反馈状态 + feedback.setStatus(request.getStatus()); + + // 如果批准反馈,则更新为PENDING_ASSIGNMENT状态 + if (request.getStatus() == FeedbackStatus.APPROVED) { + feedback.setStatus(FeedbackStatus.PENDING_ASSIGNMENT); + } + + Feedback updatedFeedback = feedbackRepository.save(feedback); + + // 如果反馈被批准,发布事件通知任务管理模块 + if (request.getStatus() == FeedbackStatus.APPROVED) { + eventPublisher.publishEvent(new FeedbackApprovedEvent(updatedFeedback)); + } + + return mapToDTO(updatedFeedback); + } + + // 生成唯一的事件ID + private String generateEventId() { + return "EMS-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-" + + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + } + + // 验证状态转换是否有效 + private boolean isValidStatusTransition(FeedbackStatus current, FeedbackStatus next) { + // 实现状态机逻辑 + switch (current) { + case SUBMITTED: + return next == FeedbackStatus.AI_REVIEWED; + case AI_REVIEWED: + return next == FeedbackStatus.PENDING_REVIEW; + case PENDING_REVIEW: + return next == FeedbackStatus.APPROVED || next == FeedbackStatus.REJECTED; + case APPROVED: + return next == FeedbackStatus.PENDING_ASSIGNMENT; + case PENDING_ASSIGNMENT: + return next == FeedbackStatus.ASSIGNED; + case ASSIGNED: + return next == FeedbackStatus.PROCESSED; + case PROCESSED: + return next == FeedbackStatus.CLOSED; + default: + return false; + } + } +} +``` + +**实现要点分析**: + +1. **多部分表单处理**: 使用`@RequestPart`注解同时处理JSON数据和文件上传,实现了富媒体反馈提交。 +2. **事件驱动架构**: 通过Spring的事件机制,实现了反馈提交后的异步处理,如AI审核和任务创建,提高了系统响应速度。 +3. **状态机模式**: 使用状态机模式管理反馈的生命周期,确保状态转换的合法性,防止非法操作。 +4. **多条件动态查询**: 实现了灵活的多条件组合查询,支持按状态、类型、严重程度、地理位置和时间范围等进行过滤。 --- - + ### Report/系统实现_第三部分.md - -### 4. 任务智能分配算法 - -任务分配是系统中的关键业务流程,我实现了一个智能分配算法,能够根据多种因素为任务选择最合适的网格员。该算法综合考虑了地理距离、当前负载、专业匹配度和历史表现等因素。 - -**关键代码展示**: - -```java -@Service -@RequiredArgsConstructor -public class TaskAssignmentServiceImpl implements TaskAssignmentService { - - private final TaskRepository taskRepository; - private final UserAccountRepository userAccountRepository; - private final GridService gridService; - private final WorkerStatsService workerStatsService; - - /** - * 为任务推荐最合适的网格员 - * 综合考虑地理距离、当前负载、专业匹配度和历史表现 - */ - @Override - public UserAccount recommendWorkerForTask(Task task, List availableWorkers) { - if (availableWorkers.isEmpty()) { - return null; - } - - // 权重配置 - final double DISTANCE_WEIGHT = 0.4; // 地理距离权重 - final double LOAD_WEIGHT = 0.3; // 当前负载权重 - final double SKILL_WEIGHT = 0.2; // 专业匹配度权重 - final double PERFORMANCE_WEIGHT = 0.1; // 历史表现权重 - - // 任务位置 - Point taskLocation = new Point(task.getGridX(), task.getGridY()); - - // 计算每个网格员的评分 - Map workerScores = new HashMap<>(); - - for (UserAccount worker : availableWorkers) { - // 1. 地理距离评分 - Point workerLocation = new Point(worker.getGridX(), worker.getGridY()); - double distance = calculateDistance(workerLocation, taskLocation); - double maxDistance = 10.0; // 最大考虑距离 - double distanceScore = Math.max(0, maxDistance - distance) / maxDistance; - - // 2. 当前负载评分 - int currentTasks = taskRepository.countByAssigneeIdAndStatusIn( - worker.getId(), Arrays.asList(TaskStatus.ASSIGNED, TaskStatus.IN_PROGRESS)); - int maxTasks = 5; // 最大任务数 - double loadScore = (double)(maxTasks - currentTasks) / maxTasks; - - // 3. 专业匹配度评分 - double skillScore = calculateSkillMatch(worker, task); - - // 4. 历史表现评分 - double completionRate = workerStatsService.getCompletionRate(worker.getId()); - double qualityRating = workerStatsService.getAverageQualityRating(worker.getId()); - double performanceScore = (completionRate * 0.7) + (qualityRating * 0.3); - - // 5. 综合评分 - double totalScore = (distanceScore * DISTANCE_WEIGHT) + - (loadScore * LOAD_WEIGHT) + - (skillScore * SKILL_WEIGHT) + - (performanceScore * PERFORMANCE_WEIGHT); - - workerScores.put(worker, totalScore); - } - - // 选择评分最高的网格员 - return workerScores.entrySet().stream() - .max(Map.Entry.comparingByValue()) - .map(Map.Entry::getKey) - .orElse(null); - } - - /** - * 计算两点间的距离 - * 使用曼哈顿距离,适合网格化的城市环境 - */ - private double calculateDistance(Point a, Point b) { - return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()); - } - - /** - * 计算网格员技能与任务的匹配度 - * 返回0-1之间的分数,1表示完全匹配 - */ - private double calculateSkillMatch(UserAccount worker, Task task) { - // 获取工人的技能列表 - List workerSkills = worker.getSkills(); - if (workerSkills == null || workerSkills.isEmpty()) { - return 0.0; - } - - // 根据任务类型确定所需技能 - List requiredSkills = determineRequiredSkills(task); - if (requiredSkills.isEmpty()) { - return 1.0; // 如果任务没有特定技能要求,则任何工人都完全匹配 - } - - // 计算匹配的技能数量 - long matchedSkillsCount = requiredSkills.stream() - .filter(skill -> workerSkills.contains(skill)) - .count(); - - // 返回匹配率 - return (double) matchedSkillsCount / requiredSkills.size(); - } - - /** - * 根据任务类型确定所需技能 - */ - private List determineRequiredSkills(Task task) { - List skills = new ArrayList<>(); - - // 根据污染类型添加相关技能 - switch (task.getPollutionType()) { - case AIR: - skills.add("air_quality_monitoring"); - skills.add("emissions_control"); - break; - case WATER: - skills.add("water_sampling"); - skills.add("water_treatment"); - break; - case SOIL: - skills.add("soil_testing"); - skills.add("land_remediation"); - break; - case NOISE: - skills.add("noise_measurement"); - skills.add("sound_insulation"); - break; - default: - skills.add("general_environmental"); - } - - // 根据严重程度添加额外技能 - if (task.getSeverityLevel() == SeverityLevel.HIGH || - task.getSeverityLevel() == SeverityLevel.CRITICAL) { - skills.add("emergency_response"); - } - - return skills; - } -} -``` - -**实现要点分析**: - -1. **多因素加权评分**: 算法综合考虑了地理距离、当前负载、专业匹配度和历史表现四个因素,通过加权计算得出每个网格员的综合得分。 -2. **技能匹配机制**: 根据任务的污染类型和严重程度,动态确定所需的技能列表,然后与网格员的技能进行匹配,计算匹配度。 -3. **可配置权重**: 各因素的权重可以根据实际需求进行调整,使得算法更加灵活和适应性强。 -4. **流式API**: 使用Java 8的Stream API进行最大值查找,代码简洁且高效。 - -**程序流程图**: - -```mermaid -flowchart TD - A[开始] --> B[获取可用网格员列表] - B --> C{列表为空?} - C -->|是| D[返回null] - C -->|否| E[遍历每个网格员] - E --> F[计算地理距离评分] - F --> G[计算当前负载评分] - G --> H[计算专业匹配度评分] - H --> I[计算历史表现评分] - I --> J[计算综合评分] - J --> K[记录网格员评分] - K --> L{所有网格员已处理?} - L -->|否| E - L -->|是| M[返回评分最高的网格员] -``` - -## 3.3.3 界面展示与成果展示 - -在前端实现方面,我采用了Vue 3和Element Plus组件库,打造了美观、易用且响应式的用户界面。下面展示几个核心界面及其功能实现。 - -### 1. 主管工作台 - 反馈审核与任务分配 - -![主管工作台界面](./assets/supervisor_dashboard.png) - -**界面功能说明**: - -1. **左侧反馈列表**: 展示所有待审核的反馈,支持按状态、类型和严重程度进行筛选。 -2. **右侧反馈详情**: 显示选中反馈的详细信息,包括标题、描述、位置和附件等。 -3. **智能推荐**: 系统自动推荐最合适的网格员,并显示推荐理由。 -4. **手动分配**: 主管可以覆盖系统推荐,手动选择其他网格员。 -5. **批量操作**: 支持批量审核和分配功能,提高工作效率。 - -**前端代码实现**: - -```vue - -``` - -### 2. 网格员工作台 - 任务执行 - -![网格员工作台界面](./assets/worker_dashboard.png) - -**界面功能说明**: - -1. **任务列表**: 展示分配给当前网格员的所有任务,按状态和截止日期排序。 -2. **任务详情**: 显示选中任务的详细信息,包括位置、描述和附件等。 -3. **路径规划**: 集成A*寻路算法,为网格员提供从当前位置到任务地点的最优路径。 -4. **任务提交**: 提供表单和文件上传功能,让网格员提交任务处理结果。 - -### 3. 决策支持看板 - -![决策支持看板界面](./assets/decision_dashboard.png) - -**界面功能说明**: - -1. **核心KPI**: 展示关键业务指标,如反馈总数、任务完成率、平均处理时长等。 -2. **热力图**: 基于地理位置的问题密度热力图,直观展示问题高发区域。 -3. **趋势图**: 展示过去30天的反馈和任务数量趋势,帮助决策者了解系统运行状况。 -4. **分布图**: 展示不同类型问题的分布情况,帮助决策者了解问题类型分布。 - -**前端代码实现**: - -```vue - -``` - -## 3.3.4 实现成果总结 - -通过系统实现阶段的工作,我成功地将系统设计阶段规划的功能转化为了可运行的代码,实现了环境监督系统的核心功能。主要成果包括: - -1. **创新的数据持久化方案**: 设计并实现了基于JSON文件的数据存储服务,简化了系统部署,同时提供了类似JPA的操作接口。 -2. **高效的A*寻路算法**: 实现了能够考虑地图障碍物的A*寻路算法,为网格员提供最优路径规划。 -3. **智能的任务分配算法**: 开发了综合考虑多种因素的任务分配算法,能够为任务选择最合适的执行者。 -4. **完整的反馈管理流程**: 实现了反馈提交、AI审核、人工审核和任务创建的完整流程,支持多条件查询和文件上传。 -5. **直观的用户界面**: 设计并实现了美观、易用且响应式的用户界面,包括主管工作台、网格员工作台和决策支持看板等。 - -这些实现成果不仅满足了系统需求分析阶段定义的功能要求,也体现了系统设计阶段规划的架构和模块划分。系统的实现充分考虑了可维护性、可扩展性和用户体验,为后续的系统测试和部署奠定了坚实的基础。 + +### 4. 任务智能分配算法 + +任务分配是系统中的关键业务流程,我实现了一个智能分配算法,能够根据多种因素为任务选择最合适的网格员。该算法综合考虑了地理距离、当前负载、专业匹配度和历史表现等因素。 + +**关键代码展示**: + +```java +@Service +@RequiredArgsConstructor +public class TaskAssignmentServiceImpl implements TaskAssignmentService { + + private final TaskRepository taskRepository; + private final UserAccountRepository userAccountRepository; + private final GridService gridService; + private final WorkerStatsService workerStatsService; + + /** + * 为任务推荐最合适的网格员 + * 综合考虑地理距离、当前负载、专业匹配度和历史表现 + */ + @Override + public UserAccount recommendWorkerForTask(Task task, List availableWorkers) { + if (availableWorkers.isEmpty()) { + return null; + } + + // 权重配置 + final double DISTANCE_WEIGHT = 0.4; // 地理距离权重 + final double LOAD_WEIGHT = 0.3; // 当前负载权重 + final double SKILL_WEIGHT = 0.2; // 专业匹配度权重 + final double PERFORMANCE_WEIGHT = 0.1; // 历史表现权重 + + // 任务位置 + Point taskLocation = new Point(task.getGridX(), task.getGridY()); + + // 计算每个网格员的评分 + Map workerScores = new HashMap<>(); + + for (UserAccount worker : availableWorkers) { + // 1. 地理距离评分 + Point workerLocation = new Point(worker.getGridX(), worker.getGridY()); + double distance = calculateDistance(workerLocation, taskLocation); + double maxDistance = 10.0; // 最大考虑距离 + double distanceScore = Math.max(0, maxDistance - distance) / maxDistance; + + // 2. 当前负载评分 + int currentTasks = taskRepository.countByAssigneeIdAndStatusIn( + worker.getId(), Arrays.asList(TaskStatus.ASSIGNED, TaskStatus.IN_PROGRESS)); + int maxTasks = 5; // 最大任务数 + double loadScore = (double)(maxTasks - currentTasks) / maxTasks; + + // 3. 专业匹配度评分 + double skillScore = calculateSkillMatch(worker, task); + + // 4. 历史表现评分 + double completionRate = workerStatsService.getCompletionRate(worker.getId()); + double qualityRating = workerStatsService.getAverageQualityRating(worker.getId()); + double performanceScore = (completionRate * 0.7) + (qualityRating * 0.3); + + // 5. 综合评分 + double totalScore = (distanceScore * DISTANCE_WEIGHT) + + (loadScore * LOAD_WEIGHT) + + (skillScore * SKILL_WEIGHT) + + (performanceScore * PERFORMANCE_WEIGHT); + + workerScores.put(worker, totalScore); + } + + // 选择评分最高的网格员 + return workerScores.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse(null); + } + + /** + * 计算两点间的距离 + * 使用曼哈顿距离,适合网格化的城市环境 + */ + private double calculateDistance(Point a, Point b) { + return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()); + } + + /** + * 计算网格员技能与任务的匹配度 + * 返回0-1之间的分数,1表示完全匹配 + */ + private double calculateSkillMatch(UserAccount worker, Task task) { + // 获取工人的技能列表 + List workerSkills = worker.getSkills(); + if (workerSkills == null || workerSkills.isEmpty()) { + return 0.0; + } + + // 根据任务类型确定所需技能 + List requiredSkills = determineRequiredSkills(task); + if (requiredSkills.isEmpty()) { + return 1.0; // 如果任务没有特定技能要求,则任何工人都完全匹配 + } + + // 计算匹配的技能数量 + long matchedSkillsCount = requiredSkills.stream() + .filter(skill -> workerSkills.contains(skill)) + .count(); + + // 返回匹配率 + return (double) matchedSkillsCount / requiredSkills.size(); + } + + /** + * 根据任务类型确定所需技能 + */ + private List determineRequiredSkills(Task task) { + List skills = new ArrayList<>(); + + // 根据污染类型添加相关技能 + switch (task.getPollutionType()) { + case AIR: + skills.add("air_quality_monitoring"); + skills.add("emissions_control"); + break; + case WATER: + skills.add("water_sampling"); + skills.add("water_treatment"); + break; + case SOIL: + skills.add("soil_testing"); + skills.add("land_remediation"); + break; + case NOISE: + skills.add("noise_measurement"); + skills.add("sound_insulation"); + break; + default: + skills.add("general_environmental"); + } + + // 根据严重程度添加额外技能 + if (task.getSeverityLevel() == SeverityLevel.HIGH || + task.getSeverityLevel() == SeverityLevel.CRITICAL) { + skills.add("emergency_response"); + } + + return skills; + } +} +``` + +**实现要点分析**: + +1. **多因素加权评分**: 算法综合考虑了地理距离、当前负载、专业匹配度和历史表现四个因素,通过加权计算得出每个网格员的综合得分。 +2. **技能匹配机制**: 根据任务的污染类型和严重程度,动态确定所需的技能列表,然后与网格员的技能进行匹配,计算匹配度。 +3. **可配置权重**: 各因素的权重可以根据实际需求进行调整,使得算法更加灵活和适应性强。 +4. **流式API**: 使用Java 8的Stream API进行最大值查找,代码简洁且高效。 + +**程序流程图**: + +```mermaid +flowchart TD + A[开始] --> B[获取可用网格员列表] + B --> C{列表为空?} + C -->|是| D[返回null] + C -->|否| E[遍历每个网格员] + E --> F[计算地理距离评分] + F --> G[计算当前负载评分] + G --> H[计算专业匹配度评分] + H --> I[计算历史表现评分] + I --> J[计算综合评分] + J --> K[记录网格员评分] + K --> L{所有网格员已处理?} + L -->|否| E + L -->|是| M[返回评分最高的网格员] +``` + +## 3.3.3 界面展示与成果展示 + +在前端实现方面,我采用了Vue 3和Element Plus组件库,打造了美观、易用且响应式的用户界面。下面展示几个核心界面及其功能实现。 + +### 1. 主管工作台 - 反馈审核与任务分配 + +![主管工作台界面](./assets/supervisor_dashboard.png) + +**界面功能说明**: + +1. **左侧反馈列表**: 展示所有待审核的反馈,支持按状态、类型和严重程度进行筛选。 +2. **右侧反馈详情**: 显示选中反馈的详细信息,包括标题、描述、位置和附件等。 +3. **智能推荐**: 系统自动推荐最合适的网格员,并显示推荐理由。 +4. **手动分配**: 主管可以覆盖系统推荐,手动选择其他网格员。 +5. **批量操作**: 支持批量审核和分配功能,提高工作效率。 + +**前端代码实现**: + +```vue + +``` + +### 2. 网格员工作台 - 任务执行 + +![网格员工作台界面](./assets/worker_dashboard.png) + +**界面功能说明**: + +1. **任务列表**: 展示分配给当前网格员的所有任务,按状态和截止日期排序。 +2. **任务详情**: 显示选中任务的详细信息,包括位置、描述和附件等。 +3. **路径规划**: 集成A*寻路算法,为网格员提供从当前位置到任务地点的最优路径。 +4. **任务提交**: 提供表单和文件上传功能,让网格员提交任务处理结果。 + +### 3. 决策支持看板 + +![决策支持看板界面](./assets/decision_dashboard.png) + +**界面功能说明**: + +1. **核心KPI**: 展示关键业务指标,如反馈总数、任务完成率、平均处理时长等。 +2. **热力图**: 基于地理位置的问题密度热力图,直观展示问题高发区域。 +3. **趋势图**: 展示过去30天的反馈和任务数量趋势,帮助决策者了解系统运行状况。 +4. **分布图**: 展示不同类型问题的分布情况,帮助决策者了解问题类型分布。 + +**前端代码实现**: + +```vue + +``` + +## 3.3.4 实现成果总结 + +通过系统实现阶段的工作,我成功地将系统设计阶段规划的功能转化为了可运行的代码,实现了环境监督系统的核心功能。主要成果包括: + +1. **创新的数据持久化方案**: 设计并实现了基于JSON文件的数据存储服务,简化了系统部署,同时提供了类似JPA的操作接口。 +2. **高效的A*寻路算法**: 实现了能够考虑地图障碍物的A*寻路算法,为网格员提供最优路径规划。 +3. **智能的任务分配算法**: 开发了综合考虑多种因素的任务分配算法,能够为任务选择最合适的执行者。 +4. **完整的反馈管理流程**: 实现了反馈提交、AI审核、人工审核和任务创建的完整流程,支持多条件查询和文件上传。 +5. **直观的用户界面**: 设计并实现了美观、易用且响应式的用户界面,包括主管工作台、网格员工作台和决策支持看板等。 + +这些实现成果不仅满足了系统需求分析阶段定义的功能要求,也体现了系统设计阶段规划的架构和模块划分。系统的实现充分考虑了可维护性、可扩展性和用户体验,为后续的系统测试和部署奠定了坚实的基础。 --- - + ### Report/系统实现_第一部分.md - -# 3.3 系统实现 - -## 3.3.1 开发环境与技术栈 - -在本项目的开发过程中,我采用了现代化、高效且广泛应用于企业级开发的技术栈组合,确保系统的稳定性、可扩展性和维护性。 - -### 后端技术栈 - -- **编程语言**: Java 17 -- **框架**: Spring Boot 3.1.5 -- **API文档**: Springdoc OpenAPI (Swagger) -- **安全框架**: Spring Security + JWT -- **构建工具**: Maven 3.9 -- **数据存储**: 自定义JSON文件存储(模拟数据库) -- **异步处理**: Spring Events -- **日志框架**: SLF4J + Logback -- **单元测试**: JUnit 5 + Mockito - -### 前端技术栈 - -- **框架**: Vue 3 (Composition API) -- **构建工具**: Vite -- **UI组件库**: Element Plus -- **状态管理**: Pinia -- **路由**: Vue Router -- **HTTP客户端**: Axios -- **CSS预处理器**: SCSS -- **图表库**: ECharts - -### 开发工具 - -- **IDE**: IntelliJ IDEA / VS Code -- **版本控制**: Git -- **API测试**: Postman -- **代码质量**: SonarLint -- **调试工具**: Chrome DevTools - -## 3.3.2 核心功能模块实现 - -在系统设计阶段,我们已经详细规划了各个功能模块。在实现阶段,我负责了多个核心模块的编码工作,下面将详细介绍这些模块的实现细节。 - -### 1. 泛型JSON存储服务 - -在本项目中,我设计并实现了一个创新的数据持久化解决方案,使用JSON文件代替传统数据库进行数据存储。这种方案简化了系统部署,同时提供了类似于JPA的操作接口,使得业务层代码无需关心底层存储细节。 - -**关键代码展示**: - -```java -@Service -public class JsonStorageService { - - private static final Path STORAGE_DIRECTORY = Paths.get("json-db"); - private final ObjectMapper objectMapper; - - public JsonStorageService(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - try { - if (!Files.exists(STORAGE_DIRECTORY)) { - Files.createDirectories(STORAGE_DIRECTORY); - } - } catch (IOException e) { - throw new UncheckedIOException("无法创建存储目录", e); - } - } - - // 同步写数据方法,保证线程安全 - public synchronized void writeData(String fileName, List data) { - try { - Path filePath = STORAGE_DIRECTORY.resolve(fileName); - // 使用writeValueAsString先转为格式化的JSON字符串,再写入文件,提高可读性 - String jsonContent = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data); - Files.write(filePath, jsonContent.getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new UncheckedIOException("写入数据到 " + fileName + " 时出错", e); - } - } - - // 读数据方法 - public List readData(String fileName, TypeReference> typeReference) { - Path filePath = STORAGE_DIRECTORY.resolve(fileName); - if (!Files.exists(filePath)) { - return new ArrayList<>(); // 文件不存在是正常情况,返回空列表 - } - try { - // 使用TypeReference来处理泛型擦除问题,确保JSON能被正确反序列化为List - return objectMapper.readValue(filePath.toFile(), typeReference); - } catch (IOException e) { - log.warn("无法读取或解析文件: {}. 返回空列表。", fileName, e); - return new ArrayList<>(); - } - } -} -``` - -**实现要点分析**: - -1. **泛型设计**: 通过Java泛型和TypeReference,实现了类型安全的数据读写,支持任意模型类。 -2. **线程安全**: 使用synchronized关键字确保写操作的线程安全,防止并发写入导致的数据损坏。 -3. **容错处理**: 对文件不存在、IO异常等情况进行了妥善处理,增强了系统的健壮性。 -4. **格式化输出**: 使用Jackson的prettyPrinter功能,使生成的JSON文件具有良好的可读性,便于调试。 - -**程序流程图**: - -```mermaid -flowchart TD - A[开始] --> B{文件存在?} - B -->|是| C[读取文件内容] - B -->|否| D[返回空列表] - C --> E{解析JSON成功?} - E -->|是| F[返回对象列表] - E -->|否| G[记录警告日志] - G --> D -``` - -### 2. A*寻路算法实现 - -在网格与地图模块中,我实现了A*寻路算法,为网格员提供从当前位置到任务地点的最优路径规划。该算法考虑了地图障碍物,能够高效地找到最短路径。 - -**关键代码展示**: - -```java -@Service -@RequiredArgsConstructor -public class AStarService { - - private final MapGridRepository mapGridRepository; - - // 内部Node类,用于A*算法的节点表示 - private static class Node { - Point point; - int g; // 从起点到当前节点的实际代价 - int h; // 启发式:从当前节点到终点的估计代价 - int f; // g + h - Node parent; - - public Node(Point point, Node parent, int g, int h) { - this.point = point; - this.parent = parent; - this.g = g; - this.h = h; - this.f = g + h; - } - } - - /** - * 寻找两点间的最短路径,避开障碍物 - * 地图数据(包括尺寸和障碍物)从数据库动态加载 - */ - public List findPath(Point start, Point end) { - // 从数据库加载地图数据 - List mapGrids = mapGridRepository.findAll(); - if (mapGrids.isEmpty()) { - return Collections.emptyList(); // 无地图数据 - } - - // 动态确定地图尺寸和障碍物 - int maxX = 0, maxY = 0; - Set obstacles = new HashSet<>(); - for (MapGrid gridCell : mapGrids) { - if (gridCell.isObstacle() || gridCell.getCityName() == null || gridCell.getCityName().trim().isEmpty()) { - obstacles.add(new Point(gridCell.getX(), gridCell.getY())); - } - if (gridCell.getX() > maxX) maxX = gridCell.getX(); - if (gridCell.getY() > maxY) maxY = gridCell.getY(); - } - final int mapWidth = maxX + 1; - final int mapHeight = maxY + 1; - - // A*算法核心实现 - PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f)); - Map allNodes = new HashMap<>(); - - Node startNode = new Node(start, null, 0, calculateHeuristic(start, end)); - openSet.add(startNode); - allNodes.put(start, startNode); - - while (!openSet.isEmpty()) { - Node currentNode = openSet.poll(); - - if (currentNode.point.equals(end)) { - return reconstructPath(currentNode); - } - - for (Point neighborPoint : getNeighbors(currentNode.point, mapWidth, mapHeight)) { - if (obstacles.contains(neighborPoint)) { - continue; - } - - int tentativeG = currentNode.g + 1; // 相邻节点间距离为1 - - Node neighborNode = allNodes.get(neighborPoint); - - if (neighborNode == null) { - // 新节点 - int h = calculateHeuristic(neighborPoint, end); - neighborNode = new Node(neighborPoint, currentNode, tentativeG, h); - allNodes.put(neighborPoint, neighborNode); - openSet.add(neighborNode); - } else if (tentativeG < neighborNode.g) { - // 找到更好的路径 - neighborNode.parent = currentNode; - neighborNode.g = tentativeG; - neighborNode.f = tentativeG + neighborNode.h; - // 更新优先队列 - openSet.remove(neighborNode); - openSet.add(neighborNode); - } - } - } - return Collections.emptyList(); // 未找到路径 - } - - // 计算启发式函数值(曼哈顿距离) - private int calculateHeuristic(Point a, Point b) { - return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()); - } -} + +# 3.3 系统实现 + +## 3.3.1 开发环境与技术栈 + +在本项目的开发过程中,我采用了现代化、高效且广泛应用于企业级开发的技术栈组合,确保系统的稳定性、可扩展性和维护性。 + +### 后端技术栈 + +- **编程语言**: Java 17 +- **框架**: Spring Boot 3.1.5 +- **API文档**: Springdoc OpenAPI (Swagger) +- **安全框架**: Spring Security + JWT +- **构建工具**: Maven 3.9 +- **数据存储**: 自定义JSON文件存储(模拟数据库) +- **异步处理**: Spring Events +- **日志框架**: SLF4J + Logback +- **单元测试**: JUnit 5 + Mockito + +### 前端技术栈 + +- **框架**: Vue 3 (Composition API) +- **构建工具**: Vite +- **UI组件库**: Element Plus +- **状态管理**: Pinia +- **路由**: Vue Router +- **HTTP客户端**: Axios +- **CSS预处理器**: SCSS +- **图表库**: ECharts + +### 开发工具 + +- **IDE**: IntelliJ IDEA / VS Code +- **版本控制**: Git +- **API测试**: Postman +- **代码质量**: SonarLint +- **调试工具**: Chrome DevTools + +## 3.3.2 核心功能模块实现 + +在系统设计阶段,我们已经详细规划了各个功能模块。在实现阶段,我负责了多个核心模块的编码工作,下面将详细介绍这些模块的实现细节。 + +### 1. 泛型JSON存储服务 + +在本项目中,我设计并实现了一个创新的数据持久化解决方案,使用JSON文件代替传统数据库进行数据存储。这种方案简化了系统部署,同时提供了类似于JPA的操作接口,使得业务层代码无需关心底层存储细节。 + +**关键代码展示**: + +```java +@Service +public class JsonStorageService { + + private static final Path STORAGE_DIRECTORY = Paths.get("json-db"); + private final ObjectMapper objectMapper; + + public JsonStorageService(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + try { + if (!Files.exists(STORAGE_DIRECTORY)) { + Files.createDirectories(STORAGE_DIRECTORY); + } + } catch (IOException e) { + throw new UncheckedIOException("无法创建存储目录", e); + } + } + + // 同步写数据方法,保证线程安全 + public synchronized void writeData(String fileName, List data) { + try { + Path filePath = STORAGE_DIRECTORY.resolve(fileName); + // 使用writeValueAsString先转为格式化的JSON字符串,再写入文件,提高可读性 + String jsonContent = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data); + Files.write(filePath, jsonContent.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException("写入数据到 " + fileName + " 时出错", e); + } + } + + // 读数据方法 + public List readData(String fileName, TypeReference> typeReference) { + Path filePath = STORAGE_DIRECTORY.resolve(fileName); + if (!Files.exists(filePath)) { + return new ArrayList<>(); // 文件不存在是正常情况,返回空列表 + } + try { + // 使用TypeReference来处理泛型擦除问题,确保JSON能被正确反序列化为List + return objectMapper.readValue(filePath.toFile(), typeReference); + } catch (IOException e) { + log.warn("无法读取或解析文件: {}. 返回空列表。", fileName, e); + return new ArrayList<>(); + } + } +} +``` + +**实现要点分析**: + +1. **泛型设计**: 通过Java泛型和TypeReference,实现了类型安全的数据读写,支持任意模型类。 +2. **线程安全**: 使用synchronized关键字确保写操作的线程安全,防止并发写入导致的数据损坏。 +3. **容错处理**: 对文件不存在、IO异常等情况进行了妥善处理,增强了系统的健壮性。 +4. **格式化输出**: 使用Jackson的prettyPrinter功能,使生成的JSON文件具有良好的可读性,便于调试。 + +**程序流程图**: + +```mermaid +flowchart TD + A[开始] --> B{文件存在?} + B -->|是| C[读取文件内容] + B -->|否| D[返回空列表] + C --> E{解析JSON成功?} + E -->|是| F[返回对象列表] + E -->|否| G[记录警告日志] + G --> D +``` + +### 2. A*寻路算法实现 + +在网格与地图模块中,我实现了A*寻路算法,为网格员提供从当前位置到任务地点的最优路径规划。该算法考虑了地图障碍物,能够高效地找到最短路径。 + +**关键代码展示**: + +```java +@Service +@RequiredArgsConstructor +public class AStarService { + + private final MapGridRepository mapGridRepository; + + // 内部Node类,用于A*算法的节点表示 + private static class Node { + Point point; + int g; // 从起点到当前节点的实际代价 + int h; // 启发式:从当前节点到终点的估计代价 + int f; // g + h + Node parent; + + public Node(Point point, Node parent, int g, int h) { + this.point = point; + this.parent = parent; + this.g = g; + this.h = h; + this.f = g + h; + } + } + + /** + * 寻找两点间的最短路径,避开障碍物 + * 地图数据(包括尺寸和障碍物)从数据库动态加载 + */ + public List findPath(Point start, Point end) { + // 从数据库加载地图数据 + List mapGrids = mapGridRepository.findAll(); + if (mapGrids.isEmpty()) { + return Collections.emptyList(); // 无地图数据 + } + + // 动态确定地图尺寸和障碍物 + int maxX = 0, maxY = 0; + Set obstacles = new HashSet<>(); + for (MapGrid gridCell : mapGrids) { + if (gridCell.isObstacle() || gridCell.getCityName() == null || gridCell.getCityName().trim().isEmpty()) { + obstacles.add(new Point(gridCell.getX(), gridCell.getY())); + } + if (gridCell.getX() > maxX) maxX = gridCell.getX(); + if (gridCell.getY() > maxY) maxY = gridCell.getY(); + } + final int mapWidth = maxX + 1; + final int mapHeight = maxY + 1; + + // A*算法核心实现 + PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f)); + Map allNodes = new HashMap<>(); + + Node startNode = new Node(start, null, 0, calculateHeuristic(start, end)); + openSet.add(startNode); + allNodes.put(start, startNode); + + while (!openSet.isEmpty()) { + Node currentNode = openSet.poll(); + + if (currentNode.point.equals(end)) { + return reconstructPath(currentNode); + } + + for (Point neighborPoint : getNeighbors(currentNode.point, mapWidth, mapHeight)) { + if (obstacles.contains(neighborPoint)) { + continue; + } + + int tentativeG = currentNode.g + 1; // 相邻节点间距离为1 + + Node neighborNode = allNodes.get(neighborPoint); + + if (neighborNode == null) { + // 新节点 + int h = calculateHeuristic(neighborPoint, end); + neighborNode = new Node(neighborPoint, currentNode, tentativeG, h); + allNodes.put(neighborPoint, neighborNode); + openSet.add(neighborNode); + } else if (tentativeG < neighborNode.g) { + // 找到更好的路径 + neighborNode.parent = currentNode; + neighborNode.g = tentativeG; + neighborNode.f = tentativeG + neighborNode.h; + // 更新优先队列 + openSet.remove(neighborNode); + openSet.add(neighborNode); + } + } + } + return Collections.emptyList(); // 未找到路径 + } + + // 计算启发式函数值(曼哈顿距离) + private int calculateHeuristic(Point a, Point b) { + return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()); + } +} --- - + ### Report/系统实现.md - -# 3.3 系统实现 - -本章节详细阐述了环境监督系统(EMS)的实际编码实现过程,展示了如何将系统设计转化为可运行的代码。由于内容较多,为了便于阅读和维护,本章节分为以下三个部分: - -## 文档目录 - -1. [开发环境与技术栈 & 核心功能模块实现(上)](系统实现_第一部分.md) - - 开发环境与技术栈 - - 泛型JSON存储服务 - - A*寻路算法实现 - -2. [核心功能模块实现(中)](系统实现_第二部分.md) - - A*寻路算法实现要点分析 - - 反馈管理模块实现 - -3. [核心功能模块实现(下) & 界面展示与成果展示](系统实现_第三部分.md) - - 任务智能分配算法 - - 界面展示与成果展示 - - 实现成果总结 - -请点击上述链接查看各部分的详细内容。 - -## 3.3.1 后端关键技术实现 - -### 1. 核心数据服务:泛型JSON仓储 - -为了实现一个可复用、类型安全且与业务逻辑完全解耦的数据持久化层,我设计并实现了一个泛型的`JsonStorageService`。这是整个后端数据访问的基石,是实现仓储模式(Repository Pattern)的关键底层支持。 - -```java:ems-backend/src/main/java/com/dne/ems/service/JsonStorageService.java -@Service -public class JsonStorageService { - - private static final Path STORAGE_DIRECTORY = Paths.get("json-db"); - private final ObjectMapper objectMapper; // Jackson的核心,用于JSON序列化和反序列化 - - // 使用构造函数注入,符合Spring推荐的最佳实践 - public JsonStorageService(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - // 在服务启动时,检查并创建存储目录,保证后续操作的顺利进行 - try { - if (!Files.exists(STORAGE_DIRECTORY)) { - Files.createDirectories(STORAGE_DIRECTORY); - } - } catch (IOException e) { - // 如果目录创建失败,则抛出运行时异常,使应用启动失败,防止后续出现更严重问题 - throw new UncheckedIOException("Could not create storage directory", e); - } - } - - // 同步写数据方法,保证线程安全 - public synchronized void writeData(String fileName, List data) { - try { - Path filePath = STORAGE_DIRECTORY.resolve(fileName); - // 使用writeValueAsString先转为格式化的JSON字符串,再写入文件,提高可读性 - String jsonContent = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data); - Files.write(filePath, jsonContent.getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new UncheckedIOException("Error writing data to " + fileName, e); - } - } - - // 读数据方法 - public List readData(String fileName, TypeReference> typeReference) { - Path filePath = STORAGE_DIRECTORY.resolve(fileName); - if (!Files.exists(filePath)) { - return new ArrayList<>(); // 文件不存在是正常情况,返回空列表 - } - try { - // 使用TypeReference来处理泛型擦除问题,确保JSON能被正确反序列化为List - return objectMapper.readValue(filePath.toFile(), typeReference); - } catch (IOException e) { - // 如果文件为空或JSON格式损坏,记录日志并返回空列表,增强系统容错性 - log.warn("Could not read or parse file: {}. Returning empty list.", fileName, e); - return new ArrayList<>(); - } - } -} -``` - -**设计深度解析:** -* **泛型与类型安全:** `writeData`和`readData`方法都使用了泛型``,使得该服务可以处理任何类型的实体列表(如`List`、`List`)。通过传入`TypeReference>`,我们解决了Java泛型在运行时被擦除的问题,使得Jackson库能够准确地将JSON数组反序列化为指定类型的对象列表,保证了类型安全。 -* **线程安全与并发控制:** `writeData`方法被声明为`synchronized`,这是一个简单而有效的并发控制手段。它利用对象锁确保了在多线程环境下对同一文件的写操作是互斥的、串行的,从而从根本上防止了数据竞争和文件损坏的风险。 -* **健壮性与容错:** 代码对存储目录不存在、文件读写IO异常、JSON解析异常等情况都进行了处理。特别是当文件不存在或内容为空/损坏时,`readData`会返回一个空列表而不是抛出异常,这使得上层调用者(Repository)无需处理这些繁琐的底层细节,增强了系统的整体健-壮性。 - -### 2. 安全核心:JWT生成与校验 - -系统的认证和授权机制基于JWT(JSON Web Token)。我实现了一个`JwtTokenProvider`来封装JWT的生成和校验逻辑,使其与业务代码分离。 - -```java:ems-backend/src/main/java/com/dne/ems/security/JwtTokenProvider.java -@Component -public class JwtTokenProvider { - - @Value("${app.jwtSecret}") - private String jwtSecret; - - @Value("${app.jwtExpirationInMs}") - private int jwtExpirationInMs; - - // 根据用户信息生成JWT - public String generateToken(Authentication authentication) { - UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); - Date now = new Date(); - Date expiryDate = new Date(now.getTime() + jwtExpirationInMs); - - return Jwts.builder() - .setSubject(Long.toString(userPrincipal.getId())) - .claim("role", userPrincipal.getAuthorities().iterator().next().getAuthority()) - .setIssuedAt(new Date()) - .setExpiration(expiryDate) - .signWith(SignatureAlgorithm.HS512, jwtSecret) - .compact(); - } - - // 从JWT中解析用户ID - public Long getUserIdFromJWT(String token) { - Claims claims = Jwts.parser() - .setSigningKey(jwtSecret) - .parseClaimsJws(token) - .getBody(); - return Long.parseLong(claims.getSubject()); - } - - // 校验JWT的有效性 - public boolean validateToken(String authToken) { - try { - Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); - return true; - } catch (SignatureException | MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException ex) { - // 捕获所有可能的JWT校验异常 - log.error("Invalid JWT token: {}", ex.getMessage()); - } - return false; - } -} -``` - -**实现说明:** -* **配置化:** JWT的密钥(`jwtSecret`)和过期时间(`jwtExpirationInMs`)都通过`@Value`注解从`application.properties`配置文件中读取,便于不同环境的部署和管理。 -* **声明(Claims):** 在生成Token时,我不仅将用户ID作为`subject`,还将用户的角色(Role)作为一个自定义的`claim`存入Token中。这样做的好处是,在后续的授权判断中,我们无需再次查询数据库,直接从Token中即可获取用户角色,提高了性能。 -* **全面的异常处理:** `validateToken`方法捕获了`jjwt`库可能抛出的所有校验异常,如签名错误、格式错误、Token过期等,并记录日志,保证了校验逻辑的严谨性。 - -### 3. 统一出口:全局异常处理器 - -为了提供统一、规范的API错误响应格式,我实现了一个全局异常处理器。这避免了在每个Controller方法中都写`try-catch`块的冗余代码。 - -```java:ems-backend/src/main/java/com/dne/ems/exception/GlobalExceptionHandler.java -@ControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(value = {IllegalArgumentException.class, IllegalStateException.class}) - public ResponseEntity handleBadRequest(RuntimeException ex, WebRequest request) { - ApiResponse apiResponse = new ApiResponse(false, ex.getMessage()); - return new ResponseEntity<>(apiResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(value = ResourceNotFoundException.class) - public ResponseEntity handleResourceNotFound(ResourceNotFoundException ex, WebRequest request) { - ApiResponse apiResponse = new ApiResponse(false, ex.getMessage()); - return new ResponseEntity<>(apiResponse, HttpStatus.NOT_FOUND); - } - - @ExceptionHandler(value = Exception.class) - public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { - log.error("An unexpected error occurred: ", ex); - ApiResponse apiResponse = new ApiResponse(false, "An internal server error occurred. Please try again later."); - return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } -} -``` - -**实现说明:** -* **`@ControllerAdvice`:** 该注解使得这个类可以成为一个全局的AOP切面,用于处理所有`@Controller`中抛出的异常。 -* **`@ExceptionHandler`:** 针对不同类型的异常(如参数错误、资源未找到、未知异常),定义了不同的处理方法,返回相应的HTTP状态码和统一格式的JSON错误信息(`ApiResponse`对象),极大地改善了API的友好性和可调试性。 - -## 3.3.2 前端界面实现与展示 - -我独立负责了大部分前端界面的开发,采用了`Vue 3` + `Vite` + `Pinia` + `Element Plus`这一现代化的技术栈,实现了数据驱动的、组件化的、响应式的用户界面。 - -* **组件化开发:** 我将UI拆分为多个可复用的组件,如`TaskCard.vue`, `FeedbackList.vue`, `UserSelector.vue`等,提高了代码的可维护性和开发效率。 -* **状态管理:** 使用`Pinia`作为全局状态管理器,集中管理用户的登录信息、角色、权限以及全局的加载状态等,使得跨组件的状态共享变得简单、可预测。 -* **API交互:** 封装了`axios`实例,添加了请求和响应拦截器。请求拦截器负责在每个请求头中自动附加JWT;响应拦截器负责处理全局的API错误,如`401 Unauthorized`(自动跳转到登录页)、`500 Internal Server Error`(弹出统一的错误提示)。 - -以下是系统核心页面的UI设计与功能描述: - -**1. 主管工作台 - 任务分配页面** - -* **界面截图描述:** 页面左侧是一个可滚动、可搜索的"待处理反馈"列表,每项都清晰地展示了反馈的标题、提交时间和关键内容摘要。点击某一项后,右侧会显示该反馈的完整详情。详情下方是一个"指派网格员"的下拉选择框,其中列出了所有状态正常的网格员。选择网格员后,点击"立即分配"按钮,系统会弹出确认对话框,确认后完成任务的创建和分配,并给出成功提示。 - -**2. 网格员工作台 - 任务处理页面** - -* **界面截图描述:** 页面顶部是任务的核心信息卡片,包括任务标题、描述、截止日期和当前状态。下方是一个多标签页的表单区域。第一个标签页是"数据录入",包含了多个根据任务类型动态生成的表单项(如空气质量指数、噪声分贝、垃圾分类情况等)。第二个标签页是"现场照片",提供了一个支持拖拽和点击上传的图片上传组件,并能实时显示缩略图。所有信息填写完毕后,点击底部的"完成任务"按钮提交数据。 - -**3. 数据决策看板 (Dashboard)** - -* **界面截图描述:** 这是一个信息密集型的仪表盘页面。顶部是四个醒目的KPI卡片,分别显示"本月任务总数"、"已完成率"、"平均处理时长"和"待处理反馈数"。下方是一个占据主要区域的地图组件,地图上用不同颜色的标记点展示了各个网格区域的问题分布和严重程度。地图右侧是两个图表:一个是"近30天任务趋势"的折线图,另一个是"问题类型分布"的饼图。所有图表都是动态的,并支持按时间范围进行筛选。 + +# 3.3 系统实现 + +本章节详细阐述了环境监督系统(EMS)的实际编码实现过程,展示了如何将系统设计转化为可运行的代码。由于内容较多,为了便于阅读和维护,本章节分为以下三个部分: + +## 文档目录 + +1. [开发环境与技术栈 & 核心功能模块实现(上)](系统实现_第一部分.md) + - 开发环境与技术栈 + - 泛型JSON存储服务 + - A*寻路算法实现 + +2. [核心功能模块实现(中)](系统实现_第二部分.md) + - A*寻路算法实现要点分析 + - 反馈管理模块实现 + +3. [核心功能模块实现(下) & 界面展示与成果展示](系统实现_第三部分.md) + - 任务智能分配算法 + - 界面展示与成果展示 + - 实现成果总结 + +请点击上述链接查看各部分的详细内容。 + +## 3.3.1 后端关键技术实现 + +### 1. 核心数据服务:泛型JSON仓储 + +为了实现一个可复用、类型安全且与业务逻辑完全解耦的数据持久化层,我设计并实现了一个泛型的`JsonStorageService`。这是整个后端数据访问的基石,是实现仓储模式(Repository Pattern)的关键底层支持。 + +```java:ems-backend/src/main/java/com/dne/ems/service/JsonStorageService.java +@Service +public class JsonStorageService { + + private static final Path STORAGE_DIRECTORY = Paths.get("json-db"); + private final ObjectMapper objectMapper; // Jackson的核心,用于JSON序列化和反序列化 + + // 使用构造函数注入,符合Spring推荐的最佳实践 + public JsonStorageService(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + // 在服务启动时,检查并创建存储目录,保证后续操作的顺利进行 + try { + if (!Files.exists(STORAGE_DIRECTORY)) { + Files.createDirectories(STORAGE_DIRECTORY); + } + } catch (IOException e) { + // 如果目录创建失败,则抛出运行时异常,使应用启动失败,防止后续出现更严重问题 + throw new UncheckedIOException("Could not create storage directory", e); + } + } + + // 同步写数据方法,保证线程安全 + public synchronized void writeData(String fileName, List data) { + try { + Path filePath = STORAGE_DIRECTORY.resolve(fileName); + // 使用writeValueAsString先转为格式化的JSON字符串,再写入文件,提高可读性 + String jsonContent = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data); + Files.write(filePath, jsonContent.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new UncheckedIOException("Error writing data to " + fileName, e); + } + } + + // 读数据方法 + public List readData(String fileName, TypeReference> typeReference) { + Path filePath = STORAGE_DIRECTORY.resolve(fileName); + if (!Files.exists(filePath)) { + return new ArrayList<>(); // 文件不存在是正常情况,返回空列表 + } + try { + // 使用TypeReference来处理泛型擦除问题,确保JSON能被正确反序列化为List + return objectMapper.readValue(filePath.toFile(), typeReference); + } catch (IOException e) { + // 如果文件为空或JSON格式损坏,记录日志并返回空列表,增强系统容错性 + log.warn("Could not read or parse file: {}. Returning empty list.", fileName, e); + return new ArrayList<>(); + } + } +} +``` + +**设计深度解析:** +* **泛型与类型安全:** `writeData`和`readData`方法都使用了泛型``,使得该服务可以处理任何类型的实体列表(如`List`、`List`)。通过传入`TypeReference>`,我们解决了Java泛型在运行时被擦除的问题,使得Jackson库能够准确地将JSON数组反序列化为指定类型的对象列表,保证了类型安全。 +* **线程安全与并发控制:** `writeData`方法被声明为`synchronized`,这是一个简单而有效的并发控制手段。它利用对象锁确保了在多线程环境下对同一文件的写操作是互斥的、串行的,从而从根本上防止了数据竞争和文件损坏的风险。 +* **健壮性与容错:** 代码对存储目录不存在、文件读写IO异常、JSON解析异常等情况都进行了处理。特别是当文件不存在或内容为空/损坏时,`readData`会返回一个空列表而不是抛出异常,这使得上层调用者(Repository)无需处理这些繁琐的底层细节,增强了系统的整体健-壮性。 + +### 2. 安全核心:JWT生成与校验 + +系统的认证和授权机制基于JWT(JSON Web Token)。我实现了一个`JwtTokenProvider`来封装JWT的生成和校验逻辑,使其与业务代码分离。 + +```java:ems-backend/src/main/java/com/dne/ems/security/JwtTokenProvider.java +@Component +public class JwtTokenProvider { + + @Value("${app.jwtSecret}") + private String jwtSecret; + + @Value("${app.jwtExpirationInMs}") + private int jwtExpirationInMs; + + // 根据用户信息生成JWT + public String generateToken(Authentication authentication) { + UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtExpirationInMs); + + return Jwts.builder() + .setSubject(Long.toString(userPrincipal.getId())) + .claim("role", userPrincipal.getAuthorities().iterator().next().getAuthority()) + .setIssuedAt(new Date()) + .setExpiration(expiryDate) + .signWith(SignatureAlgorithm.HS512, jwtSecret) + .compact(); + } + + // 从JWT中解析用户ID + public Long getUserIdFromJWT(String token) { + Claims claims = Jwts.parser() + .setSigningKey(jwtSecret) + .parseClaimsJws(token) + .getBody(); + return Long.parseLong(claims.getSubject()); + } + + // 校验JWT的有效性 + public boolean validateToken(String authToken) { + try { + Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); + return true; + } catch (SignatureException | MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException ex) { + // 捕获所有可能的JWT校验异常 + log.error("Invalid JWT token: {}", ex.getMessage()); + } + return false; + } +} +``` + +**实现说明:** +* **配置化:** JWT的密钥(`jwtSecret`)和过期时间(`jwtExpirationInMs`)都通过`@Value`注解从`application.properties`配置文件中读取,便于不同环境的部署和管理。 +* **声明(Claims):** 在生成Token时,我不仅将用户ID作为`subject`,还将用户的角色(Role)作为一个自定义的`claim`存入Token中。这样做的好处是,在后续的授权判断中,我们无需再次查询数据库,直接从Token中即可获取用户角色,提高了性能。 +* **全面的异常处理:** `validateToken`方法捕获了`jjwt`库可能抛出的所有校验异常,如签名错误、格式错误、Token过期等,并记录日志,保证了校验逻辑的严谨性。 + +### 3. 统一出口:全局异常处理器 + +为了提供统一、规范的API错误响应格式,我实现了一个全局异常处理器。这避免了在每个Controller方法中都写`try-catch`块的冗余代码。 + +```java:ems-backend/src/main/java/com/dne/ems/exception/GlobalExceptionHandler.java +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(value = {IllegalArgumentException.class, IllegalStateException.class}) + public ResponseEntity handleBadRequest(RuntimeException ex, WebRequest request) { + ApiResponse apiResponse = new ApiResponse(false, ex.getMessage()); + return new ResponseEntity<>(apiResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(value = ResourceNotFoundException.class) + public ResponseEntity handleResourceNotFound(ResourceNotFoundException ex, WebRequest request) { + ApiResponse apiResponse = new ApiResponse(false, ex.getMessage()); + return new ResponseEntity<>(apiResponse, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(value = Exception.class) + public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + log.error("An unexpected error occurred: ", ex); + ApiResponse apiResponse = new ApiResponse(false, "An internal server error occurred. Please try again later."); + return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} +``` + +**实现说明:** +* **`@ControllerAdvice`:** 该注解使得这个类可以成为一个全局的AOP切面,用于处理所有`@Controller`中抛出的异常。 +* **`@ExceptionHandler`:** 针对不同类型的异常(如参数错误、资源未找到、未知异常),定义了不同的处理方法,返回相应的HTTP状态码和统一格式的JSON错误信息(`ApiResponse`对象),极大地改善了API的友好性和可调试性。 + +## 3.3.2 前端界面实现与展示 + +我独立负责了大部分前端界面的开发,采用了`Vue 3` + `Vite` + `Pinia` + `Element Plus`这一现代化的技术栈,实现了数据驱动的、组件化的、响应式的用户界面。 + +* **组件化开发:** 我将UI拆分为多个可复用的组件,如`TaskCard.vue`, `FeedbackList.vue`, `UserSelector.vue`等,提高了代码的可维护性和开发效率。 +* **状态管理:** 使用`Pinia`作为全局状态管理器,集中管理用户的登录信息、角色、权限以及全局的加载状态等,使得跨组件的状态共享变得简单、可预测。 +* **API交互:** 封装了`axios`实例,添加了请求和响应拦截器。请求拦截器负责在每个请求头中自动附加JWT;响应拦截器负责处理全局的API错误,如`401 Unauthorized`(自动跳转到登录页)、`500 Internal Server Error`(弹出统一的错误提示)。 + +以下是系统核心页面的UI设计与功能描述: + +**1. 主管工作台 - 任务分配页面** + +* **界面截图描述:** 页面左侧是一个可滚动、可搜索的"待处理反馈"列表,每项都清晰地展示了反馈的标题、提交时间和关键内容摘要。点击某一项后,右侧会显示该反馈的完整详情。详情下方是一个"指派网格员"的下拉选择框,其中列出了所有状态正常的网格员。选择网格员后,点击"立即分配"按钮,系统会弹出确认对话框,确认后完成任务的创建和分配,并给出成功提示。 + +**2. 网格员工作台 - 任务处理页面** + +* **界面截图描述:** 页面顶部是任务的核心信息卡片,包括任务标题、描述、截止日期和当前状态。下方是一个多标签页的表单区域。第一个标签页是"数据录入",包含了多个根据任务类型动态生成的表单项(如空气质量指数、噪声分贝、垃圾分类情况等)。第二个标签页是"现场照片",提供了一个支持拖拽和点击上传的图片上传组件,并能实时显示缩略图。所有信息填写完毕后,点击底部的"完成任务"按钮提交数据。 + +**3. 数据决策看板 (Dashboard)** + +* **界面截图描述:** 这是一个信息密集型的仪表盘页面。顶部是四个醒目的KPI卡片,分别显示"本月任务总数"、"已完成率"、"平均处理时长"和"待处理反馈数"。下方是一个占据主要区域的地图组件,地图上用不同颜色的标记点展示了各个网格区域的问题分布和严重程度。地图右侧是两个图表:一个是"近30天任务趋势"的折线图,另一个是"问题类型分布"的饼图。所有图表都是动态的,并支持按时间范围进行筛选。 --- - + ### Report/相关技术基础.md - -# 1. 相关技术基础 - -本项目综合运用了业界主流的开发技术与环境,构建了一个现代化Web应用。以下是项目所涉及的关键技术、开发工具和环境的介绍。 - -### 1.1 理论基础 - -项目的架构和代码设计遵循了下述成熟的软件工程理论,以确保系统的可维护性和扩展性。 - -* **面向对象编程 (OOP):** - 项目的核心编程思想,其封装、继承、多态特性在后端代码设计中得到了深入应用。 - * **封装 (Encapsulation):** 核心业务实体(如`User`, `Task`)被抽象为Java类,其属性私有化,仅通过公共方法暴露,保证了对象状态的完整性。 - * **继承 (Inheritance) 与多态 (Polymorphism):** 在自定义的JSON仓储层设计中,通过定义泛型接口`JsonRepository`和实现该接口的泛型基类`JsonRepositoryImpl`,使得具体的实体仓储(如`JsonUserRepositoryImpl`)能自动获得通用的数据操作能力。业务逻辑层依赖于抽象接口而非具体实现,实现了"面向接口编程",提高了代码的灵活性。 - -* **SOLID设计原则,尤其是依赖倒置原则 (DIP):** - 项目架构遵循SOLID原则,特别是依赖倒置原则,即"高层模块不应依赖于低层模块,两者都应依赖于抽象"。 - * **实践应用:** `Service`层(高层模块)依赖的是`UserRepository`等抽象接口,而不是`JsonRepositoryImpl`这样的具体实现(低层模块)。这种设计倒置了传统的依赖关系,极大提升了系统的可替换性和可测试性。未来更换数据源或进行单元测试时,只需更换实现或模拟接口,上层代码无需改动。 - -* **模型-视图-控制器 (MVC) 分层架构:** - 项目遵循MVC分层思想,并扩展为更精细的四层架构:Controller -> Service -> Repository -> Entity。 - * **Controller (控制器层):** 作为HTTP请求的入口,负责解析请求参数,调用`Service`层执行业务逻辑,并返回响应。 - * **Service (业务逻辑层):** 包含所有业务规则和处理流程,通过依赖注入(DI)调用`Repository`层。 - * **Repository (数据访问层):** 数据持久化的抽象,负责与数据源(本项目中为JSON文件)交互,实现业务与数据的解耦。 - * **Entity/DTO (模型层):** POJO对象,`Entity`映射数据存储结构,`DTO`(Data Transfer Object)用于各层之间的数据传输,避免持久化实体直接暴露给外部。 - -* **RESTful API 设计原则:** - 前后端通信完全基于RESTful风格的API进行。 - * **统一接口 (Uniform Interface):** 使用HTTP标准方法(`GET`, `POST`, `PUT`, `DELETE`)表达对资源的操作。 - * **无状态 (Stateless):** 服务端不保存客户端会话状态,每次请求都包含所有必要信息(如JWT),提高了系统的可伸缩性。 - * **资源导向 (Resource-Oriented):** API围绕"资源"展开,使用名词(如`/api/users`)标识资源,而非动词。 - -### 1.2 后端技术栈 - -* **核心框架 - `Spring Boot 3.x`:** - 作为后端应用基石,它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发、配置和部署。其强大的生态整合能力,使得集成`Spring Security`、`Lombok`、`Swagger`等第三方库变得非常简单。 - -* **安全框架 - `Spring Security 6.x` & `JWT`:** - 采用`Spring Security`结合`JSON Web Tokens (JWT)`,构建无状态的认证与授权体系。 - * **`Spring Security` Filter链:** 自定义`JwtAuthenticationFilter`过滤器,用于在每个请求中校验JWT并设置安全上下文。 - * **声明式权限控制:** 使用`@PreAuthorize`注解对Service方法进行方法级别的权限声明,实现了安全逻辑与业务逻辑的解耦。 - * **无状态会话 (Stateless Session):** 配置会话管理策略为`STATELESS`,使后端服务成为真正的无状态服务,提升了系统的可伸缩性。 - -* **持久化方案 - 自定义泛型JSON仓储层:** - 为满足"数据存储在JSON文件中"的需求,设计并实现了一套模拟`Spring Data JPA`接口规范的、可复用的泛型JSON仓储层。 - * **`JsonStorageService`:** 将所有文件I/O操作封装在此服务中,并使用`synchronized`块确保并发写操作的线程安全。 - * **`JsonRepositoryImpl`:** 泛型基类,实现了通用的CRUD功能,具体的实体仓储通过继承该类来复用代码。 - * 该方案遵循**依赖倒置原则**,业务层仅依赖抽象接口,为未来平滑迁移持久化方案提供了便利。 - -* **数据校验 - `Jakarta Bean Validation` (`Hibernate Validator`):** - 在DTO的字段上使用`@NotNull`, `@Size`等声明式注解,并配合Controller层的`@Valid`注解,实现对请求参数的自动化校验。 - -* **对象映射 - `MapStruct`:** - 一个编译期的代码生成器,通过定义`@Mapper`接口,自动生成Entity与DTO之间转换的高性能实现代码,提升了开发效率。 - -* **编程语言与辅助工具:** - * **`Java 17 (LTS)`:** 使用其`record`、`switch`表达式等新特性编写更简洁的代码。 - * **`Lombok`:** 通过`@Data`, `@Builder`等注解,消除样板代码。 - * **`SLF4J` & `Logback`:** 灵活的日志系统,通过配置文件实现分环境、分级别的日志输出。 - -### 1.3 前端技术栈 - -采用`Vue.js`生态的最新技术,构建响应迅速、代码可维护的现代化单页应用(SPA)。 - -* **核心框架 - `Vue.js 3.x`:** - 利用其两大核心新特性: - * **`Composition API` (组合式API):** 将相关逻辑组织在同一个`setup`函数内,提高了代码的可读性、可维护性和逻辑复用性。 - * **`Proxy`-based Reactivity:** 基于`Proxy`重写的响应式系统,解决了`Vue 2`中无法监听对象属性新增/删除等痛点。 - -* **构建工具 - `Vite`:** - 新一代前端构建工具,优势在于: - * 利用浏览器原生ESM支持,实现开发环境下的极速冷启动和闪电般的热模块替换(HMR)。 - * 生产环境使用`Rollup`打包,生成高度优化的静态资源。 - -* **状态管理 - `Pinia`:** - `Vue`官方推荐的下一代状态管理库,特点是API直观、类型支持完美,且天生模块化。废除了`Vuex`中复杂的`Mutations`等概念,心智负担小。 - -* **UI组件库 - `Element Plus`:** - `Element UI`的`Vue 3`版本,是一套高质量的企业级UI组件库,提供了丰富的组件,极大地加速了界面的开发进程。 - -* **HTTP客户端 - `Axios`封装:** - 对流行的`Axios`库进行二次封装: - * **创建实例:** 为API服务设置不同的`baseURL`、`timeout`等。 - * **请求拦截器:** 统一添加认证`token`。 - * **响应拦截器:** 统一处理业务数据和错误状态码。 - -### 1.4 测试技术 - -* **后端单元与集成测试 (`JUnit 5`, `Mockito`, `Spring Boot Test`):** - 使用`spring-boot-starter-test`集成的测试套件保障业务逻辑正确性。 - * **`JUnit 5`:** 测试的基础框架。 - * **`Mockito`:** 在单元测试中用于模拟依赖对象(如Repository),实现测试隔离。 - * **`Spring Boot Test`:** 用于集成测试,加载完整应用上下文,测试跨层级的真实交互场景。 - -* **API接口测试 (`Postman` / `Swagger UI`):** - * **`Swagger UI`:** 通过集成`SpringDoc`,根据代码注解自动生成交互式API文档,方便调试。 - * **`Postman`:** 用于执行更复杂的API测试场景、编写测试用例和自动化测试。 - -### 1.5 开发工具与环境 - -* **IDE:** 后端使用`IntelliJ IDEA Ultimate`,前端使用`Visual Studio Code`。 -* **项目管理与构建:** 后端使用`Maven`,前端使用`npm`。 -* **版本控制:** `Git` & `GitHub`,遵循`Git Flow`工作流。 + +# 1. 相关技术基础 + +本项目综合运用了业界主流的开发技术与环境,构建了一个现代化Web应用。以下是项目所涉及的关键技术、开发工具和环境的介绍。 + +### 1.1 理论基础 + +项目的架构和代码设计遵循了下述成熟的软件工程理论,以确保系统的可维护性和扩展性。 + +* **面向对象编程 (OOP):** + 项目的核心编程思想,其封装、继承、多态特性在后端代码设计中得到了深入应用。 + * **封装 (Encapsulation):** 核心业务实体(如`User`, `Task`)被抽象为Java类,其属性私有化,仅通过公共方法暴露,保证了对象状态的完整性。 + * **继承 (Inheritance) 与多态 (Polymorphism):** 在自定义的JSON仓储层设计中,通过定义泛型接口`JsonRepository`和实现该接口的泛型基类`JsonRepositoryImpl`,使得具体的实体仓储(如`JsonUserRepositoryImpl`)能自动获得通用的数据操作能力。业务逻辑层依赖于抽象接口而非具体实现,实现了"面向接口编程",提高了代码的灵活性。 + +* **SOLID设计原则,尤其是依赖倒置原则 (DIP):** + 项目架构遵循SOLID原则,特别是依赖倒置原则,即"高层模块不应依赖于低层模块,两者都应依赖于抽象"。 + * **实践应用:** `Service`层(高层模块)依赖的是`UserRepository`等抽象接口,而不是`JsonRepositoryImpl`这样的具体实现(低层模块)。这种设计倒置了传统的依赖关系,极大提升了系统的可替换性和可测试性。未来更换数据源或进行单元测试时,只需更换实现或模拟接口,上层代码无需改动。 + +* **模型-视图-控制器 (MVC) 分层架构:** + 项目遵循MVC分层思想,并扩展为更精细的四层架构:Controller -> Service -> Repository -> Entity。 + * **Controller (控制器层):** 作为HTTP请求的入口,负责解析请求参数,调用`Service`层执行业务逻辑,并返回响应。 + * **Service (业务逻辑层):** 包含所有业务规则和处理流程,通过依赖注入(DI)调用`Repository`层。 + * **Repository (数据访问层):** 数据持久化的抽象,负责与数据源(本项目中为JSON文件)交互,实现业务与数据的解耦。 + * **Entity/DTO (模型层):** POJO对象,`Entity`映射数据存储结构,`DTO`(Data Transfer Object)用于各层之间的数据传输,避免持久化实体直接暴露给外部。 + +* **RESTful API 设计原则:** + 前后端通信完全基于RESTful风格的API进行。 + * **统一接口 (Uniform Interface):** 使用HTTP标准方法(`GET`, `POST`, `PUT`, `DELETE`)表达对资源的操作。 + * **无状态 (Stateless):** 服务端不保存客户端会话状态,每次请求都包含所有必要信息(如JWT),提高了系统的可伸缩性。 + * **资源导向 (Resource-Oriented):** API围绕"资源"展开,使用名词(如`/api/users`)标识资源,而非动词。 + +### 1.2 后端技术栈 + +* **核心框架 - `Spring Boot 3.x`:** + 作为后端应用基石,它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发、配置和部署。其强大的生态整合能力,使得集成`Spring Security`、`Lombok`、`Swagger`等第三方库变得非常简单。 + +* **安全框架 - `Spring Security 6.x` & `JWT`:** + 采用`Spring Security`结合`JSON Web Tokens (JWT)`,构建无状态的认证与授权体系。 + * **`Spring Security` Filter链:** 自定义`JwtAuthenticationFilter`过滤器,用于在每个请求中校验JWT并设置安全上下文。 + * **声明式权限控制:** 使用`@PreAuthorize`注解对Service方法进行方法级别的权限声明,实现了安全逻辑与业务逻辑的解耦。 + * **无状态会话 (Stateless Session):** 配置会话管理策略为`STATELESS`,使后端服务成为真正的无状态服务,提升了系统的可伸缩性。 + +* **持久化方案 - 自定义泛型JSON仓储层:** + 为满足"数据存储在JSON文件中"的需求,设计并实现了一套模拟`Spring Data JPA`接口规范的、可复用的泛型JSON仓储层。 + * **`JsonStorageService`:** 将所有文件I/O操作封装在此服务中,并使用`synchronized`块确保并发写操作的线程安全。 + * **`JsonRepositoryImpl`:** 泛型基类,实现了通用的CRUD功能,具体的实体仓储通过继承该类来复用代码。 + * 该方案遵循**依赖倒置原则**,业务层仅依赖抽象接口,为未来平滑迁移持久化方案提供了便利。 + +* **数据校验 - `Jakarta Bean Validation` (`Hibernate Validator`):** + 在DTO的字段上使用`@NotNull`, `@Size`等声明式注解,并配合Controller层的`@Valid`注解,实现对请求参数的自动化校验。 + +* **对象映射 - `MapStruct`:** + 一个编译期的代码生成器,通过定义`@Mapper`接口,自动生成Entity与DTO之间转换的高性能实现代码,提升了开发效率。 + +* **编程语言与辅助工具:** + * **`Java 17 (LTS)`:** 使用其`record`、`switch`表达式等新特性编写更简洁的代码。 + * **`Lombok`:** 通过`@Data`, `@Builder`等注解,消除样板代码。 + * **`SLF4J` & `Logback`:** 灵活的日志系统,通过配置文件实现分环境、分级别的日志输出。 + +### 1.3 前端技术栈 + +采用`Vue.js`生态的最新技术,构建响应迅速、代码可维护的现代化单页应用(SPA)。 + +* **核心框架 - `Vue.js 3.x`:** + 利用其两大核心新特性: + * **`Composition API` (组合式API):** 将相关逻辑组织在同一个`setup`函数内,提高了代码的可读性、可维护性和逻辑复用性。 + * **`Proxy`-based Reactivity:** 基于`Proxy`重写的响应式系统,解决了`Vue 2`中无法监听对象属性新增/删除等痛点。 + +* **构建工具 - `Vite`:** + 新一代前端构建工具,优势在于: + * 利用浏览器原生ESM支持,实现开发环境下的极速冷启动和闪电般的热模块替换(HMR)。 + * 生产环境使用`Rollup`打包,生成高度优化的静态资源。 + +* **状态管理 - `Pinia`:** + `Vue`官方推荐的下一代状态管理库,特点是API直观、类型支持完美,且天生模块化。废除了`Vuex`中复杂的`Mutations`等概念,心智负担小。 + +* **UI组件库 - `Element Plus`:** + `Element UI`的`Vue 3`版本,是一套高质量的企业级UI组件库,提供了丰富的组件,极大地加速了界面的开发进程。 + +* **HTTP客户端 - `Axios`封装:** + 对流行的`Axios`库进行二次封装: + * **创建实例:** 为API服务设置不同的`baseURL`、`timeout`等。 + * **请求拦截器:** 统一添加认证`token`。 + * **响应拦截器:** 统一处理业务数据和错误状态码。 + +### 1.4 测试技术 + +* **后端单元与集成测试 (`JUnit 5`, `Mockito`, `Spring Boot Test`):** + 使用`spring-boot-starter-test`集成的测试套件保障业务逻辑正确性。 + * **`JUnit 5`:** 测试的基础框架。 + * **`Mockito`:** 在单元测试中用于模拟依赖对象(如Repository),实现测试隔离。 + * **`Spring Boot Test`:** 用于集成测试,加载完整应用上下文,测试跨层级的真实交互场景。 + +* **API接口测试 (`Postman` / `Swagger UI`):** + * **`Swagger UI`:** 通过集成`SpringDoc`,根据代码注解自动生成交互式API文档,方便调试。 + * **`Postman`:** 用于执行更复杂的API测试场景、编写测试用例和自动化测试。 + +### 1.5 开发工具与环境 + +* **IDE:** 后端使用`IntelliJ IDEA Ultimate`,前端使用`Visual Studio Code`。 +* **项目管理与构建:** 后端使用`Maven`,前端使用`npm`。 +* **版本控制:** `Git` & `GitHub`,遵循`Git Flow`工作流。 --- - + ### Report/需求定义_v2.md - -# 需求定义文档 - -## 系统分析 - -### 1. 项目介绍 - -#### 1.1 项目背景 - -环境监测系统(EMS)是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速,环境污染问题日益凸显,传统的环境问题上报和处理机制存在流程繁琐、响应缓慢、缺乏透明度等问题。本系统旨在通过数字化手段,构建一个连接公众、管理部门和执行人员的环境监测与治理平台,实现环境问题的快速发现、高效处理和全程监督。 - -#### 1.2 项目目标 - -1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。 -2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。 -3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。 -4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。 -5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。 - -### 2. 业务分析 - -#### 2.1 业务痛点 - -1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。 -2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。 -3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。 -4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。 -5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。 - -#### 2.2 业务价值 - -1. **提升公众满意度**:通过快速响应和处理环境问题,提高公众对环境治理工作的满意度。 -2. **优化资源配置**:基于数据分析和智能算法,实现人力资源的科学分配,提高资源利用效率。 -3. **降低管理成本**:减少人工干预和纸质流程,降低管理成本,提高工作效率。 -4. **提升环境质量**:通过高效处理环境问题,改善城市环境质量,提升居民生活品质。 -5. **强化问责机制**:通过全流程记录和追踪,明确责任分工,强化问责机制。 - -#### 2.3 核心业务流程 - -环境监测系统的核心业务流程包括问题发现与上报、内容审核、任务创建与分配、任务执行与监督、结果审核与反馈等环节,形成一个完整的闭环管理体系。 - -1. **问题发现与上报**:公众通过移动端应用发现环境问题并提交反馈,包括问题描述、位置信息和图片证据。 -2. **内容审核**:系统通过AI技术对上报内容进行初步审核,筛选出有效信息,并由主管进行人工复核确认。 -3. **任务创建与分配**:对于审核通过的反馈,系统自动创建任务,并基于智能算法为任务推荐最合适的处理人员。 -4. **任务执行与监督**:网格员接收任务后,前往现场处理问题,并记录处理过程和结果。 -5. **结果审核与反馈**:主管对处理结果进行审核,确认任务是否完成,并将处理结果反馈给问题上报者。 - -### 3. 功能分析 - -#### 3.1 用户角色分析 - -环境监测系统涉及四类主要用户角色,每个角色在系统中承担不同的职责: - -1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。 -2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。 -3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。 -4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。 - -#### 3.2 用例分析 - -##### 3.2.1 用例图 - -```mermaid -graph TD - %% 定义角色 - PublicUser["公众用户"] - GridWorker["网格员"] - Supervisor["主管"] - Admin["管理员"] - - %% 定义用例 - UC1["注册与登录"] - UC2["提交环境问题反馈"] - UC3["查看反馈处理进度"] - UC4["接收任务通知"] - UC5["执行任务"] - UC6["提交处理结果"] - UC7["审核反馈内容"] - UC8["分配任务"] - UC9["审核处理结果"] - UC10["查看统计数据"] - UC11["管理用户账户"] - UC12["配置系统参数"] - - %% 建立关系 - PublicUser --> UC1 - PublicUser --> UC2 - PublicUser --> UC3 - - GridWorker --> UC1 - GridWorker --> UC4 - GridWorker --> UC5 - GridWorker --> UC6 - - Supervisor --> UC1 - Supervisor --> UC7 - Supervisor --> UC8 - Supervisor --> UC9 - Supervisor --> UC10 - - Admin --> UC1 - Admin --> UC10 - Admin --> UC11 - Admin --> UC12 - - %% 设置样式 - classDef actor fill:#f9f,stroke:#333,stroke-width:2px - classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px - - class PublicUser,GridWorker,Supervisor,Admin actor - class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12 usecase -``` - -##### 3.2.2 主要用例描述 - -**用例1:提交环境问题反馈** - -- **参与者**:公众用户 -- **前置条件**:用户已登录系统 -- **基本流程**: - 1. 用户选择"提交反馈"功能 - 2. 系统显示反馈提交表单 - 3. 用户填写问题标题、描述、污染类型、严重程度 - 4. 用户上传问题现场图片 - 5. 用户标记问题发生的地理位置 - 6. 用户提交表单 - 7. 系统验证表单数据 - 8. 系统生成唯一事件ID并保存反馈信息 - 9. 系统返回提交成功提示 -- **替代流程**: - - 如果表单验证失败,系统提示错误信息并返回表单页面 - - 如果图片上传失败,系统提示重新上传 -- **后置条件**:反馈信息被保存,状态设为"待审核" - -**用例2:审核反馈内容** - -- **参与者**:主管 -- **前置条件**:主管已登录系统,有待审核的反馈 -- **基本流程**: - 1. 主管进入反馈审核页面 - 2. 系统显示待审核反馈列表 - 3. 主管选择一条反馈查看详情 - 4. 系统显示反馈详细信息和AI审核建议 - 5. 主管审核内容并做出决定(通过/驳回) - 6. 如果通过,主管确认创建任务 - 7. 如果驳回,主管填写驳回理由 - 8. 系统更新反馈状态并通知相关人员 -- **替代流程**: - - 如果需要更多信息,主管可以暂缓决定,标记为"需补充信息" -- **后置条件**:反馈状态更新为"已处理"或"已驳回" - -**用例3:执行任务** - -- **参与者**:网格员 -- **前置条件**:网格员已登录系统,已接受任务 -- **基本流程**: - 1. 网格员查看任务详情 - 2. 系统显示任务信息和位置 - 3. 网格员请求路径规划 - 4. 系统生成最优路径 - 5. 网格员前往现场处理问题 - 6. 网格员记录处理过程 - 7. 网格员上传处理结果和证明材料 - 8. 系统保存处理信息并更新任务状态 -- **替代流程**: - - 如果任务无法完成,网格员可提交说明并请求重新分配 -- **后置条件**:任务状态更新为"已提交",等待主管审核 - -#### 3.3 活动图分析 - -##### 3.3.1 反馈提交与处理活动图 - -```mermaid -stateDiagram-v2 - [*] --> 发现环境问题 - 发现环境问题 --> 填写反馈表单 - 填写反馈表单 --> 上传图片 - 上传图片 --> 标记位置 - 标记位置 --> 提交反馈 - 提交反馈 --> AI自动审核 - - state AI自动审核 { - [*] --> 内容分析 - 内容分析 --> 垃圾信息检测 - 垃圾信息检测 --> 分类与评级 - 分类与评级 --> [*] - } - - AI自动审核 --> 判断AI审核结果 - 判断AI审核结果 --> 明显无效: AI拒绝 - 判断AI审核结果 --> 需人工确认: 需确认 - - 明显无效 --> 标记为AI_REJECTED - 标记为AI_REJECTED --> 通知提交者 - 通知提交者 --> [*] - - 需人工确认 --> 主管人工审核 - 主管人工审核 --> 判断审核结果 - - 判断审核结果 --> 驳回: 不通过 - 判断审核结果 --> 通过: 通过 - - 驳回 --> 填写驳回理由 - 填写驳回理由 --> 更新状态为REJECTED - 更新状态为REJECTED --> 通知提交者反馈被驳回 - 通知提交者反馈被驳回 --> [*] - - 通过 --> 创建任务 - 创建任务 --> 更新反馈状态为PROCESSED - 更新反馈状态为PROCESSED --> 任务分配流程 - 任务分配流程 --> [*] -``` - -##### 3.3.2 任务分配与执行活动图 - -```mermaid -stateDiagram-v2 - [*] --> 任务创建完成 - 任务创建完成 --> 选择分配方式 - - state 选择分配方式 { - [*] --> 手动分配 - [*] --> 智能推荐 - - 智能推荐 --> 运行分配算法 - 运行分配算法 --> 推荐最佳人选 - 推荐最佳人选 --> 确认人选 - - 手动分配 --> 选择特定网格员 - 选择特定网格员 --> 确认人选 - - 确认人选 --> [*] - } - - 选择分配方式 --> 创建任务分配记录 - 创建任务分配记录 --> 更新任务状态为ASSIGNED - 更新任务状态为ASSIGNED --> 通知网格员 - - 通知网格员 --> 网格员接收通知 - 网格员接收通知 --> 判断是否接受 - - 判断是否接受 --> 拒绝: 拒绝 - 判断是否接受 --> 接受: 接受 - - 拒绝 --> 选择分配方式 - - 接受 --> 更新状态为IN_PROGRESS - 更新状态为IN_PROGRESS --> 获取路径规划 - 获取路径规划 --> 前往现场处理 - 前往现场处理 --> 记录处理过程 - 记录处理过程 --> 上传处理结果 - 上传处理结果 --> 提交处理结果 - 提交处理结果 --> 更新状态为SUBMITTED - - 更新状态为SUBMITTED --> 主管审核结果 - 主管审核结果 --> 判断结果是否合格 - - 判断结果是否合格 --> 不合格: 不合格 - 判断结果是否合格 --> 合格: 合格 - - 不合格 --> 填写原因要求重新处理 - 填写原因要求重新处理 --> 获取路径规划 - - 合格 --> 确认任务完成 - 确认任务完成 --> 更新任务状态为APPROVED - 更新任务状态为APPROVED --> 更新反馈状态为CLOSED - 更新反馈状态为CLOSED --> 通知反馈提交者 - 通知反馈提交者 --> 更新统计数据 - 更新统计数据 --> [*] -``` - -### 4. 可行性分析 - -#### 4.1 技术可行性 - -1. **前端技术**:采用Vue 3框架构建用户界面,结合Element Plus组件库,可以快速开发出美观、响应式的Web应用,满足不同设备的访问需求。 -2. **后端技术**:基于Spring Boot 3框架和Java 17,具备高性能、高并发处理能力,能够满足系统的稳定性和扩展性需求。 -3. **地图服务**:可以集成百度地图、高德地图等成熟的地图API,实现地理位置标记、路径规划等功能。 -4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。 -5. **数据存储**:采用JSON文件存储方案,简化部署和维护,适合中小规模系统的快速实现。 - -#### 4.2 经济可行性 - -1. **开发成本**:采用主流开源框架和技术栈,降低开发成本和技术门槛。 -2. **维护成本**:模块化设计和完善的文档,降低后期维护和升级成本。 -3. **投资回报**:通过提高环境问题处理效率,减少人力资源浪费,长期来看具有良好的投资回报。 -4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。 - -#### 4.3 操作可行性 - -1. **用户接受度**:系统界面设计简洁直观,操作流程符合用户习惯,易于被各类用户接受和使用。 -2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。 -3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。 - -#### 4.4 法律可行性 - -1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。 -2. **知识产权**:系统开发过程中使用的第三方库和组件均为开源或已获得授权,不存在知识产权风险。 -3. **合规性**:系统功能和流程设计符合相关法律法规和行业标准,确保合法合规运营。 - -## 5. 整体业务流程 - -下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程。 - -```mermaid -flowchart TD - %% 定义样式 - classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333 - classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333 - classDef worker fill:#d5e8d4,stroke:#82b366,color:#333 - classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333 - classDef decision fill:#f8cecc,stroke:#b85450,color:#333 - classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px - - %% 流程开始 - A([开始]) --> B["[公众端] 发现环境问题"] - B --> C["[公众端] 提交反馈
(标题/描述/图片/位置)"] - - %% 平台接收与AI处理 - C --> D["[平台] 接收反馈
生成唯一事件ID"] - D --> E{"[平台] AI自动审核
分析内容/分类"} - - %% AI审核分支 - E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"] - F1 --> F2["[平台] 通知提交者"] - F2 --> Z1([结束]) - - E -- "需人工确认" --> G["[主管] 查看反馈详情
进行人工审核"] - - %% 主管审核分支 - G --> H{"[主管] 审核决定"} - H -- "驳回" --> I1["[主管] 填写驳回理由"] - I1 --> I2["[平台] 更新状态为REJECTED"] - I2 --> I3["[平台] 通知提交者"] - I3 --> Z2([结束]) - - %% 审核通过,创建任务 - H -- "通过" --> J1["[主管] 确认反馈有效"] - J1 --> J2["[平台] 自动创建结构化任务"] - J2 --> J3["[平台] 更新反馈状态为PROCESSED"] - - %% 任务分配 - J3 --> K1{"[主管] 选择分配方式"} - K1 -- "手动分配" --> K2["[主管] 选择特定网格员"] - K1 -- "智能推荐" --> K3["[平台] 运行分配算法
考虑位置/负载/专长"] - K3 --> K4["[平台] 推荐最佳人选"] - K4 --> K2 - K2 --> K5["[平台] 创建任务分配记录
更新任务状态为ASSIGNED"] - K5 --> K6["[平台] 通知网格员"] - - %% 网格员处理 - K6 --> L1["[网格员] 接收任务通知"] - L1 --> L2{"[网格员] 接受任务?"} - L2 -- "拒绝" --> K1 - L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"] - L3 --> L4["[网格员] 查看任务详情
获取路径规划"] - L4 --> L5["[网格员] 前往现场处理"] - L5 --> L6["[网格员] 记录处理过程
上传证明材料"] - L6 --> L7["[网格员] 提交处理结果
更新状态为SUBMITTED"] - - %% 主管审核结果 - L7 --> M1["[主管] 审核处理结果"] - M1 --> M2{"[主管] 结果是否合格?"} - M2 -- "不合格" --> M3["[主管] 填写原因
要求重新处理"] - M3 --> L4 - - %% 完成流程 - M2 -- "合格" --> N1["[主管] 确认任务完成"] - N1 --> N2["[平台] 更新任务状态为APPROVED"] - N2 --> N3["[平台] 更新反馈状态为CLOSED"] - N3 --> N4["[平台] 通知反馈提交者"] - N4 --> N5["[平台] 更新统计数据"] - N5 --> O["[决策层] 查看数据看板
分析环境趋势"] - O --> Z3([结束]) - - %% 为节点添加类别 - class A,Z1,Z2,Z3 start_end - class B,C,F2,I3,N4 public - class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform - class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor - class L1,L2,L3,L4,L5,L6,L7 worker - class O decision -``` - -## 6. 功能性需求 - -### 6.1 功能层次方框图 - -```mermaid -flowchart TD - %% 用户交互层 - subgraph "用户交互层" - direction LR - C["公众服务模块\n(问题上报)"] - H["个人中心模块\n(我的反馈/资料)"] - D["管理驾驶舱\n(数据决策)"] - end - - %% 核心业务层 - subgraph "核心业务层" - direction LR - AI["AI分析模块\n(内容审核)"] - E["任务管理模块\n(分配、流转、执行)"] - end - - %% 应用支撑层 - subgraph "应用支撑层" - direction LR - F["网格与地图模块\n(LBS & 寻路)"] - I["文件服务模块\n(附件存取)"] - end - - %% 基础服务层 - subgraph "基础服务层" - direction LR - B["用户与认证模块"] - G["系统管理模块\n(用户/权限)"] - J["日志审计模块"] - end - - %% 定义关系 - C -- "提交反馈" --> AI - AI -- "分析结果" --> E - C -- "附件" --> I - E -- "调用" --> F - E -- "任务附件" --> I - E -- "统计数据" --> D - - H -- "查询个人数据" --> E - - %% 基础服务支撑所有上层模块 (关系隐含) - G -- "管理" --> B - - classDef userLayer fill:#d4f1f9,stroke:#05a8e5; - classDef coreLayer fill:#ffe6cc,stroke:#f7a128; - classDef appSupportLayer fill:#d5e8d4,stroke:#82b366; - classDef baseLayer fill:#e1d5e7,stroke:#9673a6; - - class C,H,D userLayer; - class AI,E coreLayer; - class F,I appSupportLayer; - class B,G,J baseLayer; -``` - -### 6.2 需求描述 - -#### 6.2.1 用户与认证模块 - -| 功能名称 | 用户与认证模块 | -| :--- | :--- | -| **优先级** | 高 | -| **业务背景** | 作为系统安全的基础,本模块负责管理所有用户的身份验证和访问控制,确保系统资源只能被授权用户访问,同时提供灵活的角色权限管理。 | -| **功能说明** | 1. **用户认证**:基于JWT (JSON Web Token) 的安全认证机制,支持账号密码登录,提供令牌刷新功能。
2. **权限控制**:基于RBAC (基于角色的访问控制) 模型,预设管理员、主管、网格员等角色,每个角色拥有特定的API访问权限。
3. **密码管理**:支持安全的密码重置流程,包括邮箱验证码验证,以及定期密码更新提醒。
4. **会话管理**:支持单点登录或多设备登录控制,可配置会话超时策略。 | -| **约束条件** | 1. 密码必须符合复杂度要求(至少8位,包含大小写字母、数字和特殊字符)。
2. 敏感操作(如修改权限)需要二次验证。
3. 密码在数据库中必须使用BCrypt等强哈希算法加密存储。 | -| **相关查询** | 1. 按用户名、邮箱或手机号查询用户信息。
2. 查询特定角色的所有用户列表。
3. 查询用户的权限和访问历史。 | -| **其他需求** | 1. 登录失败超过预设次数后,账户应被临时锁定。
2. 系统应记录所有关键安全事件(登录、权限变更等)的审计日志。 | -| **裁剪说明** | 不可裁剪,此为系统安全的基础组件。 | - -#### 6.2.2 反馈管理模块 - -| 功能名称 | 反馈管理模块 | -| :--- | :--- | -| **优先级** | 高 | -| **业务背景** | 作为系统的核心输入端口,本模块负责收集、处理和跟踪所有环境问题反馈,是连接公众与管理部门的桥梁,也是后续任务创建的数据源。 | -| **功能说明** | 1. **反馈提交**:提供结构化的表单接口,支持文字描述、污染类型分类、严重程度评估、地理位置标记和多媒体附件上传。
2. **AI内容审核**:集成智能审核服务,对反馈内容进行自动分析,识别垃圾信息、重复提交,并进行初步的分类和紧急程度评估。
3. **人工审核工作台**:为主管提供高效的反馈审核界面,支持批量处理、快速预览和详情查看。
4. **状态追踪**:完整记录反馈从提交到处理完成的全生命周期状态变更,支持多维度的统计和查询。 | -| **约束条件** | 1. 反馈提交必须包含至少一张图片和准确的地理位置信息。
2. AI审核结果仅作为参考,最终决定权在人工审核者手中。
3. 对于紧急程度被标记为"高"的反馈,系统应在1小时内完成审核。 | -| **相关查询** | 1. 按状态、时间段、区域、污染类型等多维度查询反馈列表。
2. 查看特定反馈的详细信息和处理历史。
3. 统计不同类型反馈的数量分布和处理效率。 | -| **其他需求** | 1. 支持反馈的优先级标记和升级处理。
2. 对于同一区域短时间内的多个相似反馈,系统应能智能识别并提示可能的重复。 | -| **裁剪说明** | AI审核功能可在初期简化实现,但反馈的基本提交和人工审核流程不可裁剪。 | - -#### 6.2.3 任务管理模块 - -| 功能名称 | 任务管理模块 | -| :--- | :--- | -| **优先级** | 高 | -| **业务背景** | 本模块是系统的核心业务处理单元,负责将审核通过的反馈转化为可执行的工作任务,并对任务的分配、执行和完成进行全流程管理。 | -| **功能说明** | 1. **任务创建**:支持从反馈自动生成任务,也支持主管手动创建临时任务,包含任务描述、位置、截止时间、优先级等信息。
2. **智能分配**:基于多因素(网格员位置、当前负载、专业技能、历史表现)的任务分配算法,为每个任务推荐最合适的处理人员。
3. **任务执行跟踪**:记录任务的每个状态变更(已分配、已接受、进行中、已提交、已审核),支持网格员实时上报处理进度。
4. **结果审核**:主管对网格员提交的处理结果进行审核,可以通过或驳回,并提供反馈意见。
5. **任务看板**:直观展示不同状态任务的数量和分布,支持拖拽操作进行状态更新。 | -| **约束条件** | 1. 任务必须关联到一个有效的反馈或由授权主管手动创建。
2. 任务分配时必须考虑网格员的工作区域和当前任务负载。
3. 高优先级任务应在24小时内分配并开始处理。
4. 任务提交时必须包含处理过程描述和至少一张结果照片。 | -| **相关查询** | 1. 按状态、负责人、时间段、区域等多维度查询任务列表。
2. 查看特定任务的详细信息、处理历史和相关反馈。
3. 统计不同网格员的任务完成率、平均处理时长等绩效指标。 | -| **其他需求** | 1. 支持任务的紧急程度升级和重新分配。
2. 对于长时间未处理的任务,系统应自动发送提醒通知。
3. 支持批量导出任务报告,用于绩效评估和工作汇报。 | -| **裁剪说明** | 智能分配算法可以在初期简化实现,但任务的基本创建、分配和状态管理功能不可裁剪。 | - -#### 6.2.4 网格与地图模块 - -| 功能名称 | 网格与地图模块 | -| :--- | :--- | -| **优先级** | 中 | -| **业务背景** | 本模块负责对地理空间进行网格化管理,将城市区域划分为可管理的网格单元,并为任务执行提供地理位置支持和路径规划。 | -| **功能说明** | 1. **网格定义与管理**:支持管理员定义和维护城市网格系统,包括网格的坐标、属性(如是否为障碍物)和责任人分配。
2. **A*寻路算法服务**:基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划,考虑距离、交通状况和障碍物。
3. **地图可视化**:在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。
4. **区域统计**:基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | -| **约束条件** | 1. 网格系统应支持多级划分,最小网格单元不应大于500米×500米。
2. 路径规划应考虑实际道路情况和障碍物,避免不可通行区域。
3. 地图数据应定期更新,确保准确性。 | -| **相关查询** | 1. 查询特定区域内的网格定义和属性。
2. 查询特定网格的历史问题记录和统计数据。
3. 获取两点间的最优路径规划。 | -| **其他需求** | 1. 支持网格责任人的灵活调整和临时替换。
2. 地图界面应支持常见的交互操作(缩放、平移、点选)。
3. 支持离线地图数据缓存,保证在网络不稳定情况下的基本功能。 | -| **裁剪说明** | A*寻路算法的高级功能(如考虑实时交通)可以在后期迭代中实现,但基本的网格定义和地图展示功能不应裁剪。 | - -#### 6.2.5 决策支持模块 - -| 功能名称 | 决策支持模块 (管理驾驶舱) | -| :--- | :--- | -| **优先级** | 中 | -| **业务背景** | 本模块旨在将系统中沉淀的大量业务数据转化为有价值的决策洞察,帮助管理层了解环境状况、评估治理效果、优化资源配置。 | -| **功能说明** | 1. **核心指标看板**:实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。
2. **多维度分析**:支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。
3. **热力图可视化**:在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。
4. **绩效评估**:对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。
5. **预警机制**:基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | -| **约束条件** | 1. 数据分析应基于实时或准实时的业务数据,确保决策的时效性。
2. 图表和报表应支持多种导出格式(PDF、Excel等),便于进一步分析和汇报。
3. 敏感数据(如个人绩效)的访问应受到严格权限控制。 | -| **相关查询** | 1. 按自定义时间范围查询各类统计指标。
2. 查询特定区域或网格员的历史表现数据。
3. 生成定制化的数据报表和分析图表。 | -| **其他需求** | 1. 支持数据看板的个性化配置,满足不同管理者的关注点。
2. 提供数据异常检测和提醒功能,及时发现数据波动。
3. 支持定期自动生成并发送统计报告。 | -| **裁剪说明** | 高级分析功能(如预测模型)可以在后期迭代中实现,但基本的数据统计和可视化功能不应裁剪。 | - -## 7. 需求规定 - -### 7.1 一般性需求 - -* **数据集中管理与共享**:系统应采用统一的数据存储和访问机制,确保各模块间的数据一致性和实时共享。所有业务数据应按照标准化格式进行存储,并通过规范的API进行访问,避免数据孤岛。 - -* **高可用性与可靠性**:系统应保证7x24小时的稳定运行,关键业务流程(如反馈提交、任务分配)的可用性应达到99.9%以上。应实现适当的错误处理和故障恢复机制,确保在出现异常情况时能够快速恢复。 - -* **安全性与合规性**: - * 应用SSL/TLS加密保护所有网络通信。 - * 实现严格的认证和授权机制,确保用户只能访问其权限范围内的数据和功能。 - * 敏感数据(如用户密码)必须加密存储,并限制访问权限。 - * 系统应保留完整的操作日志,支持安全审计和问题追溯。 - * 符合相关的数据保护法规和隐私要求。 - -* **可扩展性与模块化**:系统架构应支持水平扩展和功能模块的灵活添加。核心组件应设计为松耦合的,便于独立升级和替换。API设计应考虑向后兼容性,确保系统可以平滑演进。 - -* **用户体验优化**: - * 界面设计应简洁直观,符合现代Web应用的设计标准。 - * 关键操作路径应尽量简化,减少用户点击次数。 - * 系统响应时间应控制在可接受范围内,核心API的平均响应时间不超过500ms。 - * 提供适当的操作反馈和状态提示,增强用户对系统的信任感。 - * 支持响应式设计,确保在不同设备(PC、平板、手机)上的良好体验。 - -* **国际化与本地化**:系统应支持多语言界面,初期至少支持中英文切换。日期、时间、数字等格式应根据用户的区域设置进行适当显示。 - -* **可维护性与可测试性**:系统代码应遵循统一的编码规范和设计模式,保持良好的可读性和可维护性。核心业务逻辑应有充分的单元测试覆盖,便于后续的功能迭代和质量保障。 - -## 8. 数据描述 - -### 8.1 用户账户 (UserAccount) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 用户唯一标识符 | Long | 主键,自增 | 是 | -| `name` | 用户姓名 | String | 最大长度50 | 是 | -| `phone` | 手机号码 | String | 符合手机号格式 | 是 | -| `email` | 电子邮箱 | String | 符合邮箱格式 | 是 | -| `password` | 密码(加密存储) | String | 最小长度8,包含字母、数字和特殊字符 | 是 | -| `gender` | 性别 | Enum | MALE, FEMALE, OTHER | 否 | -| `role` | 用户角色 | Enum | ADMIN, SUPERVISOR, GRID_WORKER, PUBLIC_USER | 是 | -| `status` | 账户状态 | Enum | ACTIVE, INACTIVE, LOCKED | 是 | -| `gridX` | 网格X坐标(仅网格员) | Integer | 非负整数 | 否 | -| `gridY` | 网格Y坐标(仅网格员) | Integer | 非负整数 | 否 | -| `createdAt` | 账户创建时间 | DateTime | ISO 8601格式 | 是 | -| `updatedAt` | 账户更新时间 | DateTime | ISO 8601格式 | 是 | -| `lastLoginAt` | 最后登录时间 | DateTime | ISO 8601格式 | 否 | - -### 8.2 反馈 (Feedback) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 反馈唯一标识符 | Long | 主键,自增 | 是 | -| `eventId` | 事件ID(业务编号) | String | UUID格式 | 是 | -| `title` | 反馈标题 | String | 最大长度100 | 是 | -| `description` | 问题详细描述 | String | 最大长度1000 | 是 | -| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 | -| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 | -| `longitude` | 地理位置经度 | Double | 有效范围 | 是 | -| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 | -| `imageUrls` | 图片URL列表 | Array | 至少一张图片 | 是 | -| `status` | 反馈状态 | Enum | PENDING_REVIEW, AI_REJECTED, PROCESSED, CLOSED | 是 | -| `submitterId` | 提交者ID | Long | 外键关联UserAccount | 是 | -| `reviewerId` | 审核者ID | Long | 外键关联UserAccount | 否 | -| `reviewNote` | 审核备注 | String | 最大长度500 | 否 | -| `createdAt` | 提交时间 | DateTime | ISO 8601格式 | 是 | -| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | -| `processedAt` | 处理时间 | DateTime | ISO 8601格式 | 否 | - -### 8.3 任务 (Task) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 任务唯一标识符 | Long | 主键,自增 | 是 | -| `feedbackId` | 关联的反馈ID | Long | 外键关联Feedback | 否 | -| `title` | 任务标题 | String | 最大长度100 | 是 | -| `description` | 任务描述 | String | 最大长度1000 | 是 | -| `assigneeId` | 被指派的网格员ID | Long | 外键关联UserAccount | 否 | -| `createdBy` | 创建者ID | Long | 外键关联UserAccount | 是 | -| `status` | 任务状态 | Enum | CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED | 是 | -| `priority` | 优先级 | Enum | LOW, MEDIUM, HIGH, URGENT | 是 | -| `longitude` | 任务地点经度 | Double | 有效范围 | 是 | -| `latitude` | 任务地点纬度 | Double | 有效范围 | 是 | -| `deadline` | 截止日期 | DateTime | ISO 8601格式 | 否 | -| `completionReport` | 完成报告 | String | 最大长度2000 | 否 | -| `resultImageUrls` | 结果图片URL列表 | Array | 可为空 | 否 | -| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 | -| `assignedAt` | 分配时间 | DateTime | ISO 8601格式 | 否 | -| `startedAt` | 开始时间 | DateTime | ISO 8601格式 | 否 | -| `submittedAt` | 提交时间 | DateTime | ISO 8601格式 | 否 | -| `approvedAt` | 审核通过时间 | DateTime | ISO 8601格式 | 否 | -| `rejectionReason` | 拒绝原因 | String | 最大长度500 | 否 | - -### 8.4 网格 (Grid) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 网格唯一标识符 | Long | 主键,自增 | 是 | -| `gridX` | 网格X坐标 | Integer | 非负整数 | 是 | -| `gridY` | 网格Y坐标 | Integer | 非负整数 | 是 | -| `cityName` | 所属城市 | String | 最大长度50 | 是 | -| `districtName` | 所属区县 | String | 最大长度50 | 是 | -| `description` | 网格描述 | String | 最大长度200 | 否 | -| `isObstacle` | 是否为障碍物 | Boolean | true/false | 是 | -| `responsibleUserId` | 负责人ID | Long | 外键关联UserAccount | 否 | -| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 | -| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | - - + +# 需求定义文档 + +## 系统分析 + +### 1. 项目介绍 + +#### 1.1 项目背景 + +环境监测系统(EMS)是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速,环境污染问题日益凸显,传统的环境问题上报和处理机制存在流程繁琐、响应缓慢、缺乏透明度等问题。本系统旨在通过数字化手段,构建一个连接公众、管理部门和执行人员的环境监测与治理平台,实现环境问题的快速发现、高效处理和全程监督。 + +#### 1.2 项目目标 + +1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。 +2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。 +3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。 +4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。 +5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。 + +### 2. 业务分析 + +#### 2.1 业务痛点 + +1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。 +2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。 +3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。 +4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。 +5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。 + +#### 2.2 业务价值 + +1. **提升公众满意度**:通过快速响应和处理环境问题,提高公众对环境治理工作的满意度。 +2. **优化资源配置**:基于数据分析和智能算法,实现人力资源的科学分配,提高资源利用效率。 +3. **降低管理成本**:减少人工干预和纸质流程,降低管理成本,提高工作效率。 +4. **提升环境质量**:通过高效处理环境问题,改善城市环境质量,提升居民生活品质。 +5. **强化问责机制**:通过全流程记录和追踪,明确责任分工,强化问责机制。 + +#### 2.3 核心业务流程 + +环境监测系统的核心业务流程包括问题发现与上报、内容审核、任务创建与分配、任务执行与监督、结果审核与反馈等环节,形成一个完整的闭环管理体系。 + +1. **问题发现与上报**:公众通过移动端应用发现环境问题并提交反馈,包括问题描述、位置信息和图片证据。 +2. **内容审核**:系统通过AI技术对上报内容进行初步审核,筛选出有效信息,并由主管进行人工复核确认。 +3. **任务创建与分配**:对于审核通过的反馈,系统自动创建任务,并基于智能算法为任务推荐最合适的处理人员。 +4. **任务执行与监督**:网格员接收任务后,前往现场处理问题,并记录处理过程和结果。 +5. **结果审核与反馈**:主管对处理结果进行审核,确认任务是否完成,并将处理结果反馈给问题上报者。 + +### 3. 功能分析 + +#### 3.1 用户角色分析 + +环境监测系统涉及四类主要用户角色,每个角色在系统中承担不同的职责: + +1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。 +2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。 +3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。 +4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。 + +#### 3.2 用例分析 + +##### 3.2.1 用例图 + +```mermaid +graph TD + %% 定义角色 + PublicUser["公众用户"] + GridWorker["网格员"] + Supervisor["主管"] + Admin["管理员"] + + %% 定义用例 + UC1["注册与登录"] + UC2["提交环境问题反馈"] + UC3["查看反馈处理进度"] + UC4["接收任务通知"] + UC5["执行任务"] + UC6["提交处理结果"] + UC7["审核反馈内容"] + UC8["分配任务"] + UC9["审核处理结果"] + UC10["查看统计数据"] + UC11["管理用户账户"] + UC12["配置系统参数"] + + %% 建立关系 + PublicUser --> UC1 + PublicUser --> UC2 + PublicUser --> UC3 + + GridWorker --> UC1 + GridWorker --> UC4 + GridWorker --> UC5 + GridWorker --> UC6 + + Supervisor --> UC1 + Supervisor --> UC7 + Supervisor --> UC8 + Supervisor --> UC9 + Supervisor --> UC10 + + Admin --> UC1 + Admin --> UC10 + Admin --> UC11 + Admin --> UC12 + + %% 设置样式 + classDef actor fill:#f9f,stroke:#333,stroke-width:2px + classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px + + class PublicUser,GridWorker,Supervisor,Admin actor + class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12 usecase +``` + +##### 3.2.2 主要用例描述 + +**用例1:提交环境问题反馈** + +- **参与者**:公众用户 +- **前置条件**:用户已登录系统 +- **基本流程**: + 1. 用户选择"提交反馈"功能 + 2. 系统显示反馈提交表单 + 3. 用户填写问题标题、描述、污染类型、严重程度 + 4. 用户上传问题现场图片 + 5. 用户标记问题发生的地理位置 + 6. 用户提交表单 + 7. 系统验证表单数据 + 8. 系统生成唯一事件ID并保存反馈信息 + 9. 系统返回提交成功提示 +- **替代流程**: + - 如果表单验证失败,系统提示错误信息并返回表单页面 + - 如果图片上传失败,系统提示重新上传 +- **后置条件**:反馈信息被保存,状态设为"待审核" + +**用例2:审核反馈内容** + +- **参与者**:主管 +- **前置条件**:主管已登录系统,有待审核的反馈 +- **基本流程**: + 1. 主管进入反馈审核页面 + 2. 系统显示待审核反馈列表 + 3. 主管选择一条反馈查看详情 + 4. 系统显示反馈详细信息和AI审核建议 + 5. 主管审核内容并做出决定(通过/驳回) + 6. 如果通过,主管确认创建任务 + 7. 如果驳回,主管填写驳回理由 + 8. 系统更新反馈状态并通知相关人员 +- **替代流程**: + - 如果需要更多信息,主管可以暂缓决定,标记为"需补充信息" +- **后置条件**:反馈状态更新为"已处理"或"已驳回" + +**用例3:执行任务** + +- **参与者**:网格员 +- **前置条件**:网格员已登录系统,已接受任务 +- **基本流程**: + 1. 网格员查看任务详情 + 2. 系统显示任务信息和位置 + 3. 网格员请求路径规划 + 4. 系统生成最优路径 + 5. 网格员前往现场处理问题 + 6. 网格员记录处理过程 + 7. 网格员上传处理结果和证明材料 + 8. 系统保存处理信息并更新任务状态 +- **替代流程**: + - 如果任务无法完成,网格员可提交说明并请求重新分配 +- **后置条件**:任务状态更新为"已提交",等待主管审核 + +#### 3.3 活动图分析 + +##### 3.3.1 反馈提交与处理活动图 + +```mermaid +stateDiagram-v2 + [*] --> 发现环境问题 + 发现环境问题 --> 填写反馈表单 + 填写反馈表单 --> 上传图片 + 上传图片 --> 标记位置 + 标记位置 --> 提交反馈 + 提交反馈 --> AI自动审核 + + state AI自动审核 { + [*] --> 内容分析 + 内容分析 --> 垃圾信息检测 + 垃圾信息检测 --> 分类与评级 + 分类与评级 --> [*] + } + + AI自动审核 --> 判断AI审核结果 + 判断AI审核结果 --> 明显无效: AI拒绝 + 判断AI审核结果 --> 需人工确认: 需确认 + + 明显无效 --> 标记为AI_REJECTED + 标记为AI_REJECTED --> 通知提交者 + 通知提交者 --> [*] + + 需人工确认 --> 主管人工审核 + 主管人工审核 --> 判断审核结果 + + 判断审核结果 --> 驳回: 不通过 + 判断审核结果 --> 通过: 通过 + + 驳回 --> 填写驳回理由 + 填写驳回理由 --> 更新状态为REJECTED + 更新状态为REJECTED --> 通知提交者反馈被驳回 + 通知提交者反馈被驳回 --> [*] + + 通过 --> 创建任务 + 创建任务 --> 更新反馈状态为PROCESSED + 更新反馈状态为PROCESSED --> 任务分配流程 + 任务分配流程 --> [*] +``` + +##### 3.3.2 任务分配与执行活动图 + +```mermaid +stateDiagram-v2 + [*] --> 任务创建完成 + 任务创建完成 --> 选择分配方式 + + state 选择分配方式 { + [*] --> 手动分配 + [*] --> 智能推荐 + + 智能推荐 --> 运行分配算法 + 运行分配算法 --> 推荐最佳人选 + 推荐最佳人选 --> 确认人选 + + 手动分配 --> 选择特定网格员 + 选择特定网格员 --> 确认人选 + + 确认人选 --> [*] + } + + 选择分配方式 --> 创建任务分配记录 + 创建任务分配记录 --> 更新任务状态为ASSIGNED + 更新任务状态为ASSIGNED --> 通知网格员 + + 通知网格员 --> 网格员接收通知 + 网格员接收通知 --> 判断是否接受 + + 判断是否接受 --> 拒绝: 拒绝 + 判断是否接受 --> 接受: 接受 + + 拒绝 --> 选择分配方式 + + 接受 --> 更新状态为IN_PROGRESS + 更新状态为IN_PROGRESS --> 获取路径规划 + 获取路径规划 --> 前往现场处理 + 前往现场处理 --> 记录处理过程 + 记录处理过程 --> 上传处理结果 + 上传处理结果 --> 提交处理结果 + 提交处理结果 --> 更新状态为SUBMITTED + + 更新状态为SUBMITTED --> 主管审核结果 + 主管审核结果 --> 判断结果是否合格 + + 判断结果是否合格 --> 不合格: 不合格 + 判断结果是否合格 --> 合格: 合格 + + 不合格 --> 填写原因要求重新处理 + 填写原因要求重新处理 --> 获取路径规划 + + 合格 --> 确认任务完成 + 确认任务完成 --> 更新任务状态为APPROVED + 更新任务状态为APPROVED --> 更新反馈状态为CLOSED + 更新反馈状态为CLOSED --> 通知反馈提交者 + 通知反馈提交者 --> 更新统计数据 + 更新统计数据 --> [*] +``` + +### 4. 可行性分析 + +#### 4.1 技术可行性 + +1. **前端技术**:采用Vue 3框架构建用户界面,结合Element Plus组件库,可以快速开发出美观、响应式的Web应用,满足不同设备的访问需求。 +2. **后端技术**:基于Spring Boot 3框架和Java 17,具备高性能、高并发处理能力,能够满足系统的稳定性和扩展性需求。 +3. **地图服务**:可以集成百度地图、高德地图等成熟的地图API,实现地理位置标记、路径规划等功能。 +4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。 +5. **数据存储**:采用JSON文件存储方案,简化部署和维护,适合中小规模系统的快速实现。 + +#### 4.2 经济可行性 + +1. **开发成本**:采用主流开源框架和技术栈,降低开发成本和技术门槛。 +2. **维护成本**:模块化设计和完善的文档,降低后期维护和升级成本。 +3. **投资回报**:通过提高环境问题处理效率,减少人力资源浪费,长期来看具有良好的投资回报。 +4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。 + +#### 4.3 操作可行性 + +1. **用户接受度**:系统界面设计简洁直观,操作流程符合用户习惯,易于被各类用户接受和使用。 +2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。 +3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。 + +#### 4.4 法律可行性 + +1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。 +2. **知识产权**:系统开发过程中使用的第三方库和组件均为开源或已获得授权,不存在知识产权风险。 +3. **合规性**:系统功能和流程设计符合相关法律法规和行业标准,确保合法合规运营。 + +## 5. 整体业务流程 + +下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程。 + +```mermaid +flowchart TD + %% 定义样式 + classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333 + classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333 + classDef worker fill:#d5e8d4,stroke:#82b366,color:#333 + classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333 + classDef decision fill:#f8cecc,stroke:#b85450,color:#333 + classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px + + %% 流程开始 + A([开始]) --> B["[公众端] 发现环境问题"] + B --> C["[公众端] 提交反馈
(标题/描述/图片/位置)"] + + %% 平台接收与AI处理 + C --> D["[平台] 接收反馈
生成唯一事件ID"] + D --> E{"[平台] AI自动审核
分析内容/分类"} + + %% AI审核分支 + E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"] + F1 --> F2["[平台] 通知提交者"] + F2 --> Z1([结束]) + + E -- "需人工确认" --> G["[主管] 查看反馈详情
进行人工审核"] + + %% 主管审核分支 + G --> H{"[主管] 审核决定"} + H -- "驳回" --> I1["[主管] 填写驳回理由"] + I1 --> I2["[平台] 更新状态为REJECTED"] + I2 --> I3["[平台] 通知提交者"] + I3 --> Z2([结束]) + + %% 审核通过,创建任务 + H -- "通过" --> J1["[主管] 确认反馈有效"] + J1 --> J2["[平台] 自动创建结构化任务"] + J2 --> J3["[平台] 更新反馈状态为PROCESSED"] + + %% 任务分配 + J3 --> K1{"[主管] 选择分配方式"} + K1 -- "手动分配" --> K2["[主管] 选择特定网格员"] + K1 -- "智能推荐" --> K3["[平台] 运行分配算法
考虑位置/负载/专长"] + K3 --> K4["[平台] 推荐最佳人选"] + K4 --> K2 + K2 --> K5["[平台] 创建任务分配记录
更新任务状态为ASSIGNED"] + K5 --> K6["[平台] 通知网格员"] + + %% 网格员处理 + K6 --> L1["[网格员] 接收任务通知"] + L1 --> L2{"[网格员] 接受任务?"} + L2 -- "拒绝" --> K1 + L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"] + L3 --> L4["[网格员] 查看任务详情
获取路径规划"] + L4 --> L5["[网格员] 前往现场处理"] + L5 --> L6["[网格员] 记录处理过程
上传证明材料"] + L6 --> L7["[网格员] 提交处理结果
更新状态为SUBMITTED"] + + %% 主管审核结果 + L7 --> M1["[主管] 审核处理结果"] + M1 --> M2{"[主管] 结果是否合格?"} + M2 -- "不合格" --> M3["[主管] 填写原因
要求重新处理"] + M3 --> L4 + + %% 完成流程 + M2 -- "合格" --> N1["[主管] 确认任务完成"] + N1 --> N2["[平台] 更新任务状态为APPROVED"] + N2 --> N3["[平台] 更新反馈状态为CLOSED"] + N3 --> N4["[平台] 通知反馈提交者"] + N4 --> N5["[平台] 更新统计数据"] + N5 --> O["[决策层] 查看数据看板
分析环境趋势"] + O --> Z3([结束]) + + %% 为节点添加类别 + class A,Z1,Z2,Z3 start_end + class B,C,F2,I3,N4 public + class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform + class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor + class L1,L2,L3,L4,L5,L6,L7 worker + class O decision +``` + +## 6. 功能性需求 + +### 6.1 功能层次方框图 + +```mermaid +flowchart TD + %% 用户交互层 + subgraph "用户交互层" + direction LR + C["公众服务模块\n(问题上报)"] + H["个人中心模块\n(我的反馈/资料)"] + D["管理驾驶舱\n(数据决策)"] + end + + %% 核心业务层 + subgraph "核心业务层" + direction LR + AI["AI分析模块\n(内容审核)"] + E["任务管理模块\n(分配、流转、执行)"] + end + + %% 应用支撑层 + subgraph "应用支撑层" + direction LR + F["网格与地图模块\n(LBS & 寻路)"] + I["文件服务模块\n(附件存取)"] + end + + %% 基础服务层 + subgraph "基础服务层" + direction LR + B["用户与认证模块"] + G["系统管理模块\n(用户/权限)"] + J["日志审计模块"] + end + + %% 定义关系 + C -- "提交反馈" --> AI + AI -- "分析结果" --> E + C -- "附件" --> I + E -- "调用" --> F + E -- "任务附件" --> I + E -- "统计数据" --> D + + H -- "查询个人数据" --> E + + %% 基础服务支撑所有上层模块 (关系隐含) + G -- "管理" --> B + + classDef userLayer fill:#d4f1f9,stroke:#05a8e5; + classDef coreLayer fill:#ffe6cc,stroke:#f7a128; + classDef appSupportLayer fill:#d5e8d4,stroke:#82b366; + classDef baseLayer fill:#e1d5e7,stroke:#9673a6; + + class C,H,D userLayer; + class AI,E coreLayer; + class F,I appSupportLayer; + class B,G,J baseLayer; +``` + +### 6.2 需求描述 + +#### 6.2.1 用户与认证模块 + +| 功能名称 | 用户与认证模块 | +| :--- | :--- | +| **优先级** | 高 | +| **业务背景** | 作为系统安全的基础,本模块负责管理所有用户的身份验证和访问控制,确保系统资源只能被授权用户访问,同时提供灵活的角色权限管理。 | +| **功能说明** | 1. **用户认证**:基于JWT (JSON Web Token) 的安全认证机制,支持账号密码登录,提供令牌刷新功能。
2. **权限控制**:基于RBAC (基于角色的访问控制) 模型,预设管理员、主管、网格员等角色,每个角色拥有特定的API访问权限。
3. **密码管理**:支持安全的密码重置流程,包括邮箱验证码验证,以及定期密码更新提醒。
4. **会话管理**:支持单点登录或多设备登录控制,可配置会话超时策略。 | +| **约束条件** | 1. 密码必须符合复杂度要求(至少8位,包含大小写字母、数字和特殊字符)。
2. 敏感操作(如修改权限)需要二次验证。
3. 密码在数据库中必须使用BCrypt等强哈希算法加密存储。 | +| **相关查询** | 1. 按用户名、邮箱或手机号查询用户信息。
2. 查询特定角色的所有用户列表。
3. 查询用户的权限和访问历史。 | +| **其他需求** | 1. 登录失败超过预设次数后,账户应被临时锁定。
2. 系统应记录所有关键安全事件(登录、权限变更等)的审计日志。 | +| **裁剪说明** | 不可裁剪,此为系统安全的基础组件。 | + +#### 6.2.2 反馈管理模块 + +| 功能名称 | 反馈管理模块 | +| :--- | :--- | +| **优先级** | 高 | +| **业务背景** | 作为系统的核心输入端口,本模块负责收集、处理和跟踪所有环境问题反馈,是连接公众与管理部门的桥梁,也是后续任务创建的数据源。 | +| **功能说明** | 1. **反馈提交**:提供结构化的表单接口,支持文字描述、污染类型分类、严重程度评估、地理位置标记和多媒体附件上传。
2. **AI内容审核**:集成智能审核服务,对反馈内容进行自动分析,识别垃圾信息、重复提交,并进行初步的分类和紧急程度评估。
3. **人工审核工作台**:为主管提供高效的反馈审核界面,支持批量处理、快速预览和详情查看。
4. **状态追踪**:完整记录反馈从提交到处理完成的全生命周期状态变更,支持多维度的统计和查询。 | +| **约束条件** | 1. 反馈提交必须包含至少一张图片和准确的地理位置信息。
2. AI审核结果仅作为参考,最终决定权在人工审核者手中。
3. 对于紧急程度被标记为"高"的反馈,系统应在1小时内完成审核。 | +| **相关查询** | 1. 按状态、时间段、区域、污染类型等多维度查询反馈列表。
2. 查看特定反馈的详细信息和处理历史。
3. 统计不同类型反馈的数量分布和处理效率。 | +| **其他需求** | 1. 支持反馈的优先级标记和升级处理。
2. 对于同一区域短时间内的多个相似反馈,系统应能智能识别并提示可能的重复。 | +| **裁剪说明** | AI审核功能可在初期简化实现,但反馈的基本提交和人工审核流程不可裁剪。 | + +#### 6.2.3 任务管理模块 + +| 功能名称 | 任务管理模块 | +| :--- | :--- | +| **优先级** | 高 | +| **业务背景** | 本模块是系统的核心业务处理单元,负责将审核通过的反馈转化为可执行的工作任务,并对任务的分配、执行和完成进行全流程管理。 | +| **功能说明** | 1. **任务创建**:支持从反馈自动生成任务,也支持主管手动创建临时任务,包含任务描述、位置、截止时间、优先级等信息。
2. **智能分配**:基于多因素(网格员位置、当前负载、专业技能、历史表现)的任务分配算法,为每个任务推荐最合适的处理人员。
3. **任务执行跟踪**:记录任务的每个状态变更(已分配、已接受、进行中、已提交、已审核),支持网格员实时上报处理进度。
4. **结果审核**:主管对网格员提交的处理结果进行审核,可以通过或驳回,并提供反馈意见。
5. **任务看板**:直观展示不同状态任务的数量和分布,支持拖拽操作进行状态更新。 | +| **约束条件** | 1. 任务必须关联到一个有效的反馈或由授权主管手动创建。
2. 任务分配时必须考虑网格员的工作区域和当前任务负载。
3. 高优先级任务应在24小时内分配并开始处理。
4. 任务提交时必须包含处理过程描述和至少一张结果照片。 | +| **相关查询** | 1. 按状态、负责人、时间段、区域等多维度查询任务列表。
2. 查看特定任务的详细信息、处理历史和相关反馈。
3. 统计不同网格员的任务完成率、平均处理时长等绩效指标。 | +| **其他需求** | 1. 支持任务的紧急程度升级和重新分配。
2. 对于长时间未处理的任务,系统应自动发送提醒通知。
3. 支持批量导出任务报告,用于绩效评估和工作汇报。 | +| **裁剪说明** | 智能分配算法可以在初期简化实现,但任务的基本创建、分配和状态管理功能不可裁剪。 | + +#### 6.2.4 网格与地图模块 + +| 功能名称 | 网格与地图模块 | +| :--- | :--- | +| **优先级** | 中 | +| **业务背景** | 本模块负责对地理空间进行网格化管理,将城市区域划分为可管理的网格单元,并为任务执行提供地理位置支持和路径规划。 | +| **功能说明** | 1. **网格定义与管理**:支持管理员定义和维护城市网格系统,包括网格的坐标、属性(如是否为障碍物)和责任人分配。
2. **A*寻路算法服务**:基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划,考虑距离、交通状况和障碍物。
3. **地图可视化**:在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。
4. **区域统计**:基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | +| **约束条件** | 1. 网格系统应支持多级划分,最小网格单元不应大于500米×500米。
2. 路径规划应考虑实际道路情况和障碍物,避免不可通行区域。
3. 地图数据应定期更新,确保准确性。 | +| **相关查询** | 1. 查询特定区域内的网格定义和属性。
2. 查询特定网格的历史问题记录和统计数据。
3. 获取两点间的最优路径规划。 | +| **其他需求** | 1. 支持网格责任人的灵活调整和临时替换。
2. 地图界面应支持常见的交互操作(缩放、平移、点选)。
3. 支持离线地图数据缓存,保证在网络不稳定情况下的基本功能。 | +| **裁剪说明** | A*寻路算法的高级功能(如考虑实时交通)可以在后期迭代中实现,但基本的网格定义和地图展示功能不应裁剪。 | + +#### 6.2.5 决策支持模块 + +| 功能名称 | 决策支持模块 (管理驾驶舱) | +| :--- | :--- | +| **优先级** | 中 | +| **业务背景** | 本模块旨在将系统中沉淀的大量业务数据转化为有价值的决策洞察,帮助管理层了解环境状况、评估治理效果、优化资源配置。 | +| **功能说明** | 1. **核心指标看板**:实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。
2. **多维度分析**:支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。
3. **热力图可视化**:在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。
4. **绩效评估**:对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。
5. **预警机制**:基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | +| **约束条件** | 1. 数据分析应基于实时或准实时的业务数据,确保决策的时效性。
2. 图表和报表应支持多种导出格式(PDF、Excel等),便于进一步分析和汇报。
3. 敏感数据(如个人绩效)的访问应受到严格权限控制。 | +| **相关查询** | 1. 按自定义时间范围查询各类统计指标。
2. 查询特定区域或网格员的历史表现数据。
3. 生成定制化的数据报表和分析图表。 | +| **其他需求** | 1. 支持数据看板的个性化配置,满足不同管理者的关注点。
2. 提供数据异常检测和提醒功能,及时发现数据波动。
3. 支持定期自动生成并发送统计报告。 | +| **裁剪说明** | 高级分析功能(如预测模型)可以在后期迭代中实现,但基本的数据统计和可视化功能不应裁剪。 | + +## 7. 需求规定 + +### 7.1 一般性需求 + +* **数据集中管理与共享**:系统应采用统一的数据存储和访问机制,确保各模块间的数据一致性和实时共享。所有业务数据应按照标准化格式进行存储,并通过规范的API进行访问,避免数据孤岛。 + +* **高可用性与可靠性**:系统应保证7x24小时的稳定运行,关键业务流程(如反馈提交、任务分配)的可用性应达到99.9%以上。应实现适当的错误处理和故障恢复机制,确保在出现异常情况时能够快速恢复。 + +* **安全性与合规性**: + * 应用SSL/TLS加密保护所有网络通信。 + * 实现严格的认证和授权机制,确保用户只能访问其权限范围内的数据和功能。 + * 敏感数据(如用户密码)必须加密存储,并限制访问权限。 + * 系统应保留完整的操作日志,支持安全审计和问题追溯。 + * 符合相关的数据保护法规和隐私要求。 + +* **可扩展性与模块化**:系统架构应支持水平扩展和功能模块的灵活添加。核心组件应设计为松耦合的,便于独立升级和替换。API设计应考虑向后兼容性,确保系统可以平滑演进。 + +* **用户体验优化**: + * 界面设计应简洁直观,符合现代Web应用的设计标准。 + * 关键操作路径应尽量简化,减少用户点击次数。 + * 系统响应时间应控制在可接受范围内,核心API的平均响应时间不超过500ms。 + * 提供适当的操作反馈和状态提示,增强用户对系统的信任感。 + * 支持响应式设计,确保在不同设备(PC、平板、手机)上的良好体验。 + +* **国际化与本地化**:系统应支持多语言界面,初期至少支持中英文切换。日期、时间、数字等格式应根据用户的区域设置进行适当显示。 + +* **可维护性与可测试性**:系统代码应遵循统一的编码规范和设计模式,保持良好的可读性和可维护性。核心业务逻辑应有充分的单元测试覆盖,便于后续的功能迭代和质量保障。 + +## 8. 数据描述 + +### 8.1 用户账户 (UserAccount) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 用户唯一标识符 | Long | 主键,自增 | 是 | +| `name` | 用户姓名 | String | 最大长度50 | 是 | +| `phone` | 手机号码 | String | 符合手机号格式 | 是 | +| `email` | 电子邮箱 | String | 符合邮箱格式 | 是 | +| `password` | 密码(加密存储) | String | 最小长度8,包含字母、数字和特殊字符 | 是 | +| `gender` | 性别 | Enum | MALE, FEMALE, OTHER | 否 | +| `role` | 用户角色 | Enum | ADMIN, SUPERVISOR, GRID_WORKER, PUBLIC_USER | 是 | +| `status` | 账户状态 | Enum | ACTIVE, INACTIVE, LOCKED | 是 | +| `gridX` | 网格X坐标(仅网格员) | Integer | 非负整数 | 否 | +| `gridY` | 网格Y坐标(仅网格员) | Integer | 非负整数 | 否 | +| `createdAt` | 账户创建时间 | DateTime | ISO 8601格式 | 是 | +| `updatedAt` | 账户更新时间 | DateTime | ISO 8601格式 | 是 | +| `lastLoginAt` | 最后登录时间 | DateTime | ISO 8601格式 | 否 | + +### 8.2 反馈 (Feedback) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 反馈唯一标识符 | Long | 主键,自增 | 是 | +| `eventId` | 事件ID(业务编号) | String | UUID格式 | 是 | +| `title` | 反馈标题 | String | 最大长度100 | 是 | +| `description` | 问题详细描述 | String | 最大长度1000 | 是 | +| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 | +| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 | +| `longitude` | 地理位置经度 | Double | 有效范围 | 是 | +| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 | +| `imageUrls` | 图片URL列表 | Array | 至少一张图片 | 是 | +| `status` | 反馈状态 | Enum | PENDING_REVIEW, AI_REJECTED, PROCESSED, CLOSED | 是 | +| `submitterId` | 提交者ID | Long | 外键关联UserAccount | 是 | +| `reviewerId` | 审核者ID | Long | 外键关联UserAccount | 否 | +| `reviewNote` | 审核备注 | String | 最大长度500 | 否 | +| `createdAt` | 提交时间 | DateTime | ISO 8601格式 | 是 | +| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | +| `processedAt` | 处理时间 | DateTime | ISO 8601格式 | 否 | + +### 8.3 任务 (Task) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 任务唯一标识符 | Long | 主键,自增 | 是 | +| `feedbackId` | 关联的反馈ID | Long | 外键关联Feedback | 否 | +| `title` | 任务标题 | String | 最大长度100 | 是 | +| `description` | 任务描述 | String | 最大长度1000 | 是 | +| `assigneeId` | 被指派的网格员ID | Long | 外键关联UserAccount | 否 | +| `createdBy` | 创建者ID | Long | 外键关联UserAccount | 是 | +| `status` | 任务状态 | Enum | CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED | 是 | +| `priority` | 优先级 | Enum | LOW, MEDIUM, HIGH, URGENT | 是 | +| `longitude` | 任务地点经度 | Double | 有效范围 | 是 | +| `latitude` | 任务地点纬度 | Double | 有效范围 | 是 | +| `deadline` | 截止日期 | DateTime | ISO 8601格式 | 否 | +| `completionReport` | 完成报告 | String | 最大长度2000 | 否 | +| `resultImageUrls` | 结果图片URL列表 | Array | 可为空 | 否 | +| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 | +| `assignedAt` | 分配时间 | DateTime | ISO 8601格式 | 否 | +| `startedAt` | 开始时间 | DateTime | ISO 8601格式 | 否 | +| `submittedAt` | 提交时间 | DateTime | ISO 8601格式 | 否 | +| `approvedAt` | 审核通过时间 | DateTime | ISO 8601格式 | 否 | +| `rejectionReason` | 拒绝原因 | String | 最大长度500 | 否 | + +### 8.4 网格 (Grid) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 网格唯一标识符 | Long | 主键,自增 | 是 | +| `gridX` | 网格X坐标 | Integer | 非负整数 | 是 | +| `gridY` | 网格Y坐标 | Integer | 非负整数 | 是 | +| `cityName` | 所属城市 | String | 最大长度50 | 是 | +| `districtName` | 所属区县 | String | 最大长度50 | 是 | +| `description` | 网格描述 | String | 最大长度200 | 否 | +| `isObstacle` | 是否为障碍物 | Boolean | true/false | 是 | +| `responsibleUserId` | 负责人ID | Long | 外键关联UserAccount | 否 | +| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 | +| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | + + --- - + ### Report/需求定义_v3.md - -# 需求定义文档 - -## 1. 项目介绍 - -### 1.1 项目背景 - -环境监测系统(EMS)是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速,环境污染问题日益凸显,传统的环境问题上报和处理机制存在流程繁琐、响应缓慢、缺乏透明度等问题。本系统旨在通过数字化手段,构建一个连接公众、管理部门和执行人员的环境监测与治理平台,实现环境问题的快速发现、高效处理和全程监督。 - -### 1.2 项目目标 - -1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。 -2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。 -3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。 -4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。 -5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。 - -## 2. 系统分析 - -### 2.1 业务痛点分析 - -1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。 -2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。 -3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。 -4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。 -5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。 - -### 2.2 用户角色分析 - -环境监测系统涉及五类主要用户角色,每个角色在系统中承担不同的职责: - -1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。 -2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。 -3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。 -4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。 -5. **决策者**:系统的战略端,通过分析系统生成的统计数据和趋势图表,制定环境管理策略和资源分配决策,是系统的最终受益者之一。 -### 2.3 用例分析 - -#### 2.3.1 用例图 - -```mermaid -graph TD - %% 定义角色 - PublicUser["公众用户"] - GridWorker["网格员"] - Supervisor["主管"] - Admin["管理员"] - - %% 定义用例 - UC1["注册与登录"] - UC2["提交环境问题反馈"] - UC3["查看反馈处理进度"] - UC4["接收任务通知"] - UC5["执行任务"] - UC6["提交处理结果"] - UC7["审核反馈内容"] - UC8["分配任务"] - UC9["审核处理结果"] - UC10["查看统计数据"] - UC11["管理用户账户"] - UC12["配置系统参数"] - - %% 建立关系 - PublicUser --> UC1 - PublicUser --> UC2 - PublicUser --> UC3 - - GridWorker --> UC1 - GridWorker --> UC4 - GridWorker --> UC5 - GridWorker --> UC6 - - Supervisor --> UC1 - Supervisor --> UC7 - Supervisor --> UC8 - Supervisor --> UC9 - Supervisor --> UC10 - - Admin --> UC1 - Admin --> UC10 - Admin --> UC11 - Admin --> UC12 - - %% 设置样式 - classDef actor fill:#f9f,stroke:#333,stroke-width:2px - classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px - - class PublicUser,GridWorker,Supervisor,Admin actor - class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12 usecase -``` - -#### 2.3.2 活动图:反馈提交与处理流程 - -```mermaid -stateDiagram-v2 - [*] --> 发现环境问题 - 发现环境问题 --> 填写反馈表单 - 填写反馈表单 --> 上传图片 - 上传图片 --> 标记位置 - 标记位置 --> 提交反馈 - 提交反馈 --> AI自动审核 - - state AI自动审核 { - [*] --> 内容分析 - 内容分析 --> 垃圾信息检测 - 垃圾信息检测 --> 分类与评级 - 分类与评级 --> [*] - } - - AI自动审核 --> 判断AI审核结果 - 判断AI审核结果 --> 明显无效: AI拒绝 - 判断AI审核结果 --> 需人工确认: 需确认 - - 明显无效 --> 标记为AI_REJECTED - 标记为AI_REJECTED --> 通知提交者 - 通知提交者 --> [*] - - 需人工确认 --> 主管人工审核 - 主管人工审核 --> 判断审核结果 - - 判断审核结果 --> 驳回: 不通过 - 判断审核结果 --> 通过: 通过 - - 驳回 --> 填写驳回理由 - 填写驳回理由 --> 更新状态为REJECTED - 更新状态为REJECTED --> 通知提交者反馈被驳回 - 通知提交者反馈被驳回 --> [*] - - 通过 --> 创建任务 - 创建任务 --> 更新反馈状态为PROCESSED - 更新反馈状态为PROCESSED --> 任务分配流程 - 任务分配流程 --> [*] -``` - -#### 2.3.3 活动图:任务分配与执行流程 - -```mermaid -stateDiagram-v2 - [*] --> 任务创建完成 - 任务创建完成 --> 选择分配方式 - - state 选择分配方式 { - [*] --> 手动分配 - [*] --> 智能推荐 - - 智能推荐 --> 运行分配算法 - 运行分配算法 --> 推荐最佳人选 - 推荐最佳人选 --> 确认人选 - - 手动分配 --> 选择特定网格员 - 选择特定网格员 --> 确认人选 - - 确认人选 --> [*] - } - - 选择分配方式 --> 创建任务分配记录 - 创建任务分配记录 --> 更新任务状态为ASSIGNED - 更新任务状态为ASSIGNED --> 通知网格员 - - 通知网格员 --> 网格员接收通知 - 网格员接收通知 --> 判断是否接受 - - 判断是否接受 --> 拒绝: 拒绝 - 判断是否接受 --> 接受: 接受 - - 拒绝 --> 选择分配方式 - - 接受 --> 更新状态为IN_PROGRESS - 更新状态为IN_PROGRESS --> 获取路径规划 - 获取路径规划 --> 前往现场处理 - 前往现场处理 --> 记录处理过程 - 记录处理过程 --> 上传处理结果 - 上传处理结果 --> 提交处理结果 - 提交处理结果 --> 更新状态为SUBMITTED - - 更新状态为SUBMITTED --> 主管审核结果 - 主管审核结果 --> 判断结果是否合格 - - 判断结果是否合格 --> 不合格: 不合格 - 判断结果是否合格 --> 合格: 合格 - - 不合格 --> 填写原因要求重新处理 - 填写原因要求重新处理 --> 获取路径规划 - - 合格 --> 确认任务完成 - 确认任务完成 --> 更新任务状态为APPROVED - 更新任务状态为APPROVED --> 更新反馈状态为CLOSED - 更新反馈状态为CLOSED --> 通知反馈提交者 - 通知反馈提交者 --> 更新统计数据 - 更新统计数据 --> [*] -``` - -### 2.4 系统可行性分析 - -#### 2.4.1 技术可行性 - -1. **前端技术**:采用Vue 3框架构建用户界面,结合Element Plus组件库,可以快速开发出美观、响应式的Web应用,满足不同设备的访问需求。 -2. **后端技术**:基于Spring Boot 3框架和Java 17,具备高性能、高并发处理能力,能够满足系统的稳定性和扩展性需求。 -3. **地图服务**:可以集成百度地图、高德地图等成熟的地图API,实现地理位置标记、路径规划等功能。 -4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。 -5. **数据存储**:采用JSON文件存储方案,简化部署和维护,适合中小规模系统的快速实现。 - -#### 2.4.2 经济可行性 - -1. **开发成本**:采用主流开源框架和技术栈,降低开发成本和技术门槛。 -2. **维护成本**:模块化设计和完善的文档,降低后期维护和升级成本。 -3. **投资回报**:通过提高环境问题处理效率,减少人力资源浪费,长期来看具有良好的投资回报。 -4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。 - -#### 2.4.3 操作可行性 - -1. **用户接受度**:系统界面设计简洁直观,操作流程符合用户习惯,易于被各类用户接受和使用。 -2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。 -3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。 - -#### 2.4.4 法律可行性 - -1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。 -2. **知识产权**:系统开发过程中使用的第三方库和组件均为开源或已获得授权,不存在知识产权风险。 -3. **合规性**:系统功能和流程设计符合相关法律法规和行业标准,确保合法合规运营。 - -## 3. 功能性需求 - -### 3.1 功能层次方框图 - -```mermaid -flowchart TD - %% 用户交互层 - subgraph "用户交互层" - direction LR - C["公众服务模块(问题上报)"] - H["个人中心模块(我的反馈/资料)"] - D["管理驾驶舱(数据决策)"] - end - - %% 核心业务层 - subgraph "核心业务层" - direction LR - AI["AI分析模块(内容审核)"] - E["任务管理模块(分配、流转、执行)"] - end - - %% 应用支撑层 - subgraph "应用支撑层" - direction LR - F["网格与地图模块(LBS & 寻路)"] - I["文件服务模块(附件存取)"] - end - - %% 基础服务层 - subgraph "基础服务层" - direction LR - B["用户与认证模块"] - G["系统管理模块(用户/权限)"] - J["日志审计模块"] - end - - %% 定义关系 - C -- "提交反馈" --> AI - AI -- "分析结果" --> E - C -- "附件" --> I - E -- "调用" --> F - E -- "任务附件" --> I - E -- "统计数据" --> D - - H -- "查询个人数据" --> E - - %% 基础服务支撑所有上层模块 (关系隐含) - G -- "管理" --> B - - classDef userLayer fill:#d4f1f9,stroke:#05a8e5; - classDef coreLayer fill:#ffe6cc,stroke:#f7a128; - classDef appSupportLayer fill:#d5e8d4,stroke:#82b366; - classDef baseLayer fill:#e1d5e7,stroke:#9673a6; - - class C,H,D userLayer; - class AI,E coreLayer; - class F,I appSupportLayer; - class B,G,J baseLayer; -``` - -### 3.2 模块功能概述 - -| 模块名称 | 功能概述 | -| :--- | :--- | -| **用户与认证模块** | 管理用户身份验证、权限控制和会话管理,确保系统安全性。提供用户注册、登录、密码重置等功能。 | -| **反馈管理模块** | 处理公众提交的环境问题反馈,包括提交、审核、状态追踪和查询统计等功能。 | -| **任务管理模块** | 将审核通过的反馈转化为工作任务,并管理任务的分配、执行和完成全流程。 | -| **网格与地图模块** | 提供地理空间的网格化管理,支持路径规划和地图可视化,辅助任务执行。 | -| **决策支持模块** | 通过数据分析和可视化,为管理层提供决策支持,帮助优化资源配置和治理策略。 | -| **系统管理模块** | 提供系统配置、用户管理、权限设置等基础功能,保障系统正常运行。 | -| **文件服务模块** | 处理系统中的文件上传、存储和访问,支持反馈和任务的附件管理。 | -| **日志审计模块** | 记录系统操作日志,支持安全审计和问题追溯,确保系统运行的可追溯性。 | - -## 4. 整体业务流程 - -下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程。 - -```mermaid -flowchart TD - %% 定义样式 - classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333 - classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333 - classDef worker fill:#d5e8d4,stroke:#82b366,color:#333 - classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333 - classDef decision fill:#f8cecc,stroke:#b85450,color:#333 - classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px - - %% 流程开始 - A([开始]) --> B["[公众端] 发现环境问题"] - B --> C["[公众端] 提交反馈
(标题/描述/图片/位置)"] - - %% 平台接收与AI处理 - C --> D["[平台] 接收反馈
生成唯一事件ID"] - D --> E{"[平台] AI自动审核
分析内容/分类"} - - %% AI审核分支 - E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"] - F1 --> F2["[平台] 通知提交者"] - F2 --> Z1([结束]) - - E -- "需人工确认" --> G["[主管] 查看反馈详情
进行人工审核"] - - %% 主管审核分支 - G --> H{"[主管] 审核决定"} - H -- "驳回" --> I1["[主管] 填写驳回理由"] - I1 --> I2["[平台] 更新状态为REJECTED"] - I2 --> I3["[平台] 通知提交者"] - I3 --> Z2([结束]) - - %% 审核通过,创建任务 - H -- "通过" --> J1["[主管] 确认反馈有效"] - J1 --> J2["[平台] 自动创建结构化任务"] - J2 --> J3["[平台] 更新反馈状态为PROCESSED"] - - %% 任务分配 - J3 --> K1{"[主管] 选择分配方式"} - K1 -- "手动分配" --> K2["[主管] 选择特定网格员"] - K1 -- "智能推荐" --> K3["[平台] 运行分配算法
考虑位置/负载/专长"] - K3 --> K4["[平台] 推荐最佳人选"] - K4 --> K2 - K2 --> K5["[平台] 创建任务分配记录
更新任务状态为ASSIGNED"] - K5 --> K6["[平台] 通知网格员"] - - %% 网格员处理 - K6 --> L1["[网格员] 接收任务通知"] - L1 --> L2{"[网格员] 接受任务?"} - L2 -- "拒绝" --> K1 - L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"] - L3 --> L4["[网格员] 查看任务详情
获取路径规划"] - L4 --> L5["[网格员] 前往现场处理"] - L5 --> L6["[网格员] 记录处理过程
上传证明材料"] - L6 --> L7["[网格员] 提交处理结果
更新状态为SUBMITTED"] - - %% 主管审核结果 - L7 --> M1["[主管] 审核处理结果"] - M1 --> M2{"[主管] 结果是否合格?"} - M2 -- "不合格" --> M3["[主管] 填写原因
要求重新处理"] - M3 --> L4 - - %% 完成流程 - M2 -- "合格" --> N1["[主管] 确认任务完成"] - N1 --> N2["[平台] 更新任务状态为APPROVED"] - N2 --> N3["[平台] 更新反馈状态为CLOSED"] - N3 --> N4["[平台] 通知反馈提交者"] - N4 --> N5["[平台] 更新统计数据"] - N5 --> O["[决策层] 查看数据看板
分析环境趋势"] - O --> Z3([结束]) - - %% 为节点添加类别 - class A,Z1,Z2,Z3 start_end - class B,C,F2,I3,N4 public - class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform - class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor - class L1,L2,L3,L4,L5,L6,L7 worker - class O decision -``` - -## 5. 详细需求描述 - -### 5.1 用户与认证模块 - -| 功能名称 | 用户与认证模块 | -| :--- | :--- | -| **优先级** | 高 | -| **业务背景** | 作为系统安全的基础,本模块负责管理所有用户的身份验证和访问控制,确保系统资源只能被授权用户访问,同时提供灵活的角色权限管理。 | -| **功能说明** | 1. **用户认证**:基于JWT (JSON Web Token) 的安全认证机制,支持账号密码登录,提供令牌刷新功能。
2. **权限控制**:基于RBAC (基于角色的访问控制) 模型,预设管理员、主管、网格员等角色,每个角色拥有特定的API访问权限。
3. **密码管理**:支持安全的密码重置流程,包括邮箱验证码验证,以及定期密码更新提醒。
4. **会话管理**:支持单点登录或多设备登录控制,可配置会话超时策略。 | -| **约束条件** | 1. 密码必须符合复杂度要求(至少8位,包含大小写字母、数字和特殊字符)。
2. 敏感操作(如修改权限)需要二次验证。
3. 密码在数据库中必须使用BCrypt等强哈希算法加密存储。
4. 登录失败超过预设次数后,账户应被临时锁定。 | -| **相关查询** | 1. 按用户名、邮箱或手机号查询用户信息。
2. 查询特定角色的所有用户列表。
3. 查询用户的权限和访问历史。 | -| **其他需求** | 1. 系统应记录所有关键安全事件(登录、权限变更等)的审计日志。
2. 支持用户个人资料的维护和更新。 | - -**用户认证流程图**: - -```mermaid -sequenceDiagram - participant User as 用户 - participant AuthController as 认证控制器 - participant AuthService as 认证服务 - participant UserRepository as 用户仓库 - participant JwtUtil as JWT工具 - participant Database as JSON持久化存储 - - User->>AuthController: POST /api/auth/login - AuthController->>AuthService: signIn(LoginRequest) - AuthService->>UserRepository: findByEmailOrPhone() - UserRepository->>Database: 查询用户信息 - Database-->>UserRepository: 返回用户数据 - UserRepository-->>AuthService: 返回UserAccount - AuthService->>AuthService: 验证密码 - AuthService->>JwtUtil: 生成JWT令牌 - JwtUtil-->>AuthService: 返回JWT - AuthService-->>AuthController: JwtAuthenticationResponse - AuthController-->>User: 返回JWT令牌 -``` - -### 5.2 反馈管理模块 - -| 功能名称 | 反馈管理模块 | -| :--- | :--- | -| **优先级** | 高 | -| **业务背景** | 作为系统的核心输入端口,本模块负责收集、处理和跟踪所有环境问题反馈,是连接公众与管理部门的桥梁,也是后续任务创建的数据源。 | -| **功能说明** | 1. **反馈提交**:提供结构化的表单接口,支持文字描述、污染类型分类、严重程度评估、地理位置标记和多媒体附件上传。
2. **AI内容审核**:集成智能审核服务,对反馈内容进行自动分析,识别垃圾信息、重复提交,并进行初步的分类和紧急程度评估。
3. **人工审核工作台**:为主管提供高效的反馈审核界面,支持批量处理、快速预览和详情查看。
4. **状态追踪**:完整记录反馈从提交到处理完成的全生命周期状态变更,支持多维度的统计和查询。 | -| **约束条件** | 1. 反馈提交必须包含至少一张图片和准确的地理位置信息。
2. AI审核结果仅作为参考,最终决定权在人工审核者手中。
3. 对于紧急程度被标记为"高"的反馈,系统应在1小时内完成审核。
4. 同一用户在短时间内(如5分钟)不能重复提交内容高度相似的反馈。 | -| **相关查询** | 1. 按状态、时间段、区域、污染类型等多维度查询反馈列表。
2. 查看特定反馈的详细信息和处理历史。
3. 统计不同类型反馈的数量分布和处理效率。 | -| **其他需求** | 1. 支持反馈的优先级标记和升级处理。
2. 对于同一区域短时间内的多个相似反馈,系统应能智能识别并提示可能的重复。
3. 支持反馈附件的预览和下载。 | - -**反馈提交处理流程图**: - -```mermaid -sequenceDiagram - participant User as 用户 - participant FeedbackController as 反馈控制器 - participant FeedbackService as 反馈服务 - participant FeedbackRepository as 反馈仓库 - participant AIService as AI服务 - participant EventPublisher as 事件发布器 - participant Database as JSON持久化存储 - - User->>FeedbackController: POST /api/feedback/submit - FeedbackController->>FeedbackService: submitFeedback(request, files) - FeedbackService->>FeedbackService: 生成事件ID - FeedbackService->>FeedbackRepository: save(feedback) - FeedbackRepository->>Database: 保存反馈数据 - Database-->>FeedbackRepository: 返回保存结果 - FeedbackRepository-->>FeedbackService: 返回Feedback实体 - FeedbackService->>EventPublisher: 发布反馈创建事件 - EventPublisher->>AIService: 触发AI处理 - AIService->>AIService: 分析反馈内容 - FeedbackService-->>FeedbackController: 返回Feedback - FeedbackController-->>User: 201 Created -``` - -### 5.3 任务管理模块 - -| 功能名称 | 任务管理模块 | -| :--- | :--- | -| **优先级** | 高 | -| **业务背景** | 本模块是系统的核心业务处理单元,负责将审核通过的反馈转化为可执行的工作任务,并对任务的分配、执行和完成进行全流程管理。 | -| **功能说明** | 1. **任务创建**:支持从反馈自动生成任务,也支持主管手动创建临时任务,包含任务描述、位置、截止时间、优先级等信息。
2. **智能分配**:基于多因素(网格员位置、当前负载、专业技能、历史表现)的任务分配算法,为每个任务推荐最合适的处理人员。
3. **任务执行跟踪**:记录任务的每个状态变更(已分配、已接受、进行中、已提交、已审核),支持网格员实时上报处理进度。
4. **结果审核**:主管对网格员提交的处理结果进行审核,可以通过或驳回,并提供反馈意见。
5. **任务看板**:直观展示不同状态任务的数量和分布,支持拖拽操作进行状态更新。 | -| **约束条件** | 1. 任务必须关联到一个有效的反馈或由授权主管手动创建。
2. 任务分配时必须考虑网格员的工作区域和当前任务负载。
3. 高优先级任务应在24小时内分配并开始处理。
4. 任务提交时必须包含处理过程描述和至少一张结果照片。
5. 已完成的任务不能重新分配或修改。 | -| **相关查询** | 1. 按状态、负责人、时间段、区域等多维度查询任务列表。
2. 查看特定任务的详细信息、处理历史和相关反馈。
3. 统计不同网格员的任务完成率、平均处理时长等绩效指标。 | -| **其他需求** | 1. 支持任务的紧急程度升级和重新分配。
2. 对于长时间未处理的任务,系统应自动发送提醒通知。
3. 支持批量导出任务报告,用于绩效评估和工作汇报。 | - -**任务分配流程图**: - -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant TaskController as 任务控制器 - participant TaskService as 任务服务 - participant AssignmentService as 分配服务 - participant UserRepository as 用户仓库 - participant TaskRepository as 任务仓库 - participant GridWorker as 网格工作人员 - - Supervisor->>TaskController: POST /api/tasks/assign - TaskController->>TaskService: assignTask(taskId, workerId) - TaskService->>UserRepository: findById(workerId) - UserRepository-->>TaskService: 返回GridWorker - TaskService->>AssignmentService: createAssignment() - AssignmentService->>TaskRepository: updateTaskStatus() - TaskRepository-->>AssignmentService: 更新成功 - AssignmentService-->>TaskService: Assignment创建成功 - TaskService->>TaskService: 发送通知给工作人员 - TaskService-->>TaskController: 分配成功 - TaskController-->>Supervisor: 200 OK - Note over GridWorker: 接收任务通知 -``` - -### 5.4 网格与地图模块 - -| 功能名称 | 网格与地图模块 | -| :--- | :--- | -| **优先级** | 中 | -| **业务背景** | 本模块负责对地理空间进行网格化管理,将城市区域划分为可管理的网格单元,并为任务执行提供地理位置支持和路径规划。 | -| **功能说明** | 1. **网格定义与管理**:支持管理员定义和维护城市网格系统,包括网格的坐标、属性(如是否为障碍物)和责任人分配。
2. **A*寻路算法服务**:基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划,考虑距离、交通状况和障碍物。
3. **地图可视化**:在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。
4. **区域统计**:基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | -| **约束条件** | 1. 网格系统应支持多级划分,最小网格单元不应大于500米×500米。
2. 路径规划应考虑实际道路情况和障碍物,避免不可通行区域。
3. 地图数据应定期更新,确保准确性。
4. 网格坐标系统必须与地理坐标系统(经纬度)有明确的转换关系。 | -| **相关查询** | 1. 查询特定区域内的网格定义和属性。
2. 查询特定网格的历史问题记录和统计数据。
3. 获取两点间的最优路径规划。
4. 查询特定区域内的网格员分布情况。 | -| **其他需求** | 1. 支持网格责任人的灵活调整和临时替换。
2. 地图界面应支持常见的交互操作(缩放、平移、点选)。
3. 支持离线地图数据缓存,保证在网络不稳定情况下的基本功能。 | - -**路径规划流程图**: - -```mermaid -sequenceDiagram - participant User as 用户 - participant PathfindingController as 寻路控制器 - participant AStarService as A*服务 - participant MapData as 地图数据 - - User->>+PathfindingController: POST /api/pathfinding/find (起点, 终点) - PathfindingController->>+AStarService: findPath(start, end) - AStarService->>+MapData: getObstacles() - MapData-->>-AStarService: 返回障碍物信息 - AStarService->>AStarService: 执行A*算法计算路径 - AStarService-->>-PathfindingController: 返回计算出的路径 - PathfindingController-->>-User: 200 OK (路径坐标列表) -``` - -### 5.5 决策支持模块 - -| 功能名称 | 决策支持模块 (管理驾驶舱) | -| :--- | :--- | -| **优先级** | 中 | -| **业务背景** | 本模块旨在将系统中沉淀的大量业务数据转化为有价值的决策洞察,帮助管理层了解环境状况、评估治理效果、优化资源配置。 | -| **功能说明** | 1. **核心指标看板**:实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。
2. **多维度分析**:支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。
3. **热力图可视化**:在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。
4. **绩效评估**:对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。
5. **预警机制**:基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | -| **约束条件** | 1. 数据分析应基于实时或准实时的业务数据,确保决策的时效性。
2. 图表和报表应支持多种导出格式(PDF、Excel等),便于进一步分析和汇报。
3. 敏感数据(如个人绩效)的访问应受到严格权限控制。
4. 系统应能处理和分析至少一年的历史数据。 | -| **相关查询** | 1. 按自定义时间范围查询各类统计指标。
2. 查询特定区域或网格员的历史表现数据。
3. 生成定制化的数据报表和分析图表。
4. 查询环境问题的时间分布和地理分布。 | -| **其他需求** | 1. 支持数据看板的个性化配置,满足不同管理者的关注点。
2. 提供数据异常检测和提醒功能,及时发现数据波动。
3. 支持定期自动生成并发送统计报告。 | - -**决策者获取仪表盘数据流程图**: - -```mermaid -sequenceDiagram - participant DecisionMaker as 决策者 - participant DashboardController as 仪表盘控制器 - participant DashboardService as 仪表盘服务 - participant variousRepositories as 各类仓库 - participant Database as JSON持久化存储 - - DecisionMaker->>DashboardController: GET /api/dashboard/stats - DashboardController->>DashboardService: getDashboardStats() - DashboardService->>variousRepositories: (并行)调用多个仓库方法获取数据 - variousRepositories->>Database: 查询统计数据 - Database-->>variousRepositories: 返回数据 - variousRepositories-->>DashboardService: 返回统计结果 - DashboardService->>DashboardService: 聚合数据为DashboardStatsDTO - DashboardService-->>DashboardController: 返回DashboardStatsDTO - DashboardController-->>DecisionMaker: 返回核心统计数据 -``` - -### 5.6 决策者角色需求 - -| 功能名称 | 决策者角色需求 | -| :--- | :--- | -| **优先级** | 中 | -| **业务背景** | 决策者是环境监测系统的战略用户,负责根据系统提供的数据分析结果制定环境管理策略和资源分配决策。他们需要全局视角的数据展示和深度分析功能,以支持科学决策。 | -| **功能说明** | 1. **综合仪表盘**:提供环境状况、处理效率、资源利用等多维度的综合数据视图,支持按时间、区域进行筛选。
2. **趋势分析**:展示关键指标的历史变化趋势,支持多指标对比和季节性分析。
3. **资源分配建议**:基于历史数据和预测模型,为人力资源调配和预算分配提供决策建议。
4. **绩效评估报告**:生成网格员、区域、团队的绩效评估报告,支持多维度比较。
5. **预警监控**:实时监控环境问题高发区域和异常情况,支持预警规则配置。 | -| **约束条件** | 1. 决策者界面应简洁明了,突出关键指标和异常情况。
2. 复杂分析应提供直观的可视化展示,避免过多的数字和表格。
3. 报表生成不应影响系统的整体性能。
4. 敏感数据的访问应受到严格的权限控制。 | -| **相关查询** | 1. 按时间段、区域、问题类型等维度查询统计数据。
2. 查询资源利用效率和投入产出比。
3. 查询环境问题的地理分布和时间分布热点。
4. 查询预测模型生成的趋势预测结果。 | -| **其他需求** | 1. 支持报表的导出和分享功能。
2. 提供决策建议的解释性说明,增强决策透明度。
3. 支持自定义关注指标和提醒阈值。
4. 提供移动端访问能力,满足随时随地查看关键数据的需求。 | - -**决策者使用流程图**: - -```mermaid -sequenceDiagram - participant DecisionMaker as 决策者 - participant Dashboard as 综合仪表盘 - participant ReportGenerator as 报表生成器 - participant AnalyticsEngine as 分析引擎 - participant Database as 数据存储 - - DecisionMaker->>Dashboard: 登录系统 - Dashboard->>AnalyticsEngine: 请求核心指标数据 - AnalyticsEngine->>Database: 查询原始数据 - Database-->>AnalyticsEngine: 返回数据 - AnalyticsEngine->>AnalyticsEngine: 计算指标和趋势 - AnalyticsEngine-->>Dashboard: 返回处理后的数据 - Dashboard-->>DecisionMaker: 展示综合仪表盘 - - DecisionMaker->>Dashboard: 调整筛选条件(时间/区域) - Dashboard->>AnalyticsEngine: 请求筛选后的数据 - AnalyticsEngine->>Database: 查询筛选数据 - Database-->>AnalyticsEngine: 返回筛选结果 - AnalyticsEngine-->>Dashboard: 返回处理后的数据 - Dashboard-->>DecisionMaker: 更新仪表盘显示 - - DecisionMaker->>ReportGenerator: 请求生成绩效报告 - ReportGenerator->>AnalyticsEngine: 获取绩效数据 - AnalyticsEngine->>Database: 查询绩效相关数据 - Database-->>AnalyticsEngine: 返回数据 - AnalyticsEngine-->>ReportGenerator: 提供分析结果 - ReportGenerator-->>DecisionMaker: 生成并下载报告 - - DecisionMaker->>Dashboard: 查看资源分配建议 - Dashboard->>AnalyticsEngine: 请求优化建议 - AnalyticsEngine->>AnalyticsEngine: 运行资源优化算法 - AnalyticsEngine-->>Dashboard: 返回建议结果 - Dashboard-->>DecisionMaker: 展示资源分配建议 -``` - -## 6. 需求规定 - -### 6.2 技术规格需求 - -* **前端技术栈**: - * 核心框架:Vue.js 3.x - * 构建工具:Vite - * UI组件库:Element Plus - * 状态管理:Pinia - * 路由管理:Vue Router - * HTTP客户端:Axios - -* **后端技术栈**: - * 核心框架:Spring Boot 3.x - * 编程语言:Java 17 - * 安全框架:Spring Security 6.x - * API文档:SpringDoc (OpenAPI) - * 数据验证:Jakarta Bean Validation (Hibernate Validator) - * 日志系统:SLF4J & Logback - -* **数据存储**: - * 采用JSON文件存储方案 - * 实现自定义的泛型JSON仓储层 - * 确保数据操作的线程安全性 - -* **部署环境**: - * 支持Docker容器化部署 - * 支持常见的Web服务器(如Nginx、Apache) - * 最低硬件要求:4核CPU、8GB内存、50GB存储空间 - -## 7. 数据描述 - -### 7.1 用户账户 (UserAccount) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 用户唯一标识符 | Long | 主键,自增 | 是 | -| `name` | 用户姓名 | String | 最大长度50 | 是 | -| `phone` | 手机号码 | String | 符合手机号格式 | 是 | -| `email` | 电子邮箱 | String | 符合邮箱格式 | 是 | -| `password` | 密码(加密存储) | String | 最小长度8,包含字母、数字和特殊字符 | 是 | -| `gender` | 性别 | Enum | MALE, FEMALE, OTHER | 否 | -| `role` | 用户角色 | Enum | ADMIN, SUPERVISOR, GRID_WORKER, PUBLIC_USER | 是 | -| `status` | 账户状态 | Enum | ACTIVE, INACTIVE, LOCKED | 是 | -| `gridX` | 网格X坐标(仅网格员) | Integer | 非负整数 | 否 | -| `gridY` | 网格Y坐标(仅网格员) | Integer | 非负整数 | 否 | -| `region` | 地理区域或区县 | String | 最大长度255 | 否 | -| `level` | 熟练度级别(仅网格员) | Enum | JUNIOR, INTERMEDIATE, SENIOR, EXPERT | 否 | -| `skills` | 技能列表(JSON格式) | List | - | 否 | -| `createdAt` | 账户创建时间 | DateTime | ISO 8601格式 | 是 | -| `updatedAt` | 账户更新时间 | DateTime | ISO 8601格式 | 是 | -| `enabled` | 账户是否启用 | Boolean | true/false | 是 | -| `currentLatitude` | 当前纬度(仅网格员) | Double | 有效范围 | 否 | -| `currentLongitude` | 当前经度(仅网格员) | Double | 有效范围 | 否 | -| `failedLoginAttempts` | 连续登录失败次数 | Integer | 非负整数 | 是 | -| `lockoutEndTime` | 锁定结束时间 | DateTime | ISO 8601格式 | 否 | - -### 7.2 反馈 (Feedback) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 反馈唯一标识符 | Long | 主键,自增 | 是 | -| `eventId` | 事件ID(业务编号) | String | UUID格式 | 是 | -| `title` | 反馈标题 | String | 最大长度100 | 是 | -| `description` | 问题详细描述 | String | 最大长度1000 | 是 | -| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 | -| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 | -| `status` | 反馈状态 | Enum | PENDING_REVIEW, AI_REJECTED, PROCESSED, CLOSED | 是 | -| `textAddress` | 文本地址描述 | String | 最大长度200 | 否 | -| `gridX` | 网格X坐标 | Integer | 非负整数 | 否 | -| `gridY` | 网格Y坐标 | Integer | 非负整数 | 否 | -| `submitterId` | 提交者ID | Long | 外键关联UserAccount | 是 | -| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 | -| `longitude` | 地理位置经度 | Double | 有效范围 | 是 | -| `user` | 提交者用户对象 | UserAccount | 外键关联 | 是 | -| `attachments` | 附件列表 | List | 至少一张图片 | 是 | -| `task` | 关联的任务 | Task | 一对一关联 | 否 | -| `createdAt` | 提交时间 | DateTime | ISO 8601格式 | 是 | -| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | - -### 7.3 任务 (Task) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 任务唯一标识符 | Long | 主键,自增 | 是 | -| `feedback` | 关联的反馈 | Feedback | 外键关联Feedback | 否 | -| `assignee` | 被指派的网格员 | UserAccount | 外键关联UserAccount | 否 | -| `createdBy` | 创建者 | UserAccount | 外键关联UserAccount | 是 | -| `status` | 任务状态 | Enum | CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED | 是 | -| `assignedAt` | 分配时间 | DateTime | ISO 8601格式 | 否 | -| `completedAt` | 完成时间 | DateTime | ISO 8601格式 | 否 | -| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 | -| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | -| `title` | 任务标题 | String | 最大长度100 | 是 | -| `description` | 任务描述 | String | 最大长度1000 | 是 | -| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 | -| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 | -| `textAddress` | 文本地址描述 | String | 最大长度200 | 否 | -| `gridX` | 网格X坐标 | Integer | 非负整数 | 否 | -| `gridY` | 网格Y坐标 | Integer | 非负整数 | 否 | -| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 | -| `longitude` | 地理位置经度 | Double | 有效范围 | 是 | -| `history` | 任务历史记录 | List | - | 否 | -| `assignment` | 任务分配信息 | Assignment | 一对一关联 | 否 | -| `submissions` | 任务提交记录 | List | - | 否 | - -### 7.4 网格 (Grid) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 网格唯一标识符 | Long | 主键,自增 | 是 | -| `gridX` | 网格X坐标 | Integer | 非负整数 | 是 | -| `gridY` | 网格Y坐标 | Integer | 非负整数 | 是 | -| `cityName` | 所属城市 | String | 最大长度50 | 是 | -| `districtName` | 所属区县 | String | 最大长度50 | 是 | -| `description` | 网格描述 | String | 最大长度200 | 否 | -| `isObstacle` | 是否为障碍物 | Boolean | true/false | 是 | - -### 7.5 任务分配 (Assignment) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 分配唯一标识符 | Long | 主键,自增 | 是 | -| `task` | 关联的任务 | Task | 外键关联Task | 是 | -| `assigner` | 分配人 | UserAccount | 外键关联UserAccount | 是 | -| `assignmentTime` | 分配时间 | DateTime | ISO 8601格式 | 是 | -| `deadline` | 截止日期 | DateTime | ISO 8601格式 | 否 | -| `status` | 分配状态 | Enum | PENDING, ACCEPTED, REJECTED, COMPLETED | 是 | -| `remarks` | 备注说明 | String | 最大长度500 | 否 | - - + +# 需求定义文档 + +## 1. 项目介绍 + +### 1.1 项目背景 + +环境监测系统(EMS)是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速,环境污染问题日益凸显,传统的环境问题上报和处理机制存在流程繁琐、响应缓慢、缺乏透明度等问题。本系统旨在通过数字化手段,构建一个连接公众、管理部门和执行人员的环境监测与治理平台,实现环境问题的快速发现、高效处理和全程监督。 + +### 1.2 项目目标 + +1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。 +2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。 +3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。 +4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。 +5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。 + +## 2. 系统分析 + +### 2.1 业务痛点分析 + +1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。 +2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。 +3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。 +4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。 +5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。 + +### 2.2 用户角色分析 + +环境监测系统涉及五类主要用户角色,每个角色在系统中承担不同的职责: + +1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。 +2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。 +3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。 +4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。 +5. **决策者**:系统的战略端,通过分析系统生成的统计数据和趋势图表,制定环境管理策略和资源分配决策,是系统的最终受益者之一。 +### 2.3 用例分析 + +#### 2.3.1 用例图 + +```mermaid +graph TD + %% 定义角色 + PublicUser["公众用户"] + GridWorker["网格员"] + Supervisor["主管"] + Admin["管理员"] + + %% 定义用例 + UC1["注册与登录"] + UC2["提交环境问题反馈"] + UC3["查看反馈处理进度"] + UC4["接收任务通知"] + UC5["执行任务"] + UC6["提交处理结果"] + UC7["审核反馈内容"] + UC8["分配任务"] + UC9["审核处理结果"] + UC10["查看统计数据"] + UC11["管理用户账户"] + UC12["配置系统参数"] + + %% 建立关系 + PublicUser --> UC1 + PublicUser --> UC2 + PublicUser --> UC3 + + GridWorker --> UC1 + GridWorker --> UC4 + GridWorker --> UC5 + GridWorker --> UC6 + + Supervisor --> UC1 + Supervisor --> UC7 + Supervisor --> UC8 + Supervisor --> UC9 + Supervisor --> UC10 + + Admin --> UC1 + Admin --> UC10 + Admin --> UC11 + Admin --> UC12 + + %% 设置样式 + classDef actor fill:#f9f,stroke:#333,stroke-width:2px + classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px + + class PublicUser,GridWorker,Supervisor,Admin actor + class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12 usecase +``` + +#### 2.3.2 活动图:反馈提交与处理流程 + +```mermaid +stateDiagram-v2 + [*] --> 发现环境问题 + 发现环境问题 --> 填写反馈表单 + 填写反馈表单 --> 上传图片 + 上传图片 --> 标记位置 + 标记位置 --> 提交反馈 + 提交反馈 --> AI自动审核 + + state AI自动审核 { + [*] --> 内容分析 + 内容分析 --> 垃圾信息检测 + 垃圾信息检测 --> 分类与评级 + 分类与评级 --> [*] + } + + AI自动审核 --> 判断AI审核结果 + 判断AI审核结果 --> 明显无效: AI拒绝 + 判断AI审核结果 --> 需人工确认: 需确认 + + 明显无效 --> 标记为AI_REJECTED + 标记为AI_REJECTED --> 通知提交者 + 通知提交者 --> [*] + + 需人工确认 --> 主管人工审核 + 主管人工审核 --> 判断审核结果 + + 判断审核结果 --> 驳回: 不通过 + 判断审核结果 --> 通过: 通过 + + 驳回 --> 填写驳回理由 + 填写驳回理由 --> 更新状态为REJECTED + 更新状态为REJECTED --> 通知提交者反馈被驳回 + 通知提交者反馈被驳回 --> [*] + + 通过 --> 创建任务 + 创建任务 --> 更新反馈状态为PROCESSED + 更新反馈状态为PROCESSED --> 任务分配流程 + 任务分配流程 --> [*] +``` + +#### 2.3.3 活动图:任务分配与执行流程 + +```mermaid +stateDiagram-v2 + [*] --> 任务创建完成 + 任务创建完成 --> 选择分配方式 + + state 选择分配方式 { + [*] --> 手动分配 + [*] --> 智能推荐 + + 智能推荐 --> 运行分配算法 + 运行分配算法 --> 推荐最佳人选 + 推荐最佳人选 --> 确认人选 + + 手动分配 --> 选择特定网格员 + 选择特定网格员 --> 确认人选 + + 确认人选 --> [*] + } + + 选择分配方式 --> 创建任务分配记录 + 创建任务分配记录 --> 更新任务状态为ASSIGNED + 更新任务状态为ASSIGNED --> 通知网格员 + + 通知网格员 --> 网格员接收通知 + 网格员接收通知 --> 判断是否接受 + + 判断是否接受 --> 拒绝: 拒绝 + 判断是否接受 --> 接受: 接受 + + 拒绝 --> 选择分配方式 + + 接受 --> 更新状态为IN_PROGRESS + 更新状态为IN_PROGRESS --> 获取路径规划 + 获取路径规划 --> 前往现场处理 + 前往现场处理 --> 记录处理过程 + 记录处理过程 --> 上传处理结果 + 上传处理结果 --> 提交处理结果 + 提交处理结果 --> 更新状态为SUBMITTED + + 更新状态为SUBMITTED --> 主管审核结果 + 主管审核结果 --> 判断结果是否合格 + + 判断结果是否合格 --> 不合格: 不合格 + 判断结果是否合格 --> 合格: 合格 + + 不合格 --> 填写原因要求重新处理 + 填写原因要求重新处理 --> 获取路径规划 + + 合格 --> 确认任务完成 + 确认任务完成 --> 更新任务状态为APPROVED + 更新任务状态为APPROVED --> 更新反馈状态为CLOSED + 更新反馈状态为CLOSED --> 通知反馈提交者 + 通知反馈提交者 --> 更新统计数据 + 更新统计数据 --> [*] +``` + +### 2.4 系统可行性分析 + +#### 2.4.1 技术可行性 + +1. **前端技术**:采用Vue 3框架构建用户界面,结合Element Plus组件库,可以快速开发出美观、响应式的Web应用,满足不同设备的访问需求。 +2. **后端技术**:基于Spring Boot 3框架和Java 17,具备高性能、高并发处理能力,能够满足系统的稳定性和扩展性需求。 +3. **地图服务**:可以集成百度地图、高德地图等成熟的地图API,实现地理位置标记、路径规划等功能。 +4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。 +5. **数据存储**:采用JSON文件存储方案,简化部署和维护,适合中小规模系统的快速实现。 + +#### 2.4.2 经济可行性 + +1. **开发成本**:采用主流开源框架和技术栈,降低开发成本和技术门槛。 +2. **维护成本**:模块化设计和完善的文档,降低后期维护和升级成本。 +3. **投资回报**:通过提高环境问题处理效率,减少人力资源浪费,长期来看具有良好的投资回报。 +4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。 + +#### 2.4.3 操作可行性 + +1. **用户接受度**:系统界面设计简洁直观,操作流程符合用户习惯,易于被各类用户接受和使用。 +2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。 +3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。 + +#### 2.4.4 法律可行性 + +1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。 +2. **知识产权**:系统开发过程中使用的第三方库和组件均为开源或已获得授权,不存在知识产权风险。 +3. **合规性**:系统功能和流程设计符合相关法律法规和行业标准,确保合法合规运营。 + +## 3. 功能性需求 + +### 3.1 功能层次方框图 + +```mermaid +flowchart TD + %% 用户交互层 + subgraph "用户交互层" + direction LR + C["公众服务模块(问题上报)"] + H["个人中心模块(我的反馈/资料)"] + D["管理驾驶舱(数据决策)"] + end + + %% 核心业务层 + subgraph "核心业务层" + direction LR + AI["AI分析模块(内容审核)"] + E["任务管理模块(分配、流转、执行)"] + end + + %% 应用支撑层 + subgraph "应用支撑层" + direction LR + F["网格与地图模块(LBS & 寻路)"] + I["文件服务模块(附件存取)"] + end + + %% 基础服务层 + subgraph "基础服务层" + direction LR + B["用户与认证模块"] + G["系统管理模块(用户/权限)"] + J["日志审计模块"] + end + + %% 定义关系 + C -- "提交反馈" --> AI + AI -- "分析结果" --> E + C -- "附件" --> I + E -- "调用" --> F + E -- "任务附件" --> I + E -- "统计数据" --> D + + H -- "查询个人数据" --> E + + %% 基础服务支撑所有上层模块 (关系隐含) + G -- "管理" --> B + + classDef userLayer fill:#d4f1f9,stroke:#05a8e5; + classDef coreLayer fill:#ffe6cc,stroke:#f7a128; + classDef appSupportLayer fill:#d5e8d4,stroke:#82b366; + classDef baseLayer fill:#e1d5e7,stroke:#9673a6; + + class C,H,D userLayer; + class AI,E coreLayer; + class F,I appSupportLayer; + class B,G,J baseLayer; +``` + +### 3.2 模块功能概述 + +| 模块名称 | 功能概述 | +| :--- | :--- | +| **用户与认证模块** | 管理用户身份验证、权限控制和会话管理,确保系统安全性。提供用户注册、登录、密码重置等功能。 | +| **反馈管理模块** | 处理公众提交的环境问题反馈,包括提交、审核、状态追踪和查询统计等功能。 | +| **任务管理模块** | 将审核通过的反馈转化为工作任务,并管理任务的分配、执行和完成全流程。 | +| **网格与地图模块** | 提供地理空间的网格化管理,支持路径规划和地图可视化,辅助任务执行。 | +| **决策支持模块** | 通过数据分析和可视化,为管理层提供决策支持,帮助优化资源配置和治理策略。 | +| **系统管理模块** | 提供系统配置、用户管理、权限设置等基础功能,保障系统正常运行。 | +| **文件服务模块** | 处理系统中的文件上传、存储和访问,支持反馈和任务的附件管理。 | +| **日志审计模块** | 记录系统操作日志,支持安全审计和问题追溯,确保系统运行的可追溯性。 | + +## 4. 整体业务流程 + +下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程。 + +```mermaid +flowchart TD + %% 定义样式 + classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333 + classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333 + classDef worker fill:#d5e8d4,stroke:#82b366,color:#333 + classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333 + classDef decision fill:#f8cecc,stroke:#b85450,color:#333 + classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px + + %% 流程开始 + A([开始]) --> B["[公众端] 发现环境问题"] + B --> C["[公众端] 提交反馈
(标题/描述/图片/位置)"] + + %% 平台接收与AI处理 + C --> D["[平台] 接收反馈
生成唯一事件ID"] + D --> E{"[平台] AI自动审核
分析内容/分类"} + + %% AI审核分支 + E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"] + F1 --> F2["[平台] 通知提交者"] + F2 --> Z1([结束]) + + E -- "需人工确认" --> G["[主管] 查看反馈详情
进行人工审核"] + + %% 主管审核分支 + G --> H{"[主管] 审核决定"} + H -- "驳回" --> I1["[主管] 填写驳回理由"] + I1 --> I2["[平台] 更新状态为REJECTED"] + I2 --> I3["[平台] 通知提交者"] + I3 --> Z2([结束]) + + %% 审核通过,创建任务 + H -- "通过" --> J1["[主管] 确认反馈有效"] + J1 --> J2["[平台] 自动创建结构化任务"] + J2 --> J3["[平台] 更新反馈状态为PROCESSED"] + + %% 任务分配 + J3 --> K1{"[主管] 选择分配方式"} + K1 -- "手动分配" --> K2["[主管] 选择特定网格员"] + K1 -- "智能推荐" --> K3["[平台] 运行分配算法
考虑位置/负载/专长"] + K3 --> K4["[平台] 推荐最佳人选"] + K4 --> K2 + K2 --> K5["[平台] 创建任务分配记录
更新任务状态为ASSIGNED"] + K5 --> K6["[平台] 通知网格员"] + + %% 网格员处理 + K6 --> L1["[网格员] 接收任务通知"] + L1 --> L2{"[网格员] 接受任务?"} + L2 -- "拒绝" --> K1 + L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"] + L3 --> L4["[网格员] 查看任务详情
获取路径规划"] + L4 --> L5["[网格员] 前往现场处理"] + L5 --> L6["[网格员] 记录处理过程
上传证明材料"] + L6 --> L7["[网格员] 提交处理结果
更新状态为SUBMITTED"] + + %% 主管审核结果 + L7 --> M1["[主管] 审核处理结果"] + M1 --> M2{"[主管] 结果是否合格?"} + M2 -- "不合格" --> M3["[主管] 填写原因
要求重新处理"] + M3 --> L4 + + %% 完成流程 + M2 -- "合格" --> N1["[主管] 确认任务完成"] + N1 --> N2["[平台] 更新任务状态为APPROVED"] + N2 --> N3["[平台] 更新反馈状态为CLOSED"] + N3 --> N4["[平台] 通知反馈提交者"] + N4 --> N5["[平台] 更新统计数据"] + N5 --> O["[决策层] 查看数据看板
分析环境趋势"] + O --> Z3([结束]) + + %% 为节点添加类别 + class A,Z1,Z2,Z3 start_end + class B,C,F2,I3,N4 public + class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform + class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor + class L1,L2,L3,L4,L5,L6,L7 worker + class O decision +``` + +## 5. 详细需求描述 + +### 5.1 用户与认证模块 + +| 功能名称 | 用户与认证模块 | +| :--- | :--- | +| **优先级** | 高 | +| **业务背景** | 作为系统安全的基础,本模块负责管理所有用户的身份验证和访问控制,确保系统资源只能被授权用户访问,同时提供灵活的角色权限管理。 | +| **功能说明** | 1. **用户认证**:基于JWT (JSON Web Token) 的安全认证机制,支持账号密码登录,提供令牌刷新功能。
2. **权限控制**:基于RBAC (基于角色的访问控制) 模型,预设管理员、主管、网格员等角色,每个角色拥有特定的API访问权限。
3. **密码管理**:支持安全的密码重置流程,包括邮箱验证码验证,以及定期密码更新提醒。
4. **会话管理**:支持单点登录或多设备登录控制,可配置会话超时策略。 | +| **约束条件** | 1. 密码必须符合复杂度要求(至少8位,包含大小写字母、数字和特殊字符)。
2. 敏感操作(如修改权限)需要二次验证。
3. 密码在数据库中必须使用BCrypt等强哈希算法加密存储。
4. 登录失败超过预设次数后,账户应被临时锁定。 | +| **相关查询** | 1. 按用户名、邮箱或手机号查询用户信息。
2. 查询特定角色的所有用户列表。
3. 查询用户的权限和访问历史。 | +| **其他需求** | 1. 系统应记录所有关键安全事件(登录、权限变更等)的审计日志。
2. 支持用户个人资料的维护和更新。 | + +**用户认证流程图**: + +```mermaid +sequenceDiagram + participant User as 用户 + participant AuthController as 认证控制器 + participant AuthService as 认证服务 + participant UserRepository as 用户仓库 + participant JwtUtil as JWT工具 + participant Database as JSON持久化存储 + + User->>AuthController: POST /api/auth/login + AuthController->>AuthService: signIn(LoginRequest) + AuthService->>UserRepository: findByEmailOrPhone() + UserRepository->>Database: 查询用户信息 + Database-->>UserRepository: 返回用户数据 + UserRepository-->>AuthService: 返回UserAccount + AuthService->>AuthService: 验证密码 + AuthService->>JwtUtil: 生成JWT令牌 + JwtUtil-->>AuthService: 返回JWT + AuthService-->>AuthController: JwtAuthenticationResponse + AuthController-->>User: 返回JWT令牌 +``` + +### 5.2 反馈管理模块 + +| 功能名称 | 反馈管理模块 | +| :--- | :--- | +| **优先级** | 高 | +| **业务背景** | 作为系统的核心输入端口,本模块负责收集、处理和跟踪所有环境问题反馈,是连接公众与管理部门的桥梁,也是后续任务创建的数据源。 | +| **功能说明** | 1. **反馈提交**:提供结构化的表单接口,支持文字描述、污染类型分类、严重程度评估、地理位置标记和多媒体附件上传。
2. **AI内容审核**:集成智能审核服务,对反馈内容进行自动分析,识别垃圾信息、重复提交,并进行初步的分类和紧急程度评估。
3. **人工审核工作台**:为主管提供高效的反馈审核界面,支持批量处理、快速预览和详情查看。
4. **状态追踪**:完整记录反馈从提交到处理完成的全生命周期状态变更,支持多维度的统计和查询。 | +| **约束条件** | 1. 反馈提交必须包含至少一张图片和准确的地理位置信息。
2. AI审核结果仅作为参考,最终决定权在人工审核者手中。
3. 对于紧急程度被标记为"高"的反馈,系统应在1小时内完成审核。
4. 同一用户在短时间内(如5分钟)不能重复提交内容高度相似的反馈。 | +| **相关查询** | 1. 按状态、时间段、区域、污染类型等多维度查询反馈列表。
2. 查看特定反馈的详细信息和处理历史。
3. 统计不同类型反馈的数量分布和处理效率。 | +| **其他需求** | 1. 支持反馈的优先级标记和升级处理。
2. 对于同一区域短时间内的多个相似反馈,系统应能智能识别并提示可能的重复。
3. 支持反馈附件的预览和下载。 | + +**反馈提交处理流程图**: + +```mermaid +sequenceDiagram + participant User as 用户 + participant FeedbackController as 反馈控制器 + participant FeedbackService as 反馈服务 + participant FeedbackRepository as 反馈仓库 + participant AIService as AI服务 + participant EventPublisher as 事件发布器 + participant Database as JSON持久化存储 + + User->>FeedbackController: POST /api/feedback/submit + FeedbackController->>FeedbackService: submitFeedback(request, files) + FeedbackService->>FeedbackService: 生成事件ID + FeedbackService->>FeedbackRepository: save(feedback) + FeedbackRepository->>Database: 保存反馈数据 + Database-->>FeedbackRepository: 返回保存结果 + FeedbackRepository-->>FeedbackService: 返回Feedback实体 + FeedbackService->>EventPublisher: 发布反馈创建事件 + EventPublisher->>AIService: 触发AI处理 + AIService->>AIService: 分析反馈内容 + FeedbackService-->>FeedbackController: 返回Feedback + FeedbackController-->>User: 201 Created +``` + +### 5.3 任务管理模块 + +| 功能名称 | 任务管理模块 | +| :--- | :--- | +| **优先级** | 高 | +| **业务背景** | 本模块是系统的核心业务处理单元,负责将审核通过的反馈转化为可执行的工作任务,并对任务的分配、执行和完成进行全流程管理。 | +| **功能说明** | 1. **任务创建**:支持从反馈自动生成任务,也支持主管手动创建临时任务,包含任务描述、位置、截止时间、优先级等信息。
2. **智能分配**:基于多因素(网格员位置、当前负载、专业技能、历史表现)的任务分配算法,为每个任务推荐最合适的处理人员。
3. **任务执行跟踪**:记录任务的每个状态变更(已分配、已接受、进行中、已提交、已审核),支持网格员实时上报处理进度。
4. **结果审核**:主管对网格员提交的处理结果进行审核,可以通过或驳回,并提供反馈意见。
5. **任务看板**:直观展示不同状态任务的数量和分布,支持拖拽操作进行状态更新。 | +| **约束条件** | 1. 任务必须关联到一个有效的反馈或由授权主管手动创建。
2. 任务分配时必须考虑网格员的工作区域和当前任务负载。
3. 高优先级任务应在24小时内分配并开始处理。
4. 任务提交时必须包含处理过程描述和至少一张结果照片。
5. 已完成的任务不能重新分配或修改。 | +| **相关查询** | 1. 按状态、负责人、时间段、区域等多维度查询任务列表。
2. 查看特定任务的详细信息、处理历史和相关反馈。
3. 统计不同网格员的任务完成率、平均处理时长等绩效指标。 | +| **其他需求** | 1. 支持任务的紧急程度升级和重新分配。
2. 对于长时间未处理的任务,系统应自动发送提醒通知。
3. 支持批量导出任务报告,用于绩效评估和工作汇报。 | + +**任务分配流程图**: + +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant TaskController as 任务控制器 + participant TaskService as 任务服务 + participant AssignmentService as 分配服务 + participant UserRepository as 用户仓库 + participant TaskRepository as 任务仓库 + participant GridWorker as 网格工作人员 + + Supervisor->>TaskController: POST /api/tasks/assign + TaskController->>TaskService: assignTask(taskId, workerId) + TaskService->>UserRepository: findById(workerId) + UserRepository-->>TaskService: 返回GridWorker + TaskService->>AssignmentService: createAssignment() + AssignmentService->>TaskRepository: updateTaskStatus() + TaskRepository-->>AssignmentService: 更新成功 + AssignmentService-->>TaskService: Assignment创建成功 + TaskService->>TaskService: 发送通知给工作人员 + TaskService-->>TaskController: 分配成功 + TaskController-->>Supervisor: 200 OK + Note over GridWorker: 接收任务通知 +``` + +### 5.4 网格与地图模块 + +| 功能名称 | 网格与地图模块 | +| :--- | :--- | +| **优先级** | 中 | +| **业务背景** | 本模块负责对地理空间进行网格化管理,将城市区域划分为可管理的网格单元,并为任务执行提供地理位置支持和路径规划。 | +| **功能说明** | 1. **网格定义与管理**:支持管理员定义和维护城市网格系统,包括网格的坐标、属性(如是否为障碍物)和责任人分配。
2. **A*寻路算法服务**:基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划,考虑距离、交通状况和障碍物。
3. **地图可视化**:在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。
4. **区域统计**:基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | +| **约束条件** | 1. 网格系统应支持多级划分,最小网格单元不应大于500米×500米。
2. 路径规划应考虑实际道路情况和障碍物,避免不可通行区域。
3. 地图数据应定期更新,确保准确性。
4. 网格坐标系统必须与地理坐标系统(经纬度)有明确的转换关系。 | +| **相关查询** | 1. 查询特定区域内的网格定义和属性。
2. 查询特定网格的历史问题记录和统计数据。
3. 获取两点间的最优路径规划。
4. 查询特定区域内的网格员分布情况。 | +| **其他需求** | 1. 支持网格责任人的灵活调整和临时替换。
2. 地图界面应支持常见的交互操作(缩放、平移、点选)。
3. 支持离线地图数据缓存,保证在网络不稳定情况下的基本功能。 | + +**路径规划流程图**: + +```mermaid +sequenceDiagram + participant User as 用户 + participant PathfindingController as 寻路控制器 + participant AStarService as A*服务 + participant MapData as 地图数据 + + User->>+PathfindingController: POST /api/pathfinding/find (起点, 终点) + PathfindingController->>+AStarService: findPath(start, end) + AStarService->>+MapData: getObstacles() + MapData-->>-AStarService: 返回障碍物信息 + AStarService->>AStarService: 执行A*算法计算路径 + AStarService-->>-PathfindingController: 返回计算出的路径 + PathfindingController-->>-User: 200 OK (路径坐标列表) +``` + +### 5.5 决策支持模块 + +| 功能名称 | 决策支持模块 (管理驾驶舱) | +| :--- | :--- | +| **优先级** | 中 | +| **业务背景** | 本模块旨在将系统中沉淀的大量业务数据转化为有价值的决策洞察,帮助管理层了解环境状况、评估治理效果、优化资源配置。 | +| **功能说明** | 1. **核心指标看板**:实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。
2. **多维度分析**:支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。
3. **热力图可视化**:在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。
4. **绩效评估**:对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。
5. **预警机制**:基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | +| **约束条件** | 1. 数据分析应基于实时或准实时的业务数据,确保决策的时效性。
2. 图表和报表应支持多种导出格式(PDF、Excel等),便于进一步分析和汇报。
3. 敏感数据(如个人绩效)的访问应受到严格权限控制。
4. 系统应能处理和分析至少一年的历史数据。 | +| **相关查询** | 1. 按自定义时间范围查询各类统计指标。
2. 查询特定区域或网格员的历史表现数据。
3. 生成定制化的数据报表和分析图表。
4. 查询环境问题的时间分布和地理分布。 | +| **其他需求** | 1. 支持数据看板的个性化配置,满足不同管理者的关注点。
2. 提供数据异常检测和提醒功能,及时发现数据波动。
3. 支持定期自动生成并发送统计报告。 | + +**决策者获取仪表盘数据流程图**: + +```mermaid +sequenceDiagram + participant DecisionMaker as 决策者 + participant DashboardController as 仪表盘控制器 + participant DashboardService as 仪表盘服务 + participant variousRepositories as 各类仓库 + participant Database as JSON持久化存储 + + DecisionMaker->>DashboardController: GET /api/dashboard/stats + DashboardController->>DashboardService: getDashboardStats() + DashboardService->>variousRepositories: (并行)调用多个仓库方法获取数据 + variousRepositories->>Database: 查询统计数据 + Database-->>variousRepositories: 返回数据 + variousRepositories-->>DashboardService: 返回统计结果 + DashboardService->>DashboardService: 聚合数据为DashboardStatsDTO + DashboardService-->>DashboardController: 返回DashboardStatsDTO + DashboardController-->>DecisionMaker: 返回核心统计数据 +``` + +### 5.6 决策者角色需求 + +| 功能名称 | 决策者角色需求 | +| :--- | :--- | +| **优先级** | 中 | +| **业务背景** | 决策者是环境监测系统的战略用户,负责根据系统提供的数据分析结果制定环境管理策略和资源分配决策。他们需要全局视角的数据展示和深度分析功能,以支持科学决策。 | +| **功能说明** | 1. **综合仪表盘**:提供环境状况、处理效率、资源利用等多维度的综合数据视图,支持按时间、区域进行筛选。
2. **趋势分析**:展示关键指标的历史变化趋势,支持多指标对比和季节性分析。
3. **资源分配建议**:基于历史数据和预测模型,为人力资源调配和预算分配提供决策建议。
4. **绩效评估报告**:生成网格员、区域、团队的绩效评估报告,支持多维度比较。
5. **预警监控**:实时监控环境问题高发区域和异常情况,支持预警规则配置。 | +| **约束条件** | 1. 决策者界面应简洁明了,突出关键指标和异常情况。
2. 复杂分析应提供直观的可视化展示,避免过多的数字和表格。
3. 报表生成不应影响系统的整体性能。
4. 敏感数据的访问应受到严格的权限控制。 | +| **相关查询** | 1. 按时间段、区域、问题类型等维度查询统计数据。
2. 查询资源利用效率和投入产出比。
3. 查询环境问题的地理分布和时间分布热点。
4. 查询预测模型生成的趋势预测结果。 | +| **其他需求** | 1. 支持报表的导出和分享功能。
2. 提供决策建议的解释性说明,增强决策透明度。
3. 支持自定义关注指标和提醒阈值。
4. 提供移动端访问能力,满足随时随地查看关键数据的需求。 | + +**决策者使用流程图**: + +```mermaid +sequenceDiagram + participant DecisionMaker as 决策者 + participant Dashboard as 综合仪表盘 + participant ReportGenerator as 报表生成器 + participant AnalyticsEngine as 分析引擎 + participant Database as 数据存储 + + DecisionMaker->>Dashboard: 登录系统 + Dashboard->>AnalyticsEngine: 请求核心指标数据 + AnalyticsEngine->>Database: 查询原始数据 + Database-->>AnalyticsEngine: 返回数据 + AnalyticsEngine->>AnalyticsEngine: 计算指标和趋势 + AnalyticsEngine-->>Dashboard: 返回处理后的数据 + Dashboard-->>DecisionMaker: 展示综合仪表盘 + + DecisionMaker->>Dashboard: 调整筛选条件(时间/区域) + Dashboard->>AnalyticsEngine: 请求筛选后的数据 + AnalyticsEngine->>Database: 查询筛选数据 + Database-->>AnalyticsEngine: 返回筛选结果 + AnalyticsEngine-->>Dashboard: 返回处理后的数据 + Dashboard-->>DecisionMaker: 更新仪表盘显示 + + DecisionMaker->>ReportGenerator: 请求生成绩效报告 + ReportGenerator->>AnalyticsEngine: 获取绩效数据 + AnalyticsEngine->>Database: 查询绩效相关数据 + Database-->>AnalyticsEngine: 返回数据 + AnalyticsEngine-->>ReportGenerator: 提供分析结果 + ReportGenerator-->>DecisionMaker: 生成并下载报告 + + DecisionMaker->>Dashboard: 查看资源分配建议 + Dashboard->>AnalyticsEngine: 请求优化建议 + AnalyticsEngine->>AnalyticsEngine: 运行资源优化算法 + AnalyticsEngine-->>Dashboard: 返回建议结果 + Dashboard-->>DecisionMaker: 展示资源分配建议 +``` + +## 6. 需求规定 + +### 6.2 技术规格需求 + +* **前端技术栈**: + * 核心框架:Vue.js 3.x + * 构建工具:Vite + * UI组件库:Element Plus + * 状态管理:Pinia + * 路由管理:Vue Router + * HTTP客户端:Axios + +* **后端技术栈**: + * 核心框架:Spring Boot 3.x + * 编程语言:Java 17 + * 安全框架:Spring Security 6.x + * API文档:SpringDoc (OpenAPI) + * 数据验证:Jakarta Bean Validation (Hibernate Validator) + * 日志系统:SLF4J & Logback + +* **数据存储**: + * 采用JSON文件存储方案 + * 实现自定义的泛型JSON仓储层 + * 确保数据操作的线程安全性 + +* **部署环境**: + * 支持Docker容器化部署 + * 支持常见的Web服务器(如Nginx、Apache) + * 最低硬件要求:4核CPU、8GB内存、50GB存储空间 + +## 7. 数据描述 + +### 7.1 用户账户 (UserAccount) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 用户唯一标识符 | Long | 主键,自增 | 是 | +| `name` | 用户姓名 | String | 最大长度50 | 是 | +| `phone` | 手机号码 | String | 符合手机号格式 | 是 | +| `email` | 电子邮箱 | String | 符合邮箱格式 | 是 | +| `password` | 密码(加密存储) | String | 最小长度8,包含字母、数字和特殊字符 | 是 | +| `gender` | 性别 | Enum | MALE, FEMALE, OTHER | 否 | +| `role` | 用户角色 | Enum | ADMIN, SUPERVISOR, GRID_WORKER, PUBLIC_USER | 是 | +| `status` | 账户状态 | Enum | ACTIVE, INACTIVE, LOCKED | 是 | +| `gridX` | 网格X坐标(仅网格员) | Integer | 非负整数 | 否 | +| `gridY` | 网格Y坐标(仅网格员) | Integer | 非负整数 | 否 | +| `region` | 地理区域或区县 | String | 最大长度255 | 否 | +| `level` | 熟练度级别(仅网格员) | Enum | JUNIOR, INTERMEDIATE, SENIOR, EXPERT | 否 | +| `skills` | 技能列表(JSON格式) | List | - | 否 | +| `createdAt` | 账户创建时间 | DateTime | ISO 8601格式 | 是 | +| `updatedAt` | 账户更新时间 | DateTime | ISO 8601格式 | 是 | +| `enabled` | 账户是否启用 | Boolean | true/false | 是 | +| `currentLatitude` | 当前纬度(仅网格员) | Double | 有效范围 | 否 | +| `currentLongitude` | 当前经度(仅网格员) | Double | 有效范围 | 否 | +| `failedLoginAttempts` | 连续登录失败次数 | Integer | 非负整数 | 是 | +| `lockoutEndTime` | 锁定结束时间 | DateTime | ISO 8601格式 | 否 | + +### 7.2 反馈 (Feedback) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 反馈唯一标识符 | Long | 主键,自增 | 是 | +| `eventId` | 事件ID(业务编号) | String | UUID格式 | 是 | +| `title` | 反馈标题 | String | 最大长度100 | 是 | +| `description` | 问题详细描述 | String | 最大长度1000 | 是 | +| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 | +| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 | +| `status` | 反馈状态 | Enum | PENDING_REVIEW, AI_REJECTED, PROCESSED, CLOSED | 是 | +| `textAddress` | 文本地址描述 | String | 最大长度200 | 否 | +| `gridX` | 网格X坐标 | Integer | 非负整数 | 否 | +| `gridY` | 网格Y坐标 | Integer | 非负整数 | 否 | +| `submitterId` | 提交者ID | Long | 外键关联UserAccount | 是 | +| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 | +| `longitude` | 地理位置经度 | Double | 有效范围 | 是 | +| `user` | 提交者用户对象 | UserAccount | 外键关联 | 是 | +| `attachments` | 附件列表 | List | 至少一张图片 | 是 | +| `task` | 关联的任务 | Task | 一对一关联 | 否 | +| `createdAt` | 提交时间 | DateTime | ISO 8601格式 | 是 | +| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | + +### 7.3 任务 (Task) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 任务唯一标识符 | Long | 主键,自增 | 是 | +| `feedback` | 关联的反馈 | Feedback | 外键关联Feedback | 否 | +| `assignee` | 被指派的网格员 | UserAccount | 外键关联UserAccount | 否 | +| `createdBy` | 创建者 | UserAccount | 外键关联UserAccount | 是 | +| `status` | 任务状态 | Enum | CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED | 是 | +| `assignedAt` | 分配时间 | DateTime | ISO 8601格式 | 否 | +| `completedAt` | 完成时间 | DateTime | ISO 8601格式 | 否 | +| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 | +| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | +| `title` | 任务标题 | String | 最大长度100 | 是 | +| `description` | 任务描述 | String | 最大长度1000 | 是 | +| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 | +| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 | +| `textAddress` | 文本地址描述 | String | 最大长度200 | 否 | +| `gridX` | 网格X坐标 | Integer | 非负整数 | 否 | +| `gridY` | 网格Y坐标 | Integer | 非负整数 | 否 | +| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 | +| `longitude` | 地理位置经度 | Double | 有效范围 | 是 | +| `history` | 任务历史记录 | List | - | 否 | +| `assignment` | 任务分配信息 | Assignment | 一对一关联 | 否 | +| `submissions` | 任务提交记录 | List | - | 否 | + +### 7.4 网格 (Grid) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 网格唯一标识符 | Long | 主键,自增 | 是 | +| `gridX` | 网格X坐标 | Integer | 非负整数 | 是 | +| `gridY` | 网格Y坐标 | Integer | 非负整数 | 是 | +| `cityName` | 所属城市 | String | 最大长度50 | 是 | +| `districtName` | 所属区县 | String | 最大长度50 | 是 | +| `description` | 网格描述 | String | 最大长度200 | 否 | +| `isObstacle` | 是否为障碍物 | Boolean | true/false | 是 | + +### 7.5 任务分配 (Assignment) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 分配唯一标识符 | Long | 主键,自增 | 是 | +| `task` | 关联的任务 | Task | 外键关联Task | 是 | +| `assigner` | 分配人 | UserAccount | 外键关联UserAccount | 是 | +| `assignmentTime` | 分配时间 | DateTime | ISO 8601格式 | 是 | +| `deadline` | 截止日期 | DateTime | ISO 8601格式 | 否 | +| `status` | 分配状态 | Enum | PENDING, ACCEPTED, REJECTED, COMPLETED | 是 | +| `remarks` | 备注说明 | String | 最大长度500 | 否 | + + --- - + ### Report/需求定义_v4_第二部分.md - -# 需求定义文档(续) - -#### 2.3.3 活动图:任务分配与执行流程 - -以下活动图展示了从任务创建到完成的完整业务流程: - -```mermaid -stateDiagram-v2 - [*] --> 任务创建完成 - 任务创建完成 --> 选择分配方式 - - state 选择分配方式 { - [*] --> 手动分配 - [*] --> 智能推荐 - - 智能推荐 --> 运行分配算法 - 运行分配算法 --> 推荐最佳人选 - 推荐最佳人选 --> 确认人选 - - 手动分配 --> 选择特定网格员 - 选择特定网格员 --> 确认人选 - - 确认人选 --> [*] - } - - 选择分配方式 --> 创建任务分配记录 - 创建任务分配记录 --> 更新任务状态为ASSIGNED - 更新任务状态为ASSIGNED --> 通知网格员 - - 通知网格员 --> 网格员接收通知 - 网格员接收通知 --> 判断是否接受 - - 判断是否接受 --> 拒绝: 拒绝 - 判断是否接受 --> 接受: 接受 - - 拒绝 --> 选择分配方式 - - 接受 --> 更新状态为IN_PROGRESS - 更新状态为IN_PROGRESS --> 获取路径规划 - 获取路径规划 --> 前往现场处理 - 前往现场处理 --> 记录处理过程 - 记录处理过程 --> 上传处理结果 - 上传处理结果 --> 提交处理结果 - 提交处理结果 --> 更新状态为SUBMITTED - - 更新状态为SUBMITTED --> 主管审核结果 - 主管审核结果 --> 判断结果是否合格 - - 判断结果是否合格 --> 不合格: 不合格 - 判断结果是否合格 --> 合格: 合格 - - 不合格 --> 填写原因要求重新处理 - 填写原因要求重新处理 --> 获取路径规划 - - 合格 --> 确认任务完成 - 确认任务完成 --> 更新任务状态为APPROVED - 更新任务状态为APPROVED --> 更新反馈状态为CLOSED - 更新反馈状态为CLOSED --> 通知反馈提交者 - 通知反馈提交者 --> 更新统计数据 - 更新统计数据 --> [*] -``` - -### 2.4 系统可行性分析 - -#### 2.4.1 技术可行性 - -本系统的技术实现是可行的,主要基于以下分析: - -1. **前端技术**:市场上已有成熟的前端框架和组件库,可以快速开发出美观、响应式的Web应用,满足不同设备的访问需求。 - -2. **后端技术**:现有的后端框架具备高性能、高并发处理能力,能够满足系统的稳定性和扩展性需求。 - -3. **地图服务**:可以集成成熟的地图API,实现地理位置标记、路径规划等功能,无需从零开发。 - -4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。 - -5. **数据存储**:可采用多种数据存储方案,根据系统规模和性能需求灵活选择。 - -#### 2.4.2 经济可行性 - -从经济角度看,本系统的开发和运营是可行的: - -1. **开发成本**:可以采用主流开源框架和技术栈,降低开发成本和技术门槛。 - -2. **维护成本**:通过模块化设计和完善的文档,可以降低后期维护和升级成本。 - -3. **投资回报**: - - 提高环境问题处理效率,减少人力资源浪费 - - 降低环境问题带来的经济损失 - - 提升城市环境质量,间接促进经济发展 - - 长期来看具有良好的投资回报 - -4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。 - -#### 2.4.3 操作可行性 - -从操作角度看,本系统具有良好的可行性: - -1. **用户接受度**: - - 系统界面设计简洁直观,符合用户习惯 - - 操作流程简化,降低学习成本 - - 移动端支持,满足随时随地使用需求 - -2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。 - -3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。 - -4. **组织支持**:系统实施需要相关部门的协调配合,但总体上不会对现有组织架构产生颠覆性影响。 - -#### 2.4.4 法律可行性 - -从法律合规角度看,本系统的实施不存在重大障碍: - -1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。 - -2. **知识产权**:系统开发过程中使用的第三方库和组件均可采用开源或获得授权的方式,避免知识产权风险。 - -3. **合规性**:系统功能和流程设计可以确保符合相关法律法规和行业标准,确保合法合规运营。 - -## 3. 功能性需求 - -### 3.1 功能层次方框图 - -以下功能层次图展示了系统的整体功能架构: - -```mermaid -flowchart TD - %% 用户交互层 - subgraph "用户交互层" - direction LR - C["公众服务模块\n(问题上报)"] - H["个人中心模块\n(我的反馈/资料)"] - D["管理驾驶舱\n(数据决策)"] - end - - %% 核心业务层 - subgraph "核心业务层" - direction LR - AI["AI分析模块\n(内容审核)"] - E["任务管理模块\n(分配、流转、执行)"] - end - - %% 应用支撑层 - subgraph "应用支撑层" - direction LR - F["网格与地图模块\n(LBS & 寻路)"] - I["文件服务模块\n(附件存取)"] - end - - %% 基础服务层 - subgraph "基础服务层" - direction LR - B["用户与认证模块"] - G["系统管理模块\n(用户/权限)"] - J["日志审计模块"] - end - - %% 定义关系 - C -- "提交反馈" --> AI - AI -- "分析结果" --> E - C -- "附件" --> I - E -- "调用" --> F - E -- "任务附件" --> I - E -- "统计数据" --> D - - H -- "查询个人数据" --> E - - %% 基础服务支撑所有上层模块 (关系隐含) - G -- "管理" --> B - - classDef userLayer fill:#d4f1f9,stroke:#05a8e5; - classDef coreLayer fill:#ffe6cc,stroke:#f7a128; - classDef appSupportLayer fill:#d5e8d4,stroke:#82b366; - classDef baseLayer fill:#e1d5e7,stroke:#9673a6; - - class C,H,D userLayer; - class AI,E coreLayer; - class F,I appSupportLayer; - class B,G,J baseLayer; -``` + +# 需求定义文档(续) + +#### 2.3.3 活动图:任务分配与执行流程 + +以下活动图展示了从任务创建到完成的完整业务流程: + +```mermaid +stateDiagram-v2 + [*] --> 任务创建完成 + 任务创建完成 --> 选择分配方式 + + state 选择分配方式 { + [*] --> 手动分配 + [*] --> 智能推荐 + + 智能推荐 --> 运行分配算法 + 运行分配算法 --> 推荐最佳人选 + 推荐最佳人选 --> 确认人选 + + 手动分配 --> 选择特定网格员 + 选择特定网格员 --> 确认人选 + + 确认人选 --> [*] + } + + 选择分配方式 --> 创建任务分配记录 + 创建任务分配记录 --> 更新任务状态为ASSIGNED + 更新任务状态为ASSIGNED --> 通知网格员 + + 通知网格员 --> 网格员接收通知 + 网格员接收通知 --> 判断是否接受 + + 判断是否接受 --> 拒绝: 拒绝 + 判断是否接受 --> 接受: 接受 + + 拒绝 --> 选择分配方式 + + 接受 --> 更新状态为IN_PROGRESS + 更新状态为IN_PROGRESS --> 获取路径规划 + 获取路径规划 --> 前往现场处理 + 前往现场处理 --> 记录处理过程 + 记录处理过程 --> 上传处理结果 + 上传处理结果 --> 提交处理结果 + 提交处理结果 --> 更新状态为SUBMITTED + + 更新状态为SUBMITTED --> 主管审核结果 + 主管审核结果 --> 判断结果是否合格 + + 判断结果是否合格 --> 不合格: 不合格 + 判断结果是否合格 --> 合格: 合格 + + 不合格 --> 填写原因要求重新处理 + 填写原因要求重新处理 --> 获取路径规划 + + 合格 --> 确认任务完成 + 确认任务完成 --> 更新任务状态为APPROVED + 更新任务状态为APPROVED --> 更新反馈状态为CLOSED + 更新反馈状态为CLOSED --> 通知反馈提交者 + 通知反馈提交者 --> 更新统计数据 + 更新统计数据 --> [*] +``` + +### 2.4 系统可行性分析 + +#### 2.4.1 技术可行性 + +本系统的技术实现是可行的,主要基于以下分析: + +1. **前端技术**:市场上已有成熟的前端框架和组件库,可以快速开发出美观、响应式的Web应用,满足不同设备的访问需求。 + +2. **后端技术**:现有的后端框架具备高性能、高并发处理能力,能够满足系统的稳定性和扩展性需求。 + +3. **地图服务**:可以集成成熟的地图API,实现地理位置标记、路径规划等功能,无需从零开发。 + +4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。 + +5. **数据存储**:可采用多种数据存储方案,根据系统规模和性能需求灵活选择。 + +#### 2.4.2 经济可行性 + +从经济角度看,本系统的开发和运营是可行的: + +1. **开发成本**:可以采用主流开源框架和技术栈,降低开发成本和技术门槛。 + +2. **维护成本**:通过模块化设计和完善的文档,可以降低后期维护和升级成本。 + +3. **投资回报**: + - 提高环境问题处理效率,减少人力资源浪费 + - 降低环境问题带来的经济损失 + - 提升城市环境质量,间接促进经济发展 + - 长期来看具有良好的投资回报 + +4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。 + +#### 2.4.3 操作可行性 + +从操作角度看,本系统具有良好的可行性: + +1. **用户接受度**: + - 系统界面设计简洁直观,符合用户习惯 + - 操作流程简化,降低学习成本 + - 移动端支持,满足随时随地使用需求 + +2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。 + +3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。 + +4. **组织支持**:系统实施需要相关部门的协调配合,但总体上不会对现有组织架构产生颠覆性影响。 + +#### 2.4.4 法律可行性 + +从法律合规角度看,本系统的实施不存在重大障碍: + +1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。 + +2. **知识产权**:系统开发过程中使用的第三方库和组件均可采用开源或获得授权的方式,避免知识产权风险。 + +3. **合规性**:系统功能和流程设计可以确保符合相关法律法规和行业标准,确保合法合规运营。 + +## 3. 功能性需求 + +### 3.1 功能层次方框图 + +以下功能层次图展示了系统的整体功能架构: + +```mermaid +flowchart TD + %% 用户交互层 + subgraph "用户交互层" + direction LR + C["公众服务模块\n(问题上报)"] + H["个人中心模块\n(我的反馈/资料)"] + D["管理驾驶舱\n(数据决策)"] + end + + %% 核心业务层 + subgraph "核心业务层" + direction LR + AI["AI分析模块\n(内容审核)"] + E["任务管理模块\n(分配、流转、执行)"] + end + + %% 应用支撑层 + subgraph "应用支撑层" + direction LR + F["网格与地图模块\n(LBS & 寻路)"] + I["文件服务模块\n(附件存取)"] + end + + %% 基础服务层 + subgraph "基础服务层" + direction LR + B["用户与认证模块"] + G["系统管理模块\n(用户/权限)"] + J["日志审计模块"] + end + + %% 定义关系 + C -- "提交反馈" --> AI + AI -- "分析结果" --> E + C -- "附件" --> I + E -- "调用" --> F + E -- "任务附件" --> I + E -- "统计数据" --> D + + H -- "查询个人数据" --> E + + %% 基础服务支撑所有上层模块 (关系隐含) + G -- "管理" --> B + + classDef userLayer fill:#d4f1f9,stroke:#05a8e5; + classDef coreLayer fill:#ffe6cc,stroke:#f7a128; + classDef appSupportLayer fill:#d5e8d4,stroke:#82b366; + classDef baseLayer fill:#e1d5e7,stroke:#9673a6; + + class C,H,D userLayer; + class AI,E coreLayer; + class F,I appSupportLayer; + class B,G,J baseLayer; +``` --- - + ### Report/需求定义_v4_第三部分.md - -# 需求定义文档(续) - -### 3.2 模块功能概述 - -| 模块名称 | 功能概述 | -| :--- | :--- | -| **用户与认证模块** | 负责用户身份验证和权限控制,提供注册、登录、密码重置等功能,确保系统安全性。 | -| **反馈管理模块** | 处理公众提交的环境问题反馈,包括提交、审核、状态追踪和查询统计等功能。 | -| **任务管理模块** | 将审核通过的反馈转化为工作任务,管理任务的分配、执行和完成全流程。 | -| **网格与地图模块** | 提供地理空间的网格化管理,支持路径规划和地图可视化,辅助任务执行。 | -| **决策支持模块** | 通过数据分析和可视化,为管理层提供决策支持,帮助优化资源配置和治理策略。 | -| **系统管理模块** | 提供系统配置、用户管理、权限设置等基础功能,保障系统正常运行。 | -| **文件服务模块** | 处理系统中的文件上传、存储和访问,支持反馈和任务的附件管理。 | -| **日志审计模块** | 记录系统操作日志,支持安全审计和问题追溯,确保系统运行的可追溯性。 | - -## 4. 整体业务流程 - -下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程: - -```mermaid -flowchart TD - %% 定义样式 - classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333 - classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333 - classDef worker fill:#d5e8d4,stroke:#82b366,color:#333 - classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333 - classDef decision fill:#f8cecc,stroke:#b85450,color:#333 - classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px - - %% 流程开始 - A([开始]) --> B["[公众端] 发现环境问题"] - B --> C["[公众端] 提交反馈
(标题/描述/图片/位置)"] - - %% 平台接收与AI处理 - C --> D["[平台] 接收反馈
生成唯一事件ID"] - D --> E{"[平台] AI自动审核
分析内容/分类"} - - %% AI审核分支 - E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"] - F1 --> F2["[平台] 通知提交者"] - F2 --> Z1([结束]) - - E -- "需人工确认" --> G["[主管] 查看反馈详情
进行人工审核"] - - %% 主管审核分支 - G --> H{"[主管] 审核决定"} - H -- "驳回" --> I1["[主管] 填写驳回理由"] - I1 --> I2["[平台] 更新状态为REJECTED"] - I2 --> I3["[平台] 通知提交者"] - I3 --> Z2([结束]) - - %% 审核通过,创建任务 - H -- "通过" --> J1["[主管] 确认反馈有效"] - J1 --> J2["[平台] 自动创建结构化任务"] - J2 --> J3["[平台] 更新反馈状态为PROCESSED"] - - %% 任务分配 - J3 --> K1{"[主管] 选择分配方式"} - K1 -- "手动分配" --> K2["[主管] 选择特定网格员"] - K1 -- "智能推荐" --> K3["[平台] 运行分配算法
考虑位置/负载/专长"] - K3 --> K4["[平台] 推荐最佳人选"] - K4 --> K2 - K2 --> K5["[平台] 创建任务分配记录
更新任务状态为ASSIGNED"] - K5 --> K6["[平台] 通知网格员"] - - %% 网格员处理 - K6 --> L1["[网格员] 接收任务通知"] - L1 --> L2{"[网格员] 接受任务?"} - L2 -- "拒绝" --> K1 - L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"] - L3 --> L4["[网格员] 查看任务详情
获取路径规划"] - L4 --> L5["[网格员] 前往现场处理"] - L5 --> L6["[网格员] 记录处理过程
上传证明材料"] - L6 --> L7["[网格员] 提交处理结果
更新状态为SUBMITTED"] - - %% 主管审核结果 - L7 --> M1["[主管] 审核处理结果"] - M1 --> M2{"[主管] 结果是否合格?"} - M2 -- "不合格" --> M3["[主管] 填写原因
要求重新处理"] - M3 --> L4 - - %% 完成流程 - M2 -- "合格" --> N1["[主管] 确认任务完成"] - N1 --> N2["[平台] 更新任务状态为APPROVED"] - N2 --> N3["[平台] 更新反馈状态为CLOSED"] - N3 --> N4["[平台] 通知反馈提交者"] - N4 --> N5["[平台] 更新统计数据"] - N5 --> O["[决策层] 查看数据看板
分析环境趋势"] - O --> Z3([结束]) - - %% 为节点添加类别 - class A,Z1,Z2,Z3 start_end - class B,C,F2,I3,N4 public - class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform - class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor - class L1,L2,L3,L4,L5,L6,L7 worker - class O decision -``` - -## 5. 功能需求详细描述 - -### 5.1 用户与认证模块需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| AUTH-01 | 用户注册 | 系统应允许新用户通过提供基本信息(姓名、手机号、邮箱、密码等)进行注册,并验证信息的有效性和唯一性。 | 高 | -| AUTH-02 | 用户登录 | 系统应支持用户通过邮箱/手机号和密码进行身份验证,登录成功后生成安全令牌用于后续请求。 | 高 | -| AUTH-03 | 密码重置 | 系统应提供安全的密码重置机制,包括通过邮箱或手机验证码验证身份。 | 中 | -| AUTH-04 | 权限控制 | 系统应基于用户角色(公众用户、网格员、主管、管理员、决策者)实施访问控制,确保用户只能访问其权限范围内的功能和数据。 | 高 | -| AUTH-05 | 会话管理 | 系统应维护和管理用户会话状态,包括会话创建、验证和超时处理。 | 中 | -| AUTH-06 | 个人资料管理 | 系统应允许用户查看和更新其个人资料,包括基本信息、联系方式和偏好设置。 | 低 | -| AUTH-07 | 安全日志 | 系统应记录所有关键安全事件(登录尝试、权限变更等),支持安全审计。 | 中 | - -**用户与认证流程图**: - -```mermaid -graph TD - A[用户访问系统] --> B{是否已登录?} - B -- 否 --> C{是否有账号?} - B -- 是 --> D[访问系统功能] - - C -- 否 --> E[注册新账号] - C -- 是 --> F[登录系统] - - E --> G[填写注册信息] - G --> H[验证邮箱/手机] - H --> I[创建用户账号] - I --> F - - F --> J{认证是否成功?} - J -- 否 --> K{是否忘记密码?} - J -- 是 --> L[生成安全令牌] - - K -- 是 --> M[密码重置流程] - K -- 否 --> F - - M --> N[验证身份] - N --> O[设置新密码] - O --> F - - L --> P[加载用户权限] - P --> D - - D --> Q[系统记录操作日志] - - R[用户请求退出] --> S[清除会话信息] - S --> T[返回登录页面] -``` - -### 5.2 反馈管理模块需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| FEED-01 | 反馈提交 | 系统应允许公众用户提交环境问题反馈,包括问题描述、位置标记、污染类型分类和图片上传。 | 高 | -| FEED-02 | AI内容审核 | 系统应自动分析反馈内容,识别垃圾信息、重复提交,并进行初步分类和紧急程度评估。 | 中 | -| FEED-03 | 人工审核 | 系统应提供主管审核反馈的工作台,支持查看详情、批准或驳回反馈。 | 高 | -| FEED-04 | 状态追踪 | 系统应记录和展示反馈的全生命周期状态变更,允许用户查询处理进度。 | 高 | -| FEED-05 | 反馈查询 | 系统应支持按多种条件(状态、时间、区域、类型等)查询和筛选反馈列表。 | 中 | -| FEED-06 | 反馈统计 | 系统应生成反馈数据的统计分析,包括数量分布、处理效率等指标。 | 低 | -| FEED-07 | 通知提醒 | 系统应在反馈状态变更时通知相关用户,保持信息透明。 | 中 | - -**反馈管理流程图**: - -```mermaid -graph TD - A[公众用户发现环境问题] --> B[打开反馈提交界面] - B --> C[填写问题描述] - C --> D[选择污染类型] - D --> E[评估严重程度] - E --> F[标记地理位置] - F --> G[上传现场照片] - G --> H[提交反馈] - - H --> I[系统生成唯一事件ID] - I --> J[AI自动审核内容] - - J --> K{AI审核结果} - K -- 明显无效 --> L[标记为AI_REJECTED] - K -- 需人工确认 --> M[进入主管审核队列] - - L --> N[通知用户反馈被拒绝] - - M --> O[主管查看反馈详情] - O --> P{审核决定} - P -- 驳回 --> Q[填写驳回理由] - P -- 通过 --> R[标记为有效反馈] - - Q --> S[更新状态为REJECTED] - S --> T[通知用户反馈被驳回] - - R --> U[创建关联任务] - U --> V[更新状态为PROCESSED] - - W[用户查询反馈状态] --> X[系统展示处理进度] - - Y[管理员查看统计数据] --> Z[系统生成反馈分析报表] -``` + +# 需求定义文档(续) + +### 3.2 模块功能概述 + +| 模块名称 | 功能概述 | +| :--- | :--- | +| **用户与认证模块** | 负责用户身份验证和权限控制,提供注册、登录、密码重置等功能,确保系统安全性。 | +| **反馈管理模块** | 处理公众提交的环境问题反馈,包括提交、审核、状态追踪和查询统计等功能。 | +| **任务管理模块** | 将审核通过的反馈转化为工作任务,管理任务的分配、执行和完成全流程。 | +| **网格与地图模块** | 提供地理空间的网格化管理,支持路径规划和地图可视化,辅助任务执行。 | +| **决策支持模块** | 通过数据分析和可视化,为管理层提供决策支持,帮助优化资源配置和治理策略。 | +| **系统管理模块** | 提供系统配置、用户管理、权限设置等基础功能,保障系统正常运行。 | +| **文件服务模块** | 处理系统中的文件上传、存储和访问,支持反馈和任务的附件管理。 | +| **日志审计模块** | 记录系统操作日志,支持安全审计和问题追溯,确保系统运行的可追溯性。 | + +## 4. 整体业务流程 + +下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程: + +```mermaid +flowchart TD + %% 定义样式 + classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333 + classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333 + classDef worker fill:#d5e8d4,stroke:#82b366,color:#333 + classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333 + classDef decision fill:#f8cecc,stroke:#b85450,color:#333 + classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px + + %% 流程开始 + A([开始]) --> B["[公众端] 发现环境问题"] + B --> C["[公众端] 提交反馈
(标题/描述/图片/位置)"] + + %% 平台接收与AI处理 + C --> D["[平台] 接收反馈
生成唯一事件ID"] + D --> E{"[平台] AI自动审核
分析内容/分类"} + + %% AI审核分支 + E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"] + F1 --> F2["[平台] 通知提交者"] + F2 --> Z1([结束]) + + E -- "需人工确认" --> G["[主管] 查看反馈详情
进行人工审核"] + + %% 主管审核分支 + G --> H{"[主管] 审核决定"} + H -- "驳回" --> I1["[主管] 填写驳回理由"] + I1 --> I2["[平台] 更新状态为REJECTED"] + I2 --> I3["[平台] 通知提交者"] + I3 --> Z2([结束]) + + %% 审核通过,创建任务 + H -- "通过" --> J1["[主管] 确认反馈有效"] + J1 --> J2["[平台] 自动创建结构化任务"] + J2 --> J3["[平台] 更新反馈状态为PROCESSED"] + + %% 任务分配 + J3 --> K1{"[主管] 选择分配方式"} + K1 -- "手动分配" --> K2["[主管] 选择特定网格员"] + K1 -- "智能推荐" --> K3["[平台] 运行分配算法
考虑位置/负载/专长"] + K3 --> K4["[平台] 推荐最佳人选"] + K4 --> K2 + K2 --> K5["[平台] 创建任务分配记录
更新任务状态为ASSIGNED"] + K5 --> K6["[平台] 通知网格员"] + + %% 网格员处理 + K6 --> L1["[网格员] 接收任务通知"] + L1 --> L2{"[网格员] 接受任务?"} + L2 -- "拒绝" --> K1 + L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"] + L3 --> L4["[网格员] 查看任务详情
获取路径规划"] + L4 --> L5["[网格员] 前往现场处理"] + L5 --> L6["[网格员] 记录处理过程
上传证明材料"] + L6 --> L7["[网格员] 提交处理结果
更新状态为SUBMITTED"] + + %% 主管审核结果 + L7 --> M1["[主管] 审核处理结果"] + M1 --> M2{"[主管] 结果是否合格?"} + M2 -- "不合格" --> M3["[主管] 填写原因
要求重新处理"] + M3 --> L4 + + %% 完成流程 + M2 -- "合格" --> N1["[主管] 确认任务完成"] + N1 --> N2["[平台] 更新任务状态为APPROVED"] + N2 --> N3["[平台] 更新反馈状态为CLOSED"] + N3 --> N4["[平台] 通知反馈提交者"] + N4 --> N5["[平台] 更新统计数据"] + N5 --> O["[决策层] 查看数据看板
分析环境趋势"] + O --> Z3([结束]) + + %% 为节点添加类别 + class A,Z1,Z2,Z3 start_end + class B,C,F2,I3,N4 public + class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform + class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor + class L1,L2,L3,L4,L5,L6,L7 worker + class O decision +``` + +## 5. 功能需求详细描述 + +### 5.1 用户与认证模块需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| AUTH-01 | 用户注册 | 系统应允许新用户通过提供基本信息(姓名、手机号、邮箱、密码等)进行注册,并验证信息的有效性和唯一性。 | 高 | +| AUTH-02 | 用户登录 | 系统应支持用户通过邮箱/手机号和密码进行身份验证,登录成功后生成安全令牌用于后续请求。 | 高 | +| AUTH-03 | 密码重置 | 系统应提供安全的密码重置机制,包括通过邮箱或手机验证码验证身份。 | 中 | +| AUTH-04 | 权限控制 | 系统应基于用户角色(公众用户、网格员、主管、管理员、决策者)实施访问控制,确保用户只能访问其权限范围内的功能和数据。 | 高 | +| AUTH-05 | 会话管理 | 系统应维护和管理用户会话状态,包括会话创建、验证和超时处理。 | 中 | +| AUTH-06 | 个人资料管理 | 系统应允许用户查看和更新其个人资料,包括基本信息、联系方式和偏好设置。 | 低 | +| AUTH-07 | 安全日志 | 系统应记录所有关键安全事件(登录尝试、权限变更等),支持安全审计。 | 中 | + +**用户与认证流程图**: + +```mermaid +graph TD + A[用户访问系统] --> B{是否已登录?} + B -- 否 --> C{是否有账号?} + B -- 是 --> D[访问系统功能] + + C -- 否 --> E[注册新账号] + C -- 是 --> F[登录系统] + + E --> G[填写注册信息] + G --> H[验证邮箱/手机] + H --> I[创建用户账号] + I --> F + + F --> J{认证是否成功?} + J -- 否 --> K{是否忘记密码?} + J -- 是 --> L[生成安全令牌] + + K -- 是 --> M[密码重置流程] + K -- 否 --> F + + M --> N[验证身份] + N --> O[设置新密码] + O --> F + + L --> P[加载用户权限] + P --> D + + D --> Q[系统记录操作日志] + + R[用户请求退出] --> S[清除会话信息] + S --> T[返回登录页面] +``` + +### 5.2 反馈管理模块需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| FEED-01 | 反馈提交 | 系统应允许公众用户提交环境问题反馈,包括问题描述、位置标记、污染类型分类和图片上传。 | 高 | +| FEED-02 | AI内容审核 | 系统应自动分析反馈内容,识别垃圾信息、重复提交,并进行初步分类和紧急程度评估。 | 中 | +| FEED-03 | 人工审核 | 系统应提供主管审核反馈的工作台,支持查看详情、批准或驳回反馈。 | 高 | +| FEED-04 | 状态追踪 | 系统应记录和展示反馈的全生命周期状态变更,允许用户查询处理进度。 | 高 | +| FEED-05 | 反馈查询 | 系统应支持按多种条件(状态、时间、区域、类型等)查询和筛选反馈列表。 | 中 | +| FEED-06 | 反馈统计 | 系统应生成反馈数据的统计分析,包括数量分布、处理效率等指标。 | 低 | +| FEED-07 | 通知提醒 | 系统应在反馈状态变更时通知相关用户,保持信息透明。 | 中 | + +**反馈管理流程图**: + +```mermaid +graph TD + A[公众用户发现环境问题] --> B[打开反馈提交界面] + B --> C[填写问题描述] + C --> D[选择污染类型] + D --> E[评估严重程度] + E --> F[标记地理位置] + F --> G[上传现场照片] + G --> H[提交反馈] + + H --> I[系统生成唯一事件ID] + I --> J[AI自动审核内容] + + J --> K{AI审核结果} + K -- 明显无效 --> L[标记为AI_REJECTED] + K -- 需人工确认 --> M[进入主管审核队列] + + L --> N[通知用户反馈被拒绝] + + M --> O[主管查看反馈详情] + O --> P{审核决定} + P -- 驳回 --> Q[填写驳回理由] + P -- 通过 --> R[标记为有效反馈] + + Q --> S[更新状态为REJECTED] + S --> T[通知用户反馈被驳回] + + R --> U[创建关联任务] + U --> V[更新状态为PROCESSED] + + W[用户查询反馈状态] --> X[系统展示处理进度] + + Y[管理员查看统计数据] --> Z[系统生成反馈分析报表] +``` --- - + ### Report/需求定义_v4_第四部分.md - -# 需求定义文档(续) - -### 5.3 任务管理模块需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| TASK-01 | 任务创建 | 系统应支持从审核通过的反馈自动生成任务,也支持主管手动创建临时任务。 | 高 | -| TASK-02 | 智能分配 | 系统应基于多因素(网格员位置、当前负载、专业技能、历史表现)推荐最合适的处理人员。 | 中 | -| TASK-03 | 任务分配 | 系统应支持主管手动选择或确认系统推荐的网格员,并将任务分配给他们。 | 高 | -| TASK-04 | 任务通知 | 系统应在任务分配后立即通知相关网格员,并提供任务详情查看途径。 | 高 | -| TASK-05 | 任务执行跟踪 | 系统应记录任务的每个状态变更,支持网格员实时上报处理进度。 | 中 | -| TASK-06 | 路径规划 | 系统应为网格员提供从当前位置到任务地点的最优路径规划。 | 中 | -| TASK-07 | 结果提交 | 系统应支持网格员提交处理结果,包括文字描述和图片证明。 | 高 | -| TASK-08 | 结果审核 | 系统应支持主管审核网格员提交的处理结果,可以通过或驳回并提供反馈。 | 高 | -| TASK-09 | 任务查询 | 系统应支持按多种条件(状态、负责人、时间、区域等)查询和筛选任务列表。 | 中 | -| TASK-10 | 任务统计 | 系统应生成任务数据的统计分析,包括完成率、平均处理时长等绩效指标。 | 低 | -| TASK-11 | 任务看板 | 系统应提供直观的任务看板,展示不同状态任务的数量和分布。 | 低 | - -**任务管理流程图**: - -```mermaid -graph TD - A[反馈通过审核] --> B[自动创建任务] - C[主管手动创建任务] --> D[任务创建完成] - B --> D - D --> E{选择分配方式} - E -- 手动分配 --> F[主管选择网格员] - E -- 智能推荐 --> G[系统推荐最佳人选] - G --> F - F --> H[分配任务给网格员] - H --> I[通知网格员] - I --> J{网格员接受?} - J -- 否 --> E - J -- 是 --> K[更新任务状态为进行中] - K --> L[网格员获取路径规划] - L --> M[网格员处理任务] - M --> N[提交处理结果] - N --> O[主管审核结果] - O --> P{结果合格?} - P -- 否 --> Q[填写原因] - Q --> L - P -- 是 --> R[更新任务状态为已完成] - R --> S[通知反馈提交者] - S --> T[更新统计数据] -``` - -### 5.4 网格与地图模块需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| GRID-01 | 网格定义 | 系统应支持管理员定义和维护城市网格系统,包括网格的坐标、属性和责任人分配。 | 中 | -| GRID-02 | 地图可视化 | 系统应在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。 | 中 | -| GRID-03 | 位置标记 | 系统应支持用户在地图上精确标记环境问题位置,并自动关联到对应网格。 | 高 | -| GRID-04 | A*寻路算法 | 系统应基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划。 | 中 | -| GRID-05 | 区域统计 | 系统应基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | 低 | -| GRID-06 | 网格查询 | 系统应支持查询特定区域内的网格定义、属性和历史问题记录。 | 低 | -| GRID-07 | 离线地图 | 系统应支持地图数据的离线缓存,保证在网络不稳定情况下的基本功能。 | 低 | - -**网格与地图流程图**: - -```mermaid -graph TD - A[管理员定义网格系统] --> B[划分城市区域为网格单元] - B --> C[设置网格属性] - C --> D[分配网格责任人] - - E[用户标记环境问题位置] --> F[系统关联到对应网格] - F --> G[存储地理位置信息] - - H[网格员接收任务] --> I[查看任务位置] - I --> J[请求路径规划] - J --> K[A*算法计算最优路径] - K --> L[展示导航路线] - - M[管理员查看统计数据] --> N[生成环境问题热力图] - N --> O[识别问题高发区域] - O --> P[调整资源分配策略] -``` - -### 5.5 决策支持模块需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| DECI-01 | 核心指标看板 | 系统应实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。 | 中 | -| DECI-02 | 多维度分析 | 系统应支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。 | 中 | -| DECI-03 | 热力图可视化 | 系统应在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。 | 中 | -| DECI-04 | 绩效评估 | 系统应对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。 | 低 | -| DECI-05 | 预警机制 | 系统应基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | 低 | -| DECI-06 | 报表导出 | 系统应支持将分析结果导出为多种格式(PDF、Excel等),便于进一步分析和汇报。 | 低 | -| DECI-07 | 个性化配置 | 系统应支持决策者个性化配置数据看板,满足不同管理者的关注点。 | 低 | - -**决策支持流程图**: - -```mermaid -graph TD - A[系统收集业务数据] --> B[实时计算核心指标] - B --> C[展示核心指标看板] - - D[决策者选择分析维度] --> E[系统执行多维度分析] - E --> F[生成趋势图表] - - G[系统分析地理数据] --> H[生成环境问题热力图] - H --> I[标识高发区域] - - J[系统收集网格员工作数据] --> K[计算绩效指标] - K --> L[生成绩效排名] - - M[系统分析历史数据] --> N[识别异常趋势] - N --> O{是否达到预警阈值?} - O -- 是 --> P[触发预警通知] - O -- 否 --> Q[继续监控] - - R[决策者查看分析结果] --> S[选择导出格式] - S --> T[导出分析报表] -``` - -## 6. 非功能性需求 - -### 6.1 性能需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| PERF-01 | 响应时间 | 系统的页面加载时间应不超过3秒,API响应时间应不超过500毫秒(不包括文件上传下载)。 | 高 | -| PERF-02 | 并发用户 | 系统应能同时支持至少100个并发用户正常操作,不出现明显延迟。 | 中 | -| PERF-03 | 数据处理量 | 系统应能处理每日至少1000条反馈和500个任务的创建、更新和查询操作。 | 中 | -| PERF-04 | 文件处理 | 系统应支持单个文件最大10MB,总附件大小不超过50MB的上传和处理。 | 中 | -| PERF-05 | 地图渲染 | 地图界面应能在1秒内完成初始加载,并在500毫秒内响应用户的缩放和平移操作。 | 中 | - -### 6.2 可用性需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| USAB-01 | 界面一致性 | 系统界面应保持风格一致,包括颜色方案、按钮位置、导航结构等,减少用户学习成本。 | 中 | -| USAB-02 | 错误处理 | 系统应提供清晰的错误提示和恢复建议,避免用户操作中断。 | 高 | -| USAB-03 | 帮助系统 | 系统应提供上下文相关的帮助信息和操作指南,支持用户自助解决问题。 | 低 | -| USAB-04 | 响应式设计 | 系统界面应适应不同设备(PC、平板、手机)的屏幕尺寸,提供一致的用户体验。 | 高 | -| USAB-05 | 操作简化 | 核心功能的操作路径应尽量简化,减少用户点击次数和输入量。 | 中 | - -### 6.3 安全性需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| SECU-01 | 数据加密 | 敏感数据(如用户密码)必须加密存储,传输过程中应使用HTTPS加密。 | 高 | -| SECU-02 | 访问控制 | 系统应实施严格的基于角色的访问控制,确保用户只能访问其权限范围内的数据和功能。 | 高 | -| SECU-03 | 输入验证 | 系统应对所有用户输入进行验证和过滤,防止SQL注入、XSS等常见安全攻击。 | 高 | -| SECU-04 | 会话管理 | 系统应安全管理用户会话,包括会话创建、验证、超时和销毁,防止会话劫持。 | 中 | -| SECU-05 | 审计日志 | 系统应记录所有关键操作的审计日志,包括用户登录、数据修改、权限变更等。 | 中 | + +# 需求定义文档(续) + +### 5.3 任务管理模块需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| TASK-01 | 任务创建 | 系统应支持从审核通过的反馈自动生成任务,也支持主管手动创建临时任务。 | 高 | +| TASK-02 | 智能分配 | 系统应基于多因素(网格员位置、当前负载、专业技能、历史表现)推荐最合适的处理人员。 | 中 | +| TASK-03 | 任务分配 | 系统应支持主管手动选择或确认系统推荐的网格员,并将任务分配给他们。 | 高 | +| TASK-04 | 任务通知 | 系统应在任务分配后立即通知相关网格员,并提供任务详情查看途径。 | 高 | +| TASK-05 | 任务执行跟踪 | 系统应记录任务的每个状态变更,支持网格员实时上报处理进度。 | 中 | +| TASK-06 | 路径规划 | 系统应为网格员提供从当前位置到任务地点的最优路径规划。 | 中 | +| TASK-07 | 结果提交 | 系统应支持网格员提交处理结果,包括文字描述和图片证明。 | 高 | +| TASK-08 | 结果审核 | 系统应支持主管审核网格员提交的处理结果,可以通过或驳回并提供反馈。 | 高 | +| TASK-09 | 任务查询 | 系统应支持按多种条件(状态、负责人、时间、区域等)查询和筛选任务列表。 | 中 | +| TASK-10 | 任务统计 | 系统应生成任务数据的统计分析,包括完成率、平均处理时长等绩效指标。 | 低 | +| TASK-11 | 任务看板 | 系统应提供直观的任务看板,展示不同状态任务的数量和分布。 | 低 | + +**任务管理流程图**: + +```mermaid +graph TD + A[反馈通过审核] --> B[自动创建任务] + C[主管手动创建任务] --> D[任务创建完成] + B --> D + D --> E{选择分配方式} + E -- 手动分配 --> F[主管选择网格员] + E -- 智能推荐 --> G[系统推荐最佳人选] + G --> F + F --> H[分配任务给网格员] + H --> I[通知网格员] + I --> J{网格员接受?} + J -- 否 --> E + J -- 是 --> K[更新任务状态为进行中] + K --> L[网格员获取路径规划] + L --> M[网格员处理任务] + M --> N[提交处理结果] + N --> O[主管审核结果] + O --> P{结果合格?} + P -- 否 --> Q[填写原因] + Q --> L + P -- 是 --> R[更新任务状态为已完成] + R --> S[通知反馈提交者] + S --> T[更新统计数据] +``` + +### 5.4 网格与地图模块需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| GRID-01 | 网格定义 | 系统应支持管理员定义和维护城市网格系统,包括网格的坐标、属性和责任人分配。 | 中 | +| GRID-02 | 地图可视化 | 系统应在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。 | 中 | +| GRID-03 | 位置标记 | 系统应支持用户在地图上精确标记环境问题位置,并自动关联到对应网格。 | 高 | +| GRID-04 | A*寻路算法 | 系统应基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划。 | 中 | +| GRID-05 | 区域统计 | 系统应基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | 低 | +| GRID-06 | 网格查询 | 系统应支持查询特定区域内的网格定义、属性和历史问题记录。 | 低 | +| GRID-07 | 离线地图 | 系统应支持地图数据的离线缓存,保证在网络不稳定情况下的基本功能。 | 低 | + +**网格与地图流程图**: + +```mermaid +graph TD + A[管理员定义网格系统] --> B[划分城市区域为网格单元] + B --> C[设置网格属性] + C --> D[分配网格责任人] + + E[用户标记环境问题位置] --> F[系统关联到对应网格] + F --> G[存储地理位置信息] + + H[网格员接收任务] --> I[查看任务位置] + I --> J[请求路径规划] + J --> K[A*算法计算最优路径] + K --> L[展示导航路线] + + M[管理员查看统计数据] --> N[生成环境问题热力图] + N --> O[识别问题高发区域] + O --> P[调整资源分配策略] +``` + +### 5.5 决策支持模块需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| DECI-01 | 核心指标看板 | 系统应实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。 | 中 | +| DECI-02 | 多维度分析 | 系统应支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。 | 中 | +| DECI-03 | 热力图可视化 | 系统应在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。 | 中 | +| DECI-04 | 绩效评估 | 系统应对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。 | 低 | +| DECI-05 | 预警机制 | 系统应基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | 低 | +| DECI-06 | 报表导出 | 系统应支持将分析结果导出为多种格式(PDF、Excel等),便于进一步分析和汇报。 | 低 | +| DECI-07 | 个性化配置 | 系统应支持决策者个性化配置数据看板,满足不同管理者的关注点。 | 低 | + +**决策支持流程图**: + +```mermaid +graph TD + A[系统收集业务数据] --> B[实时计算核心指标] + B --> C[展示核心指标看板] + + D[决策者选择分析维度] --> E[系统执行多维度分析] + E --> F[生成趋势图表] + + G[系统分析地理数据] --> H[生成环境问题热力图] + H --> I[标识高发区域] + + J[系统收集网格员工作数据] --> K[计算绩效指标] + K --> L[生成绩效排名] + + M[系统分析历史数据] --> N[识别异常趋势] + N --> O{是否达到预警阈值?} + O -- 是 --> P[触发预警通知] + O -- 否 --> Q[继续监控] + + R[决策者查看分析结果] --> S[选择导出格式] + S --> T[导出分析报表] +``` + +## 6. 非功能性需求 + +### 6.1 性能需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| PERF-01 | 响应时间 | 系统的页面加载时间应不超过3秒,API响应时间应不超过500毫秒(不包括文件上传下载)。 | 高 | +| PERF-02 | 并发用户 | 系统应能同时支持至少100个并发用户正常操作,不出现明显延迟。 | 中 | +| PERF-03 | 数据处理量 | 系统应能处理每日至少1000条反馈和500个任务的创建、更新和查询操作。 | 中 | +| PERF-04 | 文件处理 | 系统应支持单个文件最大10MB,总附件大小不超过50MB的上传和处理。 | 中 | +| PERF-05 | 地图渲染 | 地图界面应能在1秒内完成初始加载,并在500毫秒内响应用户的缩放和平移操作。 | 中 | + +### 6.2 可用性需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| USAB-01 | 界面一致性 | 系统界面应保持风格一致,包括颜色方案、按钮位置、导航结构等,减少用户学习成本。 | 中 | +| USAB-02 | 错误处理 | 系统应提供清晰的错误提示和恢复建议,避免用户操作中断。 | 高 | +| USAB-03 | 帮助系统 | 系统应提供上下文相关的帮助信息和操作指南,支持用户自助解决问题。 | 低 | +| USAB-04 | 响应式设计 | 系统界面应适应不同设备(PC、平板、手机)的屏幕尺寸,提供一致的用户体验。 | 高 | +| USAB-05 | 操作简化 | 核心功能的操作路径应尽量简化,减少用户点击次数和输入量。 | 中 | + +### 6.3 安全性需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| SECU-01 | 数据加密 | 敏感数据(如用户密码)必须加密存储,传输过程中应使用HTTPS加密。 | 高 | +| SECU-02 | 访问控制 | 系统应实施严格的基于角色的访问控制,确保用户只能访问其权限范围内的数据和功能。 | 高 | +| SECU-03 | 输入验证 | 系统应对所有用户输入进行验证和过滤,防止SQL注入、XSS等常见安全攻击。 | 高 | +| SECU-04 | 会话管理 | 系统应安全管理用户会话,包括会话创建、验证、超时和销毁,防止会话劫持。 | 中 | +| SECU-05 | 审计日志 | 系统应记录所有关键操作的审计日志,包括用户登录、数据修改、权限变更等。 | 中 | --- - + ### Report/需求定义_v4_第五部分.md - -# 需求定义文档(续) - -### 6.4 可靠性需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| RELI-01 | 系统可用性 | 系统应保证7x24小时的稳定运行,计划内维护时间除外,系统可用性应达到99.5%以上。 | 高 | -| RELI-02 | 数据备份 | 系统应定期(至少每日一次)对全部业务数据进行备份,并支持数据恢复。 | 高 | -| RELI-03 | 故障恢复 | 系统应具备故障检测和自动恢复机制,在出现异常情况时能够快速恢复正常运行。 | 中 | -| RELI-04 | 数据一致性 | 系统应确保在各种操作条件下(包括并发访问和异常中断)保持数据的一致性和完整性。 | 高 | -| RELI-05 | 容错能力 | 系统应能够处理常见的错误情况(如网络中断、数据异常),并提供适当的降级服务。 | 中 | - -### 6.5 可维护性需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| MAIN-01 | 模块化设计 | 系统应采用模块化设计,各功能模块之间松耦合,便于独立开发、测试和维护。 | 中 | -| MAIN-02 | 配置管理 | 系统应支持通过配置文件或管理界面调整系统参数,无需修改代码和重新部署。 | 中 | -| MAIN-03 | 日志记录 | 系统应记录详细的运行日志,包括错误信息、性能指标和关键操作,便于问题诊断。 | 高 | -| MAIN-04 | 版本升级 | 系统应支持平滑的版本升级机制,最小化升级对用户的影响。 | 低 | -| MAIN-05 | 代码规范 | 系统开发应遵循统一的编码规范和设计模式,提高代码可读性和可维护性。 | 中 | - -### 6.6 可扩展性需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| SCAL-01 | 水平扩展 | 系统架构应支持通过增加服务器节点进行水平扩展,以应对用户量和数据量的增长。 | 低 | -| SCAL-02 | 功能扩展 | 系统应支持在不影响现有功能的情况下,添加新的功能模块和业务流程。 | 中 | -| SCAL-03 | 接口开放 | 系统应提供标准化的API接口,便于与其他系统集成和数据交换。 | 中 | -| SCAL-04 | 多租户支持 | 系统应具备支持多租户(如不同城市或区域)的潜力,能够在未来进行扩展。 | 低 | -| SCAL-05 | 数据量增长 | 系统应能够处理数据量的持续增长,性能不应随着数据量的增加而明显下降。 | 中 | - -## 7. 数据需求 - -### 7.1 数据实体 - -系统需要管理以下核心数据实体: - -1. **用户账户 (UserAccount)**:存储系统用户的基本信息、认证信息和角色权限。 -2. **反馈 (Feedback)**:存储公众提交的环境问题反馈,包括描述、位置、图片等信息。 -3. **任务 (Task)**:存储从反馈生成的工作任务,包括任务详情、状态和处理记录。 -4. **网格 (Grid)**:存储城市区域的网格化管理数据,包括网格坐标、属性和责任人。 -5. **任务分配 (Assignment)**:存储任务与网格员之间的分配关系,包括分配时间、状态等。 -6. **附件 (Attachment)**:存储反馈和任务相关的图片、文档等附件文件。 -7. **操作日志 (OperationLog)**:存储系统中的关键操作记录,用于审计和问题追溯。 - -### 7.2 数据量估计 - -基于系统预期使用规模,估计以下数据量: - -1. **用户账户**:预计1000个用户账户,包括公众用户、网格员、主管、管理员和决策者。 -2. **反馈**:预计每日100-200条新增反馈,年增长约5万条。 -3. **任务**:预计每日50-100个新增任务,年增长约2.5万个。 -4. **网格**:预计1000-5000个网格单元,根据城市规模和网格粒度而定。 -5. **附件**:预计每条反馈平均2张图片,每个任务平均2张图片,年增长约15万个文件。 -6. **操作日志**:预计每日1万条日志记录,主要用于短期审计,长期可归档。 - -### 7.3 数据保留策略 - -1. **核心业务数据**(用户、反馈、任务、网格):长期保留,至少保留3年。 -2. **附件文件**:保留1年,可根据存储容量考虑适当延长或缩短。 -3. **操作日志**: - - 详细日志保留30天 - - 关键审计日志保留1年 - - 统计汇总数据长期保留 - -### 7.4 数据备份要求 - -1. **备份频率**: - - 核心业务数据:每日全量备份,每小时增量备份 - - 附件文件:每周全量备份,每日增量备份 - - 配置数据:每次变更后备份 - -2. **备份保留**: - - 每日备份保留7天 - - 每周备份保留1个月 - - 每月备份保留6个月 - - 每年备份永久保留 - -3. **恢复能力**:系统应支持将数据恢复到任意备份点,恢复时间目标(RTO)不超过4小时。 - -## 8. 结论 - -### 8.1 需求优先级总结 - -基于业务重要性和实现复杂度,系统需求按以下优先级排序: - -1. **最高优先级**: - - 用户认证和权限控制 - - 反馈提交和审核 - - 任务创建和分配 - - 数据安全和完整性 - -2. **高优先级**: - - 任务执行和结果审核 - - 地图标记和路径规划 - - 系统性能和可用性 - - 用户界面和体验 - -3. **中等优先级**: - - AI内容审核 - - 决策支持和数据分析 - - 通知和提醒功能 - - 系统可维护性 - -4. **低优先级**: - - 高级统计和报表 - - 个性化配置 - - 系统扩展性 - - 辅助功能和优化 - -### 8.2 实施建议 - -为确保系统成功实施,建议采取以下策略: - -1. **分阶段实施**: - - 第一阶段:核心功能(用户管理、反馈、任务) - - 第二阶段:支撑功能(地图、AI审核) - - 第三阶段:高级功能(决策支持、报表) - -2. **持续迭代**:采用敏捷开发方法,快速交付可用版本,根据用户反馈持续改进。 - -3. **用户参与**:在开发过程中邀请各类用户参与测试和评估,确保系统满足实际需求。 - -4. **技术选型**:选择成熟稳定的技术栈,平衡创新性和可靠性,降低开发和维护风险。 - -5. **数据治理**:建立完善的数据管理和治理机制,确保数据质量和安全。 - -### 8.3 预期效益 - -成功实施环境监测系统预期将带来以下效益: - -1. **业务效益**: - - 环境问题处理效率提升50%以上 - - 问题解决质量显著提高 - - 资源利用更加合理高效 - -2. **管理效益**: - - 实现环境问题全流程可视化管理 - - 提供科学决策的数据支持 - - 优化工作流程和资源配置 - -3. **社会效益**: - - 提升公众参与环境治理的积极性 - - 增强政府工作透明度和公信力 - - 改善城市环境质量和居民生活满意度 - -通过环境监测系统的建设和应用,将有效解决当前环境问题管理中的痛点,构建起连接公众、管理部门和执行人员的桥梁,实现环境问题的快速发现、高效处理和全程监督,为城市环境治理提供有力支撑。 + +# 需求定义文档(续) + +### 6.4 可靠性需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| RELI-01 | 系统可用性 | 系统应保证7x24小时的稳定运行,计划内维护时间除外,系统可用性应达到99.5%以上。 | 高 | +| RELI-02 | 数据备份 | 系统应定期(至少每日一次)对全部业务数据进行备份,并支持数据恢复。 | 高 | +| RELI-03 | 故障恢复 | 系统应具备故障检测和自动恢复机制,在出现异常情况时能够快速恢复正常运行。 | 中 | +| RELI-04 | 数据一致性 | 系统应确保在各种操作条件下(包括并发访问和异常中断)保持数据的一致性和完整性。 | 高 | +| RELI-05 | 容错能力 | 系统应能够处理常见的错误情况(如网络中断、数据异常),并提供适当的降级服务。 | 中 | + +### 6.5 可维护性需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| MAIN-01 | 模块化设计 | 系统应采用模块化设计,各功能模块之间松耦合,便于独立开发、测试和维护。 | 中 | +| MAIN-02 | 配置管理 | 系统应支持通过配置文件或管理界面调整系统参数,无需修改代码和重新部署。 | 中 | +| MAIN-03 | 日志记录 | 系统应记录详细的运行日志,包括错误信息、性能指标和关键操作,便于问题诊断。 | 高 | +| MAIN-04 | 版本升级 | 系统应支持平滑的版本升级机制,最小化升级对用户的影响。 | 低 | +| MAIN-05 | 代码规范 | 系统开发应遵循统一的编码规范和设计模式,提高代码可读性和可维护性。 | 中 | + +### 6.6 可扩展性需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| SCAL-01 | 水平扩展 | 系统架构应支持通过增加服务器节点进行水平扩展,以应对用户量和数据量的增长。 | 低 | +| SCAL-02 | 功能扩展 | 系统应支持在不影响现有功能的情况下,添加新的功能模块和业务流程。 | 中 | +| SCAL-03 | 接口开放 | 系统应提供标准化的API接口,便于与其他系统集成和数据交换。 | 中 | +| SCAL-04 | 多租户支持 | 系统应具备支持多租户(如不同城市或区域)的潜力,能够在未来进行扩展。 | 低 | +| SCAL-05 | 数据量增长 | 系统应能够处理数据量的持续增长,性能不应随着数据量的增加而明显下降。 | 中 | + +## 7. 数据需求 + +### 7.1 数据实体 + +系统需要管理以下核心数据实体: + +1. **用户账户 (UserAccount)**:存储系统用户的基本信息、认证信息和角色权限。 +2. **反馈 (Feedback)**:存储公众提交的环境问题反馈,包括描述、位置、图片等信息。 +3. **任务 (Task)**:存储从反馈生成的工作任务,包括任务详情、状态和处理记录。 +4. **网格 (Grid)**:存储城市区域的网格化管理数据,包括网格坐标、属性和责任人。 +5. **任务分配 (Assignment)**:存储任务与网格员之间的分配关系,包括分配时间、状态等。 +6. **附件 (Attachment)**:存储反馈和任务相关的图片、文档等附件文件。 +7. **操作日志 (OperationLog)**:存储系统中的关键操作记录,用于审计和问题追溯。 + +### 7.2 数据量估计 + +基于系统预期使用规模,估计以下数据量: + +1. **用户账户**:预计1000个用户账户,包括公众用户、网格员、主管、管理员和决策者。 +2. **反馈**:预计每日100-200条新增反馈,年增长约5万条。 +3. **任务**:预计每日50-100个新增任务,年增长约2.5万个。 +4. **网格**:预计1000-5000个网格单元,根据城市规模和网格粒度而定。 +5. **附件**:预计每条反馈平均2张图片,每个任务平均2张图片,年增长约15万个文件。 +6. **操作日志**:预计每日1万条日志记录,主要用于短期审计,长期可归档。 + +### 7.3 数据保留策略 + +1. **核心业务数据**(用户、反馈、任务、网格):长期保留,至少保留3年。 +2. **附件文件**:保留1年,可根据存储容量考虑适当延长或缩短。 +3. **操作日志**: + - 详细日志保留30天 + - 关键审计日志保留1年 + - 统计汇总数据长期保留 + +### 7.4 数据备份要求 + +1. **备份频率**: + - 核心业务数据:每日全量备份,每小时增量备份 + - 附件文件:每周全量备份,每日增量备份 + - 配置数据:每次变更后备份 + +2. **备份保留**: + - 每日备份保留7天 + - 每周备份保留1个月 + - 每月备份保留6个月 + - 每年备份永久保留 + +3. **恢复能力**:系统应支持将数据恢复到任意备份点,恢复时间目标(RTO)不超过4小时。 + +## 8. 结论 + +### 8.1 需求优先级总结 + +基于业务重要性和实现复杂度,系统需求按以下优先级排序: + +1. **最高优先级**: + - 用户认证和权限控制 + - 反馈提交和审核 + - 任务创建和分配 + - 数据安全和完整性 + +2. **高优先级**: + - 任务执行和结果审核 + - 地图标记和路径规划 + - 系统性能和可用性 + - 用户界面和体验 + +3. **中等优先级**: + - AI内容审核 + - 决策支持和数据分析 + - 通知和提醒功能 + - 系统可维护性 + +4. **低优先级**: + - 高级统计和报表 + - 个性化配置 + - 系统扩展性 + - 辅助功能和优化 + +### 8.2 实施建议 + +为确保系统成功实施,建议采取以下策略: + +1. **分阶段实施**: + - 第一阶段:核心功能(用户管理、反馈、任务) + - 第二阶段:支撑功能(地图、AI审核) + - 第三阶段:高级功能(决策支持、报表) + +2. **持续迭代**:采用敏捷开发方法,快速交付可用版本,根据用户反馈持续改进。 + +3. **用户参与**:在开发过程中邀请各类用户参与测试和评估,确保系统满足实际需求。 + +4. **技术选型**:选择成熟稳定的技术栈,平衡创新性和可靠性,降低开发和维护风险。 + +5. **数据治理**:建立完善的数据管理和治理机制,确保数据质量和安全。 + +### 8.3 预期效益 + +成功实施环境监测系统预期将带来以下效益: + +1. **业务效益**: + - 环境问题处理效率提升50%以上 + - 问题解决质量显著提高 + - 资源利用更加合理高效 + +2. **管理效益**: + - 实现环境问题全流程可视化管理 + - 提供科学决策的数据支持 + - 优化工作流程和资源配置 + +3. **社会效益**: + - 提升公众参与环境治理的积极性 + - 增强政府工作透明度和公信力 + - 改善城市环境质量和居民生活满意度 + +通过环境监测系统的建设和应用,将有效解决当前环境问题管理中的痛点,构建起连接公众、管理部门和执行人员的桥梁,实现环境问题的快速发现、高效处理和全程监督,为城市环境治理提供有力支撑。 --- - + ### Report/需求定义_v4_第一部分.md - -# 需求定义文档 - -## 1. 项目介绍 - -### 1.1 项目背景 - -环境监测系统(EMS)是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速,环境污染问题日益凸显,传统的环境问题上报和处理机制存在以下痛点: - -- **流程繁琐**:公众发现环境问题后,需要通过多个渠道和部门层层上报,处理流程不透明 -- **响应缓慢**:从问题发现到最终解决,往往需要经历漫长的等待时间 -- **缺乏透明度**:公众难以了解问题处理进度,无法有效监督 -- **资源分配不合理**:缺乏科学的任务分配机制,导致人力资源利用不均衡 - -本系统旨在通过数字化手段,构建一个连接公众、管理部门和执行人员的环境监测与治理平台,实现环境问题的快速发现、高效处理和全程监督。 - -### 1.2 项目目标 - -1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。 -2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。 -3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。 -4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。 -5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。 - -## 2. 系统分析 - -### 2.1 业务痛点分析 - -1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。 -2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。 -3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。 -4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。 -5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。 - -### 2.2 用户角色分析 - -环境监测系统涉及五类主要用户角色,每个角色在系统中承担不同的职责: - -1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。 - - 需求:简单便捷的问题上报方式、透明的处理进度查询 - - 痛点:传统上报渠道繁琐、反馈周期长、处理结果不透明 - -2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。 - - 需求:清晰的任务指派、便捷的结果上报、高效的路径规划 - - 痛点:任务分配不合理、工作量分布不均、缺乏高效导航 - -3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。 - - 需求:高效的任务管理、智能的人员调配、直观的进度监控 - - 痛点:人工分配任务效率低、缺乏全局视角、绩效评估困难 - -4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。 - - 需求:灵活的权限配置、完善的日志审计、便捷的系统维护 - - 痛点:账户管理繁琐、权限控制粗放、系统维护成本高 - -5. **决策者**:系统的战略端,通过分析系统生成的统计数据和趋势图表,制定环境管理策略和资源分配决策,是系统的最终受益者之一。 - - 需求:多维度的数据分析、直观的可视化展示、科学的决策支持 - - 痛点:数据获取困难、分析维度单一、缺乏预测能力 - -### 2.3 用例分析 - -#### 2.3.1 用例图 - -以下用例图展示了系统中各角色可以执行的主要操作: - -```mermaid -graph TD - %% 定义角色 - PublicUser["公众用户"] - GridWorker["网格员"] - Supervisor["主管"] - Admin["管理员"] - DecisionMaker["决策者"] - - %% 定义用例 - UC1["注册与登录"] - UC2["提交环境问题反馈"] - UC3["查看反馈处理进度"] - UC4["接收任务通知"] - UC5["执行任务"] - UC6["提交处理结果"] - UC7["审核反馈内容"] - UC8["分配任务"] - UC9["审核处理结果"] - UC10["查看统计数据"] - UC11["管理用户账户"] - UC12["配置系统参数"] - UC13["查看决策仪表盘"] - UC14["生成分析报告"] - - %% 建立关系 - PublicUser --> UC1 - PublicUser --> UC2 - PublicUser --> UC3 - - GridWorker --> UC1 - GridWorker --> UC4 - GridWorker --> UC5 - GridWorker --> UC6 - - Supervisor --> UC1 - Supervisor --> UC7 - Supervisor --> UC8 - Supervisor --> UC9 - Supervisor --> UC10 - - Admin --> UC1 - Admin --> UC10 - Admin --> UC11 - Admin --> UC12 - - DecisionMaker --> UC1 - DecisionMaker --> UC10 - DecisionMaker --> UC13 - DecisionMaker --> UC14 - - %% 设置样式 - classDef actor fill:#f9f,stroke:#333,stroke-width:2px - classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px - - class PublicUser,GridWorker,Supervisor,Admin,DecisionMaker actor - class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12,UC13,UC14 usecase -``` - -#### 2.3.2 活动图:反馈提交与处理流程 - -以下活动图展示了从公众发现环境问题到反馈处理完成的完整业务流程: - -```mermaid -stateDiagram-v2 - [*] --> 发现环境问题 - 发现环境问题 --> 填写反馈表单 - 填写反馈表单 --> 上传图片 - 上传图片 --> 标记位置 - 标记位置 --> 提交反馈 - 提交反馈 --> AI自动审核 - - state AI自动审核 { - [*] --> 内容分析 - 内容分析 --> 垃圾信息检测 - 垃圾信息检测 --> 分类与评级 - 分类与评级 --> [*] - } - - AI自动审核 --> 判断AI审核结果 - 判断AI审核结果 --> 明显无效: AI拒绝 - 判断AI审核结果 --> 需人工确认: 需确认 - - 明显无效 --> 标记为AI_REJECTED - 标记为AI_REJECTED --> 通知提交者 - 通知提交者 --> [*] - - 需人工确认 --> 主管人工审核 - 主管人工审核 --> 判断审核结果 - - 判断审核结果 --> 驳回: 不通过 - 判断审核结果 --> 通过: 通过 - - 驳回 --> 填写驳回理由 - 填写驳回理由 --> 更新状态为REJECTED - 更新状态为REJECTED --> 通知提交者反馈被驳回 - 通知提交者反馈被驳回 --> [*] - - 通过 --> 创建任务 - 创建任务 --> 更新反馈状态为PROCESSED - 更新反馈状态为PROCESSED --> 任务分配流程 - 任务分配流程 --> [*] -``` + +# 需求定义文档 + +## 1. 项目介绍 + +### 1.1 项目背景 + +环境监测系统(EMS)是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速,环境污染问题日益凸显,传统的环境问题上报和处理机制存在以下痛点: + +- **流程繁琐**:公众发现环境问题后,需要通过多个渠道和部门层层上报,处理流程不透明 +- **响应缓慢**:从问题发现到最终解决,往往需要经历漫长的等待时间 +- **缺乏透明度**:公众难以了解问题处理进度,无法有效监督 +- **资源分配不合理**:缺乏科学的任务分配机制,导致人力资源利用不均衡 + +本系统旨在通过数字化手段,构建一个连接公众、管理部门和执行人员的环境监测与治理平台,实现环境问题的快速发现、高效处理和全程监督。 + +### 1.2 项目目标 + +1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。 +2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。 +3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。 +4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。 +5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。 + +## 2. 系统分析 + +### 2.1 业务痛点分析 + +1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。 +2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。 +3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。 +4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。 +5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。 + +### 2.2 用户角色分析 + +环境监测系统涉及五类主要用户角色,每个角色在系统中承担不同的职责: + +1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。 + - 需求:简单便捷的问题上报方式、透明的处理进度查询 + - 痛点:传统上报渠道繁琐、反馈周期长、处理结果不透明 + +2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。 + - 需求:清晰的任务指派、便捷的结果上报、高效的路径规划 + - 痛点:任务分配不合理、工作量分布不均、缺乏高效导航 + +3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。 + - 需求:高效的任务管理、智能的人员调配、直观的进度监控 + - 痛点:人工分配任务效率低、缺乏全局视角、绩效评估困难 + +4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。 + - 需求:灵活的权限配置、完善的日志审计、便捷的系统维护 + - 痛点:账户管理繁琐、权限控制粗放、系统维护成本高 + +5. **决策者**:系统的战略端,通过分析系统生成的统计数据和趋势图表,制定环境管理策略和资源分配决策,是系统的最终受益者之一。 + - 需求:多维度的数据分析、直观的可视化展示、科学的决策支持 + - 痛点:数据获取困难、分析维度单一、缺乏预测能力 + +### 2.3 用例分析 + +#### 2.3.1 用例图 + +以下用例图展示了系统中各角色可以执行的主要操作: + +```mermaid +graph TD + %% 定义角色 + PublicUser["公众用户"] + GridWorker["网格员"] + Supervisor["主管"] + Admin["管理员"] + DecisionMaker["决策者"] + + %% 定义用例 + UC1["注册与登录"] + UC2["提交环境问题反馈"] + UC3["查看反馈处理进度"] + UC4["接收任务通知"] + UC5["执行任务"] + UC6["提交处理结果"] + UC7["审核反馈内容"] + UC8["分配任务"] + UC9["审核处理结果"] + UC10["查看统计数据"] + UC11["管理用户账户"] + UC12["配置系统参数"] + UC13["查看决策仪表盘"] + UC14["生成分析报告"] + + %% 建立关系 + PublicUser --> UC1 + PublicUser --> UC2 + PublicUser --> UC3 + + GridWorker --> UC1 + GridWorker --> UC4 + GridWorker --> UC5 + GridWorker --> UC6 + + Supervisor --> UC1 + Supervisor --> UC7 + Supervisor --> UC8 + Supervisor --> UC9 + Supervisor --> UC10 + + Admin --> UC1 + Admin --> UC10 + Admin --> UC11 + Admin --> UC12 + + DecisionMaker --> UC1 + DecisionMaker --> UC10 + DecisionMaker --> UC13 + DecisionMaker --> UC14 + + %% 设置样式 + classDef actor fill:#f9f,stroke:#333,stroke-width:2px + classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px + + class PublicUser,GridWorker,Supervisor,Admin,DecisionMaker actor + class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12,UC13,UC14 usecase +``` + +#### 2.3.2 活动图:反馈提交与处理流程 + +以下活动图展示了从公众发现环境问题到反馈处理完成的完整业务流程: + +```mermaid +stateDiagram-v2 + [*] --> 发现环境问题 + 发现环境问题 --> 填写反馈表单 + 填写反馈表单 --> 上传图片 + 上传图片 --> 标记位置 + 标记位置 --> 提交反馈 + 提交反馈 --> AI自动审核 + + state AI自动审核 { + [*] --> 内容分析 + 内容分析 --> 垃圾信息检测 + 垃圾信息检测 --> 分类与评级 + 分类与评级 --> [*] + } + + AI自动审核 --> 判断AI审核结果 + 判断AI审核结果 --> 明显无效: AI拒绝 + 判断AI审核结果 --> 需人工确认: 需确认 + + 明显无效 --> 标记为AI_REJECTED + 标记为AI_REJECTED --> 通知提交者 + 通知提交者 --> [*] + + 需人工确认 --> 主管人工审核 + 主管人工审核 --> 判断审核结果 + + 判断审核结果 --> 驳回: 不通过 + 判断审核结果 --> 通过: 通过 + + 驳回 --> 填写驳回理由 + 填写驳回理由 --> 更新状态为REJECTED + 更新状态为REJECTED --> 通知提交者反馈被驳回 + 通知提交者反馈被驳回 --> [*] + + 通过 --> 创建任务 + 创建任务 --> 更新反馈状态为PROCESSED + 更新反馈状态为PROCESSED --> 任务分配流程 + 任务分配流程 --> [*] +``` --- - + ### Report/需求定义_v4.md - -# 需求定义文档 - -## 1. 项目介绍 - -### 1.1 项目背景 - -环境监测系统(EMS)是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速,环境污染问题日益凸显,传统的环境问题上报和处理机制存在以下痛点: - -- **流程繁琐**:公众发现环境问题后,需要通过多个渠道和部门层层上报,处理流程不透明 -- **响应缓慢**:从问题发现到最终解决,往往需要经历漫长的等待时间 -- **缺乏透明度**:公众难以了解问题处理进度,无法有效监督 -- **资源分配不合理**:缺乏科学的任务分配机制,导致人力资源利用不均衡 - -本系统旨在通过数字化手段,构建一个连接公众、管理部门和执行人员的环境监测与治理平台,实现环境问题的快速发现、高效处理和全程监督。 - -### 1.2 项目目标 - -1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。 -2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。 -3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。 -4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。 -5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。 - -## 2. 系统分析 - -### 2.1 业务痛点分析 - -1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。 -2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。 -3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。 -4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。 -5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。 - -### 2.2 用户角色分析 - -环境监测系统涉及五类主要用户角色,每个角色在系统中承担不同的职责: - -1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。 - - 需求:简单便捷的问题上报方式、透明的处理进度查询 - - 痛点:传统上报渠道繁琐、反馈周期长、处理结果不透明 - -2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。 - - 需求:清晰的任务指派、便捷的结果上报、高效的路径规划 - - 痛点:任务分配不合理、工作量分布不均、缺乏高效导航 - -3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。 - - 需求:高效的任务管理、智能的人员调配、直观的进度监控 - - 痛点:人工分配任务效率低、缺乏全局视角、绩效评估困难 - -4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。 - - 需求:灵活的权限配置、完善的日志审计、便捷的系统维护 - - 痛点:账户管理繁琐、权限控制粗放、系统维护成本高 - -5. **决策者**:系统的战略端,通过分析系统生成的统计数据和趋势图表,制定环境管理策略和资源分配决策,是系统的最终受益者之一。 - - 需求:多维度的数据分析、直观的可视化展示、科学的决策支持 - - 痛点:数据获取困难、分析维度单一、缺乏预测能力 - -### 2.3 用例分析 - -#### 2.3.1 用例图 - -以下用例图展示了系统中各角色可以执行的主要操作: - -```mermaid -graph TD - %% 定义角色 - PublicUser["公众用户"] - GridWorker["网格员"] - Supervisor["主管"] - Admin["管理员"] - DecisionMaker["决策者"] - - %% 定义用例 - UC1["注册与登录"] - UC2["提交环境问题反馈"] - UC3["查看反馈处理进度"] - UC4["接收任务通知"] - UC5["执行任务"] - UC6["提交处理结果"] - UC7["审核反馈内容"] - UC8["分配任务"] - UC9["审核处理结果"] - UC10["查看统计数据"] - UC11["管理用户账户"] - UC12["配置系统参数"] - UC13["查看决策仪表盘"] - UC14["生成分析报告"] - - %% 建立关系 - PublicUser --> UC1 - PublicUser --> UC2 - PublicUser --> UC3 - - GridWorker --> UC1 - GridWorker --> UC4 - GridWorker --> UC5 - GridWorker --> UC6 - - Supervisor --> UC1 - Supervisor --> UC7 - Supervisor --> UC8 - Supervisor --> UC9 - Supervisor --> UC10 - - Admin --> UC1 - Admin --> UC10 - Admin --> UC11 - Admin --> UC12 - - DecisionMaker --> UC1 - DecisionMaker --> UC10 - DecisionMaker --> UC13 - DecisionMaker --> UC14 - - %% 设置样式 - classDef actor fill:#f9f,stroke:#333,stroke-width:2px - classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px - - class PublicUser,GridWorker,Supervisor,Admin,DecisionMaker actor - class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12,UC13,UC14 usecase -``` - -#### 2.3.2 活动图:反馈提交与处理流程 - -以下活动图展示了从公众发现环境问题到反馈处理完成的完整业务流程: - -```mermaid -stateDiagram-v2 - [*] --> 发现环境问题 - 发现环境问题 --> 填写反馈表单 - 填写反馈表单 --> 上传图片 - 上传图片 --> 标记位置 - 标记位置 --> 提交反馈 - 提交反馈 --> AI自动审核 - - state AI自动审核 { - [*] --> 内容分析 - 内容分析 --> 垃圾信息检测 - 垃圾信息检测 --> 分类与评级 - 分类与评级 --> [*] - } - - AI自动审核 --> 判断AI审核结果 - 判断AI审核结果 --> 明显无效: AI拒绝 - 判断AI审核结果 --> 需人工确认: 需确认 - - 明显无效 --> 标记为AI_REJECTED - 标记为AI_REJECTED --> 通知提交者 - 通知提交者 --> [*] - - 需人工确认 --> 主管人工审核 - 主管人工审核 --> 判断审核结果 - - 判断审核结果 --> 驳回: 不通过 - 判断审核结果 --> 通过: 通过 - - 驳回 --> 填写驳回理由 - 填写驳回理由 --> 更新状态为REJECTED - 更新状态为REJECTED --> 通知提交者反馈被驳回 - 通知提交者反馈被驳回 --> [*] - - 通过 --> 创建任务 - 创建任务 --> 更新反馈状态为PROCESSED - 更新反馈状态为PROCESSED --> 任务分配流程 - 任务分配流程 --> [*] -``` -# 需求定义文档(续) - -#### 2.3.3 活动图:任务分配与执行流程 - -以下活动图展示了从任务创建到完成的完整业务流程: - -```mermaid -stateDiagram-v2 - [*] --> 任务创建完成 - 任务创建完成 --> 选择分配方式 - - state 选择分配方式 { - [*] --> 手动分配 - [*] --> 智能推荐 - - 智能推荐 --> 运行分配算法 - 运行分配算法 --> 推荐最佳人选 - 推荐最佳人选 --> 确认人选 - - 手动分配 --> 选择特定网格员 - 选择特定网格员 --> 确认人选 - - 确认人选 --> [*] - } - - 选择分配方式 --> 创建任务分配记录 - 创建任务分配记录 --> 更新任务状态为ASSIGNED - 更新任务状态为ASSIGNED --> 通知网格员 - - 通知网格员 --> 网格员接收通知 - 网格员接收通知 --> 判断是否接受 - - 判断是否接受 --> 拒绝: 拒绝 - 判断是否接受 --> 接受: 接受 - - 拒绝 --> 选择分配方式 - - 接受 --> 更新状态为IN_PROGRESS - 更新状态为IN_PROGRESS --> 获取路径规划 - 获取路径规划 --> 前往现场处理 - 前往现场处理 --> 记录处理过程 - 记录处理过程 --> 上传处理结果 - 上传处理结果 --> 提交处理结果 - 提交处理结果 --> 更新状态为SUBMITTED - - 更新状态为SUBMITTED --> 主管审核结果 - 主管审核结果 --> 判断结果是否合格 - - 判断结果是否合格 --> 不合格: 不合格 - 判断结果是否合格 --> 合格: 合格 - - 不合格 --> 填写原因要求重新处理 - 填写原因要求重新处理 --> 获取路径规划 - - 合格 --> 确认任务完成 - 确认任务完成 --> 更新任务状态为APPROVED - 更新任务状态为APPROVED --> 更新反馈状态为CLOSED - 更新反馈状态为CLOSED --> 通知反馈提交者 - 通知反馈提交者 --> 更新统计数据 - 更新统计数据 --> [*] -``` - -### 2.4 系统可行性分析 - -#### 2.4.1 技术可行性 - -本系统的技术实现是可行的,主要基于以下分析: - -1. **前端技术**:市场上已有成熟的前端框架和组件库,可以快速开发出美观、响应式的Web应用,满足不同设备的访问需求。 - -2. **后端技术**:现有的后端框架具备高性能、高并发处理能力,能够满足系统的稳定性和扩展性需求。 - -3. **地图服务**:可以集成成熟的地图API,实现地理位置标记、路径规划等功能,无需从零开发。 - -4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。 - -5. **数据存储**:可采用多种数据存储方案,根据系统规模和性能需求灵活选择。 - -#### 2.4.2 经济可行性 - -从经济角度看,本系统的开发和运营是可行的: - -1. **开发成本**:可以采用主流开源框架和技术栈,降低开发成本和技术门槛。 - -2. **维护成本**:通过模块化设计和完善的文档,可以降低后期维护和升级成本。 - -3. **投资回报**: - - 提高环境问题处理效率,减少人力资源浪费 - - 降低环境问题带来的经济损失 - - 提升城市环境质量,间接促进经济发展 - - 长期来看具有良好的投资回报 - -4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。 - -#### 2.4.3 操作可行性 - -从操作角度看,本系统具有良好的可行性: - -1. **用户接受度**: - - 系统界面设计简洁直观,符合用户习惯 - - 操作流程简化,降低学习成本 - - 移动端支持,满足随时随地使用需求 - -2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。 - -3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。 - -4. **组织支持**:系统实施需要相关部门的协调配合,但总体上不会对现有组织架构产生颠覆性影响。 - -#### 2.4.4 法律可行性 - -从法律合规角度看,本系统的实施不存在重大障碍: - -1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。 - -2. **知识产权**:系统开发过程中使用的第三方库和组件均可采用开源或获得授权的方式,避免知识产权风险。 - -3. **合规性**:系统功能和流程设计可以确保符合相关法律法规和行业标准,确保合法合规运营。 - -## 3. 功能性需求 - -### 3.1 功能层次方框图 - -以下功能层次图展示了系统的整体功能架构: - -```mermaid -flowchart TD - %% 用户交互层 - subgraph "用户交互层" - direction LR - C["公众服务模块\n(问题上报)"] - H["个人中心模块\n(我的反馈/资料)"] - D["管理驾驶舱\n(数据决策)"] - end - - %% 核心业务层 - subgraph "核心业务层" - direction LR - AI["AI分析模块\n(内容审核)"] - E["任务管理模块\n(分配、流转、执行)"] - end - - %% 应用支撑层 - subgraph "应用支撑层" - direction LR - F["网格与地图模块\n(LBS & 寻路)"] - I["文件服务模块\n(附件存取)"] - end - - %% 基础服务层 - subgraph "基础服务层" - direction LR - B["用户与认证模块"] - G["系统管理模块\n(用户/权限)"] - J["日志审计模块"] - end - - %% 定义关系 - C -- "提交反馈" --> AI - AI -- "分析结果" --> E - C -- "附件" --> I - E -- "调用" --> F - E -- "任务附件" --> I - E -- "统计数据" --> D - - H -- "查询个人数据" --> E - - %% 基础服务支撑所有上层模块 (关系隐含) - G -- "管理" --> B - - classDef userLayer fill:#d4f1f9,stroke:#05a8e5; - classDef coreLayer fill:#ffe6cc,stroke:#f7a128; - classDef appSupportLayer fill:#d5e8d4,stroke:#82b366; - classDef baseLayer fill:#e1d5e7,stroke:#9673a6; - - class C,H,D userLayer; - class AI,E coreLayer; - class F,I appSupportLayer; - class B,G,J baseLayer; -``` -# 需求定义文档(续) - -### 3.2 模块功能概述 - -| 模块名称 | 功能概述 | -| :--- | :--- | -| **用户与认证模块** | 负责用户身份验证和权限控制,提供注册、登录、密码重置等功能,确保系统安全性。 | -| **反馈管理模块** | 处理公众提交的环境问题反馈,包括提交、审核、状态追踪和查询统计等功能。 | -| **任务管理模块** | 将审核通过的反馈转化为工作任务,管理任务的分配、执行和完成全流程。 | -| **网格与地图模块** | 提供地理空间的网格化管理,支持路径规划和地图可视化,辅助任务执行。 | -| **决策支持模块** | 通过数据分析和可视化,为管理层提供决策支持,帮助优化资源配置和治理策略。 | -| **系统管理模块** | 提供系统配置、用户管理、权限设置等基础功能,保障系统正常运行。 | -| **文件服务模块** | 处理系统中的文件上传、存储和访问,支持反馈和任务的附件管理。 | -| **日志审计模块** | 记录系统操作日志,支持安全审计和问题追溯,确保系统运行的可追溯性。 | - -## 4. 整体业务流程 - -下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程: - -```mermaid -flowchart TD - %% 定义样式 - classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333 - classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333 - classDef worker fill:#d5e8d4,stroke:#82b366,color:#333 - classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333 - classDef decision fill:#f8cecc,stroke:#b85450,color:#333 - classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px - - %% 流程开始 - A([开始]) --> B["[公众端] 发现环境问题"] - B --> C["[公众端] 提交反馈
(标题/描述/图片/位置)"] - - %% 平台接收与AI处理 - C --> D["[平台] 接收反馈
生成唯一事件ID"] - D --> E{"[平台] AI自动审核
分析内容/分类"} - - %% AI审核分支 - E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"] - F1 --> F2["[平台] 通知提交者"] - F2 --> Z1([结束]) - - E -- "需人工确认" --> G["[主管] 查看反馈详情
进行人工审核"] - - %% 主管审核分支 - G --> H{"[主管] 审核决定"} - H -- "驳回" --> I1["[主管] 填写驳回理由"] - I1 --> I2["[平台] 更新状态为REJECTED"] - I2 --> I3["[平台] 通知提交者"] - I3 --> Z2([结束]) - - %% 审核通过,创建任务 - H -- "通过" --> J1["[主管] 确认反馈有效"] - J1 --> J2["[平台] 自动创建结构化任务"] - J2 --> J3["[平台] 更新反馈状态为PROCESSED"] - - %% 任务分配 - J3 --> K1{"[主管] 选择分配方式"} - K1 -- "手动分配" --> K2["[主管] 选择特定网格员"] - K1 -- "智能推荐" --> K3["[平台] 运行分配算法
考虑位置/负载/专长"] - K3 --> K4["[平台] 推荐最佳人选"] - K4 --> K2 - K2 --> K5["[平台] 创建任务分配记录
更新任务状态为ASSIGNED"] - K5 --> K6["[平台] 通知网格员"] - - %% 网格员处理 - K6 --> L1["[网格员] 接收任务通知"] - L1 --> L2{"[网格员] 接受任务?"} - L2 -- "拒绝" --> K1 - L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"] - L3 --> L4["[网格员] 查看任务详情
获取路径规划"] - L4 --> L5["[网格员] 前往现场处理"] - L5 --> L6["[网格员] 记录处理过程
上传证明材料"] - L6 --> L7["[网格员] 提交处理结果
更新状态为SUBMITTED"] - - %% 主管审核结果 - L7 --> M1["[主管] 审核处理结果"] - M1 --> M2{"[主管] 结果是否合格?"} - M2 -- "不合格" --> M3["[主管] 填写原因
要求重新处理"] - M3 --> L4 - - %% 完成流程 - M2 -- "合格" --> N1["[主管] 确认任务完成"] - N1 --> N2["[平台] 更新任务状态为APPROVED"] - N2 --> N3["[平台] 更新反馈状态为CLOSED"] - N3 --> N4["[平台] 通知反馈提交者"] - N4 --> N5["[平台] 更新统计数据"] - N5 --> O["[决策层] 查看数据看板
分析环境趋势"] - O --> Z3([结束]) - - %% 为节点添加类别 - class A,Z1,Z2,Z3 start_end - class B,C,F2,I3,N4 public - class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform - class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor - class L1,L2,L3,L4,L5,L6,L7 worker - class O decision -``` - -## 5. 功能需求详细描述 - -### 5.1 用户与认证模块需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| AUTH-01 | 用户注册 | 系统应允许新用户通过提供基本信息(姓名、手机号、邮箱、密码等)进行注册,并验证信息的有效性和唯一性。 | 高 | -| AUTH-02 | 用户登录 | 系统应支持用户通过邮箱/手机号和密码进行身份验证,登录成功后生成安全令牌用于后续请求。 | 高 | -| AUTH-03 | 密码重置 | 系统应提供安全的密码重置机制,包括通过邮箱或手机验证码验证身份。 | 中 | -| AUTH-04 | 权限控制 | 系统应基于用户角色(公众用户、网格员、主管、管理员、决策者)实施访问控制,确保用户只能访问其权限范围内的功能和数据。 | 高 | -| AUTH-05 | 会话管理 | 系统应维护和管理用户会话状态,包括会话创建、验证和超时处理。 | 中 | -| AUTH-06 | 个人资料管理 | 系统应允许用户查看和更新其个人资料,包括基本信息、联系方式和偏好设置。 | 低 | -| AUTH-07 | 安全日志 | 系统应记录所有关键安全事件(登录尝试、权限变更等),支持安全审计。 | 中 | - -**用户与认证流程图**: - -```mermaid -graph TD - A[用户访问系统] --> B{是否已登录?} - B -- 否 --> C{是否有账号?} - B -- 是 --> D[访问系统功能] - - C -- 否 --> E[注册新账号] - C -- 是 --> F[登录系统] - - E --> G[填写注册信息] - G --> H[验证邮箱/手机] - H --> I[创建用户账号] - I --> F - - F --> J{认证是否成功?} - J -- 否 --> K{是否忘记密码?} - J -- 是 --> L[生成安全令牌] - - K -- 是 --> M[密码重置流程] - K -- 否 --> F - - M --> N[验证身份] - N --> O[设置新密码] - O --> F - - L --> P[加载用户权限] - P --> D - - D --> Q[系统记录操作日志] - - R[用户请求退出] --> S[清除会话信息] - S --> T[返回登录页面] -``` - -### 5.2 反馈管理模块需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| FEED-01 | 反馈提交 | 系统应允许公众用户提交环境问题反馈,包括问题描述、位置标记、污染类型分类和图片上传。 | 高 | -| FEED-02 | AI内容审核 | 系统应自动分析反馈内容,识别垃圾信息、重复提交,并进行初步分类和紧急程度评估。 | 中 | -| FEED-03 | 人工审核 | 系统应提供主管审核反馈的工作台,支持查看详情、批准或驳回反馈。 | 高 | -| FEED-04 | 状态追踪 | 系统应记录和展示反馈的全生命周期状态变更,允许用户查询处理进度。 | 高 | -| FEED-05 | 反馈查询 | 系统应支持按多种条件(状态、时间、区域、类型等)查询和筛选反馈列表。 | 中 | -| FEED-06 | 反馈统计 | 系统应生成反馈数据的统计分析,包括数量分布、处理效率等指标。 | 低 | -| FEED-07 | 通知提醒 | 系统应在反馈状态变更时通知相关用户,保持信息透明。 | 中 | - -**反馈管理流程图**: - -```mermaid -graph TD - A[公众用户发现环境问题] --> B[打开反馈提交界面] - B --> C[填写问题描述] - C --> D[选择污染类型] - D --> E[评估严重程度] - E --> F[标记地理位置] - F --> G[上传现场照片] - G --> H[提交反馈] - - H --> I[系统生成唯一事件ID] - I --> J[AI自动审核内容] - - J --> K{AI审核结果} - K -- 明显无效 --> L[标记为AI_REJECTED] - K -- 需人工确认 --> M[进入主管审核队列] - - L --> N[通知用户反馈被拒绝] - - M --> O[主管查看反馈详情] - O --> P{审核决定} - P -- 驳回 --> Q[填写驳回理由] - P -- 通过 --> R[标记为有效反馈] - - Q --> S[更新状态为REJECTED] - S --> T[通知用户反馈被驳回] - - R --> U[创建关联任务] - U --> V[更新状态为PROCESSED] - - W[用户查询反馈状态] --> X[系统展示处理进度] - - Y[管理员查看统计数据] --> Z[系统生成反馈分析报表] -``` - -# 需求定义文档(续) - -### 5.3 任务管理模块需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| TASK-01 | 任务创建 | 系统应支持从审核通过的反馈自动生成任务,也支持主管手动创建临时任务。 | 高 | -| TASK-02 | 智能分配 | 系统应基于多因素(网格员位置、当前负载、专业技能、历史表现)推荐最合适的处理人员。 | 中 | -| TASK-03 | 任务分配 | 系统应支持主管手动选择或确认系统推荐的网格员,并将任务分配给他们。 | 高 | -| TASK-04 | 任务通知 | 系统应在任务分配后立即通知相关网格员,并提供任务详情查看途径。 | 高 | -| TASK-05 | 任务执行跟踪 | 系统应记录任务的每个状态变更,支持网格员实时上报处理进度。 | 中 | -| TASK-06 | 路径规划 | 系统应为网格员提供从当前位置到任务地点的最优路径规划。 | 中 | -| TASK-07 | 结果提交 | 系统应支持网格员提交处理结果,包括文字描述和图片证明。 | 高 | -| TASK-08 | 结果审核 | 系统应支持主管审核网格员提交的处理结果,可以通过或驳回并提供反馈。 | 高 | -| TASK-09 | 任务查询 | 系统应支持按多种条件(状态、负责人、时间、区域等)查询和筛选任务列表。 | 中 | -| TASK-10 | 任务统计 | 系统应生成任务数据的统计分析,包括完成率、平均处理时长等绩效指标。 | 低 | -| TASK-11 | 任务看板 | 系统应提供直观的任务看板,展示不同状态任务的数量和分布。 | 低 | - -**任务管理流程图**: - -```mermaid -graph TD - A[反馈通过审核] --> B[自动创建任务] - C[主管手动创建任务] --> D[任务创建完成] - B --> D - D --> E{选择分配方式} - E -- 手动分配 --> F[主管选择网格员] - E -- 智能推荐 --> G[系统推荐最佳人选] - G --> F - F --> H[分配任务给网格员] - H --> I[通知网格员] - I --> J{网格员接受?} - J -- 否 --> E - J -- 是 --> K[更新任务状态为进行中] - K --> L[网格员获取路径规划] - L --> M[网格员处理任务] - M --> N[提交处理结果] - N --> O[主管审核结果] - O --> P{结果合格?} - P -- 否 --> Q[填写原因] - Q --> L - P -- 是 --> R[更新任务状态为已完成] - R --> S[通知反馈提交者] - S --> T[更新统计数据] -``` - -### 5.4 网格与地图模块需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| GRID-01 | 网格定义 | 系统应支持管理员定义和维护城市网格系统,包括网格的坐标、属性和责任人分配。 | 中 | -| GRID-02 | 地图可视化 | 系统应在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。 | 中 | -| GRID-03 | 位置标记 | 系统应支持用户在地图上精确标记环境问题位置,并自动关联到对应网格。 | 高 | -| GRID-04 | A*寻路算法 | 系统应基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划。 | 中 | -| GRID-05 | 区域统计 | 系统应基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | 低 | -| GRID-06 | 网格查询 | 系统应支持查询特定区域内的网格定义、属性和历史问题记录。 | 低 | -| GRID-07 | 离线地图 | 系统应支持地图数据的离线缓存,保证在网络不稳定情况下的基本功能。 | 低 | - -**网格与地图流程图**: - -```mermaid -graph TD - A[管理员定义网格系统] --> B[划分城市区域为网格单元] - B --> C[设置网格属性] - C --> D[分配网格责任人] - - E[用户标记环境问题位置] --> F[系统关联到对应网格] - F --> G[存储地理位置信息] - - H[网格员接收任务] --> I[查看任务位置] - I --> J[请求路径规划] - J --> K[A*算法计算最优路径] - K --> L[展示导航路线] - - M[管理员查看统计数据] --> N[生成环境问题热力图] - N --> O[识别问题高发区域] - O --> P[调整资源分配策略] -``` - -### 5.5 决策支持模块需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| DECI-01 | 核心指标看板 | 系统应实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。 | 中 | -| DECI-02 | 多维度分析 | 系统应支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。 | 中 | -| DECI-03 | 热力图可视化 | 系统应在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。 | 中 | -| DECI-04 | 绩效评估 | 系统应对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。 | 低 | -| DECI-05 | 预警机制 | 系统应基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | 低 | -| DECI-06 | 报表导出 | 系统应支持将分析结果导出为多种格式(PDF、Excel等),便于进一步分析和汇报。 | 低 | -| DECI-07 | 个性化配置 | 系统应支持决策者个性化配置数据看板,满足不同管理者的关注点。 | 低 | - -**决策支持流程图**: - -```mermaid -graph TD - A[系统收集业务数据] --> B[实时计算核心指标] - B --> C[展示核心指标看板] - - D[决策者选择分析维度] --> E[系统执行多维度分析] - E --> F[生成趋势图表] - - G[系统分析地理数据] --> H[生成环境问题热力图] - H --> I[标识高发区域] - - J[系统收集网格员工作数据] --> K[计算绩效指标] - K --> L[生成绩效排名] - - M[系统分析历史数据] --> N[识别异常趋势] - N --> O{是否达到预警阈值?} - O -- 是 --> P[触发预警通知] - O -- 否 --> Q[继续监控] - - R[决策者查看分析结果] --> S[选择导出格式] - S --> T[导出分析报表] -``` - -## 6. 非功能性需求 - -### 6.1 性能需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| PERF-01 | 响应时间 | 系统的页面加载时间应不超过3秒,API响应时间应不超过500毫秒(不包括文件上传下载)。 | 高 | -| PERF-02 | 并发用户 | 系统应能同时支持至少100个并发用户正常操作,不出现明显延迟。 | 中 | -| PERF-03 | 数据处理量 | 系统应能处理每日至少1000条反馈和500个任务的创建、更新和查询操作。 | 中 | -| PERF-04 | 文件处理 | 系统应支持单个文件最大10MB,总附件大小不超过50MB的上传和处理。 | 中 | -| PERF-05 | 地图渲染 | 地图界面应能在1秒内完成初始加载,并在500毫秒内响应用户的缩放和平移操作。 | 中 | - -### 6.2 可用性需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| USAB-01 | 界面一致性 | 系统界面应保持风格一致,包括颜色方案、按钮位置、导航结构等,减少用户学习成本。 | 中 | -| USAB-02 | 错误处理 | 系统应提供清晰的错误提示和恢复建议,避免用户操作中断。 | 高 | -| USAB-03 | 帮助系统 | 系统应提供上下文相关的帮助信息和操作指南,支持用户自助解决问题。 | 低 | -| USAB-04 | 响应式设计 | 系统界面应适应不同设备(PC、平板、手机)的屏幕尺寸,提供一致的用户体验。 | 高 | -| USAB-05 | 操作简化 | 核心功能的操作路径应尽量简化,减少用户点击次数和输入量。 | 中 | - -### 6.3 安全性需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| SECU-01 | 数据加密 | 敏感数据(如用户密码)必须加密存储,传输过程中应使用HTTPS加密。 | 高 | -| SECU-02 | 访问控制 | 系统应实施严格的基于角色的访问控制,确保用户只能访问其权限范围内的数据和功能。 | 高 | -| SECU-03 | 输入验证 | 系统应对所有用户输入进行验证和过滤,防止SQL注入、XSS等常见安全攻击。 | 高 | -| SECU-04 | 会话管理 | 系统应安全管理用户会话,包括会话创建、验证、超时和销毁,防止会话劫持。 | 中 | -| SECU-05 | 审计日志 | 系统应记录所有关键操作的审计日志,包括用户登录、数据修改、权限变更等。 | 中 | - -# 需求定义文档(续) - -### 6.4 可靠性需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| RELI-01 | 系统可用性 | 系统应保证7x24小时的稳定运行,计划内维护时间除外,系统可用性应达到99.5%以上。 | 高 | -| RELI-02 | 数据备份 | 系统应定期(至少每日一次)对全部业务数据进行备份,并支持数据恢复。 | 高 | -| RELI-03 | 故障恢复 | 系统应具备故障检测和自动恢复机制,在出现异常情况时能够快速恢复正常运行。 | 中 | -| RELI-04 | 数据一致性 | 系统应确保在各种操作条件下(包括并发访问和异常中断)保持数据的一致性和完整性。 | 高 | -| RELI-05 | 容错能力 | 系统应能够处理常见的错误情况(如网络中断、数据异常),并提供适当的降级服务。 | 中 | - -### 6.5 可维护性需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| MAIN-01 | 模块化设计 | 系统应采用模块化设计,各功能模块之间松耦合,便于独立开发、测试和维护。 | 中 | -| MAIN-02 | 配置管理 | 系统应支持通过配置文件或管理界面调整系统参数,无需修改代码和重新部署。 | 中 | -| MAIN-03 | 日志记录 | 系统应记录详细的运行日志,包括错误信息、性能指标和关键操作,便于问题诊断。 | 高 | -| MAIN-04 | 版本升级 | 系统应支持平滑的版本升级机制,最小化升级对用户的影响。 | 低 | -| MAIN-05 | 代码规范 | 系统开发应遵循统一的编码规范和设计模式,提高代码可读性和可维护性。 | 中 | - -### 6.6 可扩展性需求 - -| 需求ID | 需求名称 | 需求描述 | 优先级 | -| :--- | :--- | :--- | :--- | -| SCAL-01 | 水平扩展 | 系统架构应支持通过增加服务器节点进行水平扩展,以应对用户量和数据量的增长。 | 低 | -| SCAL-02 | 功能扩展 | 系统应支持在不影响现有功能的情况下,添加新的功能模块和业务流程。 | 中 | -| SCAL-03 | 接口开放 | 系统应提供标准化的API接口,便于与其他系统集成和数据交换。 | 中 | -| SCAL-04 | 多租户支持 | 系统应具备支持多租户(如不同城市或区域)的潜力,能够在未来进行扩展。 | 低 | -| SCAL-05 | 数据量增长 | 系统应能够处理数据量的持续增长,性能不应随着数据量的增加而明显下降。 | 中 | - -## 7. 数据需求 - -### 7.1 数据实体 - -系统需要管理以下核心数据实体: - -1. **用户账户 (UserAccount)**:存储系统用户的基本信息、认证信息和角色权限。 -2. **反馈 (Feedback)**:存储公众提交的环境问题反馈,包括描述、位置、图片等信息。 -3. **任务 (Task)**:存储从反馈生成的工作任务,包括任务详情、状态和处理记录。 -4. **网格 (Grid)**:存储城市区域的网格化管理数据,包括网格坐标、属性和责任人。 -5. **任务分配 (Assignment)**:存储任务与网格员之间的分配关系,包括分配时间、状态等。 -6. **附件 (Attachment)**:存储反馈和任务相关的图片、文档等附件文件。 -7. **操作日志 (OperationLog)**:存储系统中的关键操作记录,用于审计和问题追溯。 - -### 7.2 数据量估计 - -基于系统预期使用规模,估计以下数据量: - -1. **用户账户**:预计1000个用户账户,包括公众用户、网格员、主管、管理员和决策者。 -2. **反馈**:预计每日100-200条新增反馈,年增长约5万条。 -3. **任务**:预计每日50-100个新增任务,年增长约2.5万个。 -4. **网格**:预计1000-5000个网格单元,根据城市规模和网格粒度而定。 -5. **附件**:预计每条反馈平均2张图片,每个任务平均2张图片,年增长约15万个文件。 -6. **操作日志**:预计每日1万条日志记录,主要用于短期审计,长期可归档。 - -### 7.3 数据保留策略 - -1. **核心业务数据**(用户、反馈、任务、网格):长期保留,至少保留3年。 -2. **附件文件**:保留1年,可根据存储容量考虑适当延长或缩短。 -3. **操作日志**: - - 详细日志保留30天 - - 关键审计日志保留1年 - - 统计汇总数据长期保留 - -### 7.4 数据备份要求 - -1. **备份频率**: - - 核心业务数据:每日全量备份,每小时增量备份 - - 附件文件:每周全量备份,每日增量备份 - - 配置数据:每次变更后备份 - -2. **备份保留**: - - 每日备份保留7天 - - 每周备份保留1个月 - - 每月备份保留6个月 - - 每年备份永久保留 - -3. **恢复能力**:系统应支持将数据恢复到任意备份点,恢复时间目标(RTO)不超过4小时。 - -## 8. 结论 - -### 8.1 需求优先级总结 - -基于业务重要性和实现复杂度,系统需求按以下优先级排序: - -1. **最高优先级**: - - 用户认证和权限控制 - - 反馈提交和审核 - - 任务创建和分配 - - 数据安全和完整性 - -2. **高优先级**: - - 任务执行和结果审核 - - 地图标记和路径规划 - - 系统性能和可用性 - - 用户界面和体验 - -3. **中等优先级**: - - AI内容审核 - - 决策支持和数据分析 - - 通知和提醒功能 - - 系统可维护性 - -4. **低优先级**: - - 高级统计和报表 - - 个性化配置 - - 系统扩展性 - - 辅助功能和优化 - -### 8.2 实施建议 - -为确保系统成功实施,建议采取以下策略: - -1. **分阶段实施**: - - 第一阶段:核心功能(用户管理、反馈、任务) - - 第二阶段:支撑功能(地图、AI审核) - - 第三阶段:高级功能(决策支持、报表) - -2. **持续迭代**:采用敏捷开发方法,快速交付可用版本,根据用户反馈持续改进。 - -3. **用户参与**:在开发过程中邀请各类用户参与测试和评估,确保系统满足实际需求。 - -4. **技术选型**:选择成熟稳定的技术栈,平衡创新性和可靠性,降低开发和维护风险。 - -5. **数据治理**:建立完善的数据管理和治理机制,确保数据质量和安全。 - -### 8.3 预期效益 - -成功实施环境监测系统预期将带来以下效益: - -1. **业务效益**: - - 环境问题处理效率提升50%以上 - - 问题解决质量显著提高 - - 资源利用更加合理高效 - -2. **管理效益**: - - 实现环境问题全流程可视化管理 - - 提供科学决策的数据支持 - - 优化工作流程和资源配置 - -3. **社会效益**: - - 提升公众参与环境治理的积极性 - - 增强政府工作透明度和公信力 - - 改善城市环境质量和居民生活满意度 - -通过环境监测系统的建设和应用,将有效解决当前环境问题管理中的痛点,构建起连接公众、管理部门和执行人员的桥梁,实现环境问题的快速发现、高效处理和全程监督,为城市环境治理提供有力支撑。 + +# 需求定义文档 + +## 1. 项目介绍 + +### 1.1 项目背景 + +环境监测系统(EMS)是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速,环境污染问题日益凸显,传统的环境问题上报和处理机制存在以下痛点: + +- **流程繁琐**:公众发现环境问题后,需要通过多个渠道和部门层层上报,处理流程不透明 +- **响应缓慢**:从问题发现到最终解决,往往需要经历漫长的等待时间 +- **缺乏透明度**:公众难以了解问题处理进度,无法有效监督 +- **资源分配不合理**:缺乏科学的任务分配机制,导致人力资源利用不均衡 + +本系统旨在通过数字化手段,构建一个连接公众、管理部门和执行人员的环境监测与治理平台,实现环境问题的快速发现、高效处理和全程监督。 + +### 1.2 项目目标 + +1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。 +2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。 +3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。 +4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。 +5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。 + +## 2. 系统分析 + +### 2.1 业务痛点分析 + +1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。 +2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。 +3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。 +4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。 +5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。 + +### 2.2 用户角色分析 + +环境监测系统涉及五类主要用户角色,每个角色在系统中承担不同的职责: + +1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。 + - 需求:简单便捷的问题上报方式、透明的处理进度查询 + - 痛点:传统上报渠道繁琐、反馈周期长、处理结果不透明 + +2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。 + - 需求:清晰的任务指派、便捷的结果上报、高效的路径规划 + - 痛点:任务分配不合理、工作量分布不均、缺乏高效导航 + +3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。 + - 需求:高效的任务管理、智能的人员调配、直观的进度监控 + - 痛点:人工分配任务效率低、缺乏全局视角、绩效评估困难 + +4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。 + - 需求:灵活的权限配置、完善的日志审计、便捷的系统维护 + - 痛点:账户管理繁琐、权限控制粗放、系统维护成本高 + +5. **决策者**:系统的战略端,通过分析系统生成的统计数据和趋势图表,制定环境管理策略和资源分配决策,是系统的最终受益者之一。 + - 需求:多维度的数据分析、直观的可视化展示、科学的决策支持 + - 痛点:数据获取困难、分析维度单一、缺乏预测能力 + +### 2.3 用例分析 + +#### 2.3.1 用例图 + +以下用例图展示了系统中各角色可以执行的主要操作: + +```mermaid +graph TD + %% 定义角色 + PublicUser["公众用户"] + GridWorker["网格员"] + Supervisor["主管"] + Admin["管理员"] + DecisionMaker["决策者"] + + %% 定义用例 + UC1["注册与登录"] + UC2["提交环境问题反馈"] + UC3["查看反馈处理进度"] + UC4["接收任务通知"] + UC5["执行任务"] + UC6["提交处理结果"] + UC7["审核反馈内容"] + UC8["分配任务"] + UC9["审核处理结果"] + UC10["查看统计数据"] + UC11["管理用户账户"] + UC12["配置系统参数"] + UC13["查看决策仪表盘"] + UC14["生成分析报告"] + + %% 建立关系 + PublicUser --> UC1 + PublicUser --> UC2 + PublicUser --> UC3 + + GridWorker --> UC1 + GridWorker --> UC4 + GridWorker --> UC5 + GridWorker --> UC6 + + Supervisor --> UC1 + Supervisor --> UC7 + Supervisor --> UC8 + Supervisor --> UC9 + Supervisor --> UC10 + + Admin --> UC1 + Admin --> UC10 + Admin --> UC11 + Admin --> UC12 + + DecisionMaker --> UC1 + DecisionMaker --> UC10 + DecisionMaker --> UC13 + DecisionMaker --> UC14 + + %% 设置样式 + classDef actor fill:#f9f,stroke:#333,stroke-width:2px + classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px + + class PublicUser,GridWorker,Supervisor,Admin,DecisionMaker actor + class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12,UC13,UC14 usecase +``` + +#### 2.3.2 活动图:反馈提交与处理流程 + +以下活动图展示了从公众发现环境问题到反馈处理完成的完整业务流程: + +```mermaid +stateDiagram-v2 + [*] --> 发现环境问题 + 发现环境问题 --> 填写反馈表单 + 填写反馈表单 --> 上传图片 + 上传图片 --> 标记位置 + 标记位置 --> 提交反馈 + 提交反馈 --> AI自动审核 + + state AI自动审核 { + [*] --> 内容分析 + 内容分析 --> 垃圾信息检测 + 垃圾信息检测 --> 分类与评级 + 分类与评级 --> [*] + } + + AI自动审核 --> 判断AI审核结果 + 判断AI审核结果 --> 明显无效: AI拒绝 + 判断AI审核结果 --> 需人工确认: 需确认 + + 明显无效 --> 标记为AI_REJECTED + 标记为AI_REJECTED --> 通知提交者 + 通知提交者 --> [*] + + 需人工确认 --> 主管人工审核 + 主管人工审核 --> 判断审核结果 + + 判断审核结果 --> 驳回: 不通过 + 判断审核结果 --> 通过: 通过 + + 驳回 --> 填写驳回理由 + 填写驳回理由 --> 更新状态为REJECTED + 更新状态为REJECTED --> 通知提交者反馈被驳回 + 通知提交者反馈被驳回 --> [*] + + 通过 --> 创建任务 + 创建任务 --> 更新反馈状态为PROCESSED + 更新反馈状态为PROCESSED --> 任务分配流程 + 任务分配流程 --> [*] +``` +# 需求定义文档(续) + +#### 2.3.3 活动图:任务分配与执行流程 + +以下活动图展示了从任务创建到完成的完整业务流程: + +```mermaid +stateDiagram-v2 + [*] --> 任务创建完成 + 任务创建完成 --> 选择分配方式 + + state 选择分配方式 { + [*] --> 手动分配 + [*] --> 智能推荐 + + 智能推荐 --> 运行分配算法 + 运行分配算法 --> 推荐最佳人选 + 推荐最佳人选 --> 确认人选 + + 手动分配 --> 选择特定网格员 + 选择特定网格员 --> 确认人选 + + 确认人选 --> [*] + } + + 选择分配方式 --> 创建任务分配记录 + 创建任务分配记录 --> 更新任务状态为ASSIGNED + 更新任务状态为ASSIGNED --> 通知网格员 + + 通知网格员 --> 网格员接收通知 + 网格员接收通知 --> 判断是否接受 + + 判断是否接受 --> 拒绝: 拒绝 + 判断是否接受 --> 接受: 接受 + + 拒绝 --> 选择分配方式 + + 接受 --> 更新状态为IN_PROGRESS + 更新状态为IN_PROGRESS --> 获取路径规划 + 获取路径规划 --> 前往现场处理 + 前往现场处理 --> 记录处理过程 + 记录处理过程 --> 上传处理结果 + 上传处理结果 --> 提交处理结果 + 提交处理结果 --> 更新状态为SUBMITTED + + 更新状态为SUBMITTED --> 主管审核结果 + 主管审核结果 --> 判断结果是否合格 + + 判断结果是否合格 --> 不合格: 不合格 + 判断结果是否合格 --> 合格: 合格 + + 不合格 --> 填写原因要求重新处理 + 填写原因要求重新处理 --> 获取路径规划 + + 合格 --> 确认任务完成 + 确认任务完成 --> 更新任务状态为APPROVED + 更新任务状态为APPROVED --> 更新反馈状态为CLOSED + 更新反馈状态为CLOSED --> 通知反馈提交者 + 通知反馈提交者 --> 更新统计数据 + 更新统计数据 --> [*] +``` + +### 2.4 系统可行性分析 + +#### 2.4.1 技术可行性 + +本系统的技术实现是可行的,主要基于以下分析: + +1. **前端技术**:市场上已有成熟的前端框架和组件库,可以快速开发出美观、响应式的Web应用,满足不同设备的访问需求。 + +2. **后端技术**:现有的后端框架具备高性能、高并发处理能力,能够满足系统的稳定性和扩展性需求。 + +3. **地图服务**:可以集成成熟的地图API,实现地理位置标记、路径规划等功能,无需从零开发。 + +4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。 + +5. **数据存储**:可采用多种数据存储方案,根据系统规模和性能需求灵活选择。 + +#### 2.4.2 经济可行性 + +从经济角度看,本系统的开发和运营是可行的: + +1. **开发成本**:可以采用主流开源框架和技术栈,降低开发成本和技术门槛。 + +2. **维护成本**:通过模块化设计和完善的文档,可以降低后期维护和升级成本。 + +3. **投资回报**: + - 提高环境问题处理效率,减少人力资源浪费 + - 降低环境问题带来的经济损失 + - 提升城市环境质量,间接促进经济发展 + - 长期来看具有良好的投资回报 + +4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。 + +#### 2.4.3 操作可行性 + +从操作角度看,本系统具有良好的可行性: + +1. **用户接受度**: + - 系统界面设计简洁直观,符合用户习惯 + - 操作流程简化,降低学习成本 + - 移动端支持,满足随时随地使用需求 + +2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。 + +3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。 + +4. **组织支持**:系统实施需要相关部门的协调配合,但总体上不会对现有组织架构产生颠覆性影响。 + +#### 2.4.4 法律可行性 + +从法律合规角度看,本系统的实施不存在重大障碍: + +1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。 + +2. **知识产权**:系统开发过程中使用的第三方库和组件均可采用开源或获得授权的方式,避免知识产权风险。 + +3. **合规性**:系统功能和流程设计可以确保符合相关法律法规和行业标准,确保合法合规运营。 + +## 3. 功能性需求 + +### 3.1 功能层次方框图 + +以下功能层次图展示了系统的整体功能架构: + +```mermaid +flowchart TD + %% 用户交互层 + subgraph "用户交互层" + direction LR + C["公众服务模块\n(问题上报)"] + H["个人中心模块\n(我的反馈/资料)"] + D["管理驾驶舱\n(数据决策)"] + end + + %% 核心业务层 + subgraph "核心业务层" + direction LR + AI["AI分析模块\n(内容审核)"] + E["任务管理模块\n(分配、流转、执行)"] + end + + %% 应用支撑层 + subgraph "应用支撑层" + direction LR + F["网格与地图模块\n(LBS & 寻路)"] + I["文件服务模块\n(附件存取)"] + end + + %% 基础服务层 + subgraph "基础服务层" + direction LR + B["用户与认证模块"] + G["系统管理模块\n(用户/权限)"] + J["日志审计模块"] + end + + %% 定义关系 + C -- "提交反馈" --> AI + AI -- "分析结果" --> E + C -- "附件" --> I + E -- "调用" --> F + E -- "任务附件" --> I + E -- "统计数据" --> D + + H -- "查询个人数据" --> E + + %% 基础服务支撑所有上层模块 (关系隐含) + G -- "管理" --> B + + classDef userLayer fill:#d4f1f9,stroke:#05a8e5; + classDef coreLayer fill:#ffe6cc,stroke:#f7a128; + classDef appSupportLayer fill:#d5e8d4,stroke:#82b366; + classDef baseLayer fill:#e1d5e7,stroke:#9673a6; + + class C,H,D userLayer; + class AI,E coreLayer; + class F,I appSupportLayer; + class B,G,J baseLayer; +``` +# 需求定义文档(续) + +### 3.2 模块功能概述 + +| 模块名称 | 功能概述 | +| :--- | :--- | +| **用户与认证模块** | 负责用户身份验证和权限控制,提供注册、登录、密码重置等功能,确保系统安全性。 | +| **反馈管理模块** | 处理公众提交的环境问题反馈,包括提交、审核、状态追踪和查询统计等功能。 | +| **任务管理模块** | 将审核通过的反馈转化为工作任务,管理任务的分配、执行和完成全流程。 | +| **网格与地图模块** | 提供地理空间的网格化管理,支持路径规划和地图可视化,辅助任务执行。 | +| **决策支持模块** | 通过数据分析和可视化,为管理层提供决策支持,帮助优化资源配置和治理策略。 | +| **系统管理模块** | 提供系统配置、用户管理、权限设置等基础功能,保障系统正常运行。 | +| **文件服务模块** | 处理系统中的文件上传、存储和访问,支持反馈和任务的附件管理。 | +| **日志审计模块** | 记录系统操作日志,支持安全审计和问题追溯,确保系统运行的可追溯性。 | + +## 4. 整体业务流程 + +下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程: + +```mermaid +flowchart TD + %% 定义样式 + classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333 + classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333 + classDef worker fill:#d5e8d4,stroke:#82b366,color:#333 + classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333 + classDef decision fill:#f8cecc,stroke:#b85450,color:#333 + classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px + + %% 流程开始 + A([开始]) --> B["[公众端] 发现环境问题"] + B --> C["[公众端] 提交反馈
(标题/描述/图片/位置)"] + + %% 平台接收与AI处理 + C --> D["[平台] 接收反馈
生成唯一事件ID"] + D --> E{"[平台] AI自动审核
分析内容/分类"} + + %% AI审核分支 + E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"] + F1 --> F2["[平台] 通知提交者"] + F2 --> Z1([结束]) + + E -- "需人工确认" --> G["[主管] 查看反馈详情
进行人工审核"] + + %% 主管审核分支 + G --> H{"[主管] 审核决定"} + H -- "驳回" --> I1["[主管] 填写驳回理由"] + I1 --> I2["[平台] 更新状态为REJECTED"] + I2 --> I3["[平台] 通知提交者"] + I3 --> Z2([结束]) + + %% 审核通过,创建任务 + H -- "通过" --> J1["[主管] 确认反馈有效"] + J1 --> J2["[平台] 自动创建结构化任务"] + J2 --> J3["[平台] 更新反馈状态为PROCESSED"] + + %% 任务分配 + J3 --> K1{"[主管] 选择分配方式"} + K1 -- "手动分配" --> K2["[主管] 选择特定网格员"] + K1 -- "智能推荐" --> K3["[平台] 运行分配算法
考虑位置/负载/专长"] + K3 --> K4["[平台] 推荐最佳人选"] + K4 --> K2 + K2 --> K5["[平台] 创建任务分配记录
更新任务状态为ASSIGNED"] + K5 --> K6["[平台] 通知网格员"] + + %% 网格员处理 + K6 --> L1["[网格员] 接收任务通知"] + L1 --> L2{"[网格员] 接受任务?"} + L2 -- "拒绝" --> K1 + L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"] + L3 --> L4["[网格员] 查看任务详情
获取路径规划"] + L4 --> L5["[网格员] 前往现场处理"] + L5 --> L6["[网格员] 记录处理过程
上传证明材料"] + L6 --> L7["[网格员] 提交处理结果
更新状态为SUBMITTED"] + + %% 主管审核结果 + L7 --> M1["[主管] 审核处理结果"] + M1 --> M2{"[主管] 结果是否合格?"} + M2 -- "不合格" --> M3["[主管] 填写原因
要求重新处理"] + M3 --> L4 + + %% 完成流程 + M2 -- "合格" --> N1["[主管] 确认任务完成"] + N1 --> N2["[平台] 更新任务状态为APPROVED"] + N2 --> N3["[平台] 更新反馈状态为CLOSED"] + N3 --> N4["[平台] 通知反馈提交者"] + N4 --> N5["[平台] 更新统计数据"] + N5 --> O["[决策层] 查看数据看板
分析环境趋势"] + O --> Z3([结束]) + + %% 为节点添加类别 + class A,Z1,Z2,Z3 start_end + class B,C,F2,I3,N4 public + class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform + class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor + class L1,L2,L3,L4,L5,L6,L7 worker + class O decision +``` + +## 5. 功能需求详细描述 + +### 5.1 用户与认证模块需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| AUTH-01 | 用户注册 | 系统应允许新用户通过提供基本信息(姓名、手机号、邮箱、密码等)进行注册,并验证信息的有效性和唯一性。 | 高 | +| AUTH-02 | 用户登录 | 系统应支持用户通过邮箱/手机号和密码进行身份验证,登录成功后生成安全令牌用于后续请求。 | 高 | +| AUTH-03 | 密码重置 | 系统应提供安全的密码重置机制,包括通过邮箱或手机验证码验证身份。 | 中 | +| AUTH-04 | 权限控制 | 系统应基于用户角色(公众用户、网格员、主管、管理员、决策者)实施访问控制,确保用户只能访问其权限范围内的功能和数据。 | 高 | +| AUTH-05 | 会话管理 | 系统应维护和管理用户会话状态,包括会话创建、验证和超时处理。 | 中 | +| AUTH-06 | 个人资料管理 | 系统应允许用户查看和更新其个人资料,包括基本信息、联系方式和偏好设置。 | 低 | +| AUTH-07 | 安全日志 | 系统应记录所有关键安全事件(登录尝试、权限变更等),支持安全审计。 | 中 | + +**用户与认证流程图**: + +```mermaid +graph TD + A[用户访问系统] --> B{是否已登录?} + B -- 否 --> C{是否有账号?} + B -- 是 --> D[访问系统功能] + + C -- 否 --> E[注册新账号] + C -- 是 --> F[登录系统] + + E --> G[填写注册信息] + G --> H[验证邮箱/手机] + H --> I[创建用户账号] + I --> F + + F --> J{认证是否成功?} + J -- 否 --> K{是否忘记密码?} + J -- 是 --> L[生成安全令牌] + + K -- 是 --> M[密码重置流程] + K -- 否 --> F + + M --> N[验证身份] + N --> O[设置新密码] + O --> F + + L --> P[加载用户权限] + P --> D + + D --> Q[系统记录操作日志] + + R[用户请求退出] --> S[清除会话信息] + S --> T[返回登录页面] +``` + +### 5.2 反馈管理模块需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| FEED-01 | 反馈提交 | 系统应允许公众用户提交环境问题反馈,包括问题描述、位置标记、污染类型分类和图片上传。 | 高 | +| FEED-02 | AI内容审核 | 系统应自动分析反馈内容,识别垃圾信息、重复提交,并进行初步分类和紧急程度评估。 | 中 | +| FEED-03 | 人工审核 | 系统应提供主管审核反馈的工作台,支持查看详情、批准或驳回反馈。 | 高 | +| FEED-04 | 状态追踪 | 系统应记录和展示反馈的全生命周期状态变更,允许用户查询处理进度。 | 高 | +| FEED-05 | 反馈查询 | 系统应支持按多种条件(状态、时间、区域、类型等)查询和筛选反馈列表。 | 中 | +| FEED-06 | 反馈统计 | 系统应生成反馈数据的统计分析,包括数量分布、处理效率等指标。 | 低 | +| FEED-07 | 通知提醒 | 系统应在反馈状态变更时通知相关用户,保持信息透明。 | 中 | + +**反馈管理流程图**: + +```mermaid +graph TD + A[公众用户发现环境问题] --> B[打开反馈提交界面] + B --> C[填写问题描述] + C --> D[选择污染类型] + D --> E[评估严重程度] + E --> F[标记地理位置] + F --> G[上传现场照片] + G --> H[提交反馈] + + H --> I[系统生成唯一事件ID] + I --> J[AI自动审核内容] + + J --> K{AI审核结果} + K -- 明显无效 --> L[标记为AI_REJECTED] + K -- 需人工确认 --> M[进入主管审核队列] + + L --> N[通知用户反馈被拒绝] + + M --> O[主管查看反馈详情] + O --> P{审核决定} + P -- 驳回 --> Q[填写驳回理由] + P -- 通过 --> R[标记为有效反馈] + + Q --> S[更新状态为REJECTED] + S --> T[通知用户反馈被驳回] + + R --> U[创建关联任务] + U --> V[更新状态为PROCESSED] + + W[用户查询反馈状态] --> X[系统展示处理进度] + + Y[管理员查看统计数据] --> Z[系统生成反馈分析报表] +``` + +# 需求定义文档(续) + +### 5.3 任务管理模块需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| TASK-01 | 任务创建 | 系统应支持从审核通过的反馈自动生成任务,也支持主管手动创建临时任务。 | 高 | +| TASK-02 | 智能分配 | 系统应基于多因素(网格员位置、当前负载、专业技能、历史表现)推荐最合适的处理人员。 | 中 | +| TASK-03 | 任务分配 | 系统应支持主管手动选择或确认系统推荐的网格员,并将任务分配给他们。 | 高 | +| TASK-04 | 任务通知 | 系统应在任务分配后立即通知相关网格员,并提供任务详情查看途径。 | 高 | +| TASK-05 | 任务执行跟踪 | 系统应记录任务的每个状态变更,支持网格员实时上报处理进度。 | 中 | +| TASK-06 | 路径规划 | 系统应为网格员提供从当前位置到任务地点的最优路径规划。 | 中 | +| TASK-07 | 结果提交 | 系统应支持网格员提交处理结果,包括文字描述和图片证明。 | 高 | +| TASK-08 | 结果审核 | 系统应支持主管审核网格员提交的处理结果,可以通过或驳回并提供反馈。 | 高 | +| TASK-09 | 任务查询 | 系统应支持按多种条件(状态、负责人、时间、区域等)查询和筛选任务列表。 | 中 | +| TASK-10 | 任务统计 | 系统应生成任务数据的统计分析,包括完成率、平均处理时长等绩效指标。 | 低 | +| TASK-11 | 任务看板 | 系统应提供直观的任务看板,展示不同状态任务的数量和分布。 | 低 | + +**任务管理流程图**: + +```mermaid +graph TD + A[反馈通过审核] --> B[自动创建任务] + C[主管手动创建任务] --> D[任务创建完成] + B --> D + D --> E{选择分配方式} + E -- 手动分配 --> F[主管选择网格员] + E -- 智能推荐 --> G[系统推荐最佳人选] + G --> F + F --> H[分配任务给网格员] + H --> I[通知网格员] + I --> J{网格员接受?} + J -- 否 --> E + J -- 是 --> K[更新任务状态为进行中] + K --> L[网格员获取路径规划] + L --> M[网格员处理任务] + M --> N[提交处理结果] + N --> O[主管审核结果] + O --> P{结果合格?} + P -- 否 --> Q[填写原因] + Q --> L + P -- 是 --> R[更新任务状态为已完成] + R --> S[通知反馈提交者] + S --> T[更新统计数据] +``` + +### 5.4 网格与地图模块需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| GRID-01 | 网格定义 | 系统应支持管理员定义和维护城市网格系统,包括网格的坐标、属性和责任人分配。 | 中 | +| GRID-02 | 地图可视化 | 系统应在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。 | 中 | +| GRID-03 | 位置标记 | 系统应支持用户在地图上精确标记环境问题位置,并自动关联到对应网格。 | 高 | +| GRID-04 | A*寻路算法 | 系统应基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划。 | 中 | +| GRID-05 | 区域统计 | 系统应基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | 低 | +| GRID-06 | 网格查询 | 系统应支持查询特定区域内的网格定义、属性和历史问题记录。 | 低 | +| GRID-07 | 离线地图 | 系统应支持地图数据的离线缓存,保证在网络不稳定情况下的基本功能。 | 低 | + +**网格与地图流程图**: + +```mermaid +graph TD + A[管理员定义网格系统] --> B[划分城市区域为网格单元] + B --> C[设置网格属性] + C --> D[分配网格责任人] + + E[用户标记环境问题位置] --> F[系统关联到对应网格] + F --> G[存储地理位置信息] + + H[网格员接收任务] --> I[查看任务位置] + I --> J[请求路径规划] + J --> K[A*算法计算最优路径] + K --> L[展示导航路线] + + M[管理员查看统计数据] --> N[生成环境问题热力图] + N --> O[识别问题高发区域] + O --> P[调整资源分配策略] +``` + +### 5.5 决策支持模块需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| DECI-01 | 核心指标看板 | 系统应实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。 | 中 | +| DECI-02 | 多维度分析 | 系统应支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。 | 中 | +| DECI-03 | 热力图可视化 | 系统应在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。 | 中 | +| DECI-04 | 绩效评估 | 系统应对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。 | 低 | +| DECI-05 | 预警机制 | 系统应基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | 低 | +| DECI-06 | 报表导出 | 系统应支持将分析结果导出为多种格式(PDF、Excel等),便于进一步分析和汇报。 | 低 | +| DECI-07 | 个性化配置 | 系统应支持决策者个性化配置数据看板,满足不同管理者的关注点。 | 低 | + +**决策支持流程图**: + +```mermaid +graph TD + A[系统收集业务数据] --> B[实时计算核心指标] + B --> C[展示核心指标看板] + + D[决策者选择分析维度] --> E[系统执行多维度分析] + E --> F[生成趋势图表] + + G[系统分析地理数据] --> H[生成环境问题热力图] + H --> I[标识高发区域] + + J[系统收集网格员工作数据] --> K[计算绩效指标] + K --> L[生成绩效排名] + + M[系统分析历史数据] --> N[识别异常趋势] + N --> O{是否达到预警阈值?} + O -- 是 --> P[触发预警通知] + O -- 否 --> Q[继续监控] + + R[决策者查看分析结果] --> S[选择导出格式] + S --> T[导出分析报表] +``` + +## 6. 非功能性需求 + +### 6.1 性能需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| PERF-01 | 响应时间 | 系统的页面加载时间应不超过3秒,API响应时间应不超过500毫秒(不包括文件上传下载)。 | 高 | +| PERF-02 | 并发用户 | 系统应能同时支持至少100个并发用户正常操作,不出现明显延迟。 | 中 | +| PERF-03 | 数据处理量 | 系统应能处理每日至少1000条反馈和500个任务的创建、更新和查询操作。 | 中 | +| PERF-04 | 文件处理 | 系统应支持单个文件最大10MB,总附件大小不超过50MB的上传和处理。 | 中 | +| PERF-05 | 地图渲染 | 地图界面应能在1秒内完成初始加载,并在500毫秒内响应用户的缩放和平移操作。 | 中 | + +### 6.2 可用性需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| USAB-01 | 界面一致性 | 系统界面应保持风格一致,包括颜色方案、按钮位置、导航结构等,减少用户学习成本。 | 中 | +| USAB-02 | 错误处理 | 系统应提供清晰的错误提示和恢复建议,避免用户操作中断。 | 高 | +| USAB-03 | 帮助系统 | 系统应提供上下文相关的帮助信息和操作指南,支持用户自助解决问题。 | 低 | +| USAB-04 | 响应式设计 | 系统界面应适应不同设备(PC、平板、手机)的屏幕尺寸,提供一致的用户体验。 | 高 | +| USAB-05 | 操作简化 | 核心功能的操作路径应尽量简化,减少用户点击次数和输入量。 | 中 | + +### 6.3 安全性需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| SECU-01 | 数据加密 | 敏感数据(如用户密码)必须加密存储,传输过程中应使用HTTPS加密。 | 高 | +| SECU-02 | 访问控制 | 系统应实施严格的基于角色的访问控制,确保用户只能访问其权限范围内的数据和功能。 | 高 | +| SECU-03 | 输入验证 | 系统应对所有用户输入进行验证和过滤,防止SQL注入、XSS等常见安全攻击。 | 高 | +| SECU-04 | 会话管理 | 系统应安全管理用户会话,包括会话创建、验证、超时和销毁,防止会话劫持。 | 中 | +| SECU-05 | 审计日志 | 系统应记录所有关键操作的审计日志,包括用户登录、数据修改、权限变更等。 | 中 | + +# 需求定义文档(续) + +### 6.4 可靠性需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| RELI-01 | 系统可用性 | 系统应保证7x24小时的稳定运行,计划内维护时间除外,系统可用性应达到99.5%以上。 | 高 | +| RELI-02 | 数据备份 | 系统应定期(至少每日一次)对全部业务数据进行备份,并支持数据恢复。 | 高 | +| RELI-03 | 故障恢复 | 系统应具备故障检测和自动恢复机制,在出现异常情况时能够快速恢复正常运行。 | 中 | +| RELI-04 | 数据一致性 | 系统应确保在各种操作条件下(包括并发访问和异常中断)保持数据的一致性和完整性。 | 高 | +| RELI-05 | 容错能力 | 系统应能够处理常见的错误情况(如网络中断、数据异常),并提供适当的降级服务。 | 中 | + +### 6.5 可维护性需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| MAIN-01 | 模块化设计 | 系统应采用模块化设计,各功能模块之间松耦合,便于独立开发、测试和维护。 | 中 | +| MAIN-02 | 配置管理 | 系统应支持通过配置文件或管理界面调整系统参数,无需修改代码和重新部署。 | 中 | +| MAIN-03 | 日志记录 | 系统应记录详细的运行日志,包括错误信息、性能指标和关键操作,便于问题诊断。 | 高 | +| MAIN-04 | 版本升级 | 系统应支持平滑的版本升级机制,最小化升级对用户的影响。 | 低 | +| MAIN-05 | 代码规范 | 系统开发应遵循统一的编码规范和设计模式,提高代码可读性和可维护性。 | 中 | + +### 6.6 可扩展性需求 + +| 需求ID | 需求名称 | 需求描述 | 优先级 | +| :--- | :--- | :--- | :--- | +| SCAL-01 | 水平扩展 | 系统架构应支持通过增加服务器节点进行水平扩展,以应对用户量和数据量的增长。 | 低 | +| SCAL-02 | 功能扩展 | 系统应支持在不影响现有功能的情况下,添加新的功能模块和业务流程。 | 中 | +| SCAL-03 | 接口开放 | 系统应提供标准化的API接口,便于与其他系统集成和数据交换。 | 中 | +| SCAL-04 | 多租户支持 | 系统应具备支持多租户(如不同城市或区域)的潜力,能够在未来进行扩展。 | 低 | +| SCAL-05 | 数据量增长 | 系统应能够处理数据量的持续增长,性能不应随着数据量的增加而明显下降。 | 中 | + +## 7. 数据需求 + +### 7.1 数据实体 + +系统需要管理以下核心数据实体: + +1. **用户账户 (UserAccount)**:存储系统用户的基本信息、认证信息和角色权限。 +2. **反馈 (Feedback)**:存储公众提交的环境问题反馈,包括描述、位置、图片等信息。 +3. **任务 (Task)**:存储从反馈生成的工作任务,包括任务详情、状态和处理记录。 +4. **网格 (Grid)**:存储城市区域的网格化管理数据,包括网格坐标、属性和责任人。 +5. **任务分配 (Assignment)**:存储任务与网格员之间的分配关系,包括分配时间、状态等。 +6. **附件 (Attachment)**:存储反馈和任务相关的图片、文档等附件文件。 +7. **操作日志 (OperationLog)**:存储系统中的关键操作记录,用于审计和问题追溯。 + +### 7.2 数据量估计 + +基于系统预期使用规模,估计以下数据量: + +1. **用户账户**:预计1000个用户账户,包括公众用户、网格员、主管、管理员和决策者。 +2. **反馈**:预计每日100-200条新增反馈,年增长约5万条。 +3. **任务**:预计每日50-100个新增任务,年增长约2.5万个。 +4. **网格**:预计1000-5000个网格单元,根据城市规模和网格粒度而定。 +5. **附件**:预计每条反馈平均2张图片,每个任务平均2张图片,年增长约15万个文件。 +6. **操作日志**:预计每日1万条日志记录,主要用于短期审计,长期可归档。 + +### 7.3 数据保留策略 + +1. **核心业务数据**(用户、反馈、任务、网格):长期保留,至少保留3年。 +2. **附件文件**:保留1年,可根据存储容量考虑适当延长或缩短。 +3. **操作日志**: + - 详细日志保留30天 + - 关键审计日志保留1年 + - 统计汇总数据长期保留 + +### 7.4 数据备份要求 + +1. **备份频率**: + - 核心业务数据:每日全量备份,每小时增量备份 + - 附件文件:每周全量备份,每日增量备份 + - 配置数据:每次变更后备份 + +2. **备份保留**: + - 每日备份保留7天 + - 每周备份保留1个月 + - 每月备份保留6个月 + - 每年备份永久保留 + +3. **恢复能力**:系统应支持将数据恢复到任意备份点,恢复时间目标(RTO)不超过4小时。 + +## 8. 结论 + +### 8.1 需求优先级总结 + +基于业务重要性和实现复杂度,系统需求按以下优先级排序: + +1. **最高优先级**: + - 用户认证和权限控制 + - 反馈提交和审核 + - 任务创建和分配 + - 数据安全和完整性 + +2. **高优先级**: + - 任务执行和结果审核 + - 地图标记和路径规划 + - 系统性能和可用性 + - 用户界面和体验 + +3. **中等优先级**: + - AI内容审核 + - 决策支持和数据分析 + - 通知和提醒功能 + - 系统可维护性 + +4. **低优先级**: + - 高级统计和报表 + - 个性化配置 + - 系统扩展性 + - 辅助功能和优化 + +### 8.2 实施建议 + +为确保系统成功实施,建议采取以下策略: + +1. **分阶段实施**: + - 第一阶段:核心功能(用户管理、反馈、任务) + - 第二阶段:支撑功能(地图、AI审核) + - 第三阶段:高级功能(决策支持、报表) + +2. **持续迭代**:采用敏捷开发方法,快速交付可用版本,根据用户反馈持续改进。 + +3. **用户参与**:在开发过程中邀请各类用户参与测试和评估,确保系统满足实际需求。 + +4. **技术选型**:选择成熟稳定的技术栈,平衡创新性和可靠性,降低开发和维护风险。 + +5. **数据治理**:建立完善的数据管理和治理机制,确保数据质量和安全。 + +### 8.3 预期效益 + +成功实施环境监测系统预期将带来以下效益: + +1. **业务效益**: + - 环境问题处理效率提升50%以上 + - 问题解决质量显著提高 + - 资源利用更加合理高效 + +2. **管理效益**: + - 实现环境问题全流程可视化管理 + - 提供科学决策的数据支持 + - 优化工作流程和资源配置 + +3. **社会效益**: + - 提升公众参与环境治理的积极性 + - 增强政府工作透明度和公信力 + - 改善城市环境质量和居民生活满意度 + +通过环境监测系统的建设和应用,将有效解决当前环境问题管理中的痛点,构建起连接公众、管理部门和执行人员的桥梁,实现环境问题的快速发现、高效处理和全程监督,为城市环境治理提供有力支撑。 --- - + ### Report/需求定义.md - -# 需求定义文档 - -## 1. 整体业务流程 - -下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程。 - -```mermaid -flowchart TD - %% 定义样式 - classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333 - classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333 - classDef worker fill:#d5e8d4,stroke:#82b366,color:#333 - classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333 - classDef decision fill:#f8cecc,stroke:#b85450,color:#333 - classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px - - %% 流程开始 - A([开始]) --> B["[公众端] 发现环境问题"] - B --> C["[公众端] 提交反馈
(标题/描述/图片/位置)"] - - %% 平台接收与AI处理 - C --> D["[平台] 接收反馈
生成唯一事件ID"] - D --> E{"[平台] AI自动审核
分析内容/分类"} - - %% AI审核分支 - E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"] - F1 --> F2["[平台] 通知提交者"] - F2 --> Z1([结束]) - - E -- "需人工确认" --> G["[主管] 查看反馈详情
进行人工审核"] - - %% 主管审核分支 - G --> H{"[主管] 审核决定"} - H -- "驳回" --> I1["[主管] 填写驳回理由"] - I1 --> I2["[平台] 更新状态为REJECTED"] - I2 --> I3["[平台] 通知提交者"] - I3 --> Z2([结束]) - - %% 审核通过,创建任务 - H -- "通过" --> J1["[主管] 确认反馈有效"] - J1 --> J2["[平台] 自动创建结构化任务"] - J2 --> J3["[平台] 更新反馈状态为PROCESSED"] - - %% 任务分配 - J3 --> K1{"[主管] 选择分配方式"} - K1 -- "手动分配" --> K2["[主管] 选择特定网格员"] - K1 -- "智能推荐" --> K3["[平台] 运行分配算法
考虑位置/负载/专长"] - K3 --> K4["[平台] 推荐最佳人选"] - K4 --> K2 - K2 --> K5["[平台] 创建任务分配记录
更新任务状态为ASSIGNED"] - K5 --> K6["[平台] 通知网格员"] - - %% 网格员处理 - K6 --> L1["[网格员] 接收任务通知"] - L1 --> L2{"[网格员] 接受任务?"} - L2 -- "拒绝" --> K1 - L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"] - L3 --> L4["[网格员] 查看任务详情
获取路径规划"] - L4 --> L5["[网格员] 前往现场处理"] - L5 --> L6["[网格员] 记录处理过程
上传证明材料"] - L6 --> L7["[网格员] 提交处理结果
更新状态为SUBMITTED"] - - %% 主管审核结果 - L7 --> M1["[主管] 审核处理结果"] - M1 --> M2{"[主管] 结果是否合格?"} - M2 -- "不合格" --> M3["[主管] 填写原因
要求重新处理"] - M3 --> L4 - - %% 完成流程 - M2 -- "合格" --> N1["[主管] 确认任务完成"] - N1 --> N2["[平台] 更新任务状态为APPROVED"] - N2 --> N3["[平台] 更新反馈状态为CLOSED"] - N3 --> N4["[平台] 通知反馈提交者"] - N4 --> N5["[平台] 更新统计数据"] - N5 --> O["[决策层] 查看数据看板
分析环境趋势"] - O --> Z3([结束]) - - %% 为节点添加类别 - class A,Z1,Z2,Z3 start_end - class B,C,F2,I3,N4 public - class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform - class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor - class L1,L2,L3,L4,L5,L6,L7 worker - class O decision -``` - -## 2. 功能性需求 - -### 2.1 功能层次方框图 - -```mermaid -flowchart TD - %% 用户交互层 - subgraph "用户交互层" - direction LR - C["公众服务模块(问题上报)"] - H["个人中心模块(我的反馈/资料)"] - D["管理驾驶舱(数据决策)"] - end - - %% 核心业务层 - subgraph "核心业务层" - direction LR - AI["AI分析模块(内容审核)"] - E["任务管理模块(分配、流转、执行)"] - end - - %% 应用支撑层 - subgraph "应用支撑层" - direction LR - F["网格与地图模块(LBS & 寻路)"] - I["文件服务模块(附件存取)"] - end - - %% 基础服务层 - subgraph "基础服务层" - direction LR - B["用户与认证模块"] - G["系统管理模块(用户/权限)"] - J["日志审计模块"] - end - - %% 定义关系 - C -- "提交反馈" --> AI - AI -- "分析结果" --> E - C -- "附件" --> I - E -- "调用" --> F - E -- "任务附件" --> I - E -- "统计数据" --> D - - H -- "查询个人数据" --> E - - %% 基础服务支撑所有上层模块 (关系隐含) - G -- "管理" --> B - - classDef userLayer fill:#d4f1f9,stroke:#05a8e5; - classDef coreLayer fill:#ffe6cc,stroke:#f7a128; - classDef appSupportLayer fill:#d5e8d4,stroke:#82b366; - classDef baseLayer fill:#e1d5e7,stroke:#9673a6; - - class C,H,D userLayer; - class AI,E coreLayer; - class F,I appSupportLayer; - class B,G,J baseLayer; -``` - -### 2.2 需求描述 - -#### 2.2.1 用户与认证模块 - -| 功能名称 | 用户与认证模块 | -| :--- | :--- | -| **优先级** | 高 | -| **业务背景** | 作为系统安全的基础,本模块负责管理所有用户的身份验证和访问控制,确保系统资源只能被授权用户访问,同时提供灵活的角色权限管理。 | -| **功能说明** | 1. **用户认证**:基于JWT (JSON Web Token) 的安全认证机制,支持账号密码登录,提供令牌刷新功能。
2. **权限控制**:基于RBAC (基于角色的访问控制) 模型,预设管理员、主管、网格员等角色,每个角色拥有特定的API访问权限。
3. **密码管理**:支持安全的密码重置流程,包括邮箱验证码验证,以及定期密码更新提醒。
4. **会话管理**:支持单点登录或多设备登录控制,可配置会话超时策略。 | -| **约束条件** | 1. 密码必须符合复杂度要求(至少8位,包含大小写字母、数字和特殊字符)。
2. 敏感操作(如修改权限)需要二次验证。
3. 密码在数据库中必须使用BCrypt等强哈希算法加密存储。 | -| **相关查询** | 1. 按用户名、邮箱或手机号查询用户信息。
2. 查询特定角色的所有用户列表。
3. 查询用户的权限和访问历史。 | -| **其他需求** | 1. 登录失败超过预设次数后,账户应被临时锁定。
2. 系统应记录所有关键安全事件(登录、权限变更等)的审计日志。 | -| **裁剪说明** | 不可裁剪,此为系统安全的基础组件。 | - -#### 2.2.2 反馈管理模块 - -| 功能名称 | 反馈管理模块 | -| :--- | :--- | -| **优先级** | 高 | -| **业务背景** | 作为系统的核心输入端口,本模块负责收集、处理和跟踪所有环境问题反馈,是连接公众与管理部门的桥梁,也是后续任务创建的数据源。 | -| **功能说明** | 1. **反馈提交**:提供结构化的表单接口,支持文字描述、污染类型分类、严重程度评估、地理位置标记和多媒体附件上传。
2. **AI内容审核**:集成智能审核服务,对反馈内容进行自动分析,识别垃圾信息、重复提交,并进行初步的分类和紧急程度评估。
3. **人工审核工作台**:为主管提供高效的反馈审核界面,支持批量处理、快速预览和详情查看。
4. **状态追踪**:完整记录反馈从提交到处理完成的全生命周期状态变更,支持多维度的统计和查询。 | -| **约束条件** | 1. 反馈提交必须包含至少一张图片和准确的地理位置信息。
2. AI审核结果仅作为参考,最终决定权在人工审核者手中。
3. 对于紧急程度被标记为"高"的反馈,系统应在1小时内完成审核。 | -| **相关查询** | 1. 按状态、时间段、区域、污染类型等多维度查询反馈列表。
2. 查看特定反馈的详细信息和处理历史。
3. 统计不同类型反馈的数量分布和处理效率。 | -| **其他需求** | 1. 支持反馈的优先级标记和升级处理。
2. 对于同一区域短时间内的多个相似反馈,系统应能智能识别并提示可能的重复。 | -| **裁剪说明** | AI审核功能可在初期简化实现,但反馈的基本提交和人工审核流程不可裁剪。 | - -#### 2.2.3 任务管理模块 - -| 功能名称 | 任务管理模块 | -| :--- | :--- | -| **优先级** | 高 | -| **业务背景** | 本模块是系统的核心业务处理单元,负责将审核通过的反馈转化为可执行的工作任务,并对任务的分配、执行和完成进行全流程管理。 | -| **功能说明** | 1. **任务创建**:支持从反馈自动生成任务,也支持主管手动创建临时任务,包含任务描述、位置、截止时间、优先级等信息。
2. **智能分配**:基于多因素(网格员位置、当前负载、专业技能、历史表现)的任务分配算法,为每个任务推荐最合适的处理人员。
3. **任务执行跟踪**:记录任务的每个状态变更(已分配、已接受、进行中、已提交、已审核),支持网格员实时上报处理进度。
4. **结果审核**:主管对网格员提交的处理结果进行审核,可以通过或驳回,并提供反馈意见。
5. **任务看板**:直观展示不同状态任务的数量和分布,支持拖拽操作进行状态更新。 | -| **约束条件** | 1. 任务必须关联到一个有效的反馈或由授权主管手动创建。
2. 任务分配时必须考虑网格员的工作区域和当前任务负载。
3. 高优先级任务应在24小时内分配并开始处理。
4. 任务提交时必须包含处理过程描述和至少一张结果照片。 | -| **相关查询** | 1. 按状态、负责人、时间段、区域等多维度查询任务列表。
2. 查看特定任务的详细信息、处理历史和相关反馈。
3. 统计不同网格员的任务完成率、平均处理时长等绩效指标。 | -| **其他需求** | 1. 支持任务的紧急程度升级和重新分配。
2. 对于长时间未处理的任务,系统应自动发送提醒通知。
3. 支持批量导出任务报告,用于绩效评估和工作汇报。 | -| **裁剪说明** | 智能分配算法可以在初期简化实现,但任务的基本创建、分配和状态管理功能不可裁剪。 | - -#### 2.2.4 网格与地图模块 - -| 功能名称 | 网格与地图模块 | -| :--- | :--- | -| **优先级** | 中 | -| **业务背景** | 本模块负责对地理空间进行网格化管理,将城市区域划分为可管理的网格单元,并为任务执行提供地理位置支持和路径规划。 | -| **功能说明** | 1. **网格定义与管理**:支持管理员定义和维护城市网格系统,包括网格的坐标、属性(如是否为障碍物)和责任人分配。
2. **A*寻路算法服务**:基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划,考虑距离、交通状况和障碍物。
3. **地图可视化**:在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。
4. **区域统计**:基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | -| **约束条件** | 1. 网格系统应支持多级划分,最小网格单元不应大于500米×500米。
2. 路径规划应考虑实际道路情况和障碍物,避免不可通行区域。
3. 地图数据应定期更新,确保准确性。 | -| **相关查询** | 1. 查询特定区域内的网格定义和属性。
2. 查询特定网格的历史问题记录和统计数据。
3. 获取两点间的最优路径规划。 | -| **其他需求** | 1. 支持网格责任人的灵活调整和临时替换。
2. 地图界面应支持常见的交互操作(缩放、平移、点选)。
3. 支持离线地图数据缓存,保证在网络不稳定情况下的基本功能。 | -| **裁剪说明** | A*寻路算法的高级功能(如考虑实时交通)可以在后期迭代中实现,但基本的网格定义和地图展示功能不应裁剪。 | - -#### 2.2.5 决策支持模块 - -| 功能名称 | 决策支持模块 (管理驾驶舱) | -| :--- | :--- | -| **优先级** | 中 | -| **业务背景** | 本模块旨在将系统中沉淀的大量业务数据转化为有价值的决策洞察,帮助管理层了解环境状况、评估治理效果、优化资源配置。 | -| **功能说明** | 1. **核心指标看板**:实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。
2. **多维度分析**:支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。
3. **热力图可视化**:在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。
4. **绩效评估**:对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。
5. **预警机制**:基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | -| **约束条件** | 1. 数据分析应基于实时或准实时的业务数据,确保决策的时效性。
2. 图表和报表应支持多种导出格式(PDF、Excel等),便于进一步分析和汇报。
3. 敏感数据(如个人绩效)的访问应受到严格权限控制。 | -| **相关查询** | 1. 按自定义时间范围查询各类统计指标。
2. 查询特定区域或网格员的历史表现数据。
3. 生成定制化的数据报表和分析图表。 | -| **其他需求** | 1. 支持数据看板的个性化配置,满足不同管理者的关注点。
2. 提供数据异常检测和提醒功能,及时发现数据波动。
3. 支持定期自动生成并发送统计报告。 | -| **裁剪说明** | 高级分析功能(如预测模型)可以在后期迭代中实现,但基本的数据统计和可视化功能不应裁剪。 | - -## 3. 需求规定 - -### 3.1 一般性需求 - -* **数据集中管理与共享**:系统应采用统一的数据存储和访问机制,确保各模块间的数据一致性和实时共享。所有业务数据应按照标准化格式进行存储,并通过规范的API进行访问,避免数据孤岛。 - -* **高可用性与可靠性**:系统应保证7x24小时的稳定运行,关键业务流程(如反馈提交、任务分配)的可用性应达到99.9%以上。应实现适当的错误处理和故障恢复机制,确保在出现异常情况时能够快速恢复。 - -* **安全性与合规性**: - * 应用SSL/TLS加密保护所有网络通信。 - * 实现严格的认证和授权机制,确保用户只能访问其权限范围内的数据和功能。 - * 敏感数据(如用户密码)必须加密存储,并限制访问权限。 - * 系统应保留完整的操作日志,支持安全审计和问题追溯。 - * 符合相关的数据保护法规和隐私要求。 - -* **可扩展性与模块化**:系统架构应支持水平扩展和功能模块的灵活添加。核心组件应设计为松耦合的,便于独立升级和替换。API设计应考虑向后兼容性,确保系统可以平滑演进。 - -* **用户体验优化**: - * 界面设计应简洁直观,符合现代Web应用的设计标准。 - * 关键操作路径应尽量简化,减少用户点击次数。 - * 系统响应时间应控制在可接受范围内,核心API的平均响应时间不超过500ms。 - * 提供适当的操作反馈和状态提示,增强用户对系统的信任感。 - * 支持响应式设计,确保在不同设备(PC、平板、手机)上的良好体验。 - -* **国际化与本地化**:系统应支持多语言界面,初期至少支持中英文切换。日期、时间、数字等格式应根据用户的区域设置进行适当显示。 - -* **可维护性与可测试性**:系统代码应遵循统一的编码规范和设计模式,保持良好的可读性和可维护性。核心业务逻辑应有充分的单元测试覆盖,便于后续的功能迭代和质量保障。 - -## 4. 数据描述 - -### 4.1 用户账户 (UserAccount) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 用户唯一标识符 | Long | 主键,自增 | 是 | -| `name` | 用户姓名 | String | 最大长度50 | 是 | -| `phone` | 手机号码 | String | 符合手机号格式 | 是 | -| `email` | 电子邮箱 | String | 符合邮箱格式 | 是 | -| `password` | 密码(加密存储) | String | 最小长度8,包含字母、数字和特殊字符 | 是 | -| `gender` | 性别 | Enum | MALE, FEMALE, OTHER | 否 | -| `role` | 用户角色 | Enum | ADMIN, SUPERVISOR, GRID_WORKER, PUBLIC_USER | 是 | -| `status` | 账户状态 | Enum | ACTIVE, INACTIVE, LOCKED | 是 | -| `gridX` | 网格X坐标(仅网格员) | Integer | 非负整数 | 否 | -| `gridY` | 网格Y坐标(仅网格员) | Integer | 非负整数 | 否 | -| `createdAt` | 账户创建时间 | DateTime | ISO 8601格式 | 是 | -| `updatedAt` | 账户更新时间 | DateTime | ISO 8601格式 | 是 | -| `lastLoginAt` | 最后登录时间 | DateTime | ISO 8601格式 | 否 | - -### 4.2 反馈 (Feedback) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 反馈唯一标识符 | Long | 主键,自增 | 是 | -| `eventId` | 事件ID(业务编号) | String | UUID格式 | 是 | -| `title` | 反馈标题 | String | 最大长度100 | 是 | -| `description` | 问题详细描述 | String | 最大长度1000 | 是 | -| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 | -| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 | -| `longitude` | 地理位置经度 | Double | 有效范围 | 是 | -| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 | -| `imageUrls` | 图片URL列表 | Array | 至少一张图片 | 是 | -| `status` | 反馈状态 | Enum | PENDING_REVIEW, AI_REJECTED, PROCESSED, CLOSED | 是 | -| `submitterId` | 提交者ID | Long | 外键关联UserAccount | 是 | -| `reviewerId` | 审核者ID | Long | 外键关联UserAccount | 否 | -| `reviewNote` | 审核备注 | String | 最大长度500 | 否 | -| `createdAt` | 提交时间 | DateTime | ISO 8601格式 | 是 | -| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | -| `processedAt` | 处理时间 | DateTime | ISO 8601格式 | 否 | - -### 4.3 任务 (Task) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 任务唯一标识符 | Long | 主键,自增 | 是 | -| `feedbackId` | 关联的反馈ID | Long | 外键关联Feedback | 否 | -| `title` | 任务标题 | String | 最大长度100 | 是 | -| `description` | 任务描述 | String | 最大长度1000 | 是 | -| `assigneeId` | 被指派的网格员ID | Long | 外键关联UserAccount | 否 | -| `createdBy` | 创建者ID | Long | 外键关联UserAccount | 是 | -| `status` | 任务状态 | Enum | CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED | 是 | -| `priority` | 优先级 | Enum | LOW, MEDIUM, HIGH, URGENT | 是 | -| `longitude` | 任务地点经度 | Double | 有效范围 | 是 | -| `latitude` | 任务地点纬度 | Double | 有效范围 | 是 | -| `deadline` | 截止日期 | DateTime | ISO 8601格式 | 否 | -| `completionReport` | 完成报告 | String | 最大长度2000 | 否 | -| `resultImageUrls` | 结果图片URL列表 | Array | 可为空 | 否 | -| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 | -| `assignedAt` | 分配时间 | DateTime | ISO 8601格式 | 否 | -| `startedAt` | 开始时间 | DateTime | ISO 8601格式 | 否 | -| `submittedAt` | 提交时间 | DateTime | ISO 8601格式 | 否 | -| `approvedAt` | 审核通过时间 | DateTime | ISO 8601格式 | 否 | -| `rejectionReason` | 拒绝原因 | String | 最大长度500 | 否 | - -### 4.4 网格 (Grid) 数据结构 - -| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | -| :--- | :--- | :--- | :--- | :--- | -| `id` | 网格唯一标识符 | Long | 主键,自增 | 是 | -| `gridX` | 网格X坐标 | Integer | 非负整数 | 是 | -| `gridY` | 网格Y坐标 | Integer | 非负整数 | 是 | -| `cityName` | 所属城市 | String | 最大长度50 | 是 | -| `districtName` | 所属区县 | String | 最大长度50 | 是 | -| `description` | 网格描述 | String | 最大长度200 | 否 | -| `isObstacle` | 是否为障碍物 | Boolean | true/false | 是 | -| `responsibleUserId` | 负责人ID | Long | 外键关联UserAccount | 否 | -| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 | -| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | + +# 需求定义文档 + +## 1. 整体业务流程 + +下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程。 + +```mermaid +flowchart TD + %% 定义样式 + classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333 + classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333 + classDef worker fill:#d5e8d4,stroke:#82b366,color:#333 + classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333 + classDef decision fill:#f8cecc,stroke:#b85450,color:#333 + classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px + + %% 流程开始 + A([开始]) --> B["[公众端] 发现环境问题"] + B --> C["[公众端] 提交反馈
(标题/描述/图片/位置)"] + + %% 平台接收与AI处理 + C --> D["[平台] 接收反馈
生成唯一事件ID"] + D --> E{"[平台] AI自动审核
分析内容/分类"} + + %% AI审核分支 + E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"] + F1 --> F2["[平台] 通知提交者"] + F2 --> Z1([结束]) + + E -- "需人工确认" --> G["[主管] 查看反馈详情
进行人工审核"] + + %% 主管审核分支 + G --> H{"[主管] 审核决定"} + H -- "驳回" --> I1["[主管] 填写驳回理由"] + I1 --> I2["[平台] 更新状态为REJECTED"] + I2 --> I3["[平台] 通知提交者"] + I3 --> Z2([结束]) + + %% 审核通过,创建任务 + H -- "通过" --> J1["[主管] 确认反馈有效"] + J1 --> J2["[平台] 自动创建结构化任务"] + J2 --> J3["[平台] 更新反馈状态为PROCESSED"] + + %% 任务分配 + J3 --> K1{"[主管] 选择分配方式"} + K1 -- "手动分配" --> K2["[主管] 选择特定网格员"] + K1 -- "智能推荐" --> K3["[平台] 运行分配算法
考虑位置/负载/专长"] + K3 --> K4["[平台] 推荐最佳人选"] + K4 --> K2 + K2 --> K5["[平台] 创建任务分配记录
更新任务状态为ASSIGNED"] + K5 --> K6["[平台] 通知网格员"] + + %% 网格员处理 + K6 --> L1["[网格员] 接收任务通知"] + L1 --> L2{"[网格员] 接受任务?"} + L2 -- "拒绝" --> K1 + L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"] + L3 --> L4["[网格员] 查看任务详情
获取路径规划"] + L4 --> L5["[网格员] 前往现场处理"] + L5 --> L6["[网格员] 记录处理过程
上传证明材料"] + L6 --> L7["[网格员] 提交处理结果
更新状态为SUBMITTED"] + + %% 主管审核结果 + L7 --> M1["[主管] 审核处理结果"] + M1 --> M2{"[主管] 结果是否合格?"} + M2 -- "不合格" --> M3["[主管] 填写原因
要求重新处理"] + M3 --> L4 + + %% 完成流程 + M2 -- "合格" --> N1["[主管] 确认任务完成"] + N1 --> N2["[平台] 更新任务状态为APPROVED"] + N2 --> N3["[平台] 更新反馈状态为CLOSED"] + N3 --> N4["[平台] 通知反馈提交者"] + N4 --> N5["[平台] 更新统计数据"] + N5 --> O["[决策层] 查看数据看板
分析环境趋势"] + O --> Z3([结束]) + + %% 为节点添加类别 + class A,Z1,Z2,Z3 start_end + class B,C,F2,I3,N4 public + class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform + class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor + class L1,L2,L3,L4,L5,L6,L7 worker + class O decision +``` + +## 2. 功能性需求 + +### 2.1 功能层次方框图 + +```mermaid +flowchart TD + %% 用户交互层 + subgraph "用户交互层" + direction LR + C["公众服务模块(问题上报)"] + H["个人中心模块(我的反馈/资料)"] + D["管理驾驶舱(数据决策)"] + end + + %% 核心业务层 + subgraph "核心业务层" + direction LR + AI["AI分析模块(内容审核)"] + E["任务管理模块(分配、流转、执行)"] + end + + %% 应用支撑层 + subgraph "应用支撑层" + direction LR + F["网格与地图模块(LBS & 寻路)"] + I["文件服务模块(附件存取)"] + end + + %% 基础服务层 + subgraph "基础服务层" + direction LR + B["用户与认证模块"] + G["系统管理模块(用户/权限)"] + J["日志审计模块"] + end + + %% 定义关系 + C -- "提交反馈" --> AI + AI -- "分析结果" --> E + C -- "附件" --> I + E -- "调用" --> F + E -- "任务附件" --> I + E -- "统计数据" --> D + + H -- "查询个人数据" --> E + + %% 基础服务支撑所有上层模块 (关系隐含) + G -- "管理" --> B + + classDef userLayer fill:#d4f1f9,stroke:#05a8e5; + classDef coreLayer fill:#ffe6cc,stroke:#f7a128; + classDef appSupportLayer fill:#d5e8d4,stroke:#82b366; + classDef baseLayer fill:#e1d5e7,stroke:#9673a6; + + class C,H,D userLayer; + class AI,E coreLayer; + class F,I appSupportLayer; + class B,G,J baseLayer; +``` + +### 2.2 需求描述 + +#### 2.2.1 用户与认证模块 + +| 功能名称 | 用户与认证模块 | +| :--- | :--- | +| **优先级** | 高 | +| **业务背景** | 作为系统安全的基础,本模块负责管理所有用户的身份验证和访问控制,确保系统资源只能被授权用户访问,同时提供灵活的角色权限管理。 | +| **功能说明** | 1. **用户认证**:基于JWT (JSON Web Token) 的安全认证机制,支持账号密码登录,提供令牌刷新功能。
2. **权限控制**:基于RBAC (基于角色的访问控制) 模型,预设管理员、主管、网格员等角色,每个角色拥有特定的API访问权限。
3. **密码管理**:支持安全的密码重置流程,包括邮箱验证码验证,以及定期密码更新提醒。
4. **会话管理**:支持单点登录或多设备登录控制,可配置会话超时策略。 | +| **约束条件** | 1. 密码必须符合复杂度要求(至少8位,包含大小写字母、数字和特殊字符)。
2. 敏感操作(如修改权限)需要二次验证。
3. 密码在数据库中必须使用BCrypt等强哈希算法加密存储。 | +| **相关查询** | 1. 按用户名、邮箱或手机号查询用户信息。
2. 查询特定角色的所有用户列表。
3. 查询用户的权限和访问历史。 | +| **其他需求** | 1. 登录失败超过预设次数后,账户应被临时锁定。
2. 系统应记录所有关键安全事件(登录、权限变更等)的审计日志。 | +| **裁剪说明** | 不可裁剪,此为系统安全的基础组件。 | + +#### 2.2.2 反馈管理模块 + +| 功能名称 | 反馈管理模块 | +| :--- | :--- | +| **优先级** | 高 | +| **业务背景** | 作为系统的核心输入端口,本模块负责收集、处理和跟踪所有环境问题反馈,是连接公众与管理部门的桥梁,也是后续任务创建的数据源。 | +| **功能说明** | 1. **反馈提交**:提供结构化的表单接口,支持文字描述、污染类型分类、严重程度评估、地理位置标记和多媒体附件上传。
2. **AI内容审核**:集成智能审核服务,对反馈内容进行自动分析,识别垃圾信息、重复提交,并进行初步的分类和紧急程度评估。
3. **人工审核工作台**:为主管提供高效的反馈审核界面,支持批量处理、快速预览和详情查看。
4. **状态追踪**:完整记录反馈从提交到处理完成的全生命周期状态变更,支持多维度的统计和查询。 | +| **约束条件** | 1. 反馈提交必须包含至少一张图片和准确的地理位置信息。
2. AI审核结果仅作为参考,最终决定权在人工审核者手中。
3. 对于紧急程度被标记为"高"的反馈,系统应在1小时内完成审核。 | +| **相关查询** | 1. 按状态、时间段、区域、污染类型等多维度查询反馈列表。
2. 查看特定反馈的详细信息和处理历史。
3. 统计不同类型反馈的数量分布和处理效率。 | +| **其他需求** | 1. 支持反馈的优先级标记和升级处理。
2. 对于同一区域短时间内的多个相似反馈,系统应能智能识别并提示可能的重复。 | +| **裁剪说明** | AI审核功能可在初期简化实现,但反馈的基本提交和人工审核流程不可裁剪。 | + +#### 2.2.3 任务管理模块 + +| 功能名称 | 任务管理模块 | +| :--- | :--- | +| **优先级** | 高 | +| **业务背景** | 本模块是系统的核心业务处理单元,负责将审核通过的反馈转化为可执行的工作任务,并对任务的分配、执行和完成进行全流程管理。 | +| **功能说明** | 1. **任务创建**:支持从反馈自动生成任务,也支持主管手动创建临时任务,包含任务描述、位置、截止时间、优先级等信息。
2. **智能分配**:基于多因素(网格员位置、当前负载、专业技能、历史表现)的任务分配算法,为每个任务推荐最合适的处理人员。
3. **任务执行跟踪**:记录任务的每个状态变更(已分配、已接受、进行中、已提交、已审核),支持网格员实时上报处理进度。
4. **结果审核**:主管对网格员提交的处理结果进行审核,可以通过或驳回,并提供反馈意见。
5. **任务看板**:直观展示不同状态任务的数量和分布,支持拖拽操作进行状态更新。 | +| **约束条件** | 1. 任务必须关联到一个有效的反馈或由授权主管手动创建。
2. 任务分配时必须考虑网格员的工作区域和当前任务负载。
3. 高优先级任务应在24小时内分配并开始处理。
4. 任务提交时必须包含处理过程描述和至少一张结果照片。 | +| **相关查询** | 1. 按状态、负责人、时间段、区域等多维度查询任务列表。
2. 查看特定任务的详细信息、处理历史和相关反馈。
3. 统计不同网格员的任务完成率、平均处理时长等绩效指标。 | +| **其他需求** | 1. 支持任务的紧急程度升级和重新分配。
2. 对于长时间未处理的任务,系统应自动发送提醒通知。
3. 支持批量导出任务报告,用于绩效评估和工作汇报。 | +| **裁剪说明** | 智能分配算法可以在初期简化实现,但任务的基本创建、分配和状态管理功能不可裁剪。 | + +#### 2.2.4 网格与地图模块 + +| 功能名称 | 网格与地图模块 | +| :--- | :--- | +| **优先级** | 中 | +| **业务背景** | 本模块负责对地理空间进行网格化管理,将城市区域划分为可管理的网格单元,并为任务执行提供地理位置支持和路径规划。 | +| **功能说明** | 1. **网格定义与管理**:支持管理员定义和维护城市网格系统,包括网格的坐标、属性(如是否为障碍物)和责任人分配。
2. **A*寻路算法服务**:基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划,考虑距离、交通状况和障碍物。
3. **地图可视化**:在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。
4. **区域统计**:基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | +| **约束条件** | 1. 网格系统应支持多级划分,最小网格单元不应大于500米×500米。
2. 路径规划应考虑实际道路情况和障碍物,避免不可通行区域。
3. 地图数据应定期更新,确保准确性。 | +| **相关查询** | 1. 查询特定区域内的网格定义和属性。
2. 查询特定网格的历史问题记录和统计数据。
3. 获取两点间的最优路径规划。 | +| **其他需求** | 1. 支持网格责任人的灵活调整和临时替换。
2. 地图界面应支持常见的交互操作(缩放、平移、点选)。
3. 支持离线地图数据缓存,保证在网络不稳定情况下的基本功能。 | +| **裁剪说明** | A*寻路算法的高级功能(如考虑实时交通)可以在后期迭代中实现,但基本的网格定义和地图展示功能不应裁剪。 | + +#### 2.2.5 决策支持模块 + +| 功能名称 | 决策支持模块 (管理驾驶舱) | +| :--- | :--- | +| **优先级** | 中 | +| **业务背景** | 本模块旨在将系统中沉淀的大量业务数据转化为有价值的决策洞察,帮助管理层了解环境状况、评估治理效果、优化资源配置。 | +| **功能说明** | 1. **核心指标看板**:实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。
2. **多维度分析**:支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。
3. **热力图可视化**:在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。
4. **绩效评估**:对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。
5. **预警机制**:基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | +| **约束条件** | 1. 数据分析应基于实时或准实时的业务数据,确保决策的时效性。
2. 图表和报表应支持多种导出格式(PDF、Excel等),便于进一步分析和汇报。
3. 敏感数据(如个人绩效)的访问应受到严格权限控制。 | +| **相关查询** | 1. 按自定义时间范围查询各类统计指标。
2. 查询特定区域或网格员的历史表现数据。
3. 生成定制化的数据报表和分析图表。 | +| **其他需求** | 1. 支持数据看板的个性化配置,满足不同管理者的关注点。
2. 提供数据异常检测和提醒功能,及时发现数据波动。
3. 支持定期自动生成并发送统计报告。 | +| **裁剪说明** | 高级分析功能(如预测模型)可以在后期迭代中实现,但基本的数据统计和可视化功能不应裁剪。 | + +## 3. 需求规定 + +### 3.1 一般性需求 + +* **数据集中管理与共享**:系统应采用统一的数据存储和访问机制,确保各模块间的数据一致性和实时共享。所有业务数据应按照标准化格式进行存储,并通过规范的API进行访问,避免数据孤岛。 + +* **高可用性与可靠性**:系统应保证7x24小时的稳定运行,关键业务流程(如反馈提交、任务分配)的可用性应达到99.9%以上。应实现适当的错误处理和故障恢复机制,确保在出现异常情况时能够快速恢复。 + +* **安全性与合规性**: + * 应用SSL/TLS加密保护所有网络通信。 + * 实现严格的认证和授权机制,确保用户只能访问其权限范围内的数据和功能。 + * 敏感数据(如用户密码)必须加密存储,并限制访问权限。 + * 系统应保留完整的操作日志,支持安全审计和问题追溯。 + * 符合相关的数据保护法规和隐私要求。 + +* **可扩展性与模块化**:系统架构应支持水平扩展和功能模块的灵活添加。核心组件应设计为松耦合的,便于独立升级和替换。API设计应考虑向后兼容性,确保系统可以平滑演进。 + +* **用户体验优化**: + * 界面设计应简洁直观,符合现代Web应用的设计标准。 + * 关键操作路径应尽量简化,减少用户点击次数。 + * 系统响应时间应控制在可接受范围内,核心API的平均响应时间不超过500ms。 + * 提供适当的操作反馈和状态提示,增强用户对系统的信任感。 + * 支持响应式设计,确保在不同设备(PC、平板、手机)上的良好体验。 + +* **国际化与本地化**:系统应支持多语言界面,初期至少支持中英文切换。日期、时间、数字等格式应根据用户的区域设置进行适当显示。 + +* **可维护性与可测试性**:系统代码应遵循统一的编码规范和设计模式,保持良好的可读性和可维护性。核心业务逻辑应有充分的单元测试覆盖,便于后续的功能迭代和质量保障。 + +## 4. 数据描述 + +### 4.1 用户账户 (UserAccount) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 用户唯一标识符 | Long | 主键,自增 | 是 | +| `name` | 用户姓名 | String | 最大长度50 | 是 | +| `phone` | 手机号码 | String | 符合手机号格式 | 是 | +| `email` | 电子邮箱 | String | 符合邮箱格式 | 是 | +| `password` | 密码(加密存储) | String | 最小长度8,包含字母、数字和特殊字符 | 是 | +| `gender` | 性别 | Enum | MALE, FEMALE, OTHER | 否 | +| `role` | 用户角色 | Enum | ADMIN, SUPERVISOR, GRID_WORKER, PUBLIC_USER | 是 | +| `status` | 账户状态 | Enum | ACTIVE, INACTIVE, LOCKED | 是 | +| `gridX` | 网格X坐标(仅网格员) | Integer | 非负整数 | 否 | +| `gridY` | 网格Y坐标(仅网格员) | Integer | 非负整数 | 否 | +| `createdAt` | 账户创建时间 | DateTime | ISO 8601格式 | 是 | +| `updatedAt` | 账户更新时间 | DateTime | ISO 8601格式 | 是 | +| `lastLoginAt` | 最后登录时间 | DateTime | ISO 8601格式 | 否 | + +### 4.2 反馈 (Feedback) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 反馈唯一标识符 | Long | 主键,自增 | 是 | +| `eventId` | 事件ID(业务编号) | String | UUID格式 | 是 | +| `title` | 反馈标题 | String | 最大长度100 | 是 | +| `description` | 问题详细描述 | String | 最大长度1000 | 是 | +| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 | +| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 | +| `longitude` | 地理位置经度 | Double | 有效范围 | 是 | +| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 | +| `imageUrls` | 图片URL列表 | Array | 至少一张图片 | 是 | +| `status` | 反馈状态 | Enum | PENDING_REVIEW, AI_REJECTED, PROCESSED, CLOSED | 是 | +| `submitterId` | 提交者ID | Long | 外键关联UserAccount | 是 | +| `reviewerId` | 审核者ID | Long | 外键关联UserAccount | 否 | +| `reviewNote` | 审核备注 | String | 最大长度500 | 否 | +| `createdAt` | 提交时间 | DateTime | ISO 8601格式 | 是 | +| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | +| `processedAt` | 处理时间 | DateTime | ISO 8601格式 | 否 | + +### 4.3 任务 (Task) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 任务唯一标识符 | Long | 主键,自增 | 是 | +| `feedbackId` | 关联的反馈ID | Long | 外键关联Feedback | 否 | +| `title` | 任务标题 | String | 最大长度100 | 是 | +| `description` | 任务描述 | String | 最大长度1000 | 是 | +| `assigneeId` | 被指派的网格员ID | Long | 外键关联UserAccount | 否 | +| `createdBy` | 创建者ID | Long | 外键关联UserAccount | 是 | +| `status` | 任务状态 | Enum | CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED | 是 | +| `priority` | 优先级 | Enum | LOW, MEDIUM, HIGH, URGENT | 是 | +| `longitude` | 任务地点经度 | Double | 有效范围 | 是 | +| `latitude` | 任务地点纬度 | Double | 有效范围 | 是 | +| `deadline` | 截止日期 | DateTime | ISO 8601格式 | 否 | +| `completionReport` | 完成报告 | String | 最大长度2000 | 否 | +| `resultImageUrls` | 结果图片URL列表 | Array | 可为空 | 否 | +| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 | +| `assignedAt` | 分配时间 | DateTime | ISO 8601格式 | 否 | +| `startedAt` | 开始时间 | DateTime | ISO 8601格式 | 否 | +| `submittedAt` | 提交时间 | DateTime | ISO 8601格式 | 否 | +| `approvedAt` | 审核通过时间 | DateTime | ISO 8601格式 | 否 | +| `rejectionReason` | 拒绝原因 | String | 最大长度500 | 否 | + +### 4.4 网格 (Grid) 数据结构 + +| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 | +| :--- | :--- | :--- | :--- | :--- | +| `id` | 网格唯一标识符 | Long | 主键,自增 | 是 | +| `gridX` | 网格X坐标 | Integer | 非负整数 | 是 | +| `gridY` | 网格Y坐标 | Integer | 非负整数 | 是 | +| `cityName` | 所属城市 | String | 最大长度50 | 是 | +| `districtName` | 所属区县 | String | 最大长度50 | 是 | +| `description` | 网格描述 | String | 最大长度200 | 否 | +| `isObstacle` | 是否为障碍物 | Boolean | true/false | 是 | +| `responsibleUserId` | 负责人ID | Long | 外键关联UserAccount | 否 | +| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 | +| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 | --- - + ### Report/需求规约.md - + --- - + ### Report/要求.md - -1\. 能够按照面向对象的思想对系统进行设计。使用UML类图对系统整体建模,抽取出系统中的类、属性、方法、关联,设计合理,符合系统要求;能够应用文件设计该系统的持久化存储结构和机制,存储结构设计合理;界面设计美观实用,符合用户习惯; - -(满足毕业要求3.1:掌握软件生命周期要素,了解软件开发过程管理模型,熟悉软件需求分析、设计、实现、测试、维护以及过程与管理的方法和技术) - -2\. 能够在集成开发环境中使用Java语言实现设计的系统,程序结构清晰,代码书写规范、简洁,能够对实现的系统进行调试、测试和评价,保证实现的系统功能完善,运行稳定; - -(满足毕业要求3.3:能够设计满足特定功能需求与性能需求的解决方案,并体现创新意识) - -3\. 深入理解面向对象的设计思想,掌握面向对象的设计方法,能够应用已有的框架、设计模式解决相应的问题。在解决问题的过程中有自己独到的见解,并能够有所创新; - -(满足毕业要求4.2:能够理解系统软件的设计思路和基本原理,掌握应用软件技术、科学方法,具备创新性地解决软件工程具体问题的能力) - -4\. 能够自学Java语言中关于图形用户界面的知识,并能为开发的系统构造图形用户界面。能够通过网络、课堂、书籍等多处获取所需的知识,并应用这些知识解决相应的问题; - -(满足毕业要求5.1:能够利用图书馆和互联网进行文献检索和资料查询,掌握获取技术、资源、现代工程工具和信息技术工具的能力;) - -5\. 能够将设计、实现和测试过程总结形成实践报告,报告格式规范,报告内容充实、正确,报告叙述逻辑严密,可准确反映出设计和实现的结果,实践过程中出现的问题和解决方案,以及独立分析问题和解决问题的能力。 - -(满足毕业要求10.1:具备一定的社交技能和技巧,能够就与本专业相关的当前热点问题发表自己的观点,能够以口头、文稿、图表等方式与业界同行进行技术交流与沟通,能使用通俗易懂的语言与社会公众进行表达与沟通) - -本次实践的项目系统是《东软环保应急》的其中子模块《东软环保公众监督平台》。该系统用于建立环保公众监督平台,拓宽监督渠道,增加环保工作透明度,不断完善公众监督机制,切实增强环境保护实效。 - -主要考查线性结构(数组,链表,队列)、树、查找结构以及相关算法的设计与实现。 - -整体要求 - -(1)所有数据以文件格式保存,文件存储在工程目录中。 - -(2)文件数据格式可以是JSON格式,也可以是以对象序列化的方式存储。 - - - - - - - - + +1\. 能够按照面向对象的思想对系统进行设计。使用UML类图对系统整体建模,抽取出系统中的类、属性、方法、关联,设计合理,符合系统要求;能够应用文件设计该系统的持久化存储结构和机制,存储结构设计合理;界面设计美观实用,符合用户习惯; + +(满足毕业要求3.1:掌握软件生命周期要素,了解软件开发过程管理模型,熟悉软件需求分析、设计、实现、测试、维护以及过程与管理的方法和技术) + +2\. 能够在集成开发环境中使用Java语言实现设计的系统,程序结构清晰,代码书写规范、简洁,能够对实现的系统进行调试、测试和评价,保证实现的系统功能完善,运行稳定; + +(满足毕业要求3.3:能够设计满足特定功能需求与性能需求的解决方案,并体现创新意识) + +3\. 深入理解面向对象的设计思想,掌握面向对象的设计方法,能够应用已有的框架、设计模式解决相应的问题。在解决问题的过程中有自己独到的见解,并能够有所创新; + +(满足毕业要求4.2:能够理解系统软件的设计思路和基本原理,掌握应用软件技术、科学方法,具备创新性地解决软件工程具体问题的能力) + +4\. 能够自学Java语言中关于图形用户界面的知识,并能为开发的系统构造图形用户界面。能够通过网络、课堂、书籍等多处获取所需的知识,并应用这些知识解决相应的问题; + +(满足毕业要求5.1:能够利用图书馆和互联网进行文献检索和资料查询,掌握获取技术、资源、现代工程工具和信息技术工具的能力;) + +5\. 能够将设计、实现和测试过程总结形成实践报告,报告格式规范,报告内容充实、正确,报告叙述逻辑严密,可准确反映出设计和实现的结果,实践过程中出现的问题和解决方案,以及独立分析问题和解决问题的能力。 + +(满足毕业要求10.1:具备一定的社交技能和技巧,能够就与本专业相关的当前热点问题发表自己的观点,能够以口头、文稿、图表等方式与业界同行进行技术交流与沟通,能使用通俗易懂的语言与社会公众进行表达与沟通) + +本次实践的项目系统是《东软环保应急》的其中子模块《东软环保公众监督平台》。该系统用于建立环保公众监督平台,拓宽监督渠道,增加环保工作透明度,不断完善公众监督机制,切实增强环境保护实效。 + +主要考查线性结构(数组,链表,队列)、树、查找结构以及相关算法的设计与实现。 + +整体要求 + +(1)所有数据以文件格式保存,文件存储在工程目录中。 + +(2)文件数据格式可以是JSON格式,也可以是以对象序列化的方式存储。 + + + + + + + + --- - + ### Report/EMS_Backend_Technical_Diagrams.md - -# EMS后端系统技术图表文档 - -## 项目概述 - -环境管理系统(EMS)后端是一个基于Spring Boot的Java应用程序,用于处理环境问题反馈、任务分配和用户管理。系统采用分层架构,包含控制器、服务、仓库和模型层。 - -## 1. 系统时序图 -- **见时序图.md** -## 2. UML类图 -- **见UML类图.md** -## 3. ER图 -- **见ER图.md** - - -## 4. 系统架构说明 - -### 4.1 分层架构 - -- **控制器层(Controller)**: 处理HTTP请求,参数验证,响应格式化 -- **服务层(Service)**: 业务逻辑处理,事务管理 -- **仓库层(Repository)**: 数据访问,数据库操作 -- **模型层(Model)**: 实体定义,数据结构 - -### 4.2 核心业务流程 - -1. **用户认证**: JWT令牌生成和验证 -2. **反馈管理**: 环境问题反馈的提交、审核、处理 -3. **任务分配**: 基于反馈创建任务并分配给网格工作人员 -4. **状态跟踪**: 反馈和任务状态的生命周期管理 - -### 4.3 **安全机制** - -- 基于角色的访问控制(RBAC) -- JWT令牌认证 -- 密码加密存储 -- 操作日志记录 - -### 4.4 数据完整性 - -- 外键约束确保数据一致性 -- 唯一约束防止重复数据 -- 枚举类型确保状态值有效性 -- 时间戳记录数据变更历史 - -## 5. 技术栈 - -- **框架**: Spring Boot 3.x -- **数据库**: MySQL/PostgreSQL -- **ORM**: Spring Data JPA + Hibernate -- **安全**: Spring Security + JWT -- **文档**: Swagger/OpenAPI 3 -- **构建工具**: Maven -- **Java版本**: JDK 17+ - ---- - -*本文档基于EMS后端项目代码分析生成,包含完整的系统设计图表,可用于系统理解、开发指导和文档维护。* + +# EMS后端系统技术图表文档 + +## 项目概述 + +环境管理系统(EMS)后端是一个基于Spring Boot的Java应用程序,用于处理环境问题反馈、任务分配和用户管理。系统采用分层架构,包含控制器、服务、仓库和模型层。 + +## 1. 系统时序图 +- **见时序图.md** +## 2. UML类图 +- **见UML类图.md** +## 3. ER图 +- **见ER图.md** + + +## 4. 系统架构说明 + +### 4.1 分层架构 + +- **控制器层(Controller)**: 处理HTTP请求,参数验证,响应格式化 +- **服务层(Service)**: 业务逻辑处理,事务管理 +- **仓库层(Repository)**: 数据访问,数据库操作 +- **模型层(Model)**: 实体定义,数据结构 + +### 4.2 核心业务流程 + +1. **用户认证**: JWT令牌生成和验证 +2. **反馈管理**: 环境问题反馈的提交、审核、处理 +3. **任务分配**: 基于反馈创建任务并分配给网格工作人员 +4. **状态跟踪**: 反馈和任务状态的生命周期管理 + +### 4.3 **安全机制** + +- 基于角色的访问控制(RBAC) +- JWT令牌认证 +- 密码加密存储 +- 操作日志记录 + +### 4.4 数据完整性 + +- 外键约束确保数据一致性 +- 唯一约束防止重复数据 +- 枚举类型确保状态值有效性 +- 时间戳记录数据变更历史 + +## 5. 技术栈 + +- **框架**: Spring Boot 3.x +- **数据库**: MySQL/PostgreSQL +- **ORM**: Spring Data JPA + Hibernate +- **安全**: Spring Security + JWT +- **文档**: Swagger/OpenAPI 3 +- **构建工具**: Maven +- **Java版本**: JDK 17+ + +--- + +*本文档基于EMS后端项目代码分析生成,包含完整的系统设计图表,可用于系统理解、开发指导和文档维护。* --- - + ### Report/ems-backend-improvements-summary-student-edition.md - -# ems-backend 项目改进建议 (面向学习者) - -`ems-backend`项目当前已具备良好的架构和完整的功能。为了在此基础上进一步深化对核心技术的理解和应用,可以从以下几个方面对项目进行优化与改进。这些建议旨在提供清晰、可行的学习路径,以巩固和扩展现有知识。 - - - -### 1. 邮件通知服务的增强 -- **当前实现**: 邮件服务在主业务流程中同步执行,且邮件内容以字符串形式硬编码在Java代码中。 -- **分析与评估**: 同步发送可能因网络延迟而阻塞API响应,影响用户体验。硬编码的内容使得文案修改需要重新编译和部署整个应用,缺乏灵活性。 -- **改进建议**: - - **实现异步发送**: 将邮件发送操作置于独立的线程中执行。可以利用Java内置的`ExecutorService`线程池来管理这些后台任务,从而使主线程能够立即响应用户请求。 - - **引入模板引擎**: 采用如`Thymeleaf`或`Freemarker`等模板引擎。将邮件内容定义为外部HTML模板文件,Java代码仅负责传递动态数据(如验证码)。这样,内容与逻辑得以分离,修改邮件样式和文案将变得非常方便。 - -### 2. 安全策略的强化 -- **当前实现**: 系统已具备基于JWT的用户认证和角色授权。 -- **分析与评估**: 当前实现未对认证接口的请求频率进行限制,存在被自动化脚本进行暴力破解的风险。 -- **改进建议**: - - **实现登录尝试限制**: 开发一个`LoginAttemptService`,用于追踪特定用户或IP地址在单位时间内的登录失败次数。当失败次数超过预设阈值(例如,5次/分钟),可暂时锁定账户或要求输入验证码,有效防御暴力破解攻击。 - -### 3. 前端数据同步的优化 -- **当前实现**: 前端获取最新数据依赖于用户的手动刷新操作。 -- **分析与评估**: 在任务分配、状态变更等场景下,信息的被动更新会导致用户感知的延迟。 -- **改进建议**: - - **实现客户端轮询**: 在前端关键页面(如任务列表)采用定时轮询机制。通过`setInterval`等JavaScript函数,每隔一个固定时间(如10-30秒)自动向后端请求最新数据并更新视图。这是实现准实时数据同步的最直接、最易于理解的方式。 - -### 4. API接口的健壮性提升 -- **当前实现**: 主要通过全局异常处理器来捕获和响应错误。 -- **分析与评估**: 对于可预期的业务错误(如"用户名已存在"),可以提供更具体、结构化的错误信息,以方便前端进行针对性处理。 -- **改进建议**: - - **设计更具体的错误响应**: 在`GlobalExceptionHandler`中,除了返回HTTP状态码和错误消息外,可以为特定的业务异常定义内部错误码(如`1001`代表"用户已存在")。前端可以根据这个错误码,精确地向用户展示提示信息(如高亮对应的输入框),而不仅仅是弹出一个通用的错误提示。 + +# ems-backend 项目改进建议 (面向学习者) + +`ems-backend`项目当前已具备良好的架构和完整的功能。为了在此基础上进一步深化对核心技术的理解和应用,可以从以下几个方面对项目进行优化与改进。这些建议旨在提供清晰、可行的学习路径,以巩固和扩展现有知识。 + + + +### 1. 邮件通知服务的增强 +- **当前实现**: 邮件服务在主业务流程中同步执行,且邮件内容以字符串形式硬编码在Java代码中。 +- **分析与评估**: 同步发送可能因网络延迟而阻塞API响应,影响用户体验。硬编码的内容使得文案修改需要重新编译和部署整个应用,缺乏灵活性。 +- **改进建议**: + - **实现异步发送**: 将邮件发送操作置于独立的线程中执行。可以利用Java内置的`ExecutorService`线程池来管理这些后台任务,从而使主线程能够立即响应用户请求。 + - **引入模板引擎**: 采用如`Thymeleaf`或`Freemarker`等模板引擎。将邮件内容定义为外部HTML模板文件,Java代码仅负责传递动态数据(如验证码)。这样,内容与逻辑得以分离,修改邮件样式和文案将变得非常方便。 + +### 2. 安全策略的强化 +- **当前实现**: 系统已具备基于JWT的用户认证和角色授权。 +- **分析与评估**: 当前实现未对认证接口的请求频率进行限制,存在被自动化脚本进行暴力破解的风险。 +- **改进建议**: + - **实现登录尝试限制**: 开发一个`LoginAttemptService`,用于追踪特定用户或IP地址在单位时间内的登录失败次数。当失败次数超过预设阈值(例如,5次/分钟),可暂时锁定账户或要求输入验证码,有效防御暴力破解攻击。 + +### 3. 前端数据同步的优化 +- **当前实现**: 前端获取最新数据依赖于用户的手动刷新操作。 +- **分析与评估**: 在任务分配、状态变更等场景下,信息的被动更新会导致用户感知的延迟。 +- **改进建议**: + - **实现客户端轮询**: 在前端关键页面(如任务列表)采用定时轮询机制。通过`setInterval`等JavaScript函数,每隔一个固定时间(如10-30秒)自动向后端请求最新数据并更新视图。这是实现准实时数据同步的最直接、最易于理解的方式。 + +### 4. API接口的健壮性提升 +- **当前实现**: 主要通过全局异常处理器来捕获和响应错误。 +- **分析与评估**: 对于可预期的业务错误(如"用户名已存在"),可以提供更具体、结构化的错误信息,以方便前端进行针对性处理。 +- **改进建议**: + - **设计更具体的错误响应**: 在`GlobalExceptionHandler`中,除了返回HTTP状态码和错误消息外,可以为特定的业务异常定义内部错误码(如`1001`代表"用户已存在")。前端可以根据这个错误码,精确地向用户展示提示信息(如高亮对应的输入框),而不仅仅是弹出一个通用的错误提示。 --- - + ### Report/ems-backend-improvements-summary.md - -# ems-backend 项目不足与可改进之处 - -`ems-backend`项目当前已经构建了一个功能完善、架构清晰的系统。然而,从一个准生产级应用迈向一个高可用、高扩展、可长期演进的企业级平台的道路上,我们可以在以下几个方面进行深化和改进。这些改进点并非对现有设计的否定,而是基于更高标准的技术展望。 - -### 1. 持久化层的演进与扩展 -当前的JSON文件存储方案虽然巧妙地满足了项目初期的约束,但它也是未来系统演进中最先需要被替换的模块。 - -- **当前状态**: 使用JSON文件作为数据库,通过文件锁保证单机并发安全。 -- **潜在瓶颈**: - - **性能限制**: 随着数据量增长,全文件读写和内存中的过滤、排序将变得极慢,无法处理复杂查询(如JOIN、聚合分析)。 - - **功能缺失**: 缺乏数据库事务的ACID保证,无法确保跨多个文件操作的原子性。 - - **扩展性差**: 无法进行水平扩展,是典型的单点瓶颈。 -- **改进方向**: - - **迁移至关系型数据库**: 全面采用`PostgreSQL`或`MySQL`。这将使我们能够利用强大的SQL查询优化器、索引机制、以及成熟的事务管理,从根本上解决性能和数据一致性问题。`Repository`层的接口化设计将使这次迁移变得相对平滑。 - - **引入分布式缓存**: 集成`Redis`,对访问频繁且不常变化的数据(如用户信息、配置信息、地图网格数据)进行缓存。这将极大降低数据库的读取压力,显著提升API响应速度。 - -### 2. 消息驱动架构的工业化升级 -系统内的事件驱动(`ApplicationEventPublisher`)是优秀的单体应用解耦实践,但它无法满足分布式架构的需求。 - -- **当前状态**: 使用Spring内置的事件机制实现应用内异步解耦。 -- **潜在瓶颈**: - - **生命周期绑定**: 事件总线与应用进程绑定,若应用宕机,未处理的事件会丢失。 - - **缺乏高级特性**: 不支持持久化、失败重试、延迟消息、死信队列等工业级消息系统特性。 - - **无法跨服务**: 无法用于未来可能的微服务化改造。 -- **改进方向**: - - **引入专业消息队列(MQ)**: 采用`RabbitMQ`(适用于复杂路由和事务性消息)或`Kafka`(适用于高吞吐量日志和数据流)来替换`ApplicationEventPublisher`。 - - **带来的优势**: 实现真正的服务间异步通信、流量削峰填谷、保证事件的可靠投递,并为未来将通知、AI分析等模块拆分为独立微服务奠定坚实基础。 - -### 3. 安全体系的纵深防御 -当前的安全体系保证了认证和基础授权,但可以构建更深层次的防御。 - -- **当前状态**: JWT认证 + `@PreAuthorize`角色授权。 -- **潜在瓶颈**: - - **缺乏流量防护**: 无法防御针对登录接口的暴力破解或API的滥用攻击。 - - **缺乏敏感操作审计**: 对"谁删除了某个重要任务"这类操作缺乏追溯能力。 -- **改进方向**: - - **实现API限流(Rate Limiting)**: 使用`Resilience4j`或`Guava RateLimiter`等库,对登录、发送验证码等关键API接口配置精细化的访问速率限制,从入口处防止恶意攻击。 - - **实现双因素认证(2FA)**: 对管理员、决策者等高权限角色,在密码认证之外,增加一层基于时间的一次性密码(TOTP,如Google Authenticator)验证,实现银行级别的账户安全。 - - **构建完善的审计日志**: 创建独立的审计日志系统,以AOP切面或事件监听的方式,详细记录所有关键写操作(谁、在何时、从何IP、做了什么、结果如何),确保所有敏感操作都可被追溯和审计。 - -### 4. 前端体验的现代化与实时化 -当前的前后端交互是传统的"请求-响应"模式,可以升级为实时推送模式。 - -- **当前状态**: 前端通过轮询或手动刷新获取最新数据。 -- **潜在瓶颈**: 信息传递有延迟,用户体验不够流畅,尤其是在需要协同工作的场景。 -- **改进方向**: - - **引入WebSocket**: 使用`WebSocket`技术,在前后端之间建立长连接,实现双向实时通信。 - - **应用场景**: 当一个任务的状态被主管更新后,服务器可以立即将最新的状态**主动推送**给正在查看该任务的网格员的前端界面,无需用户手动刷新。这将极大提升系统的即时性和用户的沉浸式体验。 - -### 5. 运维与部署的自动化 (DevOps) -这是从"能用"到"好用、可靠"的关键一步。 - -- **当前状态**: 项目需要开发者手动执行`mvn package`,然后通过FTP或SSH将jar包上传到服务器并手动运行。 -- **潜在瓶颈**: 部署流程繁琐、耗时、极易因人工操作而出错,无法做到快速迭代和持续交付。 -- **改进方向**: - - **容器化**: 使用`Docker`将`ems-backend`应用及其所有依赖(如特定版本的Java环境)打包成一个标准化的、可移植的容器镜像。 - - **引入CI/CD (持续集成/持续部署)**: 搭建`GitHub Actions`或`Jenkins`自动化流水线。当代码被推送到主分支后,流水线会自动执行:①编译代码 -> ②运行所有单元测试 -> ③构建Docker镜像 -> ④将镜像推送到镜像仓库 -> ⑤(可选)自动触发部署脚本,更新服务器上的应用。这将实现从代码提交到上线的全流程自动化。 - -### 6. 通知服务的模板化与多渠道扩展 -当前的邮件服务功能正确,但可维护性和扩展性可以进一步提升。 - -- **当前状态**: 邮件HTML内容在Java代码中硬编码,发送过程是同步的。 -- **潜在瓶颈**: - - **难以维护**: 每次修改邮件文案都需要修改Java代码并重新部署。 - - **性能风险**: 同步发送邮件会阻塞主线程,如果邮件服务器响应慢,将拖慢API的整体响应时间。 -- **改进方向**: - - **引入模板引擎**: 使用`Thymeleaf`或`Freemarker`将邮件内容定义为独立的HTML模板文件。Java代码只负责传递动态数据(如验证码、用户名),由模板引擎渲染最终的HTML,实现内容与逻辑的分离。 - - **异步化发送**: 将`sendHtmlEmail`方法标记为`@Async`,使其在独立的线程池中执行,实现"发布即返回",主业务流程不再等待邮件发送完成。 - - **多渠道抽象**: 将`MailService`抽象为更通用的`NotificationService`,并提供`EmailNotificationProvider`、`SmsNotificationProvider`等多种实现,未来可以根据用户偏好或业务场景,灵活选择通知渠道。 + +# ems-backend 项目不足与可改进之处 + +`ems-backend`项目当前已经构建了一个功能完善、架构清晰的系统。然而,从一个准生产级应用迈向一个高可用、高扩展、可长期演进的企业级平台的道路上,我们可以在以下几个方面进行深化和改进。这些改进点并非对现有设计的否定,而是基于更高标准的技术展望。 + +### 1. 持久化层的演进与扩展 +当前的JSON文件存储方案虽然巧妙地满足了项目初期的约束,但它也是未来系统演进中最先需要被替换的模块。 + +- **当前状态**: 使用JSON文件作为数据库,通过文件锁保证单机并发安全。 +- **潜在瓶颈**: + - **性能限制**: 随着数据量增长,全文件读写和内存中的过滤、排序将变得极慢,无法处理复杂查询(如JOIN、聚合分析)。 + - **功能缺失**: 缺乏数据库事务的ACID保证,无法确保跨多个文件操作的原子性。 + - **扩展性差**: 无法进行水平扩展,是典型的单点瓶颈。 +- **改进方向**: + - **迁移至关系型数据库**: 全面采用`PostgreSQL`或`MySQL`。这将使我们能够利用强大的SQL查询优化器、索引机制、以及成熟的事务管理,从根本上解决性能和数据一致性问题。`Repository`层的接口化设计将使这次迁移变得相对平滑。 + - **引入分布式缓存**: 集成`Redis`,对访问频繁且不常变化的数据(如用户信息、配置信息、地图网格数据)进行缓存。这将极大降低数据库的读取压力,显著提升API响应速度。 + +### 2. 消息驱动架构的工业化升级 +系统内的事件驱动(`ApplicationEventPublisher`)是优秀的单体应用解耦实践,但它无法满足分布式架构的需求。 + +- **当前状态**: 使用Spring内置的事件机制实现应用内异步解耦。 +- **潜在瓶颈**: + - **生命周期绑定**: 事件总线与应用进程绑定,若应用宕机,未处理的事件会丢失。 + - **缺乏高级特性**: 不支持持久化、失败重试、延迟消息、死信队列等工业级消息系统特性。 + - **无法跨服务**: 无法用于未来可能的微服务化改造。 +- **改进方向**: + - **引入专业消息队列(MQ)**: 采用`RabbitMQ`(适用于复杂路由和事务性消息)或`Kafka`(适用于高吞吐量日志和数据流)来替换`ApplicationEventPublisher`。 + - **带来的优势**: 实现真正的服务间异步通信、流量削峰填谷、保证事件的可靠投递,并为未来将通知、AI分析等模块拆分为独立微服务奠定坚实基础。 + +### 3. 安全体系的纵深防御 +当前的安全体系保证了认证和基础授权,但可以构建更深层次的防御。 + +- **当前状态**: JWT认证 + `@PreAuthorize`角色授权。 +- **潜在瓶颈**: + - **缺乏流量防护**: 无法防御针对登录接口的暴力破解或API的滥用攻击。 + - **缺乏敏感操作审计**: 对"谁删除了某个重要任务"这类操作缺乏追溯能力。 +- **改进方向**: + - **实现API限流(Rate Limiting)**: 使用`Resilience4j`或`Guava RateLimiter`等库,对登录、发送验证码等关键API接口配置精细化的访问速率限制,从入口处防止恶意攻击。 + - **实现双因素认证(2FA)**: 对管理员、决策者等高权限角色,在密码认证之外,增加一层基于时间的一次性密码(TOTP,如Google Authenticator)验证,实现银行级别的账户安全。 + - **构建完善的审计日志**: 创建独立的审计日志系统,以AOP切面或事件监听的方式,详细记录所有关键写操作(谁、在何时、从何IP、做了什么、结果如何),确保所有敏感操作都可被追溯和审计。 + +### 4. 前端体验的现代化与实时化 +当前的前后端交互是传统的"请求-响应"模式,可以升级为实时推送模式。 + +- **当前状态**: 前端通过轮询或手动刷新获取最新数据。 +- **潜在瓶颈**: 信息传递有延迟,用户体验不够流畅,尤其是在需要协同工作的场景。 +- **改进方向**: + - **引入WebSocket**: 使用`WebSocket`技术,在前后端之间建立长连接,实现双向实时通信。 + - **应用场景**: 当一个任务的状态被主管更新后,服务器可以立即将最新的状态**主动推送**给正在查看该任务的网格员的前端界面,无需用户手动刷新。这将极大提升系统的即时性和用户的沉浸式体验。 + +### 5. 运维与部署的自动化 (DevOps) +这是从"能用"到"好用、可靠"的关键一步。 + +- **当前状态**: 项目需要开发者手动执行`mvn package`,然后通过FTP或SSH将jar包上传到服务器并手动运行。 +- **潜在瓶颈**: 部署流程繁琐、耗时、极易因人工操作而出错,无法做到快速迭代和持续交付。 +- **改进方向**: + - **容器化**: 使用`Docker`将`ems-backend`应用及其所有依赖(如特定版本的Java环境)打包成一个标准化的、可移植的容器镜像。 + - **引入CI/CD (持续集成/持续部署)**: 搭建`GitHub Actions`或`Jenkins`自动化流水线。当代码被推送到主分支后,流水线会自动执行:①编译代码 -> ②运行所有单元测试 -> ③构建Docker镜像 -> ④将镜像推送到镜像仓库 -> ⑤(可选)自动触发部署脚本,更新服务器上的应用。这将实现从代码提交到上线的全流程自动化。 + +### 6. 通知服务的模板化与多渠道扩展 +当前的邮件服务功能正确,但可维护性和扩展性可以进一步提升。 + +- **当前状态**: 邮件HTML内容在Java代码中硬编码,发送过程是同步的。 +- **潜在瓶颈**: + - **难以维护**: 每次修改邮件文案都需要修改Java代码并重新部署。 + - **性能风险**: 同步发送邮件会阻塞主线程,如果邮件服务器响应慢,将拖慢API的整体响应时间。 +- **改进方向**: + - **引入模板引擎**: 使用`Thymeleaf`或`Freemarker`将邮件内容定义为独立的HTML模板文件。Java代码只负责传递动态数据(如验证码、用户名),由模板引擎渲染最终的HTML,实现内容与逻辑的分离。 + - **异步化发送**: 将`sendHtmlEmail`方法标记为`@Async`,使其在独立的线程池中执行,实现"发布即返回",主业务流程不再等待邮件发送完成。 + - **多渠道抽象**: 将`MailService`抽象为更通用的`NotificationService`,并提供`EmailNotificationProvider`、`SmsNotificationProvider`等多种实现,未来可以根据用户偏好或业务场景,灵活选择通知渠道。 --- - + ### Report/ems-backend-innovation-summary.md - -# ems-backend 项目核心创新点总览 - -基于对 `ems-backend` 项目源代码的深入分析,该系统在设计与实现上体现了现代化、企业级的工程实践。其核心创新点可总结为以下八个方面: - -### 1. 模拟JPA的泛型JSON仓储层 -在不引入传统数据库的约束下,项目极具创造性地构建了一套数据持久化框架。 -- **泛型设计与类型安全**: 底层的 `JsonStorageService` 通过泛型``和Jackson的`TypeReference`,实现了对任意实体列表的类型安全读写。 -- **精细化并发控制**: 没有采用简单的全局方法锁,而是通过 `ConcurrentHashMap` 为每个JSON文件分配了独立的`ReentrantLock`,实现了文件级别的并发控制,显著提升了在多文件写入场景下的性能。 -- **模拟JPA接口**: `Repository`层定义了与Spring Data JPA高度相似的接口(如`save`, `findById`, `findAll`),使得上层业务代码可以面向接口编程,与底层的文件存储实现完全解耦,未来可平滑迁移至真实数据库。 - -### 2. 事件驱动的异步化业务流程 -系统广泛采用事件驱动架构(EDA)来解耦模块、提升系统响应能力。 -- **异步化核心流程**: 将反馈提交后的AI审核、任务创建成功后的智能分配等耗时或非核心操作,通过发布Spring事件(如`FeedbackSubmittedForAiReviewEvent`)的方式进行异步处理,极大缩短了API的响应时间。 -- **业务模块高度解耦**: 核心服务(如`FeedbackService`)与下游模块(如`TaskService`, `MailService`)无直接代码依赖。新增业务监听器(如"短信通知")无需修改任何现有业务代码,完美遵循"开闭原则",提高了系统的可维护性和可扩展性。 - -### 3. 融合多种策略的智能算法 -在关键业务场景中,系统应用了智能算法取代了僵硬的业务规则,提升了运营效率和决策科学性。 -- **高效的A*寻路算法**: 为网格员规划路径时,不仅实现了A*算法,还结合了**优先队列(PriorityQueue)**进行性能优化,并能**动态加载地图和障碍物信息**,具有很强的适应性。 -- **多因素加权的任务推荐算法**: 在分配任务时,系统通过一个异步触发的智能分配服务,综合考虑网格员的**地理距离、当前负载、技能匹配度、历史表现**等多个维度进行加权评分,为任务自动推荐最合适的执行者。 - -### 4. 声明式、细粒度的安全权限控制 -项目的安全体系并非简单的身份认证,而是构建了一套声明式、与业务逻辑分离的权限控制体系。 -- **自定义JWT安全过滤链**: 通过自定义的`JwtAuthenticationFilter`和`SecurityConfig`配置,精确控制了JWT的解析、校验及用户权限加载流程。 -- **声明式方法级权限**: 在Controller层广泛使用`@PreAuthorize`注解,将权限规则(如`hasRole('ADMIN')`)从业务逻辑中抽离,使安全策略一目了然,易于审计和维护。 - -### 5. 主动、可定制的业务规则校验体系 -系统对所有外部输入都采取"主动防御"姿态,构建了前置的、可扩展的校验体系。 -- **分层校验**: 同时使用JSR 303标准注解(如`@NotNull`, `@Email`)和`@Valid`进行基础校验。 -- **自定义业务校验**: 针对复杂业务规则(如密码强度),创建了自定义校验注解(如`@ValidPassword`)及其实现(`PasswordValidator`)。这种方式将复杂的校验逻辑封装成一个可复用的简单注解,极为优雅和高效。 - -### 6. 统一、健壮的API设计与响应体系 -项目的API设计构建了一套完整的、面向开发者的友好体系。 -- **全局统一异常处理**: 通过`@ControllerAdvice`和`@ExceptionHandler`,将所有异常(业务异常、系统异常)集中捕获,并返回结构统一的`ErrorResponseDTO`,极大地简化了前端的错误处理,并避免了向客户端暴露敏感的系统内部信息。 -- **面向视图的DTO模式**: 严格区分领域模型(Entity)和数据传输对象(DTO),并为不同场景(如列表摘要`TaskSummaryDTO` vs. 详情`TaskDetailDTO`)提供专属DTO。这确保了API契约的稳定,优化了网络传输性能,并从根本上杜绝了敏感字段的泄露。 - -### 7. 安全、分层的媒体文件存储服务 -项目实现了一个安全、分层的媒体文件存储服务,以处理用户上传的现场证据。 -- **安全优先**: 在存储文件前,通过**生成唯一文件名**防止冲突和覆盖,通过**校验文件类型**防止恶意脚本上传,并通过**规范化路径**防止目录遍历攻击。 -- **逻辑与物理分离**: 服务返回的是内部文件名而非直接URL,业务层将此文件名与业务实体关联。访问时需通过专门的Controller根据内部文件名加载文件流。此分层架构不仅增强了安全,也便于未来将底层存储平滑迁移至云存储(如OSS)。 - -### 8. 与核心业务解耦的可扩展通知服务 -项目构建了一个与核心业务逻辑完全解耦的通知服务体系。 -- **事件驱动触发**: 邮件发送(如注册验证码)由业务事件(如`UserRegisteredEvent`)触发,由独立的监听器负责调用邮件服务,实现了业务流程与通知功能的分离。 -- **面向接口设计**: 通过定义`MailService`接口,使系统不依赖于具体的邮件发送实现。这为未来切换邮件服务商,或在测试环境中提供一个模拟实现(Mock)提供了极大的灵活性,无需改动任何业务代码。 + +# ems-backend 项目核心创新点总览 + +基于对 `ems-backend` 项目源代码的深入分析,该系统在设计与实现上体现了现代化、企业级的工程实践。其核心创新点可总结为以下八个方面: + +### 1. 模拟JPA的泛型JSON仓储层 +在不引入传统数据库的约束下,项目极具创造性地构建了一套数据持久化框架。 +- **泛型设计与类型安全**: 底层的 `JsonStorageService` 通过泛型``和Jackson的`TypeReference`,实现了对任意实体列表的类型安全读写。 +- **精细化并发控制**: 没有采用简单的全局方法锁,而是通过 `ConcurrentHashMap` 为每个JSON文件分配了独立的`ReentrantLock`,实现了文件级别的并发控制,显著提升了在多文件写入场景下的性能。 +- **模拟JPA接口**: `Repository`层定义了与Spring Data JPA高度相似的接口(如`save`, `findById`, `findAll`),使得上层业务代码可以面向接口编程,与底层的文件存储实现完全解耦,未来可平滑迁移至真实数据库。 + +### 2. 事件驱动的异步化业务流程 +系统广泛采用事件驱动架构(EDA)来解耦模块、提升系统响应能力。 +- **异步化核心流程**: 将反馈提交后的AI审核、任务创建成功后的智能分配等耗时或非核心操作,通过发布Spring事件(如`FeedbackSubmittedForAiReviewEvent`)的方式进行异步处理,极大缩短了API的响应时间。 +- **业务模块高度解耦**: 核心服务(如`FeedbackService`)与下游模块(如`TaskService`, `MailService`)无直接代码依赖。新增业务监听器(如"短信通知")无需修改任何现有业务代码,完美遵循"开闭原则",提高了系统的可维护性和可扩展性。 + +### 3. 融合多种策略的智能算法 +在关键业务场景中,系统应用了智能算法取代了僵硬的业务规则,提升了运营效率和决策科学性。 +- **高效的A*寻路算法**: 为网格员规划路径时,不仅实现了A*算法,还结合了**优先队列(PriorityQueue)**进行性能优化,并能**动态加载地图和障碍物信息**,具有很强的适应性。 +- **多因素加权的任务推荐算法**: 在分配任务时,系统通过一个异步触发的智能分配服务,综合考虑网格员的**地理距离、当前负载、技能匹配度、历史表现**等多个维度进行加权评分,为任务自动推荐最合适的执行者。 + +### 4. 声明式、细粒度的安全权限控制 +项目的安全体系并非简单的身份认证,而是构建了一套声明式、与业务逻辑分离的权限控制体系。 +- **自定义JWT安全过滤链**: 通过自定义的`JwtAuthenticationFilter`和`SecurityConfig`配置,精确控制了JWT的解析、校验及用户权限加载流程。 +- **声明式方法级权限**: 在Controller层广泛使用`@PreAuthorize`注解,将权限规则(如`hasRole('ADMIN')`)从业务逻辑中抽离,使安全策略一目了然,易于审计和维护。 + +### 5. 主动、可定制的业务规则校验体系 +系统对所有外部输入都采取"主动防御"姿态,构建了前置的、可扩展的校验体系。 +- **分层校验**: 同时使用JSR 303标准注解(如`@NotNull`, `@Email`)和`@Valid`进行基础校验。 +- **自定义业务校验**: 针对复杂业务规则(如密码强度),创建了自定义校验注解(如`@ValidPassword`)及其实现(`PasswordValidator`)。这种方式将复杂的校验逻辑封装成一个可复用的简单注解,极为优雅和高效。 + +### 6. 统一、健壮的API设计与响应体系 +项目的API设计构建了一套完整的、面向开发者的友好体系。 +- **全局统一异常处理**: 通过`@ControllerAdvice`和`@ExceptionHandler`,将所有异常(业务异常、系统异常)集中捕获,并返回结构统一的`ErrorResponseDTO`,极大地简化了前端的错误处理,并避免了向客户端暴露敏感的系统内部信息。 +- **面向视图的DTO模式**: 严格区分领域模型(Entity)和数据传输对象(DTO),并为不同场景(如列表摘要`TaskSummaryDTO` vs. 详情`TaskDetailDTO`)提供专属DTO。这确保了API契约的稳定,优化了网络传输性能,并从根本上杜绝了敏感字段的泄露。 + +### 7. 安全、分层的媒体文件存储服务 +项目实现了一个安全、分层的媒体文件存储服务,以处理用户上传的现场证据。 +- **安全优先**: 在存储文件前,通过**生成唯一文件名**防止冲突和覆盖,通过**校验文件类型**防止恶意脚本上传,并通过**规范化路径**防止目录遍历攻击。 +- **逻辑与物理分离**: 服务返回的是内部文件名而非直接URL,业务层将此文件名与业务实体关联。访问时需通过专门的Controller根据内部文件名加载文件流。此分层架构不仅增强了安全,也便于未来将底层存储平滑迁移至云存储(如OSS)。 + +### 8. 与核心业务解耦的可扩展通知服务 +项目构建了一个与核心业务逻辑完全解耦的通知服务体系。 +- **事件驱动触发**: 邮件发送(如注册验证码)由业务事件(如`UserRegisteredEvent`)触发,由独立的监听器负责调用邮件服务,实现了业务流程与通知功能的分离。 +- **面向接口设计**: 通过定义`MailService`接口,使系统不依赖于具体的邮件发送实现。这为未来切换邮件服务商,或在测试环境中提供一个模拟实现(Mock)提供了极大的灵活性,无需改动任何业务代码。 --- - + ### Report/ER图.md - -## 3. ER图 - -```mermaid -erDiagram - USER_ACCOUNT { - BIGINT id PK - VARCHAR username - VARCHAR password - VARCHAR email - VARCHAR phone - BIGINT role_id FK - BIGINT grid_id FK - } - - ROLE { - BIGINT id PK - VARCHAR name - } - - FEEDBACK { - BIGINT id PK - TEXT content - VARCHAR location - VARCHAR status - BIGINT submitter_id FK - } - - TASK { - BIGINT id PK - TEXT description - VARCHAR status - BIGINT feedback_id FK - BIGINT assignee_id FK - } - - GRID { - BIGINT id PK - INT grid_x - INT grid_y - } - - OPERATION_LOG { - BIGINT id PK - VARCHAR operation_type - TEXT details - BIGINT operator_id FK - } - - ATTACHMENT { - BIGINT id PK - BIGINT feedback_id FK - VARCHAR file_url - } - - USER_ACCOUNT ||--o{ ROLE : "has" - USER_ACCOUNT ||--o{ FEEDBACK : "submits" - USER_ACCOUNT ||--o{ TASK : "assigned" - USER_ACCOUNT ||--o{ GRID : "belongs to" - FEEDBACK ||--o{ TASK : "leads to" - USER_ACCOUNT ||--o{ OPERATION_LOG : "performs" - FEEDBACK ||--o{ ATTACHMENT : "has" -``` - participant Database as 数据库 - - Admin->>+OperationLogController: GET /api/logs (过滤条件) - OperationLogController->>+OperationLogService: queryOperationLogs(filters) - OperationLogService->>+OperationLogRepository: findByCriteria(filters) - OperationLogRepository->>+Database: SELECT * FROM operation_logs WHERE ... - Database-->>-OperationLogRepository: 返回日志记录 - OperationLogRepository-->>-OperationLogService: 返回日志DTO列表 - OperationLogService-->>-OperationLogController: 返回日志DTO列表 - OperationLogController-->>-Admin: 200 OK (日志列表) -``` - -### 1.16 管理员创建新用户 - -```mermaid -sequenceDiagram - participant Admin as 管理员 - participant PersonnelController as 人员控制器 - participant PersonnelService as 人员服务 - participant UserAccountRepository as 用户账户仓库 - participant PasswordEncoder as 密码编码器 - participant Database as 数据库 - - Admin->>+PersonnelController: POST /api/personnel/users (用户信息) - PersonnelController->>+PersonnelService: createUser(request) - PersonnelService->>+UserAccountRepository: findByUsername(username) - UserAccountRepository-->>-PersonnelService: (检查用户是否存在) - PersonnelService->>+PasswordEncoder: encode(password) - PasswordEncoder-->>-PersonnelService: 返回加密后的密码 - PersonnelService->>+UserAccountRepository: save(user) - UserAccountRepository->>+Database: INSERT INTO user_accounts - Database-->>-UserAccountRepository: 返回已保存的用户 - UserAccountRepository-->>-PersonnelService: 返回已保存的用户 - PersonnelService-->>-PersonnelController: 返回创建的用户 - PersonnelController-->>-Admin: 201 CREATED (用户详情) -``` - -### 1.17 用户获取自己的反馈历史 - -```mermaid -sequenceDiagram - participant User as 用户 - participant ProfileController as 个人资料控制器 - participant UserFeedbackService as 用户反馈服务 - participant CustomUserDetails as 用户认证详情 - participant Database as 数据库 - - User->>+ProfileController: GET /api/me/feedback - ProfileController->>+CustomUserDetails: 获取用户ID - CustomUserDetails-->>-ProfileController: 返回用户ID - ProfileController->>+UserFeedbackService: getFeedbackHistoryByUserId(userId, pageable) - UserFeedbackService->>+Database: SELECT * FROM feedback WHERE user_id = ? - Database-->>-UserFeedbackService: 返回反馈记录 - UserFeedbackService-->>-ProfileController: 返回反馈摘要DTO列表 - ProfileController-->>-User: 200 OK (反馈历史列表) -``` - -### 1.18 公众提交反馈 - -```mermaid -sequenceDiagram - participant Public as 公众 - participant PublicController as 公共控制器 - participant FeedbackService as 反馈服务 - participant FileStorageService as 文件存储服务 - participant FeedbackRepository as 反馈仓库 - participant Database as 数据库 - - Public->>+PublicController: POST /api/public/feedback (反馈信息和文件) - PublicController->>+FeedbackService: createPublicFeedback(request, files) - alt 如果有文件 - FeedbackService->>+FileStorageService: store(file) - FileStorageService-->>-FeedbackService: 返回文件路径 - end - FeedbackService->>+FeedbackRepository: save(feedback) - FeedbackRepository->>+Database: INSERT INTO feedback - Database-->>-FeedbackRepository: 返回已保存的反馈 - FeedbackRepository-->>-FeedbackService: 返回已保存的反馈 - FeedbackService-->>-PublicController: 返回创建的反馈 - PublicController-->>-Public: 201 CREATED (反馈详情) -``` - -### 1.19 分配任务给工作人员 - -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant TaskAssignmentController as 任务分配控制器 - participant TaskAssignmentService as 任务分配服务 - participant AssignmentRepository as 分配仓库 - participant FeedbackRepository as 反馈仓库 - participant Database as 数据库 - - Supervisor->>+TaskAssignmentController: POST /api/tasks/assign (任务ID, 分配对象ID) - TaskAssignmentController->>+TaskAssignmentService: assignTask(feedbackId, assigneeId, assignerId) - TaskAssignmentService->>+FeedbackRepository: findById(feedbackId) - FeedbackRepository-->>-TaskAssignmentService: 返回任务详情 - TaskAssignmentService->>+AssignmentRepository: save(assignment) - AssignmentRepository->>+Database: INSERT INTO assignments - Database-->>-AssignmentRepository: 返回已保存的分配 - AssignmentRepository-->>-TaskAssignmentService: 返回已保存的分配 - TaskAssignmentService-->>-TaskAssignmentController: 返回新的分配 - TaskAssignmentController-->>-Supervisor: 200 OK (分配详情) -``` - -### 1.20 从反馈创建任务 - -```mermaid -sequenceDiagram - participant Supervisor as 主管 - participant TaskManagementController as 任务管理控制器 - participant TaskManagementService as 任务管理服务 - participant FeedbackRepository as 反馈仓库 - participant TaskRepository as 任务仓库 - participant Database as 数据库 - - Supervisor->>+TaskManagementController: POST /api/management/tasks/feedback/{feedbackId}/create-task (任务信息) - TaskManagementController->>+TaskManagementService: createTaskFromFeedback(feedbackId, request) - TaskManagementService->>+FeedbackRepository: findById(feedbackId) - FeedbackRepository-->>-TaskManagementService: 返回反馈详情 - TaskManagementService->>+TaskRepository: save(task) - TaskRepository->>+Database: INSERT INTO tasks - Database-->>-TaskRepository: 返回已保存的任务 - TaskRepository-->>-TaskManagementService: 返回已保存的任务 - TaskManagementService-->>-TaskManagementController: 返回创建的任务详情DTO - TaskManagementController-->>-Supervisor: 201 CREATED (任务详情) -``` - -## 2. UML类图 - -### 2.1 核心领域模型类图 - -```mermaid -classDiagram - class UserAccount { - -Long id - -String name - -String phone - -String email - -String password - -Gender gender - -Role role - -UserStatus status - -Level level - -String department - -List~String~ skills - -LocalDateTime createdAt - -LocalDateTime updatedAt - +login() boolean - +updateProfile() void - +changePassword() void - } - - class Feedback { - -Long id - -String eventId - -String title - -String description - -PollutionType pollutionType - -SeverityLevel severityLevel - -FeedbackStatus status - -String location - -Double latitude - -Double longitude - -String cityName - -String districtName - -UserAccount submitter - -LocalDateTime createdAt - -LocalDateTime updatedAt - +updateStatus() void - +assignToTask() Task - } - - class Task { - -Long id - -Feedback feedback - -UserAccount assignee - -UserAccount createdBy - -TaskStatus status - -String title - -String description - -String location - -LocalDateTime assignedAt - -LocalDateTime completedAt - -LocalDateTime createdAt - +assign(UserAccount) void - +complete() void - +updateProgress() void - } - - class Assignment { - -Long id - -Task task - -UserAccount assigner - -LocalDateTime assignmentTime - -LocalDateTime deadline - -AssignmentStatus status - -String remarks - +setDeadline() void - +updateStatus() void - } - - class Grid { - -Long id - -Integer gridX - -Integer gridY - -String cityName - -String districtName - -String description - -Boolean isObstacle - +getCoordinates() Point - +isAccessible() boolean - } - - class Attachment { - -Long id - -String fileName - -String filePath - -String fileType - -Long fileSize - -LocalDateTime uploadedAt - } - - class TaskHistory { - -Long id - -Task task - -UserAccount updatedBy - -TaskStatus oldStatus - -TaskStatus newStatus - -String remarks - -LocalDateTime updatedAt - } - - %% 枚举类 - class Role { - <> - PUBLIC_SUPERVISOR - SUPERVISOR - GRID_WORKER - ADMIN - DECISION_MAKER - } - - class FeedbackStatus { - <> - PENDING_REVIEW - AI_REVIEWING - AI_PROCESSING - PENDING_ASSIGNMENT - ASSIGNED - CONFIRMED - RESOLVED - REJECTED - } - - class TaskStatus { - <> - PENDING_ASSIGNMENT - ASSIGNED - IN_PROGRESS - SUBMITTED - COMPLETED - CANCELLED - } - - class PollutionType { - <> - AIR - WATER - SOIL - NOISE - WASTE - OTHER - } - - class SeverityLevel { - <> - LOW - MEDIUM - HIGH - CRITICAL - } - - %% 关系 - UserAccount "1" -- "0..*" Feedback : submits - UserAccount "1" -- "0..*" Task : assigned_to - UserAccount "1" -- "0..*" Task : created_by - UserAccount "1" -- "0..*" Assignment : assigns - Feedback "1" -- "1" Task : generates - Task "1" -- "1" Assignment : has - Task "1" -- "0..*" TaskHistory : logs - Feedback "1" -- "0..*" Attachment : has - UserAccount -- Role - Feedback -- FeedbackStatus - Feedback -- PollutionType : categorized_as - Feedback -- SeverityLevel : rated_as - Task -- TaskStatus"}]}}} -``` - -### 2.2 控制器层类图 - -```mermaid -classDiagram - class AuthController { - -AuthService authService - -VerificationCodeService verificationCodeService - +signUp(SignUpRequest) ResponseEntity - +signIn(LoginRequest) ResponseEntity - +logout() ResponseEntity - +sendVerificationCode(String) ResponseEntity - +requestPasswordReset(String) ResponseEntity - +resetPassword(PasswordResetRequest) ResponseEntity - } - - class FeedbackController { - -FeedbackService feedbackService - +submitFeedback(FeedbackSubmissionRequest, MultipartFile[]) ResponseEntity - +submitFeedbackJson(FeedbackSubmissionRequest) ResponseEntity - +submitPublicFeedback(PublicFeedbackRequest, MultipartFile[]) ResponseEntity - +getAllFeedback(filters, Pageable) ResponseEntity - +getFeedbackById(Long) ResponseEntity - +getFeedbackStats(filters) ResponseEntity - +processFeedback(Long, ProcessFeedbackRequest) ResponseEntity - } - - class UserController { - -UserService userService - +getCurrentUser() ResponseEntity - +updateProfile(UserProfileUpdateRequest) ResponseEntity - +getAllUsers(Pageable) ResponseEntity - +getUserById(Long) ResponseEntity - +updateUserRole(Long, Role) ResponseEntity - +deactivateUser(Long) ResponseEntity - } - - %% 服务层接口 - class AuthService { - <> - +registerUser(SignUpRequest) void - +signIn(LoginRequest) JwtAuthenticationResponse - +logout() void - +requestPasswordReset(String) void - +resetPassword(String, String) void - } - - class FeedbackService { - <> - +submitFeedback(FeedbackSubmissionRequest, MultipartFile[]) Feedback - +getAllFeedback(filters, Pageable) Page~Feedback~ - +getFeedbackById(Long) Feedback - +processFeedback(Long, ProcessFeedbackRequest) Feedback - +getFeedbackStats(filters) FeedbackStatsResponse - } - - %% 关系 - AuthController --> AuthService : uses - FeedbackController --> FeedbackService : uses - UserController --> UserService : uses -``` - -### 2.3 服务层实现类图 - -```mermaid -classDiagram - class AuthServiceImpl { - -UserRepository userRepository - -PasswordEncoder passwordEncoder - -JwtService jwtService - -VerificationCodeService verificationCodeService - +registerUser(SignUpRequest) void - +signIn(LoginRequest) JwtAuthenticationResponse - +logout() void - +requestPasswordReset(String) void - +resetPassword(String, String) void - -validateUser(UserAccount) void - -generateJwtToken(UserAccount) String - } - - class FeedbackServiceImpl { - -FeedbackRepository feedbackRepository - -UserRepository userRepository - -ApplicationEventPublisher eventPublisher - -FileStorageService fileStorageService - +submitFeedback(FeedbackSubmissionRequest, MultipartFile[]) Feedback - +getAllFeedback(filters, Pageable) Page~Feedback~ - +getFeedbackById(Long) Feedback - +processFeedback(Long, ProcessFeedbackRequest) Feedback - -generateEventId() String - -validateFeedbackData(FeedbackSubmissionRequest) void - } - - class TaskServiceImpl { - -TaskRepository taskRepository - -UserRepository userRepository - -AssignmentRepository assignmentRepository - +createTask(TaskCreationRequest) Task - +assignTask(Long, Long) Assignment - +updateTaskStatus(Long, TaskStatus) Task - +getTasksByAssignee(Long, Pageable) Page~Task~ - -validateTaskAssignment(Task, UserAccount) void - } - - %% 接口实现关系 - AuthServiceImpl ..|> AuthService : implements - FeedbackServiceImpl ..|> FeedbackService : implements - TaskServiceImpl ..|> TaskService : implements -``` - -## 3. 数据库ER图 - -```mermaid -erDiagram - USER_ACCOUNT { - bigint id PK - varchar name - varchar phone UK - varchar email UK - varchar password - enum gender - enum role - enum status - enum level - varchar department - text skills - datetime created_at - datetime updated_at - } - - FEEDBACK { - bigint id PK - varchar event_id UK - varchar title - text description - enum pollution_type - enum severity_level - enum status - varchar location - decimal latitude - decimal longitude - varchar city_name - varchar district_name - bigint submitter_id FK - datetime created_at - datetime updated_at - } - - TASK { - bigint id PK - bigint feedback_id FK - bigint assignee_id FK - bigint created_by FK - enum status - varchar title - text description - varchar location - decimal latitude - decimal longitude - datetime assigned_at - datetime completed_at - datetime created_at - datetime updated_at - } - - ASSIGNMENT { - bigint id PK - bigint task_id FK - bigint assigner_id FK - datetime assignment_time - datetime deadline - enum status - text remarks - datetime created_at - datetime updated_at - } - - GRID { - bigint id PK - int gridx - int gridy - varchar city_name - varchar district_name - text description - boolean is_obstacle - } - - ATTACHMENT { - bigint id PK - bigint feedback_id FK - varchar file_name - varchar file_path - varchar file_type - bigint file_size - datetime uploaded_at - } - - OPERATION_LOG { - bigint id PK - bigint user_id FK - varchar operation_type - varchar target_type - bigint target_id - text description - varchar ip_address - datetime created_at - } - - VERIFICATION_CODE { - bigint id PK - varchar email - varchar code - enum type - datetime expires_at - boolean used - datetime created_at - } - - TASK_HISTORY { - bigint id PK - bigint task_id FK - bigint updated_by_id FK - enum old_status - enum new_status - text remarks - datetime updated_at - } - - %% 关系定义 - USER_ACCOUNT ||--o{ FEEDBACK : submits - USER_ACCOUNT ||--o{ TASK : assigned_to - USER_ACCOUNT ||--o{ TASK : created_by - USER_ACCOUNT ||--o{ ASSIGNMENT : assigns - USER_ACCOUNT ||--o{ OPERATION_LOG : performs - - FEEDBACK ||--|| TASK : generates - FEEDBACK ||--o{ ATTACHMENT : has - - TASK ||--|| ASSIGNMENT : has - TASK ||--o{ TASK_HISTORY : logs - - VERIFICATION_CODE }o--|| USER_ACCOUNT : sent_to - USER_ACCOUNT ||--o{ TASK_HISTORY : updates -``` - -## 4. 系统架构说明 - -### 4.1 分层架构 - -- **控制器层(Controller)**: 处理HTTP请求,参数验证,响应格式化 -- **服务层(Service)**: 业务逻辑处理,事务管理 -- **仓库层(Repository)**: 数据访问,数据库操作 -- **模型层(Model)**: 实体定义,数据结构 - -### 4.2 核心业务流程 - -1. **用户认证**: JWT令牌生成和验证 -2. **反馈管理**: 环境问题反馈的提交、审核、处理 -3. **任务分配**: 基于反馈创建任务并分配给网格工作人员 -4. **状态跟踪**: 反馈和任务状态的生命周期管理 - -### 4.3 安全机制 - -- 基于角色的访问控制(RBAC) -- JWT令牌认证 -- 密码加密存储 -- 操作日志记录 - -### 4.4 数据完整性 - -- 外键约束确保数据一致性 -- 唯一约束防止重复数据 -- 枚举类型确保状态值有效性 -- 时间戳记录数据变更历史 - -## 5. 技术栈 - -- **框架**: Spring Boot 3.x -- **数据库**: MySQL/PostgreSQL -- **ORM**: Spring Data JPA + Hibernate -- **安全**: Spring Security + JWT -- **文档**: Swagger/OpenAPI 3 -- **构建工具**: Maven -- **Java版本**: JDK 17+ - ---- - -*本文档基于EMS后端项目代码分析生成,包含完整的系统设计图表,可用于系统理解、开发指导和文档维护。* + +## 3. ER图 + +```mermaid +erDiagram + USER_ACCOUNT { + BIGINT id PK + VARCHAR username + VARCHAR password + VARCHAR email + VARCHAR phone + BIGINT role_id FK + BIGINT grid_id FK + } + + ROLE { + BIGINT id PK + VARCHAR name + } + + FEEDBACK { + BIGINT id PK + TEXT content + VARCHAR location + VARCHAR status + BIGINT submitter_id FK + } + + TASK { + BIGINT id PK + TEXT description + VARCHAR status + BIGINT feedback_id FK + BIGINT assignee_id FK + } + + GRID { + BIGINT id PK + INT grid_x + INT grid_y + } + + OPERATION_LOG { + BIGINT id PK + VARCHAR operation_type + TEXT details + BIGINT operator_id FK + } + + ATTACHMENT { + BIGINT id PK + BIGINT feedback_id FK + VARCHAR file_url + } + + USER_ACCOUNT ||--o{ ROLE : "has" + USER_ACCOUNT ||--o{ FEEDBACK : "submits" + USER_ACCOUNT ||--o{ TASK : "assigned" + USER_ACCOUNT ||--o{ GRID : "belongs to" + FEEDBACK ||--o{ TASK : "leads to" + USER_ACCOUNT ||--o{ OPERATION_LOG : "performs" + FEEDBACK ||--o{ ATTACHMENT : "has" +``` + participant Database as 数据库 + + Admin->>+OperationLogController: GET /api/logs (过滤条件) + OperationLogController->>+OperationLogService: queryOperationLogs(filters) + OperationLogService->>+OperationLogRepository: findByCriteria(filters) + OperationLogRepository->>+Database: SELECT * FROM operation_logs WHERE ... + Database-->>-OperationLogRepository: 返回日志记录 + OperationLogRepository-->>-OperationLogService: 返回日志DTO列表 + OperationLogService-->>-OperationLogController: 返回日志DTO列表 + OperationLogController-->>-Admin: 200 OK (日志列表) +``` + +### 1.16 管理员创建新用户 + +```mermaid +sequenceDiagram + participant Admin as 管理员 + participant PersonnelController as 人员控制器 + participant PersonnelService as 人员服务 + participant UserAccountRepository as 用户账户仓库 + participant PasswordEncoder as 密码编码器 + participant Database as 数据库 + + Admin->>+PersonnelController: POST /api/personnel/users (用户信息) + PersonnelController->>+PersonnelService: createUser(request) + PersonnelService->>+UserAccountRepository: findByUsername(username) + UserAccountRepository-->>-PersonnelService: (检查用户是否存在) + PersonnelService->>+PasswordEncoder: encode(password) + PasswordEncoder-->>-PersonnelService: 返回加密后的密码 + PersonnelService->>+UserAccountRepository: save(user) + UserAccountRepository->>+Database: INSERT INTO user_accounts + Database-->>-UserAccountRepository: 返回已保存的用户 + UserAccountRepository-->>-PersonnelService: 返回已保存的用户 + PersonnelService-->>-PersonnelController: 返回创建的用户 + PersonnelController-->>-Admin: 201 CREATED (用户详情) +``` + +### 1.17 用户获取自己的反馈历史 + +```mermaid +sequenceDiagram + participant User as 用户 + participant ProfileController as 个人资料控制器 + participant UserFeedbackService as 用户反馈服务 + participant CustomUserDetails as 用户认证详情 + participant Database as 数据库 + + User->>+ProfileController: GET /api/me/feedback + ProfileController->>+CustomUserDetails: 获取用户ID + CustomUserDetails-->>-ProfileController: 返回用户ID + ProfileController->>+UserFeedbackService: getFeedbackHistoryByUserId(userId, pageable) + UserFeedbackService->>+Database: SELECT * FROM feedback WHERE user_id = ? + Database-->>-UserFeedbackService: 返回反馈记录 + UserFeedbackService-->>-ProfileController: 返回反馈摘要DTO列表 + ProfileController-->>-User: 200 OK (反馈历史列表) +``` + +### 1.18 公众提交反馈 + +```mermaid +sequenceDiagram + participant Public as 公众 + participant PublicController as 公共控制器 + participant FeedbackService as 反馈服务 + participant FileStorageService as 文件存储服务 + participant FeedbackRepository as 反馈仓库 + participant Database as 数据库 + + Public->>+PublicController: POST /api/public/feedback (反馈信息和文件) + PublicController->>+FeedbackService: createPublicFeedback(request, files) + alt 如果有文件 + FeedbackService->>+FileStorageService: store(file) + FileStorageService-->>-FeedbackService: 返回文件路径 + end + FeedbackService->>+FeedbackRepository: save(feedback) + FeedbackRepository->>+Database: INSERT INTO feedback + Database-->>-FeedbackRepository: 返回已保存的反馈 + FeedbackRepository-->>-FeedbackService: 返回已保存的反馈 + FeedbackService-->>-PublicController: 返回创建的反馈 + PublicController-->>-Public: 201 CREATED (反馈详情) +``` + +### 1.19 分配任务给工作人员 + +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant TaskAssignmentController as 任务分配控制器 + participant TaskAssignmentService as 任务分配服务 + participant AssignmentRepository as 分配仓库 + participant FeedbackRepository as 反馈仓库 + participant Database as 数据库 + + Supervisor->>+TaskAssignmentController: POST /api/tasks/assign (任务ID, 分配对象ID) + TaskAssignmentController->>+TaskAssignmentService: assignTask(feedbackId, assigneeId, assignerId) + TaskAssignmentService->>+FeedbackRepository: findById(feedbackId) + FeedbackRepository-->>-TaskAssignmentService: 返回任务详情 + TaskAssignmentService->>+AssignmentRepository: save(assignment) + AssignmentRepository->>+Database: INSERT INTO assignments + Database-->>-AssignmentRepository: 返回已保存的分配 + AssignmentRepository-->>-TaskAssignmentService: 返回已保存的分配 + TaskAssignmentService-->>-TaskAssignmentController: 返回新的分配 + TaskAssignmentController-->>-Supervisor: 200 OK (分配详情) +``` + +### 1.20 从反馈创建任务 + +```mermaid +sequenceDiagram + participant Supervisor as 主管 + participant TaskManagementController as 任务管理控制器 + participant TaskManagementService as 任务管理服务 + participant FeedbackRepository as 反馈仓库 + participant TaskRepository as 任务仓库 + participant Database as 数据库 + + Supervisor->>+TaskManagementController: POST /api/management/tasks/feedback/{feedbackId}/create-task (任务信息) + TaskManagementController->>+TaskManagementService: createTaskFromFeedback(feedbackId, request) + TaskManagementService->>+FeedbackRepository: findById(feedbackId) + FeedbackRepository-->>-TaskManagementService: 返回反馈详情 + TaskManagementService->>+TaskRepository: save(task) + TaskRepository->>+Database: INSERT INTO tasks + Database-->>-TaskRepository: 返回已保存的任务 + TaskRepository-->>-TaskManagementService: 返回已保存的任务 + TaskManagementService-->>-TaskManagementController: 返回创建的任务详情DTO + TaskManagementController-->>-Supervisor: 201 CREATED (任务详情) +``` + +## 2. UML类图 + +### 2.1 核心领域模型类图 + +```mermaid +classDiagram + class UserAccount { + -Long id + -String name + -String phone + -String email + -String password + -Gender gender + -Role role + -UserStatus status + -Level level + -String department + -List~String~ skills + -LocalDateTime createdAt + -LocalDateTime updatedAt + +login() boolean + +updateProfile() void + +changePassword() void + } + + class Feedback { + -Long id + -String eventId + -String title + -String description + -PollutionType pollutionType + -SeverityLevel severityLevel + -FeedbackStatus status + -String location + -Double latitude + -Double longitude + -String cityName + -String districtName + -UserAccount submitter + -LocalDateTime createdAt + -LocalDateTime updatedAt + +updateStatus() void + +assignToTask() Task + } + + class Task { + -Long id + -Feedback feedback + -UserAccount assignee + -UserAccount createdBy + -TaskStatus status + -String title + -String description + -String location + -LocalDateTime assignedAt + -LocalDateTime completedAt + -LocalDateTime createdAt + +assign(UserAccount) void + +complete() void + +updateProgress() void + } + + class Assignment { + -Long id + -Task task + -UserAccount assigner + -LocalDateTime assignmentTime + -LocalDateTime deadline + -AssignmentStatus status + -String remarks + +setDeadline() void + +updateStatus() void + } + + class Grid { + -Long id + -Integer gridX + -Integer gridY + -String cityName + -String districtName + -String description + -Boolean isObstacle + +getCoordinates() Point + +isAccessible() boolean + } + + class Attachment { + -Long id + -String fileName + -String filePath + -String fileType + -Long fileSize + -LocalDateTime uploadedAt + } + + class TaskHistory { + -Long id + -Task task + -UserAccount updatedBy + -TaskStatus oldStatus + -TaskStatus newStatus + -String remarks + -LocalDateTime updatedAt + } + + %% 枚举类 + class Role { + <> + PUBLIC_SUPERVISOR + SUPERVISOR + GRID_WORKER + ADMIN + DECISION_MAKER + } + + class FeedbackStatus { + <> + PENDING_REVIEW + AI_REVIEWING + AI_PROCESSING + PENDING_ASSIGNMENT + ASSIGNED + CONFIRMED + RESOLVED + REJECTED + } + + class TaskStatus { + <> + PENDING_ASSIGNMENT + ASSIGNED + IN_PROGRESS + SUBMITTED + COMPLETED + CANCELLED + } + + class PollutionType { + <> + AIR + WATER + SOIL + NOISE + WASTE + OTHER + } + + class SeverityLevel { + <> + LOW + MEDIUM + HIGH + CRITICAL + } + + %% 关系 + UserAccount "1" -- "0..*" Feedback : submits + UserAccount "1" -- "0..*" Task : assigned_to + UserAccount "1" -- "0..*" Task : created_by + UserAccount "1" -- "0..*" Assignment : assigns + Feedback "1" -- "1" Task : generates + Task "1" -- "1" Assignment : has + Task "1" -- "0..*" TaskHistory : logs + Feedback "1" -- "0..*" Attachment : has + UserAccount -- Role + Feedback -- FeedbackStatus + Feedback -- PollutionType : categorized_as + Feedback -- SeverityLevel : rated_as + Task -- TaskStatus"}]}}} +``` + +### 2.2 控制器层类图 + +```mermaid +classDiagram + class AuthController { + -AuthService authService + -VerificationCodeService verificationCodeService + +signUp(SignUpRequest) ResponseEntity + +signIn(LoginRequest) ResponseEntity + +logout() ResponseEntity + +sendVerificationCode(String) ResponseEntity + +requestPasswordReset(String) ResponseEntity + +resetPassword(PasswordResetRequest) ResponseEntity + } + + class FeedbackController { + -FeedbackService feedbackService + +submitFeedback(FeedbackSubmissionRequest, MultipartFile[]) ResponseEntity + +submitFeedbackJson(FeedbackSubmissionRequest) ResponseEntity + +submitPublicFeedback(PublicFeedbackRequest, MultipartFile[]) ResponseEntity + +getAllFeedback(filters, Pageable) ResponseEntity + +getFeedbackById(Long) ResponseEntity + +getFeedbackStats(filters) ResponseEntity + +processFeedback(Long, ProcessFeedbackRequest) ResponseEntity + } + + class UserController { + -UserService userService + +getCurrentUser() ResponseEntity + +updateProfile(UserProfileUpdateRequest) ResponseEntity + +getAllUsers(Pageable) ResponseEntity + +getUserById(Long) ResponseEntity + +updateUserRole(Long, Role) ResponseEntity + +deactivateUser(Long) ResponseEntity + } + + %% 服务层接口 + class AuthService { + <> + +registerUser(SignUpRequest) void + +signIn(LoginRequest) JwtAuthenticationResponse + +logout() void + +requestPasswordReset(String) void + +resetPassword(String, String) void + } + + class FeedbackService { + <> + +submitFeedback(FeedbackSubmissionRequest, MultipartFile[]) Feedback + +getAllFeedback(filters, Pageable) Page~Feedback~ + +getFeedbackById(Long) Feedback + +processFeedback(Long, ProcessFeedbackRequest) Feedback + +getFeedbackStats(filters) FeedbackStatsResponse + } + + %% 关系 + AuthController --> AuthService : uses + FeedbackController --> FeedbackService : uses + UserController --> UserService : uses +``` + +### 2.3 服务层实现类图 + +```mermaid +classDiagram + class AuthServiceImpl { + -UserRepository userRepository + -PasswordEncoder passwordEncoder + -JwtService jwtService + -VerificationCodeService verificationCodeService + +registerUser(SignUpRequest) void + +signIn(LoginRequest) JwtAuthenticationResponse + +logout() void + +requestPasswordReset(String) void + +resetPassword(String, String) void + -validateUser(UserAccount) void + -generateJwtToken(UserAccount) String + } + + class FeedbackServiceImpl { + -FeedbackRepository feedbackRepository + -UserRepository userRepository + -ApplicationEventPublisher eventPublisher + -FileStorageService fileStorageService + +submitFeedback(FeedbackSubmissionRequest, MultipartFile[]) Feedback + +getAllFeedback(filters, Pageable) Page~Feedback~ + +getFeedbackById(Long) Feedback + +processFeedback(Long, ProcessFeedbackRequest) Feedback + -generateEventId() String + -validateFeedbackData(FeedbackSubmissionRequest) void + } + + class TaskServiceImpl { + -TaskRepository taskRepository + -UserRepository userRepository + -AssignmentRepository assignmentRepository + +createTask(TaskCreationRequest) Task + +assignTask(Long, Long) Assignment + +updateTaskStatus(Long, TaskStatus) Task + +getTasksByAssignee(Long, Pageable) Page~Task~ + -validateTaskAssignment(Task, UserAccount) void + } + + %% 接口实现关系 + AuthServiceImpl ..|> AuthService : implements + FeedbackServiceImpl ..|> FeedbackService : implements + TaskServiceImpl ..|> TaskService : implements +``` + +## 3. 数据库ER图 + +```mermaid +erDiagram + USER_ACCOUNT { + bigint id PK + varchar name + varchar phone UK + varchar email UK + varchar password + enum gender + enum role + enum status + enum level + varchar department + text skills + datetime created_at + datetime updated_at + } + + FEEDBACK { + bigint id PK + varchar event_id UK + varchar title + text description + enum pollution_type + enum severity_level + enum status + varchar location + decimal latitude + decimal longitude + varchar city_name + varchar district_name + bigint submitter_id FK + datetime created_at + datetime updated_at + } + + TASK { + bigint id PK + bigint feedback_id FK + bigint assignee_id FK + bigint created_by FK + enum status + varchar title + text description + varchar location + decimal latitude + decimal longitude + datetime assigned_at + datetime completed_at + datetime created_at + datetime updated_at + } + + ASSIGNMENT { + bigint id PK + bigint task_id FK + bigint assigner_id FK + datetime assignment_time + datetime deadline + enum status + text remarks + datetime created_at + datetime updated_at + } + + GRID { + bigint id PK + int gridx + int gridy + varchar city_name + varchar district_name + text description + boolean is_obstacle + } + + ATTACHMENT { + bigint id PK + bigint feedback_id FK + varchar file_name + varchar file_path + varchar file_type + bigint file_size + datetime uploaded_at + } + + OPERATION_LOG { + bigint id PK + bigint user_id FK + varchar operation_type + varchar target_type + bigint target_id + text description + varchar ip_address + datetime created_at + } + + VERIFICATION_CODE { + bigint id PK + varchar email + varchar code + enum type + datetime expires_at + boolean used + datetime created_at + } + + TASK_HISTORY { + bigint id PK + bigint task_id FK + bigint updated_by_id FK + enum old_status + enum new_status + text remarks + datetime updated_at + } + + %% 关系定义 + USER_ACCOUNT ||--o{ FEEDBACK : submits + USER_ACCOUNT ||--o{ TASK : assigned_to + USER_ACCOUNT ||--o{ TASK : created_by + USER_ACCOUNT ||--o{ ASSIGNMENT : assigns + USER_ACCOUNT ||--o{ OPERATION_LOG : performs + + FEEDBACK ||--|| TASK : generates + FEEDBACK ||--o{ ATTACHMENT : has + + TASK ||--|| ASSIGNMENT : has + TASK ||--o{ TASK_HISTORY : logs + + VERIFICATION_CODE }o--|| USER_ACCOUNT : sent_to + USER_ACCOUNT ||--o{ TASK_HISTORY : updates +``` + +## 4. 系统架构说明 + +### 4.1 分层架构 + +- **控制器层(Controller)**: 处理HTTP请求,参数验证,响应格式化 +- **服务层(Service)**: 业务逻辑处理,事务管理 +- **仓库层(Repository)**: 数据访问,数据库操作 +- **模型层(Model)**: 实体定义,数据结构 + +### 4.2 核心业务流程 + +1. **用户认证**: JWT令牌生成和验证 +2. **反馈管理**: 环境问题反馈的提交、审核、处理 +3. **任务分配**: 基于反馈创建任务并分配给网格工作人员 +4. **状态跟踪**: 反馈和任务状态的生命周期管理 + +### 4.3 安全机制 + +- 基于角色的访问控制(RBAC) +- JWT令牌认证 +- 密码加密存储 +- 操作日志记录 + +### 4.4 数据完整性 + +- 外键约束确保数据一致性 +- 唯一约束防止重复数据 +- 枚举类型确保状态值有效性 +- 时间戳记录数据变更历史 + +## 5. 技术栈 + +- **框架**: Spring Boot 3.x +- **数据库**: MySQL/PostgreSQL +- **ORM**: Spring Data JPA + Hibernate +- **安全**: Spring Security + JWT +- **文档**: Swagger/OpenAPI 3 +- **构建工具**: Maven +- **Java版本**: JDK 17+ + +--- + +*本文档基于EMS后端项目代码分析生成,包含完整的系统设计图表,可用于系统理解、开发指导和文档维护。* --- - + ### Report/temp.md - -```plantuml - -@startuml -title 任务核心模型 (Task Core Models) - -skinparam classAttributeIconSize 0 - -class Task { - - Long id - - String title - - String description - - PollutionType pollutionType - - SeverityLevel severityLevel - - TaskStatus status - - String textAddress - - Integer gridX - - Integer gridY - - LocalDateTime assignedAt - - LocalDateTime completedAt -} - -class Assignment { - - Long id - - LocalDateTime assignmentTime - - LocalDateTime deadline - - AssignmentStatus status - - String remarks -} - -class TaskHistory { - - Long id - - TaskStatus oldStatus - - TaskStatus newStatus - - String comments - - LocalDateTime changedAt -} - -class TaskSubmission { - - Long id - - String notes - - LocalDateTime submittedAt -} - -' --- Relationships --- -' Task is the central entity -Task "1" *-- "1" Assignment : (has one) -Task "1" *-- "0..*" TaskHistory : (logs) -Task "1" *-- "0..*" TaskSubmission : (has) - -' --- External Relationships for Context --- -Task ..> Feedback : (created from) -Task ..> UserAccount : (assignee) -Task ..> UserAccount : (createdBy) - -Assignment ..> UserAccount : (assigner) -TaskHistory ..> UserAccount : (changedBy) -TaskSubmission ..> "*" Attachment : (has) - -@enduml - - - -@startuml -title 网格与地图模型 (Grid & Map Models) - -skinparam classAttributeIconSize 0 - -class Grid { - - Long id - - Integer gridX - - Integer gridY - - String cityName - - String districtName - - String description - - Boolean isObstacle -} - -class MapGrid { - - Long id - - int x - - int y - - String cityName - - boolean isObstacle - - String terrainType -} -@enduml - - -@startuml -title 数据与日志模型 (Data & Logging Models) - -skinparam classAttributeIconSize 0 - -class AqiData { - - Long id - - Integer aqiValue - - Double pm25 - - Double pm10 - - String primaryPollutant - - LocalDateTime recordTime -} - -class AqiRecord { - - Long id - - String cityName - - Integer aqiValue - - Double pm25 - - Double pm10 - - LocalDateTime recordTime -} - -class OperationLog { - - Long id - - OperationType operationType - - String description - - String targetId - - String targetType - - String ipAddress - - LocalDateTime createdAt -} - -class AssignmentRecord { - - Long id - - AssignmentMethod assignmentMethod - - String algorithmDetails - - AssignmentStatus status - - LocalDateTime createdAt -} - -' --- Relationships --- -AqiData ..> Grid : (recorded at) -AqiData ..> UserAccount : (reported by) - -AqiRecord ..> Grid : (related to) - -OperationLog ..> UserAccount : (performed by) - -AssignmentRecord ..> Feedback : (for) -AssignmentRecord ..> UserAccount : (assigned to worker) -AssignmentRecord ..> UserAccount : (assigned by admin) -@enduml - - -@startuml -title 辅助与配置模型 (Utility & Configuration Models) - -skinparam classAttributeIconSize 0 - -class PollutantThreshold { - - Long id - - PollutionType pollutionType - - String pollutantName - - Double threshold - - String unit -} - -class PasswordResetToken { - - Long id - - String token - - LocalDateTime expiryDate - .. - + isExpired(): boolean -} - -class Attachment { - - Long id - - String fileName - - String fileType - - String storedFileName - - Long fileSize - - LocalDateTime uploadDate -} - -' --- Relationships --- -PasswordResetToken ..> UserAccount : (for) - -Attachment ..> Feedback : (belongs to) -Attachment ..> TaskSubmission : (belongs to) -@enduml -``` - -```plantuml -@startuml -title 系统核心业务模型 (Core Business Models) -skinparam classAttributeIconSize 0 - -enum Gender{ - -Male - -Female - -OTHER -} -enum Role{ - -} -enum UserStatus -enum PollutionType -enum SeverityLevel -enum FeedbackStatus -enum TaskStatus -enum AssignmentStatus - -class UserAccount { - - Long id - - String name - - String phone - - String email - - String password - - Gender gender - - Role role - - UserStatus status - - Integer gridX - - Integer gridY - + isValid() -} - -class Feedback { - - Long id - - String eventId - - String title - - PollutionType pollutionType - - SeverityLevel severityLevel - - FeedbackStatus status - - Long submitterId - + process() - + approve() -} - -class Task { - - Long id - - Long feedbackId - - Long assigneeId - - Long createdBy - - TaskStatus status - - String title - + assignTo(workerId) - + complete() - + approve() -} - -class Assignment { - - Long id - - Long taskId - - Long assignerId - - AssignmentStatus status - - String remarks - - LocalDateTime assignmentTime -} - -class Grid { - - Long id - - Integer gridX - - Integer gridY - - String cityName - - boolean isObstacle -} - -UserAccount "1" *-- "1" Gender -UserAccount "1" *-- "1" Role -UserAccount "1" *-- "1" UserStatus -Feedback "1" *-- "1" PollutionType -Feedback "1" *-- "1" SeverityLevel -Feedback "1" *-- "1" FeedbackStatus -Task "1" *-- "1" TaskStatus -Assignment "1" *-- "1" AssignmentStatus - -Task "1" --> "1" Feedback : "generated from" -Task "1" o-- "1" Assignment : "has" -UserAccount "1" --> "0..*" Feedback : "submits" -UserAccount "1" --> "0..*" Task : "creates" -Task "0..*" --o "1" UserAccount : "assigned to" -UserAccount "1" --> "0..*" Assignment : "assigns" -Task "1" ..> "1" Grid : "located in" -UserAccount "1" ..> "1" Grid : "operates in" -@enduml -``` - - -```plantuml -@startuml -title EMS核心业务模型全景图 - -skinparam classAttributeIconSize 0 - - -' --- Enumeration classes (using stereotype for max compatibility) --- -class Gender <> { - MALE, FEMALE, OTHER -} -class Role <> { - ADMIN, SUPERVISOR, GRID_WORKER -} -class UserStatus <> { - ACTIVE, INACTIVE -} -class PollutionType <> { - AIR, WATER, SOIL, NOISE -} -class SeverityLevel <> { - LOW, MEDIUM, HIGH, CRITICAL -} -class FeedbackStatus <> { - PENDING_REVIEW, AI_REJECTED, PROCESSED -} -class TaskStatus <> { - CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED -} -class AssignmentStatus <> { - PENDING, ACCEPTED, REJECTED -} - -' --- Entity classes --- -class UserAccount -class Feedback -class Task -class Assignment -class Grid -class PasswordResetToken -class Attachment - -' --- Relationships --- -UserAccount "1" --> "1" Gender -UserAccount "1" --> "1" Role -UserAccount "1" --> "1" UserStatus - -Feedback "1" --> "1" PollutionType -Feedback "1" --> "1" SeverityLevel -Feedback "1" --> "1" FeedbackStatus - -Task "1" --> "1" TaskStatus -Assignment "1" --> "1" AssignmentStatus - -Task "1" --> "1" Feedback : "generated from" -Task "1" o-- "1" Assignment : "has" -UserAccount "1" --> "0..*" Feedback : "submits" -UserAccount "1" --> "0..*" Task : "creates" -Task "0..*" --o "1" UserAccount : "assigned to" -UserAccount "1" --> "0..*" Assignment : "assigns" -PasswordResetToken "1" --> "1" UserAccount : "for user" -Feedback "1" --> "0..*" Attachment : "has" - -Task "1" ..> "1" Grid : "located in" -UserAccount "1" ..> "1" Grid : "operates in" - -@enduml - -``` - - -```plantnml - - - - -``` + +```plantuml + +@startuml +title 任务核心模型 (Task Core Models) + +skinparam classAttributeIconSize 0 + +class Task { + - Long id + - String title + - String description + - PollutionType pollutionType + - SeverityLevel severityLevel + - TaskStatus status + - String textAddress + - Integer gridX + - Integer gridY + - LocalDateTime assignedAt + - LocalDateTime completedAt +} + +class Assignment { + - Long id + - LocalDateTime assignmentTime + - LocalDateTime deadline + - AssignmentStatus status + - String remarks +} + +class TaskHistory { + - Long id + - TaskStatus oldStatus + - TaskStatus newStatus + - String comments + - LocalDateTime changedAt +} + +class TaskSubmission { + - Long id + - String notes + - LocalDateTime submittedAt +} + +' --- Relationships --- +' Task is the central entity +Task "1" *-- "1" Assignment : (has one) +Task "1" *-- "0..*" TaskHistory : (logs) +Task "1" *-- "0..*" TaskSubmission : (has) + +' --- External Relationships for Context --- +Task ..> Feedback : (created from) +Task ..> UserAccount : (assignee) +Task ..> UserAccount : (createdBy) + +Assignment ..> UserAccount : (assigner) +TaskHistory ..> UserAccount : (changedBy) +TaskSubmission ..> "*" Attachment : (has) + +@enduml + + + +@startuml +title 网格与地图模型 (Grid & Map Models) + +skinparam classAttributeIconSize 0 + +class Grid { + - Long id + - Integer gridX + - Integer gridY + - String cityName + - String districtName + - String description + - Boolean isObstacle +} + +class MapGrid { + - Long id + - int x + - int y + - String cityName + - boolean isObstacle + - String terrainType +} +@enduml + + +@startuml +title 数据与日志模型 (Data & Logging Models) + +skinparam classAttributeIconSize 0 + +class AqiData { + - Long id + - Integer aqiValue + - Double pm25 + - Double pm10 + - String primaryPollutant + - LocalDateTime recordTime +} + +class AqiRecord { + - Long id + - String cityName + - Integer aqiValue + - Double pm25 + - Double pm10 + - LocalDateTime recordTime +} + +class OperationLog { + - Long id + - OperationType operationType + - String description + - String targetId + - String targetType + - String ipAddress + - LocalDateTime createdAt +} + +class AssignmentRecord { + - Long id + - AssignmentMethod assignmentMethod + - String algorithmDetails + - AssignmentStatus status + - LocalDateTime createdAt +} + +' --- Relationships --- +AqiData ..> Grid : (recorded at) +AqiData ..> UserAccount : (reported by) + +AqiRecord ..> Grid : (related to) + +OperationLog ..> UserAccount : (performed by) + +AssignmentRecord ..> Feedback : (for) +AssignmentRecord ..> UserAccount : (assigned to worker) +AssignmentRecord ..> UserAccount : (assigned by admin) +@enduml + + +@startuml +title 辅助与配置模型 (Utility & Configuration Models) + +skinparam classAttributeIconSize 0 + +class PollutantThreshold { + - Long id + - PollutionType pollutionType + - String pollutantName + - Double threshold + - String unit +} + +class PasswordResetToken { + - Long id + - String token + - LocalDateTime expiryDate + .. + + isExpired(): boolean +} + +class Attachment { + - Long id + - String fileName + - String fileType + - String storedFileName + - Long fileSize + - LocalDateTime uploadDate +} + +' --- Relationships --- +PasswordResetToken ..> UserAccount : (for) + +Attachment ..> Feedback : (belongs to) +Attachment ..> TaskSubmission : (belongs to) +@enduml +``` + +```plantuml +@startuml +title 系统核心业务模型 (Core Business Models) +skinparam classAttributeIconSize 0 + +enum Gender{ + -Male + -Female + -OTHER +} +enum Role{ + +} +enum UserStatus +enum PollutionType +enum SeverityLevel +enum FeedbackStatus +enum TaskStatus +enum AssignmentStatus + +class UserAccount { + - Long id + - String name + - String phone + - String email + - String password + - Gender gender + - Role role + - UserStatus status + - Integer gridX + - Integer gridY + + isValid() +} + +class Feedback { + - Long id + - String eventId + - String title + - PollutionType pollutionType + - SeverityLevel severityLevel + - FeedbackStatus status + - Long submitterId + + process() + + approve() +} + +class Task { + - Long id + - Long feedbackId + - Long assigneeId + - Long createdBy + - TaskStatus status + - String title + + assignTo(workerId) + + complete() + + approve() +} + +class Assignment { + - Long id + - Long taskId + - Long assignerId + - AssignmentStatus status + - String remarks + - LocalDateTime assignmentTime +} + +class Grid { + - Long id + - Integer gridX + - Integer gridY + - String cityName + - boolean isObstacle +} + +UserAccount "1" *-- "1" Gender +UserAccount "1" *-- "1" Role +UserAccount "1" *-- "1" UserStatus +Feedback "1" *-- "1" PollutionType +Feedback "1" *-- "1" SeverityLevel +Feedback "1" *-- "1" FeedbackStatus +Task "1" *-- "1" TaskStatus +Assignment "1" *-- "1" AssignmentStatus + +Task "1" --> "1" Feedback : "generated from" +Task "1" o-- "1" Assignment : "has" +UserAccount "1" --> "0..*" Feedback : "submits" +UserAccount "1" --> "0..*" Task : "creates" +Task "0..*" --o "1" UserAccount : "assigned to" +UserAccount "1" --> "0..*" Assignment : "assigns" +Task "1" ..> "1" Grid : "located in" +UserAccount "1" ..> "1" Grid : "operates in" +@enduml +``` + + +```plantuml +@startuml +title EMS核心业务模型全景图 + +skinparam classAttributeIconSize 0 + + +' --- Enumeration classes (using stereotype for max compatibility) --- +class Gender <> { + MALE, FEMALE, OTHER +} +class Role <> { + ADMIN, SUPERVISOR, GRID_WORKER +} +class UserStatus <> { + ACTIVE, INACTIVE +} +class PollutionType <> { + AIR, WATER, SOIL, NOISE +} +class SeverityLevel <> { + LOW, MEDIUM, HIGH, CRITICAL +} +class FeedbackStatus <> { + PENDING_REVIEW, AI_REJECTED, PROCESSED +} +class TaskStatus <> { + CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED +} +class AssignmentStatus <> { + PENDING, ACCEPTED, REJECTED +} + +' --- Entity classes --- +class UserAccount +class Feedback +class Task +class Assignment +class Grid +class PasswordResetToken +class Attachment + +' --- Relationships --- +UserAccount "1" --> "1" Gender +UserAccount "1" --> "1" Role +UserAccount "1" --> "1" UserStatus + +Feedback "1" --> "1" PollutionType +Feedback "1" --> "1" SeverityLevel +Feedback "1" --> "1" FeedbackStatus + +Task "1" --> "1" TaskStatus +Assignment "1" --> "1" AssignmentStatus + +Task "1" --> "1" Feedback : "generated from" +Task "1" o-- "1" Assignment : "has" +UserAccount "1" --> "0..*" Feedback : "submits" +UserAccount "1" --> "0..*" Task : "creates" +Task "0..*" --o "1" UserAccount : "assigned to" +UserAccount "1" --> "0..*" Assignment : "assigns" +PasswordResetToken "1" --> "1" UserAccount : "for user" +Feedback "1" --> "0..*" Attachment : "has" + +Task "1" ..> "1" Grid : "located in" +UserAccount "1" ..> "1" Grid : "operates in" + +@enduml + +``` + + +```plantnml + + + + +``` --- - + ### Report/temp1.md - -```plantuml -@startuml -skinparam classAttributeIconSize 0 - -' --- Enumeration classes (declaration-only for max compatibility) --- -enum Gender { - MALE, - FEMALE, - OTHER -} -enum Role { - PUBLIC_SUPERVISOR, - SUPERVISOR, - GRID_WORKER, - ADMIN, - DECISION_MAKER - -} -enum UserStatus -enum PollutionType -enum SeverityLevel -enum FeedbackStatus -enum TaskStatus -enum AssignmentStatus -enum AssignmentMethod -enum OperationType - - -' --- Entity classes with full attributes and methods --- -class UserAccount { - - Long id - - String name - - String phone - - String email - - String password - - Gender gender - - Role role - - UserStatus status - - Integer gridX - - Integer gridY - + isValid() -} -class Feedback { - - Long id - - String eventId - - String title - - PollutionType pollutionType - - SeverityLevel severityLevel - - FeedbackStatus status - - Long submitterId - + process() - + approve() -} -class Task { - - Long id - - String title - - String description - - PollutionType pollutionType - - SeverityLevel severityLevel - - TaskStatus status - - String textAddress - - Integer gridX - - Integer gridY - - LocalDateTime assignedAt - - LocalDateTime completedAt - + assignTo(workerId) - + complete() - + approve() -} -class Assignment { - - Long id - - String remarks - - LocalDateTime assignmentTime - - LocalDateTime deadline - - AssignmentStatus status -} -class TaskHistory { - - Long id - - TaskStatus oldStatus - - TaskStatus newStatus - - String comments - - LocalDateTime changedAt -} -class TaskSubmission { - - Long id - - String notes - - LocalDateTime submittedAt -} -class Attachment { - - Long id - - String fileName - - String fileType - - String storedFileName - - Long fileSize - - LocalDateTime uploadDate -} -class Grid { - - Long id - - Integer gridX - - Integer gridY - - String cityName - - String districtName - - String description - - boolean isObstacle -} -class MapGrid { - - Long id - - int x - - int y - - String cityName - - boolean isObstacle - - String terrainType -} -class PasswordResetToken { - - Long id - - String token - - LocalDateTime expiryDate - + isExpired() -} -class OperationLog { - - Long id - - OperationType operationType - - String description - - String targetId - - String targetType - - String ipAddress - - LocalDateTime createdAt -} -class AqiData { - - Long id - - Integer aqiValue - - Double pm25 - - Double pm10 - - String primaryPollutant - - LocalDateTime recordTime -} -class AqiRecord { - - Long id - - String cityName - - Integer aqiValue - - Double pm25 - - Double pm10 - - LocalDateTime recordTime -} -class AssignmentRecord { - - Long id - - AssignmentMethod assignmentMethod - - String algorithmDetails - - AssignmentStatus status - - LocalDateTime createdAt -} -class PollutantThreshold { - - Long id - - PollutionType pollutionType - - String pollutantName - - Double threshold - - String unit -} - -' --- Relationships (kept simple for compatibility) --- -UserAccount --> Gender -UserAccount --> Role -UserAccount --> UserStatus -Feedback --> PollutionType -Feedback --> SeverityLevel -Feedback --> FeedbackStatus -Task --> TaskStatus -Assignment --> AssignmentStatus -AssignmentRecord --> AssignmentMethod -OperationLog --> OperationType -PollutantThreshold --> PollutionType -Task --> Feedback -Task -- Assignment -Task -- TaskHistory -Task -- TaskSubmission -Feedback -- Attachment -TaskSubmission -- Attachment -AssignmentRecord --> Feedback -UserAccount --> Feedback -UserAccount --> Task -Task -- UserAccount -Assignment --> UserAccount -TaskHistory --> UserAccount -AssignmentRecord --> UserAccount -OperationLog --> UserAccount -PasswordResetToken --> UserAccount -AqiData --> UserAccount -Task ..> Grid -UserAccount ..> Grid -AqiData --> Grid -AqiRecord --> Grid -@enduml -``` - -```plantuml - -@startuml -title EMS系统模型全景图 (Detailed View) - -skinparam classAttributeIconSize 0 - -' --- Enumeration classes (declaration-only for max compatibility) --- -enum Gender -enum Role -enum UserStatus -enum PollutionType -enum SeverityLevel -enum FeedbackStatus -enum TaskStatus -enum AssignmentStatus -enum AssignmentMethod -enum OperationType - - -' --- Entity classes with full attributes and methods --- -class UserAccount { - - Long id - - String name - - String phone - - String email - - String password - - Gender gender - - Role role - - UserStatus status - - Integer gridX - - Integer gridY - + isValid() -} -class Feedback { - - Long id - - String eventId - - String title - - PollutionType pollutionType - - SeverityLevel severityLevel - - FeedbackStatus status - - Long submitterId - + process() - + approve() -} -class Task { - - Long id - - String title - - String description - - PollutionType pollutionType - - SeverityLevel severityLevel - - TaskStatus status - - String textAddress - - Integer gridX - - Integer gridY - - LocalDateTime assignedAt - - LocalDateTime completedAt - + assignTo(workerId) - + complete() - + approve() -} -class Assignment { - - Long id - - String remarks - - LocalDateTime assignmentTime - - LocalDateTime deadline - - AssignmentStatus status -} -class TaskHistory { - - Long id - - TaskStatus oldStatus - - TaskStatus newStatus - - String comments - - LocalDateTime changedAt -} -class TaskSubmission { - - Long id - - String notes - - LocalDateTime submittedAt -} -class Attachment { - - Long id - - String fileName - - String fileType - - String storedFileName - - Long fileSize - - LocalDateTime uploadDate -} -class Grid { - - Long id - - Integer gridX - - Integer gridY - - String cityName - - String districtName - - String description - - boolean isObstacle -} -class MapGrid { - - Long id - - int x - - int y - - String cityName - - boolean isObstacle - - String terrainType -} -class PasswordResetToken { - - Long id - - String token - - LocalDateTime expiryDate - + isExpired() -} -class OperationLog { - - Long id - - OperationType operationType - - String description - - String targetId - - String targetType - - String ipAddress - - LocalDateTime createdAt -} -class AqiData { - - Long id - - Integer aqiValue - - Double pm25 - - Double pm10 - - String primaryPollutant - - LocalDateTime recordTime -} -class AqiRecord { - - Long id - - String cityName - - Integer aqiValue - - Double pm25 - - Double pm10 - - LocalDateTime recordTime -} -class AssignmentRecord { - - Long id - - AssignmentMethod assignmentMethod - - String algorithmDetails - - AssignmentStatus status - - LocalDateTime createdAt -} -class PollutantThreshold { - - Long id - - PollutionType pollutionType - - String pollutantName - - Double threshold - - String unit -} - -' --- Relationships with Full Details --- - -' --- Composition with Enums --- -UserAccount "1" *-- "1" Gender -UserAccount "1" *-- "1" Role -UserAccount "1" *-- "1" UserStatus -Feedback "1" *-- "1" PollutionType -Feedback "1" *-- "1" SeverityLevel -Feedback "1" *-- "1" FeedbackStatus -Task "1" *-- "1" TaskStatus -Assignment "1" *-- "1" AssignmentStatus -AssignmentRecord "1" *-- "1" AssignmentMethod -OperationLog "1" *-- "1" OperationType -PollutantThreshold "1" *-- "1" PollutionType - -' --- Aggregation & Association --- -Task "1" --> "1" Feedback : "generated from" -Task "1" o-- "1" Assignment : "has" -Task "1" o-- "0..*" TaskHistory : "logs" -Task "1" o-- "0..*" TaskSubmission : "receives" -Feedback "1" o-- "0..*" Attachment : "has" -TaskSubmission "1" o-- "0..*" Attachment : "has" -AssignmentRecord "1" --> "1" Feedback : "for" -UserAccount "1" --> "0..*" Feedback : "submits" -UserAccount "1" --> "0..*" Task : "creates" -Task "0..*" o-- "1" UserAccount : "assigned to" -Assignment "1" --> "1" UserAccount : "assigned by" -TaskHistory "1" --> "1" UserAccount : "changed by" -AssignmentRecord "1" --> "1" UserAccount : "assigned to" -AssignmentRecord "1" --> "1" UserAccount : "assigned by" -OperationLog "1" --> "1" UserAccount : "performed by" -PasswordResetToken "1" --> "1" UserAccount : "for" -AqiData "1" --> "1" UserAccount : "reported by" - -' --- Dependency & Other Associations --- -Task "1" ..> "1" Grid : "located in" -UserAccount "1" ..> "1" Grid : "operates in" -AqiData "1" --> "1" Grid : "recorded at" -AqiRecord "1" --> "1" Grid : "related to" - -@enduml -``` + +```plantuml +@startuml +skinparam classAttributeIconSize 0 + +' --- Enumeration classes (declaration-only for max compatibility) --- +enum Gender { + MALE, + FEMALE, + OTHER +} +enum Role { + PUBLIC_SUPERVISOR, + SUPERVISOR, + GRID_WORKER, + ADMIN, + DECISION_MAKER + +} +enum UserStatus +enum PollutionType +enum SeverityLevel +enum FeedbackStatus +enum TaskStatus +enum AssignmentStatus +enum AssignmentMethod +enum OperationType + + +' --- Entity classes with full attributes and methods --- +class UserAccount { + - Long id + - String name + - String phone + - String email + - String password + - Gender gender + - Role role + - UserStatus status + - Integer gridX + - Integer gridY + + isValid() +} +class Feedback { + - Long id + - String eventId + - String title + - PollutionType pollutionType + - SeverityLevel severityLevel + - FeedbackStatus status + - Long submitterId + + process() + + approve() +} +class Task { + - Long id + - String title + - String description + - PollutionType pollutionType + - SeverityLevel severityLevel + - TaskStatus status + - String textAddress + - Integer gridX + - Integer gridY + - LocalDateTime assignedAt + - LocalDateTime completedAt + + assignTo(workerId) + + complete() + + approve() +} +class Assignment { + - Long id + - String remarks + - LocalDateTime assignmentTime + - LocalDateTime deadline + - AssignmentStatus status +} +class TaskHistory { + - Long id + - TaskStatus oldStatus + - TaskStatus newStatus + - String comments + - LocalDateTime changedAt +} +class TaskSubmission { + - Long id + - String notes + - LocalDateTime submittedAt +} +class Attachment { + - Long id + - String fileName + - String fileType + - String storedFileName + - Long fileSize + - LocalDateTime uploadDate +} +class Grid { + - Long id + - Integer gridX + - Integer gridY + - String cityName + - String districtName + - String description + - boolean isObstacle +} +class MapGrid { + - Long id + - int x + - int y + - String cityName + - boolean isObstacle + - String terrainType +} +class PasswordResetToken { + - Long id + - String token + - LocalDateTime expiryDate + + isExpired() +} +class OperationLog { + - Long id + - OperationType operationType + - String description + - String targetId + - String targetType + - String ipAddress + - LocalDateTime createdAt +} +class AqiData { + - Long id + - Integer aqiValue + - Double pm25 + - Double pm10 + - String primaryPollutant + - LocalDateTime recordTime +} +class AqiRecord { + - Long id + - String cityName + - Integer aqiValue + - Double pm25 + - Double pm10 + - LocalDateTime recordTime +} +class AssignmentRecord { + - Long id + - AssignmentMethod assignmentMethod + - String algorithmDetails + - AssignmentStatus status + - LocalDateTime createdAt +} +class PollutantThreshold { + - Long id + - PollutionType pollutionType + - String pollutantName + - Double threshold + - String unit +} + +' --- Relationships (kept simple for compatibility) --- +UserAccount --> Gender +UserAccount --> Role +UserAccount --> UserStatus +Feedback --> PollutionType +Feedback --> SeverityLevel +Feedback --> FeedbackStatus +Task --> TaskStatus +Assignment --> AssignmentStatus +AssignmentRecord --> AssignmentMethod +OperationLog --> OperationType +PollutantThreshold --> PollutionType +Task --> Feedback +Task -- Assignment +Task -- TaskHistory +Task -- TaskSubmission +Feedback -- Attachment +TaskSubmission -- Attachment +AssignmentRecord --> Feedback +UserAccount --> Feedback +UserAccount --> Task +Task -- UserAccount +Assignment --> UserAccount +TaskHistory --> UserAccount +AssignmentRecord --> UserAccount +OperationLog --> UserAccount +PasswordResetToken --> UserAccount +AqiData --> UserAccount +Task ..> Grid +UserAccount ..> Grid +AqiData --> Grid +AqiRecord --> Grid +@enduml +``` + +```plantuml + +@startuml +title EMS系统模型全景图 (Detailed View) + +skinparam classAttributeIconSize 0 + +' --- Enumeration classes (declaration-only for max compatibility) --- +enum Gender +enum Role +enum UserStatus +enum PollutionType +enum SeverityLevel +enum FeedbackStatus +enum TaskStatus +enum AssignmentStatus +enum AssignmentMethod +enum OperationType + + +' --- Entity classes with full attributes and methods --- +class UserAccount { + - Long id + - String name + - String phone + - String email + - String password + - Gender gender + - Role role + - UserStatus status + - Integer gridX + - Integer gridY + + isValid() +} +class Feedback { + - Long id + - String eventId + - String title + - PollutionType pollutionType + - SeverityLevel severityLevel + - FeedbackStatus status + - Long submitterId + + process() + + approve() +} +class Task { + - Long id + - String title + - String description + - PollutionType pollutionType + - SeverityLevel severityLevel + - TaskStatus status + - String textAddress + - Integer gridX + - Integer gridY + - LocalDateTime assignedAt + - LocalDateTime completedAt + + assignTo(workerId) + + complete() + + approve() +} +class Assignment { + - Long id + - String remarks + - LocalDateTime assignmentTime + - LocalDateTime deadline + - AssignmentStatus status +} +class TaskHistory { + - Long id + - TaskStatus oldStatus + - TaskStatus newStatus + - String comments + - LocalDateTime changedAt +} +class TaskSubmission { + - Long id + - String notes + - LocalDateTime submittedAt +} +class Attachment { + - Long id + - String fileName + - String fileType + - String storedFileName + - Long fileSize + - LocalDateTime uploadDate +} +class Grid { + - Long id + - Integer gridX + - Integer gridY + - String cityName + - String districtName + - String description + - boolean isObstacle +} +class MapGrid { + - Long id + - int x + - int y + - String cityName + - boolean isObstacle + - String terrainType +} +class PasswordResetToken { + - Long id + - String token + - LocalDateTime expiryDate + + isExpired() +} +class OperationLog { + - Long id + - OperationType operationType + - String description + - String targetId + - String targetType + - String ipAddress + - LocalDateTime createdAt +} +class AqiData { + - Long id + - Integer aqiValue + - Double pm25 + - Double pm10 + - String primaryPollutant + - LocalDateTime recordTime +} +class AqiRecord { + - Long id + - String cityName + - Integer aqiValue + - Double pm25 + - Double pm10 + - LocalDateTime recordTime +} +class AssignmentRecord { + - Long id + - AssignmentMethod assignmentMethod + - String algorithmDetails + - AssignmentStatus status + - LocalDateTime createdAt +} +class PollutantThreshold { + - Long id + - PollutionType pollutionType + - String pollutantName + - Double threshold + - String unit +} + +' --- Relationships with Full Details --- + +' --- Composition with Enums --- +UserAccount "1" *-- "1" Gender +UserAccount "1" *-- "1" Role +UserAccount "1" *-- "1" UserStatus +Feedback "1" *-- "1" PollutionType +Feedback "1" *-- "1" SeverityLevel +Feedback "1" *-- "1" FeedbackStatus +Task "1" *-- "1" TaskStatus +Assignment "1" *-- "1" AssignmentStatus +AssignmentRecord "1" *-- "1" AssignmentMethod +OperationLog "1" *-- "1" OperationType +PollutantThreshold "1" *-- "1" PollutionType + +' --- Aggregation & Association --- +Task "1" --> "1" Feedback : "generated from" +Task "1" o-- "1" Assignment : "has" +Task "1" o-- "0..*" TaskHistory : "logs" +Task "1" o-- "0..*" TaskSubmission : "receives" +Feedback "1" o-- "0..*" Attachment : "has" +TaskSubmission "1" o-- "0..*" Attachment : "has" +AssignmentRecord "1" --> "1" Feedback : "for" +UserAccount "1" --> "0..*" Feedback : "submits" +UserAccount "1" --> "0..*" Task : "creates" +Task "0..*" o-- "1" UserAccount : "assigned to" +Assignment "1" --> "1" UserAccount : "assigned by" +TaskHistory "1" --> "1" UserAccount : "changed by" +AssignmentRecord "1" --> "1" UserAccount : "assigned to" +AssignmentRecord "1" --> "1" UserAccount : "assigned by" +OperationLog "1" --> "1" UserAccount : "performed by" +PasswordResetToken "1" --> "1" UserAccount : "for" +AqiData "1" --> "1" UserAccount : "reported by" + +' --- Dependency & Other Associations --- +Task "1" ..> "1" Grid : "located in" +UserAccount "1" ..> "1" Grid : "operates in" +AqiData "1" --> "1" Grid : "recorded at" +AqiRecord "1" --> "1" Grid : "related to" + +@enduml +``` --- - + ### Report/temp3.md - -以下是所有API的URL: - -### 基础URL -`http://localhost:8080` - -### 认证模块 (/api/auth) -- `/api/auth/login` -- `/api/auth/signup` -- `/api/auth/logout` -- `/api/auth/send-verification-code` -- `/api/auth/send-password-reset-code` -- `/api/auth/reset-password-with-code` - -### 仪表盘模块 (/api/dashboard) -- `/api/dashboard/stats` -- `/api/dashboard/reports/aqi-distribution` -- `/api/dashboard/reports/monthly-exceedance-trend` -- `/api/dashboard/reports/grid-coverage` -- `/api/dashboard/reports/pollution-stats` -- `/api/dashboard/reports/task-completion-stats` -- `/api/dashboard/reports/pollutant-monthly-trends` -- `/api/dashboard/map/heatmap` -- `/api/dashboard/map/aqi-heatmap` -- `/api/dashboard/thresholds` -- `/api/dashboard/thresholds/{pollutantName}` - -### 反馈模块 (/api/feedback) -- `/api/feedback/submit` -- `/api/feedback` -- `/api/feedback/{id}` -- `/api/feedback/stats` -- `/api/feedback/{id}/process` - -### 公共接口模块 (/api/public) -- `/api/public/feedback` - -### 文件模块 (/api) -- `/api/files/{filename}` -- `/api/view/{filename}` - -### 网格管理模块 (/api/grids) -- `/api/grids` -- `/api/grids/{id}` -- `/api/grids/coverage` -- `/api/grids/{gridId}/assign` -- `/api/grids/{gridId}/unassign` -- `/api/grids/coordinates/{gridX}/{gridY}/assign` -- `/api/grids/coordinates/{gridX}/{gridY}/unassign` - -### 人员管理模块 (/api/personnel) -- `/api/personnel/users` -- `/api/personnel/users/{userId}` -- `/api/personnel/users/{userId}/role` - -### 网格员任务模块 (/api/worker) -- `/api/worker` -- `/api/worker/{taskId}` -- `/api/worker/{taskId}/accept` -- `/api/worker/{taskId}/submit` - -### 地图与寻路模块 -- `/api/map/grid` -- `/api/map/initialize` -- `/api/pathfinding/find` - -### 个人资料模块 (/api/me) -- `/api/me/feedback` - -### 主管审核模块 (/api/supervisor) -- `/api/supervisor/reviews` -- `/api/supervisor/reviews/{feedbackId}/approve` -- `/api/supervisor/reviews/{feedbackId}/reject` - -### 任务分配模块 (/api/tasks) -- `/api/tasks/unassigned` -- `/api/tasks/grid-workers` -- `/api/tasks/assign` + +以下是所有API的URL: + +### 基础URL +`http://localhost:8080` + +### 认证模块 (/api/auth) +- `/api/auth/login` +- `/api/auth/signup` +- `/api/auth/logout` +- `/api/auth/send-verification-code` +- `/api/auth/send-password-reset-code` +- `/api/auth/reset-password-with-code` + +### 仪表盘模块 (/api/dashboard) +- `/api/dashboard/stats` +- `/api/dashboard/reports/aqi-distribution` +- `/api/dashboard/reports/monthly-exceedance-trend` +- `/api/dashboard/reports/grid-coverage` +- `/api/dashboard/reports/pollution-stats` +- `/api/dashboard/reports/task-completion-stats` +- `/api/dashboard/reports/pollutant-monthly-trends` +- `/api/dashboard/map/heatmap` +- `/api/dashboard/map/aqi-heatmap` +- `/api/dashboard/thresholds` +- `/api/dashboard/thresholds/{pollutantName}` + +### 反馈模块 (/api/feedback) +- `/api/feedback/submit` +- `/api/feedback` +- `/api/feedback/{id}` +- `/api/feedback/stats` +- `/api/feedback/{id}/process` + +### 公共接口模块 (/api/public) +- `/api/public/feedback` + +### 文件模块 (/api) +- `/api/files/{filename}` +- `/api/view/{filename}` + +### 网格管理模块 (/api/grids) +- `/api/grids` +- `/api/grids/{id}` +- `/api/grids/coverage` +- `/api/grids/{gridId}/assign` +- `/api/grids/{gridId}/unassign` +- `/api/grids/coordinates/{gridX}/{gridY}/assign` +- `/api/grids/coordinates/{gridX}/{gridY}/unassign` + +### 人员管理模块 (/api/personnel) +- `/api/personnel/users` +- `/api/personnel/users/{userId}` +- `/api/personnel/users/{userId}/role` + +### 网格员任务模块 (/api/worker) +- `/api/worker` +- `/api/worker/{taskId}` +- `/api/worker/{taskId}/accept` +- `/api/worker/{taskId}/submit` + +### 地图与寻路模块 +- `/api/map/grid` +- `/api/map/initialize` +- `/api/pathfinding/find` + +### 个人资料模块 (/api/me) +- `/api/me/feedback` + +### 主管审核模块 (/api/supervisor) +- `/api/supervisor/reviews` +- `/api/supervisor/reviews/{feedbackId}/approve` +- `/api/supervisor/reviews/{feedbackId}/reject` + +### 任务分配模块 (/api/tasks) +- `/api/tasks/unassigned` +- `/api/tasks/grid-workers` +- `/api/tasks/assign` --- - + ### Report/UML类图.md - -``` -@startuml -class UserAccount { - - Long id - - String name - - String email - - Role role - - UserStatus status - - Integer gridX - - Integer gridY - + isAccountLocked(): boolean - + hasSkill(String skill): boolean -} - -enum Role { ADMIN, SUPERVISOR, GRID_WORKER } -enum UserStatus { ACTIVE, INACTIVE, SUSPENDED } -UserAccount *-- Role -UserAccount *-- UserStatus -@enduml - -``` -@startuml -class Feedback { - - Long id - - String eventId - - String title - - PollutionType pollutionType - - SeverityLevel severityLevel - - FeedbackStatus status - - Long submitterId - + updateStatus(newStatus) - + isHighPriority(): boolean -} - -class Attachment { - + id: Long - + fileName: String - + fileType: String - + storedFileName: String -} - -enum FeedbackStatus { PENDING_REVIEW, PROCESSED, AI_REJECTED, TASK_CREATED, CLOSED } -Feedback "1" -- "*" Attachment : has -Feedback *-- FeedbackStatus -UserAccount "1" -- "*" Feedback : submits -@enduml - -@startuml -class Task { - - Long id - - Long feedbackId - - Long assigneeId - - Long creatorId - - TaskStatus status - - SeverityLevel priority - + assignTo(userId) - + markAsCompleted() -} - - class Assignment { - - Long id - - Long taskId - - Long assigneeId - - AssignmentStatus status - + accept() -} - -class TaskHistory { - - Long id - - Task task - - TaskStatus oldStatus - - TaskStatus newStatus - - UserAccount changedBy -} - -enum TaskStatus { CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED, CANCELED } -Task "1" -- "1" Feedback : created_from -Task "1" -- "*" Assignment -Task "1" -- "*" TaskHistory -UserAccount "1" -- "*" Task : assigned_to -Task *-- TaskStatus -@enduml - -@startuml -class Grid { - - Long id - - Integer gridX - - Integer gridY - - String cityName - - boolean isObstacle -} - -class MapGrid { - - Long id - - int x - - int y - - boolean isObstacle - - String terrainType -} -@enduml -``` - -@startuml -class AqiData { - - Long id - - Grid grid - - Long reporterId - - Integer aqiValue - - String primaryPollutant -} - -class AqiRecord { - - Long id - - String cityName - - Integer aqiValue - - LocalDateTime recordTime -} - -class PollutantThreshold { - - Long id - - String pollutantName - - Double threshold - - String unit -} -@enduml -``` - -@startuml -class OperationLog { - - Long id - - UserAccount user - - OperationType operationType - - String description - - LocalDateTime createdAt -} - -class PasswordResetToken { - - Long id - - String token - - UserAccount user - - LocalDateTime expiryDate - + isExpired(): boolean -} - -class TaskSubmission { - - Long id - - Task task - - String notes - - LocalDateTime submittedAt -} - -class AssignmentRecord { - - Long id - - Feedback feedback - - UserAccount gridWorker - - UserAccount admin - - AssignmentMethod assignmentMethod -} -@enduml + +``` +@startuml +class UserAccount { + - Long id + - String name + - String email + - Role role + - UserStatus status + - Integer gridX + - Integer gridY + + isAccountLocked(): boolean + + hasSkill(String skill): boolean +} + +enum Role { ADMIN, SUPERVISOR, GRID_WORKER } +enum UserStatus { ACTIVE, INACTIVE, SUSPENDED } +UserAccount *-- Role +UserAccount *-- UserStatus +@enduml + +``` +@startuml +class Feedback { + - Long id + - String eventId + - String title + - PollutionType pollutionType + - SeverityLevel severityLevel + - FeedbackStatus status + - Long submitterId + + updateStatus(newStatus) + + isHighPriority(): boolean +} + +class Attachment { + + id: Long + + fileName: String + + fileType: String + + storedFileName: String +} + +enum FeedbackStatus { PENDING_REVIEW, PROCESSED, AI_REJECTED, TASK_CREATED, CLOSED } +Feedback "1" -- "*" Attachment : has +Feedback *-- FeedbackStatus +UserAccount "1" -- "*" Feedback : submits +@enduml + +@startuml +class Task { + - Long id + - Long feedbackId + - Long assigneeId + - Long creatorId + - TaskStatus status + - SeverityLevel priority + + assignTo(userId) + + markAsCompleted() +} + + class Assignment { + - Long id + - Long taskId + - Long assigneeId + - AssignmentStatus status + + accept() +} + +class TaskHistory { + - Long id + - Task task + - TaskStatus oldStatus + - TaskStatus newStatus + - UserAccount changedBy +} + +enum TaskStatus { CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED, CANCELED } +Task "1" -- "1" Feedback : created_from +Task "1" -- "*" Assignment +Task "1" -- "*" TaskHistory +UserAccount "1" -- "*" Task : assigned_to +Task *-- TaskStatus +@enduml + +@startuml +class Grid { + - Long id + - Integer gridX + - Integer gridY + - String cityName + - boolean isObstacle +} + +class MapGrid { + - Long id + - int x + - int y + - boolean isObstacle + - String terrainType +} +@enduml +``` + +@startuml +class AqiData { + - Long id + - Grid grid + - Long reporterId + - Integer aqiValue + - String primaryPollutant +} + +class AqiRecord { + - Long id + - String cityName + - Integer aqiValue + - LocalDateTime recordTime +} + +class PollutantThreshold { + - Long id + - String pollutantName + - Double threshold + - String unit +} +@enduml +``` + +@startuml +class OperationLog { + - Long id + - UserAccount user + - OperationType operationType + - String description + - LocalDateTime createdAt +} + +class PasswordResetToken { + - Long id + - String token + - UserAccount user + - LocalDateTime expiryDate + + isExpired(): boolean +} + +class TaskSubmission { + - Long id + - Task task + - String notes + - LocalDateTime submittedAt +} + +class AssignmentRecord { + - Long id + - Feedback feedback + - UserAccount gridWorker + - UserAccount admin + - AssignmentMethod assignmentMethod +} +@enduml --- - + diff --git a/README.md b/README.md index 89e8e74..dfe06cf 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,47 @@ -# 环境监督系统 (EMS) - -## 📖 项目简介 - -环境监督系统 (EMS) 是一个全栈的Web应用程序,旨在提供一个全面的平台,用于环境事件的报告、监控、管理和分析。系统支持多角色访问,包括管理员、决策者、网格员、监督员和公众监督员,每个角色都有特定的权限和定制化的用户界面。 - -## ✨ 主要功能 - -- **仪表盘**: 为决策者和管理员提供关键指标和数据的可视化概览。 -- **网格化地图**: 在地图上直观地展示和管理环境监督网格。 -- **任务管理**: 网格员可以接收、处理和汇报环境监督任务。 -- **反馈系统**: 公众监督员和监督员可以提交、查看和管理环境问题反馈。 -- **系统管理**: 管理员可以管理用户信息和查看系统操作日志。 -- **个性化设置**: 所有用户都可以查看个人信息和操作日志。 -- **动态主题**: 系统界面颜色会根据用户角色动态变化,提供更好的用户体验。 - -## 🛠️ 技术栈 - -本项目采用前后端分离的架构。 - -### 后端 (ems-backend) - -- **框架**: [Spring Boot 3](https://spring.io/projects/spring-boot) -- **语言**: Java 17 -- **文件存储**: json -- **API文档**: SpringDoc (Swagger UI) -- **身份认证**: Spring Security with JWT -- **构建工具**: Maven - -### 前端 (ems-frontend) - -- **框架**: [Vue 3](https://vuejs.org/) -- **构建工具**: [Vite](https://vitejs.dev/) -- **语言**: TypeScript -- **UI 组件库**: [Element Plus](https://element-plus.org/) -- **状态管理**: [Pinia](https://pinia.vuejs.org/) -- **路由**: [Vue Router](https://router.vuejs.org/) -- **HTTP客户端**: Axios -- **代码风格**: Prettier - -## 📁 项目结构 - -``` -. +# 环境监督系统 (EMS) + +## 📖 项目简介 + +环境监督系统 (EMS) 是一个全栈的Web应用程序,旨在提供一个全面的平台,用于环境事件的报告、监控、管理和分析。系统支持多角色访问,包括管理员、决策者、网格员、监督员和公众监督员,每个角色都有特定的权限和定制化的用户界面。 + +## ✨ 主要功能 + +- **仪表盘**: 为决策者和管理员提供关键指标和数据的可视化概览。 +- **网格化地图**: 在地图上直观地展示和管理环境监督网格。 +- **任务管理**: 网格员可以接收、处理和汇报环境监督任务。 +- **反馈系统**: 公众监督员和监督员可以提交、查看和管理环境问题反馈。 +- **系统管理**: 管理员可以管理用户信息和查看系统操作日志。 +- **个性化设置**: 所有用户都可以查看个人信息和操作日志。 +- **动态主题**: 系统界面颜色会根据用户角色动态变化,提供更好的用户体验。 + +## 🛠️ 技术栈 + +本项目采用前后端分离的架构。 + +### 后端 (ems-backend) + +- **框架**: [Spring Boot 3](https://spring.io/projects/spring-boot) +- **语言**: Java 17 +- **文件存储**: json +- **API文档**: SpringDoc (Swagger UI) +- **身份认证**: Spring Security with JWT +- **构建工具**: Maven + +### 前端 (ems-frontend) + +- **框架**: [Vue 3](https://vuejs.org/) +- **构建工具**: [Vite](https://vitejs.dev/) +- **语言**: TypeScript +- **UI 组件库**: [Element Plus](https://element-plus.org/) +- **状态管理**: [Pinia](https://pinia.vuejs.org/) +- **路由**: [Vue Router](https://router.vuejs.org/) +- **HTTP客户端**: Axios +- **代码风格**: Prettier + +## 📁 项目结构 + +``` +. ├── ems-backend/ # 后端 Spring Boot 项目 │ ├── src/ │ └── pom.xml @@ -51,59 +51,59 @@ │ └── package.json └── README.md # 项目说明文件 ``` - -## 🚀 快速开始 - -### 环境准备 - -- [JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) 或更高版本 -- [Maven 3.8](https://maven.apache.org/download.cgi) 或更高版本 -- [Node.js 22.x](https://nodejs.org/) 或更高版本 - -### 后端启动 - -1. **进入后端目录**: - ```bash - cd ems-backend - ``` - -2. **安装依赖**: - ```bash - mvn install - ``` - -3. **运行项目**: - ```bash - mvn spring-boot:run - ``` - 后端服务将启动在默认端口 (通常是 `8080`)。 - -### 前端启动 - -1. **进入前端目录**: + +## 🚀 快速开始 + +### 环境准备 + +- [JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) 或更高版本 +- [Maven 3.8](https://maven.apache.org/download.cgi) 或更高版本 +- [Node.js 22.x](https://nodejs.org/) 或更高版本 + +### 后端启动 + +1. **进入后端目录**: + ```bash + cd ems-backend + ``` + +2. **安装依赖**: + ```bash + mvn install + ``` + +3. **运行项目**: + ```bash + mvn spring-boot:run + ``` + 后端服务将启动在默认端口 (通常是 `8080`)。 + +### 前端启动 + +1. **进入前端目录**: ```bash cd ems-frontend ``` - -2. **安装依赖**: - ```bash - npm install - ``` - -3. **启动开发服务器**: - ```bash - npm run dev - ``` - 前端应用将启动在 `http://localhost:5173` (或其他Vite指定的端口)。 - -## 👥 用户角色 - -系统预设了以下五种角色,拥有不同的职责和访问权限: - -1. **ADMIN (管理员)**: 拥有最高权限,可以访问所有功能,包括用户管理和系统设置。 -2. **DECISION_MAKER (决策者)**: 关注宏观数据,主要访问仪表盘和地图。 -3. **GRID_WORKER (网格员)**: 负责执行具体任务,主要使用任务管理和地图功能。 -4. **SUPERVISOR (监督员)**: 负责管理和审查收到的反馈信息。 -5. **PUBLIC_SUPERVISOR (公众监督员)**: 可以提交环境问题的反馈。 - + +2. **安装依赖**: + ```bash + npm install + ``` + +3. **启动开发服务器**: + ```bash + npm run dev + ``` + 前端应用将启动在 `http://localhost:5173` (或其他Vite指定的端口)。 + +## 👥 用户角色 + +系统预设了以下五种角色,拥有不同的职责和访问权限: + +1. **ADMIN (管理员)**: 拥有最高权限,可以访问所有功能,包括用户管理和系统设置。 +2. **DECISION_MAKER (决策者)**: 关注宏观数据,主要访问仪表盘和地图。 +3. **GRID_WORKER (网格员)**: 负责执行具体任务,主要使用任务管理和地图功能。 +4. **SUPERVISOR (监督员)**: 负责管理和审查收到的反馈信息。 +5. **PUBLIC_SUPERVISOR (公众监督员)**: 可以提交环境问题的反馈。 + 登录后,系统会根据您的角色,展示不同的菜单和界面主题。 diff --git a/docker-compose.hub.yml b/docker-compose.hub.yml deleted file mode 100644 index fc395d5..0000000 --- a/docker-compose.hub.yml +++ /dev/null @@ -1,26 +0,0 @@ -services: - backend: - image: chuxunyu/ems-backend:latest - ports: - - "8080:8080" - environment: - APP_BASE_URL: ${APP_BASE_URL:-http://localhost:5173} - TOKEN_SIGNING_KEY: ${TOKEN_SIGNING_KEY:-} - JWT_SECRET: ${JWT_SECRET:-} - VOLCANO_API_KEY: ${VOLCANO_API_KEY:-} - SPRING_MAIL_USERNAME: ${SPRING_MAIL_USERNAME:-} - SPRING_MAIL_PASSWORD: ${SPRING_MAIL_PASSWORD:-} - volumes: - - ems_json_db:/app/json-db - - ems_uploads:/app/uploads - - frontend: - image: chuxunyu/ems-frontend:latest - ports: - - "5173:80" - depends_on: - - backend - -volumes: - ems_json_db: - ems_uploads: diff --git a/docs/DOCKER_COMPOSE_USAGE.md b/docs/DOCKER_COMPOSE_USAGE.md new file mode 100644 index 0000000..6af3866 --- /dev/null +++ b/docs/DOCKER_COMPOSE_USAGE.md @@ -0,0 +1,89 @@ +# Docker Compose 使用说明 + +本项目在根目录提供三个 Docker Compose 配置文件,分别面向不同的运行场景。下面说明各自用途与使用方式。 + +## 公共说明 + +- 端口映射: + - 后端:`8080:8080` + - 前端:`5173:80` +- 运行前建议在根目录准备 `.env`(已提供示例),用于配置以下环境变量: + - `APP_BASE_URL` + - `TOKEN_SIGNING_KEY` + - `JWT_SECRET` + - `VOLCANO_API_KEY` + - `SPRING_MAIL_USERNAME` + - `SPRING_MAIL_PASSWORD` + +> 说明:三个 Compose 文件都依赖这些环境变量;其中 `TOKEN_SIGNING_KEY`/`JWT_SECRET` 等不设置会使用空值默认。 + +## 1) `docker-compose.yml`(本地构建) + +用途:本地开发或需要基于源码构建镜像时使用。 + +特点: +- `backend` 和 `frontend` 使用 `build` 从本地目录构建镜像。 +- 挂载 `./ems-backend/json-db` 与 `./ems-backend/uploads` 到容器内,便于持久化数据。 + +使用方式(PowerShell): +```bash +docker compose -f docker-compose.yml up -d --build +``` + +停止并清理(保留挂载目录): +```bash +docker compose -f docker-compose.yml down +``` + +## 2) `docker-compose.hub.yml`(Docker Hub 镜像) + +用途:不想本地构建,直接使用 Docker Hub 公共镜像。 + +特点: +- `backend`/`frontend` 使用 `chuxunyu/ems-backend:latest`、`chuxunyu/ems-frontend:latest`。 +- 使用 Docker 卷 `ems_json_db`、`ems_uploads` 持久化数据。 + +使用方式(PowerShell): +```bash +docker compose -f docker-compose.hub.yml up -d +``` + +停止并清理(保留卷): +```bash +docker compose -f docker-compose.hub.yml down +``` + +如需删除卷(会清空数据): +```bash +docker compose -f docker-compose.hub.yml down -v +``` + +## 3) `docker-compose.private.yml`(私有镜像仓库) + +用途:使用私有镜像仓库 `docker.aizhangz.top` 的镜像部署。 + +特点: +- `backend`/`frontend` 使用 `docker.aizhangz.top/ems-backend:latest`、`docker.aizhangz.top/ems-frontend:latest`。 +- 使用 Docker 卷 `ems_json_db`、`ems_uploads` 持久化数据。 +- 通常需要先登录私有仓库。 + +使用方式(PowerShell): +```bash +docker login docker.aizhangz.top +docker compose -f docker-compose.private.yml up -d +``` + +停止并清理(保留卷): +```bash +docker compose -f docker-compose.private.yml down +``` + +如需删除卷(会清空数据): +```bash +docker compose -f docker-compose.private.yml down -v +``` + +## 常见检查 + +- 前端:访问 `http://localhost:5173` +- 后端:访问 `http://localhost:8080`(可结合后端 Swagger UI) diff --git a/ems-backend/.mvn/wrapper/maven-wrapper.properties b/ems-backend/.mvn/wrapper/maven-wrapper.properties index 2f94e61..b5a4e55 100644 --- a/ems-backend/.mvn/wrapper/maven-wrapper.properties +++ b/ems-backend/.mvn/wrapper/maven-wrapper.properties @@ -1,19 +1,19 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -wrapperVersion=3.3.2 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.10/apache-maven-3.9.10-bin.zip +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.10/apache-maven-3.9.10-bin.zip diff --git a/ems-backend/api测试文档.md b/ems-backend/api测试文档.md index 7313d72..ee58528 100644 --- a/ems-backend/api测试文档.md +++ b/ems-backend/api测试文档.md @@ -1,444 +1,444 @@ -# EMS 后端 API 测试文档 - -本文档基于项目源代码生成,包含了所有后端 API 接口的详细信息,用于开发和测试。 - ---- - -## 目录 -1. [认证模块 (Auth)](#1-认证模块-auth) -2. [仪表盘模块 (Dashboard)](#2-仪表盘模块-dashboard) -3. [反馈模块 (Feedback)](#3-反馈模块-feedback) -4. [文件模块 (File)](#4-文件模块-file) -5. [网格模块 (Grid)](#5-网格模块-grid) -6. [网格员任务模块 (Worker)](#6-网格员任务模块-worker) -7. [地图模块 (Map)](#7-地图模块-map) -8. [路径规划模块 (Pathfinding)](#8-路径规划模块-pathfinding) -9. [人员管理模块 (Personnel)](#9-人员管理模块-personnel) -10. [个人资料模块 (Me)](#10-个人资料模块-me) -11. [公共接口模块 (Public)](#11-公共接口模块-public) -12. [主管模块 (Supervisor)](#12-主管模块-supervisor) -13. [任务分配模块 (Tasks)](#13-任务分配模块-tasks) -14. [任务管理模块 (Management)](#14-任务管理模块-management) - ---- - -## 1. 认证模块 (Auth) -**基础路径**: `/api/auth` - -### 1.1 用户注册 -- **功能描述**: 注册一个新用户。 -- **请求路径**: `/api/auth/signup` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `SignUpRequest` -- **示例请求**: - ```json - { - "name": "测试用户", - "email": "test@example.com", - "phone": "13800138000", - "password": "password123", - "verificationCode": "123456", - "role": "GRID_WORKER" - } - ``` - -### 1.2 用户登录 -- **功能描述**: 用户登录并获取 JWT Token。 -- **请求路径**: `/api/auth/login` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `LoginRequest` -- **示例请求**: - ```json - { - "email": "admin@aizhangz.top", - "password": "Admin@123" - } - ``` - -### 1.3 发送验证码 -- **功能描述**: 请求发送验证码到指定邮箱。 -- **请求路径**: `/api/auth/send-verification-code` -- **请求方法**: `POST` -- **所需权限**: 无 -- **查询参数**: `email` (string, 必需) -- **示例请求**: `POST /api/auth/send-verification-code?email=test@example.com` - -### 1.4 请求重置密码 -- **功能描述**: 请求发送重置密码的链接或令牌。 -- **请求路径**: `/api/auth/request-password-reset` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `PasswordResetRequest` -- **示例请求**: - ```json - { - "email": "test@example.com" - } - ``` - -### 1.5 重置密码 -- **功能描述**: 使用令牌重置密码。 -- **请求路径**: `/api/auth/reset-password` -- **请求方法**: `POST` -- **所需权限**: 无 -- **请求体**: `PasswordResetDto` -- **示例请求**: - ```json - { - "token": "some-reset-token", - "newPassword": "newPassword123" - } - ``` - ---- - -## 2. 仪表盘模块 (Dashboard) -**基础路径**: `/api/dashboard` -**所需权限**: `DECISION_MAKER` - -### 2.1 获取仪表盘统计数据 -- **请求路径**: `/api/dashboard/stats` -- **请求方法**: `GET` - -### 2.2 获取 AQI 分布 -- **请求路径**: `/api/dashboard/reports/aqi-distribution` -- **请求方法**: `GET` - -### 2.3 获取污染超标月度趋势 -- **请求路径**: `/api/dashboard/reports/pollution-trend` -- **请求方法**: `GET` - -### 2.4 获取网格覆盖率 -- **请求路径**: `/api/dashboard/reports/grid-coverage` -- **请求方法**: `GET` - -### 2.5 获取反馈热力图数据 -- **请求路径**: `/api/dashboard/map/heatmap` -- **请求方法**: `GET` - -### 2.6 获取污染类型统计 -- **请求路径**: `/api/dashboard/reports/pollution-stats` -- **请求方法**: `GET` - -### 2.7 获取任务完成情况统计 -- **请求路径**: `/api/dashboard/reports/task-completion-stats` -- **请求方法**: `GET` - -### 2.8 获取 AQI 热力图数据 -- **请求路径**: `/api/dashboard/map/aqi-heatmap` -- **请求方法**: `GET` - ---- - -## 3. 反馈模块 (Feedback) -**基础路径**: `/api/feedback` - -### 3.1 提交反馈 (JSON) -- **功能描述**: 用于测试,使用 JSON 提交反馈。 -- **请求路径**: `/api/feedback/submit-json` -- **请求方法**: `POST` -- **所需权限**: 已认证用户 -- **请求体**: `FeedbackSubmissionRequest` - -### 3.2 提交反馈 (Multipart) -- **功能描述**: 提交反馈,可包含文件。 -- **请求路径**: `/api/feedback/submit` -- **请求方法**: `POST` -- **所需权限**: 已认证用户 -- **请求体**: `multipart/form-data` - - `feedback` (part): `FeedbackSubmissionRequest` (JSON) - - `files` (part, optional): `MultipartFile[]` - -### 3.3 获取所有反馈 -- **功能描述**: 获取所有反馈(分页)。 -- **请求路径**: `/api/feedback` -- **请求方法**: `GET` -- **所需权限**: `ADMIN` -- **查询参数**: `page`, `size`, `sort` - ---- - -## 4. 文件模块 (File) -**基础路径**: `/api/files` -**所需权限**: 公开访问 - -### 4.1 下载文件 -- **请求路径**: `/api/files/download/{fileName}` -- **请求方法**: `GET` -- **路径参数**: `fileName` (string, 必需) - -### 4.2 查看文件 -- **请求路径**: `/api/files/view/{fileName}` -- **请求方法**: `GET` -- **路径参数**: `fileName` (string, 必需) - ---- - -## 5. 网格模块 (Grid) -**基础路径**: `/api/grids` -**所需权限**: `ADMIN` 或 `DECISION_MAKER` - -### 5.1 获取网格列表 -- **请求路径**: `/api/grids` -- **请求方法**: `GET` -- **查询参数**: - - `cityName` (string, 可选) - - `districtName` (string, 可选) - - `page`, `size`, `sort` - -### 5.2 更新网格信息 -- **请求路径**: `/api/grids/{gridId}` -- **请求方法**: `PATCH` -- **所需权限**: `ADMIN` -- **路径参数**: `gridId` (long, 必需) -- **请求体**: `GridUpdateRequest` - ---- - -## 6. 网格员任务模块 (Worker) -**基础路径**: `/api/worker` -**所需权限**: `GRID_WORKER` - -### 6.1 获取我的任务 -- **请求路径**: `/api/worker` -- **请求方法**: `GET` -- **查询参数**: - - `status` (enum: `TaskStatus`, 可选) - - `page`, `size`, `sort` - -### 6.2 获取任务详情 -- **请求路径**: `/api/worker/{taskId}` -- **请求方法**: `GET` -- **路径参数**: `taskId` (long, 必需) - -### 6.3 接受任务 -- **请求路径**: `/api/worker/{taskId}/accept` -- **请求方法**: `POST` -- **路径参数**: `taskId` (long, 必需) - -### 6.4 提交任务 -- **请求路径**: `/api/worker/{taskId}/submit` -- **请求方法**: `POST` -- **路径参数**: `taskId` (long, 必需) -- **请求体**: `TaskSubmissionRequest` - ---- - -## 7. 地图模块 (Map) -**基础路径**: `/api/map` -**所需权限**: 公开访问 - -### 7.1 获取完整地图网格 -- **请求路径**: `/api/map/grid` -- **请求方法**: `GET` - -### 7.2 创建或更新地图单元格 -- **请求路径**: `/api/map/grid` -- **请求方法**: `POST` -- **请求体**: `MapGrid` - -### 7.3 初始化地图 -- **请求路径**: `/api/map/initialize` -- **请求方法**: `POST` -- **查询参数**: - - `width` (int, 可选, 默认 20) - - `height` (int, 可选, 默认 20) - ---- - -## 8. 路径规划模块 (Pathfinding) -**基础路径**: `/api/pathfinding` -**所需权限**: 公开访问 - -### 8.1 寻找路径 -- **功能描述**: 使用 A* 算法在两点之间寻找最短路径。 -- **请求路径**: `/api/pathfinding/find` -- **请求方法**: `POST` -- **请求体**: `PathfindingRequest` -- **示例请求**: - ```json - { - "startX": 0, - "startY": 0, - "endX": 19, - "endY": 19 - } - ``` - ---- - -## 9. 人员管理模块 (Personnel) -**基础路径**: `/api/personnel` -**所需权限**: `ADMIN` - -### 9.1 创建用户 -- **请求路径**: `/api/personnel/users` -- **请求方法**: `POST` -- **请求体**: `UserCreationRequest` - -### 9.2 获取用户列表 -- **请求路径**: `/api/personnel/users` -- **请求方法**: `GET` -- **查询参数**: - - `role` (enum: `Role`, 可选) - - `name` (string, 可选) - - `page`, `size`, `sort` - -### 9.3 获取单个用户 -- **请求路径**: `/api/personnel/users/{userId}` -- **请求方法**: `GET` -- **路径参数**: `userId` (long, 必需) - -### 9.4 更新用户信息 -- **请求路径**: `/api/personnel/users/{userId}` -- **请求方法**: `PATCH` -- **路径参数**: `userId` (long, 必需) -- **请求体**: `UserUpdateRequest` - -### 9.5 更新用户角色 -- **请求路径**: `/api/personnel/users/{userId}/role` -- **请求方法**: `PUT` -- **路径参数**: `userId` (long, 必需) -- **请求体**: `UserRoleUpdateRequest` - -### 9.6 删除用户 -- **请求路径**: `/api/personnel/users/{userId}` -- **请求方法**: `DELETE` -- **路径参数**: `userId` (long, 必需) - ---- - -## 10. 个人资料模块 (Me) -**基础路径**: `/api/me` -**所需权限**: 已认证用户 - -### 10.1 更新个人资料 -- **请求路径**: `/api/me` -- **请求方法**: `PATCH` -- **请求体**: `UserUpdateRequest` - -### 10.2 更新我的位置 -- **请求路径**: `/api/me/location` -- **请求方法**: `POST` -- **请求体**: `LocationUpdateRequest` - -### 10.3 获取我的反馈历史 -- **请求路径**: `/api/me/feedback` -- **请求方法**: `GET` -- **查询参数**: `page`, `size`, `sort` - ---- - -## 11. 公共接口模块 (Public) -**基础路径**: `/api/public` -**所需权限**: 公开访问 - -### 11.1 公众提交反馈 -- **请求路径**: `/api/public/feedback` -- **请求方法**: `POST` -- **请求体**: `multipart/form-data` - - `feedback` (part): `PublicFeedbackRequest` (JSON) - - `files` (part, optional): `MultipartFile[]` - ---- - -## 12. 主管模块 (Supervisor) -**基础路径**: `/api/supervisor` -**所需权限**: `SUPERVISOR` 或 `ADMIN` - -### 12.1 获取待审核的反馈列表 -- **请求路径**: `/api/supervisor/reviews` -- **请求方法**: `GET` - -### 12.2 批准反馈 -- **请求路径**: `/api/supervisor/reviews/{feedbackId}/approve` -- **请求方法**: `POST` -- **路径参数**: `feedbackId` (long, 必需) - -### 12.3 拒绝反馈 -- **请求路径**: `/api/supervisor/reviews/{feedbackId}/reject` -- **请求方法**: `POST` -- **路径参数**: `feedbackId` (long, 必需) - ---- - -## 13. 任务分配模块 (Tasks) -**基础路径**: `/api/tasks` -**所需权限**: `ADMIN` 或 `SUPERVISOR` - -### 13.1 获取未分配的反馈 -- **请求路径**: `/api/tasks/unassigned` -- **请求方法**: `GET` - -### 13.2 获取可用的网格员 -- **请求路径**: `/api/tasks/grid-workers` -- **请求方法**: `GET` - -### 13.3 分配任务 -- **请求路径**: `/api/tasks/assign` -- **请求方法**: `POST` -- **请求体**: - ```json - { - "feedbackId": 1, - "assigneeId": 2 - } - ``` - ---- - -## 14. 任务管理模块 (Management) -**基础路径**: `/api/management/tasks` -**所需权限**: `SUPERVISOR` - -### 14.1 获取任务列表 -- **请求路径**: `/api/management/tasks` -- **请求方法**: `GET` -- **查询参数**: - - `status` (enum: `TaskStatus`, 可选) - - `assigneeId` (long, 可选) - - `severity` (enum: `SeverityLevel`, 可选) - - `pollutionType` (enum: `PollutionType`, 可选) - - `startDate` (date, 可选, 格式: YYYY-MM-DD) - - `endDate` (date, 可选, 格式: YYYY-MM-DD) - - `page`, `size`, `sort` - -### 14.2 分配任务 -- **请求路径**: `/api/management/tasks/{taskId}/assign` -- **请求方法**: `POST` -- **路径参数**: `taskId` (long, 必需) -- **请求体**: `TaskAssignmentRequest` - -### 14.3 获取任务详情 -- **请求路径**: `/api/management/tasks/{taskId}` -- **请求方法**: `GET` -- **路径参数**: `taskId` (long, 必需) - -### 14.4 审核任务 -- **请求路径**: `/api/management/tasks/{taskId}/review` -- **请求方法**: `POST` -- **路径参数**: `taskId` (long, 必需) -- **请求体**: `TaskApprovalRequest` - -### 14.5 取消任务 -- **请求路径**: `/api/management/tasks/{taskId}/cancel` -- **请求方法**: `POST` -- **路径参数**: `taskId` (long, 必需) - -### 14.6 直接创建任务 -- **请求路径**: `/api/management/tasks` -- **请求方法**: `POST` -- **请求体**: `TaskCreationRequest` - -### 14.7 获取待处理的反馈 -- **请求路径**: `/api/management/tasks/feedback` -- **请求方法**: `GET` -- **所需权限**: `SUPERVISOR` 或 `ADMIN` - -### 14.8 从反馈创建任务 -- **请求路径**: `/api/management/tasks/feedback/{feedbackId}/create-task` -- **请求方法**: `POST` -- **路径参数**: `feedbackId` (long, 必需) +# EMS 后端 API 测试文档 + +本文档基于项目源代码生成,包含了所有后端 API 接口的详细信息,用于开发和测试。 + +--- + +## 目录 +1. [认证模块 (Auth)](#1-认证模块-auth) +2. [仪表盘模块 (Dashboard)](#2-仪表盘模块-dashboard) +3. [反馈模块 (Feedback)](#3-反馈模块-feedback) +4. [文件模块 (File)](#4-文件模块-file) +5. [网格模块 (Grid)](#5-网格模块-grid) +6. [网格员任务模块 (Worker)](#6-网格员任务模块-worker) +7. [地图模块 (Map)](#7-地图模块-map) +8. [路径规划模块 (Pathfinding)](#8-路径规划模块-pathfinding) +9. [人员管理模块 (Personnel)](#9-人员管理模块-personnel) +10. [个人资料模块 (Me)](#10-个人资料模块-me) +11. [公共接口模块 (Public)](#11-公共接口模块-public) +12. [主管模块 (Supervisor)](#12-主管模块-supervisor) +13. [任务分配模块 (Tasks)](#13-任务分配模块-tasks) +14. [任务管理模块 (Management)](#14-任务管理模块-management) + +--- + +## 1. 认证模块 (Auth) +**基础路径**: `/api/auth` + +### 1.1 用户注册 +- **功能描述**: 注册一个新用户。 +- **请求路径**: `/api/auth/signup` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `SignUpRequest` +- **示例请求**: + ```json + { + "name": "测试用户", + "email": "test@example.com", + "phone": "13800138000", + "password": "password123", + "verificationCode": "123456", + "role": "GRID_WORKER" + } + ``` + +### 1.2 用户登录 +- **功能描述**: 用户登录并获取 JWT Token。 +- **请求路径**: `/api/auth/login` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `LoginRequest` +- **示例请求**: + ```json + { + "email": "admin@aizhangz.top", + "password": "Admin@123" + } + ``` + +### 1.3 发送验证码 +- **功能描述**: 请求发送验证码到指定邮箱。 +- **请求路径**: `/api/auth/send-verification-code` +- **请求方法**: `POST` +- **所需权限**: 无 +- **查询参数**: `email` (string, 必需) +- **示例请求**: `POST /api/auth/send-verification-code?email=test@example.com` + +### 1.4 请求重置密码 +- **功能描述**: 请求发送重置密码的链接或令牌。 +- **请求路径**: `/api/auth/request-password-reset` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `PasswordResetRequest` +- **示例请求**: + ```json + { + "email": "test@example.com" + } + ``` + +### 1.5 重置密码 +- **功能描述**: 使用令牌重置密码。 +- **请求路径**: `/api/auth/reset-password` +- **请求方法**: `POST` +- **所需权限**: 无 +- **请求体**: `PasswordResetDto` +- **示例请求**: + ```json + { + "token": "some-reset-token", + "newPassword": "newPassword123" + } + ``` + +--- + +## 2. 仪表盘模块 (Dashboard) +**基础路径**: `/api/dashboard` +**所需权限**: `DECISION_MAKER` + +### 2.1 获取仪表盘统计数据 +- **请求路径**: `/api/dashboard/stats` +- **请求方法**: `GET` + +### 2.2 获取 AQI 分布 +- **请求路径**: `/api/dashboard/reports/aqi-distribution` +- **请求方法**: `GET` + +### 2.3 获取污染超标月度趋势 +- **请求路径**: `/api/dashboard/reports/pollution-trend` +- **请求方法**: `GET` + +### 2.4 获取网格覆盖率 +- **请求路径**: `/api/dashboard/reports/grid-coverage` +- **请求方法**: `GET` + +### 2.5 获取反馈热力图数据 +- **请求路径**: `/api/dashboard/map/heatmap` +- **请求方法**: `GET` + +### 2.6 获取污染类型统计 +- **请求路径**: `/api/dashboard/reports/pollution-stats` +- **请求方法**: `GET` + +### 2.7 获取任务完成情况统计 +- **请求路径**: `/api/dashboard/reports/task-completion-stats` +- **请求方法**: `GET` + +### 2.8 获取 AQI 热力图数据 +- **请求路径**: `/api/dashboard/map/aqi-heatmap` +- **请求方法**: `GET` + +--- + +## 3. 反馈模块 (Feedback) +**基础路径**: `/api/feedback` + +### 3.1 提交反馈 (JSON) +- **功能描述**: 用于测试,使用 JSON 提交反馈。 +- **请求路径**: `/api/feedback/submit-json` +- **请求方法**: `POST` +- **所需权限**: 已认证用户 +- **请求体**: `FeedbackSubmissionRequest` + +### 3.2 提交反馈 (Multipart) +- **功能描述**: 提交反馈,可包含文件。 +- **请求路径**: `/api/feedback/submit` +- **请求方法**: `POST` +- **所需权限**: 已认证用户 +- **请求体**: `multipart/form-data` + - `feedback` (part): `FeedbackSubmissionRequest` (JSON) + - `files` (part, optional): `MultipartFile[]` + +### 3.3 获取所有反馈 +- **功能描述**: 获取所有反馈(分页)。 +- **请求路径**: `/api/feedback` +- **请求方法**: `GET` +- **所需权限**: `ADMIN` +- **查询参数**: `page`, `size`, `sort` + +--- + +## 4. 文件模块 (File) +**基础路径**: `/api/files` +**所需权限**: 公开访问 + +### 4.1 下载文件 +- **请求路径**: `/api/files/download/{fileName}` +- **请求方法**: `GET` +- **路径参数**: `fileName` (string, 必需) + +### 4.2 查看文件 +- **请求路径**: `/api/files/view/{fileName}` +- **请求方法**: `GET` +- **路径参数**: `fileName` (string, 必需) + +--- + +## 5. 网格模块 (Grid) +**基础路径**: `/api/grids` +**所需权限**: `ADMIN` 或 `DECISION_MAKER` + +### 5.1 获取网格列表 +- **请求路径**: `/api/grids` +- **请求方法**: `GET` +- **查询参数**: + - `cityName` (string, 可选) + - `districtName` (string, 可选) + - `page`, `size`, `sort` + +### 5.2 更新网格信息 +- **请求路径**: `/api/grids/{gridId}` +- **请求方法**: `PATCH` +- **所需权限**: `ADMIN` +- **路径参数**: `gridId` (long, 必需) +- **请求体**: `GridUpdateRequest` + +--- + +## 6. 网格员任务模块 (Worker) +**基础路径**: `/api/worker` +**所需权限**: `GRID_WORKER` + +### 6.1 获取我的任务 +- **请求路径**: `/api/worker` +- **请求方法**: `GET` +- **查询参数**: + - `status` (enum: `TaskStatus`, 可选) + - `page`, `size`, `sort` + +### 6.2 获取任务详情 +- **请求路径**: `/api/worker/{taskId}` +- **请求方法**: `GET` +- **路径参数**: `taskId` (long, 必需) + +### 6.3 接受任务 +- **请求路径**: `/api/worker/{taskId}/accept` +- **请求方法**: `POST` +- **路径参数**: `taskId` (long, 必需) + +### 6.4 提交任务 +- **请求路径**: `/api/worker/{taskId}/submit` +- **请求方法**: `POST` +- **路径参数**: `taskId` (long, 必需) +- **请求体**: `TaskSubmissionRequest` + +--- + +## 7. 地图模块 (Map) +**基础路径**: `/api/map` +**所需权限**: 公开访问 + +### 7.1 获取完整地图网格 +- **请求路径**: `/api/map/grid` +- **请求方法**: `GET` + +### 7.2 创建或更新地图单元格 +- **请求路径**: `/api/map/grid` +- **请求方法**: `POST` +- **请求体**: `MapGrid` + +### 7.3 初始化地图 +- **请求路径**: `/api/map/initialize` +- **请求方法**: `POST` +- **查询参数**: + - `width` (int, 可选, 默认 20) + - `height` (int, 可选, 默认 20) + +--- + +## 8. 路径规划模块 (Pathfinding) +**基础路径**: `/api/pathfinding` +**所需权限**: 公开访问 + +### 8.1 寻找路径 +- **功能描述**: 使用 A* 算法在两点之间寻找最短路径。 +- **请求路径**: `/api/pathfinding/find` +- **请求方法**: `POST` +- **请求体**: `PathfindingRequest` +- **示例请求**: + ```json + { + "startX": 0, + "startY": 0, + "endX": 19, + "endY": 19 + } + ``` + +--- + +## 9. 人员管理模块 (Personnel) +**基础路径**: `/api/personnel` +**所需权限**: `ADMIN` + +### 9.1 创建用户 +- **请求路径**: `/api/personnel/users` +- **请求方法**: `POST` +- **请求体**: `UserCreationRequest` + +### 9.2 获取用户列表 +- **请求路径**: `/api/personnel/users` +- **请求方法**: `GET` +- **查询参数**: + - `role` (enum: `Role`, 可选) + - `name` (string, 可选) + - `page`, `size`, `sort` + +### 9.3 获取单个用户 +- **请求路径**: `/api/personnel/users/{userId}` +- **请求方法**: `GET` +- **路径参数**: `userId` (long, 必需) + +### 9.4 更新用户信息 +- **请求路径**: `/api/personnel/users/{userId}` +- **请求方法**: `PATCH` +- **路径参数**: `userId` (long, 必需) +- **请求体**: `UserUpdateRequest` + +### 9.5 更新用户角色 +- **请求路径**: `/api/personnel/users/{userId}/role` +- **请求方法**: `PUT` +- **路径参数**: `userId` (long, 必需) +- **请求体**: `UserRoleUpdateRequest` + +### 9.6 删除用户 +- **请求路径**: `/api/personnel/users/{userId}` +- **请求方法**: `DELETE` +- **路径参数**: `userId` (long, 必需) + +--- + +## 10. 个人资料模块 (Me) +**基础路径**: `/api/me` +**所需权限**: 已认证用户 + +### 10.1 更新个人资料 +- **请求路径**: `/api/me` +- **请求方法**: `PATCH` +- **请求体**: `UserUpdateRequest` + +### 10.2 更新我的位置 +- **请求路径**: `/api/me/location` +- **请求方法**: `POST` +- **请求体**: `LocationUpdateRequest` + +### 10.3 获取我的反馈历史 +- **请求路径**: `/api/me/feedback` +- **请求方法**: `GET` +- **查询参数**: `page`, `size`, `sort` + +--- + +## 11. 公共接口模块 (Public) +**基础路径**: `/api/public` +**所需权限**: 公开访问 + +### 11.1 公众提交反馈 +- **请求路径**: `/api/public/feedback` +- **请求方法**: `POST` +- **请求体**: `multipart/form-data` + - `feedback` (part): `PublicFeedbackRequest` (JSON) + - `files` (part, optional): `MultipartFile[]` + +--- + +## 12. 主管模块 (Supervisor) +**基础路径**: `/api/supervisor` +**所需权限**: `SUPERVISOR` 或 `ADMIN` + +### 12.1 获取待审核的反馈列表 +- **请求路径**: `/api/supervisor/reviews` +- **请求方法**: `GET` + +### 12.2 批准反馈 +- **请求路径**: `/api/supervisor/reviews/{feedbackId}/approve` +- **请求方法**: `POST` +- **路径参数**: `feedbackId` (long, 必需) + +### 12.3 拒绝反馈 +- **请求路径**: `/api/supervisor/reviews/{feedbackId}/reject` +- **请求方法**: `POST` +- **路径参数**: `feedbackId` (long, 必需) + +--- + +## 13. 任务分配模块 (Tasks) +**基础路径**: `/api/tasks` +**所需权限**: `ADMIN` 或 `SUPERVISOR` + +### 13.1 获取未分配的反馈 +- **请求路径**: `/api/tasks/unassigned` +- **请求方法**: `GET` + +### 13.2 获取可用的网格员 +- **请求路径**: `/api/tasks/grid-workers` +- **请求方法**: `GET` + +### 13.3 分配任务 +- **请求路径**: `/api/tasks/assign` +- **请求方法**: `POST` +- **请求体**: + ```json + { + "feedbackId": 1, + "assigneeId": 2 + } + ``` + +--- + +## 14. 任务管理模块 (Management) +**基础路径**: `/api/management/tasks` +**所需权限**: `SUPERVISOR` + +### 14.1 获取任务列表 +- **请求路径**: `/api/management/tasks` +- **请求方法**: `GET` +- **查询参数**: + - `status` (enum: `TaskStatus`, 可选) + - `assigneeId` (long, 可选) + - `severity` (enum: `SeverityLevel`, 可选) + - `pollutionType` (enum: `PollutionType`, 可选) + - `startDate` (date, 可选, 格式: YYYY-MM-DD) + - `endDate` (date, 可选, 格式: YYYY-MM-DD) + - `page`, `size`, `sort` + +### 14.2 分配任务 +- **请求路径**: `/api/management/tasks/{taskId}/assign` +- **请求方法**: `POST` +- **路径参数**: `taskId` (long, 必需) +- **请求体**: `TaskAssignmentRequest` + +### 14.3 获取任务详情 +- **请求路径**: `/api/management/tasks/{taskId}` +- **请求方法**: `GET` +- **路径参数**: `taskId` (long, 必需) + +### 14.4 审核任务 +- **请求路径**: `/api/management/tasks/{taskId}/review` +- **请求方法**: `POST` +- **路径参数**: `taskId` (long, 必需) +- **请求体**: `TaskApprovalRequest` + +### 14.5 取消任务 +- **请求路径**: `/api/management/tasks/{taskId}/cancel` +- **请求方法**: `POST` +- **路径参数**: `taskId` (long, 必需) + +### 14.6 直接创建任务 +- **请求路径**: `/api/management/tasks` +- **请求方法**: `POST` +- **请求体**: `TaskCreationRequest` + +### 14.7 获取待处理的反馈 +- **请求路径**: `/api/management/tasks/feedback` +- **请求方法**: `GET` +- **所需权限**: `SUPERVISOR` 或 `ADMIN` + +### 14.8 从反馈创建任务 +- **请求路径**: `/api/management/tasks/feedback/{feedbackId}/create-task` +- **请求方法**: `POST` +- **路径参数**: `feedbackId` (long, 必需) - **请求体**: `TaskFromFeedbackRequest` \ No newline at end of file diff --git a/ems-backend/json-db/aqi_records.json b/ems-backend/json-db/aqi_records.json index 63d910e..5bb88be 100644 --- a/ems-backend/json-db/aqi_records.json +++ b/ems-backend/json-db/aqi_records.json @@ -1,817 +1,817 @@ -[ { - "id" : 1, - "cityName" : "北京市", - "aqiValue" : 40, - "recordTime" : "2025-06-23T23:34:02.3973662", - "pm25" : 10.5, - "pm10" : 20.3, - "so2" : 5.1, - "no2" : 15.2, - "co" : 0.8, - "o3" : 30.1, - "latitude" : null, - "longitude" : null, - "gridX" : 0, - "gridY" : 0, - "grid" : null -}, { - "id" : 2, - "cityName" : "上海市", - "aqiValue" : 35, - "recordTime" : "2025-06-22T23:34:02.3973662", - "pm25" : 9.2, - "pm10" : 18.5, - "so2" : 4.8, - "no2" : 14.0, - "co" : 0.7, - "o3" : 28.5, - "latitude" : null, - "longitude" : null, - "gridX" : 0, - "gridY" : 1, - "grid" : null -}, { - "id" : 3, - "cityName" : "广州市", - "aqiValue" : 75, - "recordTime" : "2025-06-21T23:34:02.3973662", - "pm25" : 25.3, - "pm10" : 45.7, - "so2" : 10.2, - "no2" : 30.5, - "co" : 1.5, - "o3" : 60.2, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 0, - "grid" : null -}, { - "id" : 4, - "cityName" : "深圳市", - "aqiValue" : 85, - "recordTime" : "2025-06-20T23:34:02.3973662", - "pm25" : 28.6, - "pm10" : 52.1, - "so2" : 12.3, - "no2" : 35.8, - "co" : 1.8, - "o3" : 68.5, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 1, - "grid" : null -}, { - "id" : 5, - "cityName" : "成都市", - "aqiValue" : 130, - "recordTime" : "2025-06-19T23:34:02.3973662", - "pm25" : 45.2, - "pm10" : 85.3, - "so2" : 20.5, - "no2" : 55.8, - "co" : 3.2, - "o3" : 90.5, - "latitude" : null, - "longitude" : null, - "gridX" : 2, - "gridY" : 0, - "grid" : null -}, { - "id" : 6, - "cityName" : "武汉市", - "aqiValue" : 175, - "recordTime" : "2025-06-18T23:34:02.3973662", - "pm25" : 60.5, - "pm10" : 110.8, - "so2" : 30.2, - "no2" : 70.5, - "co" : 4.5, - "o3" : 120.3, - "latitude" : null, - "longitude" : null, - "gridX" : 2, - "gridY" : 1, - "grid" : null -}, { - "id" : 7, - "cityName" : "西安市", - "aqiValue" : 250, - "recordTime" : "2025-06-17T23:34:02.3973662", - "pm25" : 90.3, - "pm10" : 160.5, - "so2" : 45.8, - "no2" : 95.2, - "co" : 6.8, - "o3" : 150.5, - "latitude" : null, - "longitude" : null, - "gridX" : 3, - "gridY" : 0, - "grid" : null -}, { - "id" : 8, - "cityName" : "兰州市", - "aqiValue" : 320, - "recordTime" : "2025-06-16T23:34:02.3973662", - "pm25" : 120.5, - "pm10" : 200.8, - "so2" : 60.2, - "no2" : 120.5, - "co" : 8.5, - "o3" : 180.3, - "latitude" : null, - "longitude" : null, - "gridX" : 3, - "gridY" : 1, - "grid" : null -}, { - "id" : 9, - "cityName" : "北京市", - "aqiValue" : 50, - "recordTime" : "2025-06-24T23:34:02.4173684", - "pm25" : 20.0, - "pm10" : 40.0, - "so2" : 8.0, - "no2" : 25.0, - "co" : 1.0, - "o3" : 50.0, - "latitude" : null, - "longitude" : null, - "gridX" : 0, - "gridY" : 0, - "grid" : null -}, { - "id" : 10, - "cityName" : "北京市", - "aqiValue" : 65, - "recordTime" : "2025-06-23T23:34:02.4173684", - "pm25" : 20.0, - "pm10" : 42.0, - "so2" : 8.0, - "no2" : 26.0, - "co" : 1.0, - "o3" : 53.0, - "latitude" : null, - "longitude" : null, - "gridX" : 0, - "gridY" : 1, - "grid" : null -}, { - "id" : 11, - "cityName" : "北京市", - "aqiValue" : 80, - "recordTime" : "2025-06-22T23:34:02.4183691", - "pm25" : 20.0, - "pm10" : 44.0, - "so2" : 8.0, - "no2" : 27.0, - "co" : 1.0, - "o3" : 56.0, - "latitude" : null, - "longitude" : null, - "gridX" : 0, - "gridY" : 2, - "grid" : null -}, { - "id" : 12, - "cityName" : "北京市", - "aqiValue" : 95, - "recordTime" : "2025-06-21T23:34:02.4183691", - "pm25" : 20.0, - "pm10" : 46.0, - "so2" : 8.0, - "no2" : 28.0, - "co" : 1.0, - "o3" : 59.0, - "latitude" : null, - "longitude" : null, - "gridX" : 0, - "gridY" : 3, - "grid" : null -}, { - "id" : 13, - "cityName" : "北京市", - "aqiValue" : 110, - "recordTime" : "2025-06-20T23:34:02.4183691", - "pm25" : 20.0, - "pm10" : 48.0, - "so2" : 8.0, - "no2" : 29.0, - "co" : 1.0, - "o3" : 62.0, - "latitude" : null, - "longitude" : null, - "gridX" : 0, - "gridY" : 4, - "grid" : null -}, { - "id" : 14, - "cityName" : "北京市", - "aqiValue" : 65, - "recordTime" : "2025-06-23T23:34:02.4183691", - "pm25" : 21.0, - "pm10" : 40.0, - "so2" : 9.0, - "no2" : 25.0, - "co" : 1.2, - "o3" : 50.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 0, - "grid" : null -}, { - "id" : 15, - "cityName" : "北京市", - "aqiValue" : 80, - "recordTime" : "2025-06-22T23:34:02.4183691", - "pm25" : 21.0, - "pm10" : 42.0, - "so2" : 9.0, - "no2" : 26.0, - "co" : 1.2, - "o3" : 53.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 1, - "grid" : null -}, { - "id" : 16, - "cityName" : "北京市", - "aqiValue" : 95, - "recordTime" : "2025-06-21T23:34:02.4183691", - "pm25" : 21.0, - "pm10" : 44.0, - "so2" : 9.0, - "no2" : 27.0, - "co" : 1.2, - "o3" : 56.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 2, - "grid" : null -}, { - "id" : 17, - "cityName" : "北京市", - "aqiValue" : 110, - "recordTime" : "2025-06-20T23:34:02.4183691", - "pm25" : 21.0, - "pm10" : 46.0, - "so2" : 9.0, - "no2" : 28.0, - "co" : 1.2, - "o3" : 59.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 3, - "grid" : null -}, { - "id" : 18, - "cityName" : "北京市", - "aqiValue" : 125, - "recordTime" : "2025-06-19T23:34:02.4183691", - "pm25" : 21.0, - "pm10" : 48.0, - "so2" : 9.0, - "no2" : 29.0, - "co" : 1.2, - "o3" : 62.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 4, - "grid" : null -}, { - "id" : 19, - "cityName" : "北京市", - "aqiValue" : 80, - "recordTime" : "2025-06-22T23:34:02.4183691", - "pm25" : 22.0, - "pm10" : 40.0, - "so2" : 10.0, - "no2" : 25.0, - "co" : 1.4, - "o3" : 50.0, - "latitude" : null, - "longitude" : null, - "gridX" : 2, - "gridY" : 0, - "grid" : null -}, { - "id" : 20, - "cityName" : "北京市", - "aqiValue" : 95, - "recordTime" : "2025-06-21T23:34:02.4183691", - "pm25" : 22.0, - "pm10" : 42.0, - "so2" : 10.0, - "no2" : 26.0, - "co" : 1.4, - "o3" : 53.0, - "latitude" : null, - "longitude" : null, - "gridX" : 2, - "gridY" : 1, - "grid" : null -}, { - "id" : 21, - "cityName" : "北京市", - "aqiValue" : 110, - "recordTime" : "2025-06-20T23:34:02.4183691", - "pm25" : 22.0, - "pm10" : 44.0, - "so2" : 10.0, - "no2" : 27.0, - "co" : 1.4, - "o3" : 56.0, - "latitude" : null, - "longitude" : null, - "gridX" : 2, - "gridY" : 2, - "grid" : null -}, { - "id" : 22, - "cityName" : "北京市", - "aqiValue" : 125, - "recordTime" : "2025-06-19T23:34:02.4183691", - "pm25" : 22.0, - "pm10" : 46.0, - "so2" : 10.0, - "no2" : 28.0, - "co" : 1.4, - "o3" : 59.0, - "latitude" : null, - "longitude" : null, - "gridX" : 2, - "gridY" : 3, - "grid" : null -}, { - "id" : 23, - "cityName" : "北京市", - "aqiValue" : 140, - "recordTime" : "2025-06-18T23:34:02.4183691", - "pm25" : 22.0, - "pm10" : 48.0, - "so2" : 10.0, - "no2" : 29.0, - "co" : 1.4, - "o3" : 62.0, - "latitude" : null, - "longitude" : null, - "gridX" : 2, - "gridY" : 4, - "grid" : null -}, { - "id" : 24, - "cityName" : "北京市", - "aqiValue" : 95, - "recordTime" : "2025-06-21T23:34:02.4183691", - "pm25" : 23.0, - "pm10" : 40.0, - "so2" : 11.0, - "no2" : 25.0, - "co" : 1.6, - "o3" : 50.0, - "latitude" : null, - "longitude" : null, - "gridX" : 3, - "gridY" : 0, - "grid" : null -}, { - "id" : 25, - "cityName" : "北京市", - "aqiValue" : 110, - "recordTime" : "2025-06-20T23:34:02.4183691", - "pm25" : 23.0, - "pm10" : 42.0, - "so2" : 11.0, - "no2" : 26.0, - "co" : 1.6, - "o3" : 53.0, - "latitude" : null, - "longitude" : null, - "gridX" : 3, - "gridY" : 1, - "grid" : null -}, { - "id" : 26, - "cityName" : "北京市", - "aqiValue" : 125, - "recordTime" : "2025-06-19T23:34:02.4183691", - "pm25" : 23.0, - "pm10" : 44.0, - "so2" : 11.0, - "no2" : 27.0, - "co" : 1.6, - "o3" : 56.0, - "latitude" : null, - "longitude" : null, - "gridX" : 3, - "gridY" : 2, - "grid" : null -}, { - "id" : 27, - "cityName" : "北京市", - "aqiValue" : 140, - "recordTime" : "2025-06-18T23:34:02.4183691", - "pm25" : 23.0, - "pm10" : 46.0, - "so2" : 11.0, - "no2" : 28.0, - "co" : 1.6, - "o3" : 59.0, - "latitude" : null, - "longitude" : null, - "gridX" : 3, - "gridY" : 3, - "grid" : null -}, { - "id" : 28, - "cityName" : "北京市", - "aqiValue" : 155, - "recordTime" : "2025-06-17T23:34:02.4183691", - "pm25" : 23.0, - "pm10" : 48.0, - "so2" : 11.0, - "no2" : 29.0, - "co" : 1.6, - "o3" : 62.0, - "latitude" : null, - "longitude" : null, - "gridX" : 3, - "gridY" : 4, - "grid" : null -}, { - "id" : 29, - "cityName" : "北京市", - "aqiValue" : 110, - "recordTime" : "2025-06-20T23:34:02.4183691", - "pm25" : 24.0, - "pm10" : 40.0, - "so2" : 12.0, - "no2" : 25.0, - "co" : 1.8, - "o3" : 50.0, - "latitude" : null, - "longitude" : null, - "gridX" : 4, - "gridY" : 0, - "grid" : null -}, { - "id" : 30, - "cityName" : "北京市", - "aqiValue" : 125, - "recordTime" : "2025-06-19T23:34:02.4183691", - "pm25" : 24.0, - "pm10" : 42.0, - "so2" : 12.0, - "no2" : 26.0, - "co" : 1.8, - "o3" : 53.0, - "latitude" : null, - "longitude" : null, - "gridX" : 4, - "gridY" : 1, - "grid" : null -}, { - "id" : 31, - "cityName" : "北京市", - "aqiValue" : 140, - "recordTime" : "2025-06-18T23:34:02.4183691", - "pm25" : 24.0, - "pm10" : 44.0, - "so2" : 12.0, - "no2" : 27.0, - "co" : 1.8, - "o3" : 56.0, - "latitude" : null, - "longitude" : null, - "gridX" : 4, - "gridY" : 2, - "grid" : null -}, { - "id" : 32, - "cityName" : "北京市", - "aqiValue" : 155, - "recordTime" : "2025-06-17T23:34:02.4183691", - "pm25" : 24.0, - "pm10" : 46.0, - "so2" : 12.0, - "no2" : 28.0, - "co" : 1.8, - "o3" : 59.0, - "latitude" : null, - "longitude" : null, - "gridX" : 4, - "gridY" : 3, - "grid" : null -}, { - "id" : 33, - "cityName" : "北京市", - "aqiValue" : 170, - "recordTime" : "2025-06-16T23:34:02.4183691", - "pm25" : 24.0, - "pm10" : 48.0, - "so2" : 12.0, - "no2" : 29.0, - "co" : 1.8, - "o3" : 62.0, - "latitude" : null, - "longitude" : null, - "gridX" : 4, - "gridY" : 4, - "grid" : null -}, { - "id" : 34, - "cityName" : "北京市", - "aqiValue" : 100, - "recordTime" : "2025-05-24T23:34:02.4574586", - "pm25" : 31.0, - "pm10" : 62.0, - "so2" : 13.0, - "no2" : 36.0, - "co" : 1.7, - "o3" : 73.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 0, - "grid" : null -}, { - "id" : 35, - "cityName" : "北京市", - "aqiValue" : 105, - "recordTime" : "2025-05-29T23:34:02.4574586", - "pm25" : 31.0, - "pm10" : 62.0, - "so2" : 13.0, - "no2" : 36.0, - "co" : 1.7, - "o3" : 73.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 1, - "grid" : null -}, { - "id" : 36, - "cityName" : "北京市", - "aqiValue" : 110, - "recordTime" : "2025-06-03T23:34:02.4574586", - "pm25" : 31.0, - "pm10" : 62.0, - "so2" : 13.0, - "no2" : 36.0, - "co" : 1.7, - "o3" : 73.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 2, - "grid" : null -}, { - "id" : 37, - "cityName" : "北京市", - "aqiValue" : 120, - "recordTime" : "2025-04-24T23:34:02.4574586", - "pm25" : 32.0, - "pm10" : 64.0, - "so2" : 14.0, - "no2" : 37.0, - "co" : 1.9, - "o3" : 76.0, - "latitude" : null, - "longitude" : null, - "gridX" : 2, - "gridY" : 0, - "grid" : null -}, { - "id" : 38, - "cityName" : "北京市", - "aqiValue" : 125, - "recordTime" : "2025-04-29T23:34:02.4574586", - "pm25" : 32.0, - "pm10" : 64.0, - "so2" : 14.0, - "no2" : 37.0, - "co" : 1.9, - "o3" : 76.0, - "latitude" : null, - "longitude" : null, - "gridX" : 2, - "gridY" : 1, - "grid" : null -}, { - "id" : 39, - "cityName" : "北京市", - "aqiValue" : 130, - "recordTime" : "2025-05-04T23:34:02.4574586", - "pm25" : 32.0, - "pm10" : 64.0, - "so2" : 14.0, - "no2" : 37.0, - "co" : 1.9, - "o3" : 76.0, - "latitude" : null, - "longitude" : null, - "gridX" : 2, - "gridY" : 2, - "grid" : null -}, { - "id" : 40, - "cityName" : "北京市", - "aqiValue" : 140, - "recordTime" : "2025-03-24T23:34:02.4574586", - "pm25" : 33.0, - "pm10" : 66.0, - "so2" : 15.0, - "no2" : 38.0, - "co" : 2.1, - "o3" : 79.0, - "latitude" : null, - "longitude" : null, - "gridX" : 3, - "gridY" : 0, - "grid" : null -}, { - "id" : 41, - "cityName" : "北京市", - "aqiValue" : 145, - "recordTime" : "2025-03-29T23:34:02.4574586", - "pm25" : 33.0, - "pm10" : 66.0, - "so2" : 15.0, - "no2" : 38.0, - "co" : 2.1, - "o3" : 79.0, - "latitude" : null, - "longitude" : null, - "gridX" : 3, - "gridY" : 1, - "grid" : null -}, { - "id" : 42, - "cityName" : "北京市", - "aqiValue" : 150, - "recordTime" : "2025-04-03T23:34:02.4574586", - "pm25" : 33.0, - "pm10" : 66.0, - "so2" : 15.0, - "no2" : 38.0, - "co" : 2.1, - "o3" : 79.0, - "latitude" : null, - "longitude" : null, - "gridX" : 3, - "gridY" : 2, - "grid" : null -}, { - "id" : 43, - "cityName" : "北京市", - "aqiValue" : 160, - "recordTime" : "2025-02-24T23:34:02.4574586", - "pm25" : 34.0, - "pm10" : 68.0, - "so2" : 16.0, - "no2" : 39.0, - "co" : 2.3, - "o3" : 82.0, - "latitude" : null, - "longitude" : null, - "gridX" : 4, - "gridY" : 0, - "grid" : null -}, { - "id" : 44, - "cityName" : "北京市", - "aqiValue" : 165, - "recordTime" : "2025-03-01T23:34:02.4574586", - "pm25" : 34.0, - "pm10" : 68.0, - "so2" : 16.0, - "no2" : 39.0, - "co" : 2.3, - "o3" : 82.0, - "latitude" : null, - "longitude" : null, - "gridX" : 4, - "gridY" : 1, - "grid" : null -}, { - "id" : 45, - "cityName" : "北京市", - "aqiValue" : 170, - "recordTime" : "2025-03-06T23:34:02.4574586", - "pm25" : 34.0, - "pm10" : 68.0, - "so2" : 16.0, - "no2" : 39.0, - "co" : 2.3, - "o3" : 82.0, - "latitude" : null, - "longitude" : null, - "gridX" : 4, - "gridY" : 2, - "grid" : null -}, { - "id" : 46, - "cityName" : "北京市", - "aqiValue" : 180, - "recordTime" : "2025-01-24T23:34:02.4574586", - "pm25" : 35.0, - "pm10" : 70.0, - "so2" : 17.0, - "no2" : 40.0, - "co" : 2.5, - "o3" : 85.0, - "latitude" : null, - "longitude" : null, - "gridX" : 0, - "gridY" : 0, - "grid" : null -}, { - "id" : 47, - "cityName" : "北京市", - "aqiValue" : 185, - "recordTime" : "2025-01-29T23:34:02.4574586", - "pm25" : 35.0, - "pm10" : 70.0, - "so2" : 17.0, - "no2" : 40.0, - "co" : 2.5, - "o3" : 85.0, - "latitude" : null, - "longitude" : null, - "gridX" : 0, - "gridY" : 1, - "grid" : null -}, { - "id" : 48, - "cityName" : "北京市", - "aqiValue" : 190, - "recordTime" : "2025-02-03T23:34:02.4574586", - "pm25" : 35.0, - "pm10" : 70.0, - "so2" : 17.0, - "no2" : 40.0, - "co" : 2.5, - "o3" : 85.0, - "latitude" : null, - "longitude" : null, - "gridX" : 0, - "gridY" : 2, - "grid" : null -}, { - "id" : 49, - "cityName" : "北京市", - "aqiValue" : 200, - "recordTime" : "2024-12-24T23:34:02.4574586", - "pm25" : 36.0, - "pm10" : 72.0, - "so2" : 18.0, - "no2" : 41.0, - "co" : 2.7, - "o3" : 88.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 0, - "grid" : null -}, { - "id" : 50, - "cityName" : "北京市", - "aqiValue" : 205, - "recordTime" : "2024-12-29T23:34:02.4574586", - "pm25" : 36.0, - "pm10" : 72.0, - "so2" : 18.0, - "no2" : 41.0, - "co" : 2.7, - "o3" : 88.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 1, - "grid" : null -}, { - "id" : 51, - "cityName" : "北京市", - "aqiValue" : 210, - "recordTime" : "2025-01-03T23:34:02.4574586", - "pm25" : 36.0, - "pm10" : 72.0, - "so2" : 18.0, - "no2" : 41.0, - "co" : 2.7, - "o3" : 88.0, - "latitude" : null, - "longitude" : null, - "gridX" : 1, - "gridY" : 2, - "grid" : null +[ { + "id" : 1, + "cityName" : "北京市", + "aqiValue" : 40, + "recordTime" : "2025-06-23T23:34:02.3973662", + "pm25" : 10.5, + "pm10" : 20.3, + "so2" : 5.1, + "no2" : 15.2, + "co" : 0.8, + "o3" : 30.1, + "latitude" : null, + "longitude" : null, + "gridX" : 0, + "gridY" : 0, + "grid" : null +}, { + "id" : 2, + "cityName" : "上海市", + "aqiValue" : 35, + "recordTime" : "2025-06-22T23:34:02.3973662", + "pm25" : 9.2, + "pm10" : 18.5, + "so2" : 4.8, + "no2" : 14.0, + "co" : 0.7, + "o3" : 28.5, + "latitude" : null, + "longitude" : null, + "gridX" : 0, + "gridY" : 1, + "grid" : null +}, { + "id" : 3, + "cityName" : "广州市", + "aqiValue" : 75, + "recordTime" : "2025-06-21T23:34:02.3973662", + "pm25" : 25.3, + "pm10" : 45.7, + "so2" : 10.2, + "no2" : 30.5, + "co" : 1.5, + "o3" : 60.2, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 0, + "grid" : null +}, { + "id" : 4, + "cityName" : "深圳市", + "aqiValue" : 85, + "recordTime" : "2025-06-20T23:34:02.3973662", + "pm25" : 28.6, + "pm10" : 52.1, + "so2" : 12.3, + "no2" : 35.8, + "co" : 1.8, + "o3" : 68.5, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 1, + "grid" : null +}, { + "id" : 5, + "cityName" : "成都市", + "aqiValue" : 130, + "recordTime" : "2025-06-19T23:34:02.3973662", + "pm25" : 45.2, + "pm10" : 85.3, + "so2" : 20.5, + "no2" : 55.8, + "co" : 3.2, + "o3" : 90.5, + "latitude" : null, + "longitude" : null, + "gridX" : 2, + "gridY" : 0, + "grid" : null +}, { + "id" : 6, + "cityName" : "武汉市", + "aqiValue" : 175, + "recordTime" : "2025-06-18T23:34:02.3973662", + "pm25" : 60.5, + "pm10" : 110.8, + "so2" : 30.2, + "no2" : 70.5, + "co" : 4.5, + "o3" : 120.3, + "latitude" : null, + "longitude" : null, + "gridX" : 2, + "gridY" : 1, + "grid" : null +}, { + "id" : 7, + "cityName" : "西安市", + "aqiValue" : 250, + "recordTime" : "2025-06-17T23:34:02.3973662", + "pm25" : 90.3, + "pm10" : 160.5, + "so2" : 45.8, + "no2" : 95.2, + "co" : 6.8, + "o3" : 150.5, + "latitude" : null, + "longitude" : null, + "gridX" : 3, + "gridY" : 0, + "grid" : null +}, { + "id" : 8, + "cityName" : "兰州市", + "aqiValue" : 320, + "recordTime" : "2025-06-16T23:34:02.3973662", + "pm25" : 120.5, + "pm10" : 200.8, + "so2" : 60.2, + "no2" : 120.5, + "co" : 8.5, + "o3" : 180.3, + "latitude" : null, + "longitude" : null, + "gridX" : 3, + "gridY" : 1, + "grid" : null +}, { + "id" : 9, + "cityName" : "北京市", + "aqiValue" : 50, + "recordTime" : "2025-06-24T23:34:02.4173684", + "pm25" : 20.0, + "pm10" : 40.0, + "so2" : 8.0, + "no2" : 25.0, + "co" : 1.0, + "o3" : 50.0, + "latitude" : null, + "longitude" : null, + "gridX" : 0, + "gridY" : 0, + "grid" : null +}, { + "id" : 10, + "cityName" : "北京市", + "aqiValue" : 65, + "recordTime" : "2025-06-23T23:34:02.4173684", + "pm25" : 20.0, + "pm10" : 42.0, + "so2" : 8.0, + "no2" : 26.0, + "co" : 1.0, + "o3" : 53.0, + "latitude" : null, + "longitude" : null, + "gridX" : 0, + "gridY" : 1, + "grid" : null +}, { + "id" : 11, + "cityName" : "北京市", + "aqiValue" : 80, + "recordTime" : "2025-06-22T23:34:02.4183691", + "pm25" : 20.0, + "pm10" : 44.0, + "so2" : 8.0, + "no2" : 27.0, + "co" : 1.0, + "o3" : 56.0, + "latitude" : null, + "longitude" : null, + "gridX" : 0, + "gridY" : 2, + "grid" : null +}, { + "id" : 12, + "cityName" : "北京市", + "aqiValue" : 95, + "recordTime" : "2025-06-21T23:34:02.4183691", + "pm25" : 20.0, + "pm10" : 46.0, + "so2" : 8.0, + "no2" : 28.0, + "co" : 1.0, + "o3" : 59.0, + "latitude" : null, + "longitude" : null, + "gridX" : 0, + "gridY" : 3, + "grid" : null +}, { + "id" : 13, + "cityName" : "北京市", + "aqiValue" : 110, + "recordTime" : "2025-06-20T23:34:02.4183691", + "pm25" : 20.0, + "pm10" : 48.0, + "so2" : 8.0, + "no2" : 29.0, + "co" : 1.0, + "o3" : 62.0, + "latitude" : null, + "longitude" : null, + "gridX" : 0, + "gridY" : 4, + "grid" : null +}, { + "id" : 14, + "cityName" : "北京市", + "aqiValue" : 65, + "recordTime" : "2025-06-23T23:34:02.4183691", + "pm25" : 21.0, + "pm10" : 40.0, + "so2" : 9.0, + "no2" : 25.0, + "co" : 1.2, + "o3" : 50.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 0, + "grid" : null +}, { + "id" : 15, + "cityName" : "北京市", + "aqiValue" : 80, + "recordTime" : "2025-06-22T23:34:02.4183691", + "pm25" : 21.0, + "pm10" : 42.0, + "so2" : 9.0, + "no2" : 26.0, + "co" : 1.2, + "o3" : 53.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 1, + "grid" : null +}, { + "id" : 16, + "cityName" : "北京市", + "aqiValue" : 95, + "recordTime" : "2025-06-21T23:34:02.4183691", + "pm25" : 21.0, + "pm10" : 44.0, + "so2" : 9.0, + "no2" : 27.0, + "co" : 1.2, + "o3" : 56.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 2, + "grid" : null +}, { + "id" : 17, + "cityName" : "北京市", + "aqiValue" : 110, + "recordTime" : "2025-06-20T23:34:02.4183691", + "pm25" : 21.0, + "pm10" : 46.0, + "so2" : 9.0, + "no2" : 28.0, + "co" : 1.2, + "o3" : 59.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 3, + "grid" : null +}, { + "id" : 18, + "cityName" : "北京市", + "aqiValue" : 125, + "recordTime" : "2025-06-19T23:34:02.4183691", + "pm25" : 21.0, + "pm10" : 48.0, + "so2" : 9.0, + "no2" : 29.0, + "co" : 1.2, + "o3" : 62.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 4, + "grid" : null +}, { + "id" : 19, + "cityName" : "北京市", + "aqiValue" : 80, + "recordTime" : "2025-06-22T23:34:02.4183691", + "pm25" : 22.0, + "pm10" : 40.0, + "so2" : 10.0, + "no2" : 25.0, + "co" : 1.4, + "o3" : 50.0, + "latitude" : null, + "longitude" : null, + "gridX" : 2, + "gridY" : 0, + "grid" : null +}, { + "id" : 20, + "cityName" : "北京市", + "aqiValue" : 95, + "recordTime" : "2025-06-21T23:34:02.4183691", + "pm25" : 22.0, + "pm10" : 42.0, + "so2" : 10.0, + "no2" : 26.0, + "co" : 1.4, + "o3" : 53.0, + "latitude" : null, + "longitude" : null, + "gridX" : 2, + "gridY" : 1, + "grid" : null +}, { + "id" : 21, + "cityName" : "北京市", + "aqiValue" : 110, + "recordTime" : "2025-06-20T23:34:02.4183691", + "pm25" : 22.0, + "pm10" : 44.0, + "so2" : 10.0, + "no2" : 27.0, + "co" : 1.4, + "o3" : 56.0, + "latitude" : null, + "longitude" : null, + "gridX" : 2, + "gridY" : 2, + "grid" : null +}, { + "id" : 22, + "cityName" : "北京市", + "aqiValue" : 125, + "recordTime" : "2025-06-19T23:34:02.4183691", + "pm25" : 22.0, + "pm10" : 46.0, + "so2" : 10.0, + "no2" : 28.0, + "co" : 1.4, + "o3" : 59.0, + "latitude" : null, + "longitude" : null, + "gridX" : 2, + "gridY" : 3, + "grid" : null +}, { + "id" : 23, + "cityName" : "北京市", + "aqiValue" : 140, + "recordTime" : "2025-06-18T23:34:02.4183691", + "pm25" : 22.0, + "pm10" : 48.0, + "so2" : 10.0, + "no2" : 29.0, + "co" : 1.4, + "o3" : 62.0, + "latitude" : null, + "longitude" : null, + "gridX" : 2, + "gridY" : 4, + "grid" : null +}, { + "id" : 24, + "cityName" : "北京市", + "aqiValue" : 95, + "recordTime" : "2025-06-21T23:34:02.4183691", + "pm25" : 23.0, + "pm10" : 40.0, + "so2" : 11.0, + "no2" : 25.0, + "co" : 1.6, + "o3" : 50.0, + "latitude" : null, + "longitude" : null, + "gridX" : 3, + "gridY" : 0, + "grid" : null +}, { + "id" : 25, + "cityName" : "北京市", + "aqiValue" : 110, + "recordTime" : "2025-06-20T23:34:02.4183691", + "pm25" : 23.0, + "pm10" : 42.0, + "so2" : 11.0, + "no2" : 26.0, + "co" : 1.6, + "o3" : 53.0, + "latitude" : null, + "longitude" : null, + "gridX" : 3, + "gridY" : 1, + "grid" : null +}, { + "id" : 26, + "cityName" : "北京市", + "aqiValue" : 125, + "recordTime" : "2025-06-19T23:34:02.4183691", + "pm25" : 23.0, + "pm10" : 44.0, + "so2" : 11.0, + "no2" : 27.0, + "co" : 1.6, + "o3" : 56.0, + "latitude" : null, + "longitude" : null, + "gridX" : 3, + "gridY" : 2, + "grid" : null +}, { + "id" : 27, + "cityName" : "北京市", + "aqiValue" : 140, + "recordTime" : "2025-06-18T23:34:02.4183691", + "pm25" : 23.0, + "pm10" : 46.0, + "so2" : 11.0, + "no2" : 28.0, + "co" : 1.6, + "o3" : 59.0, + "latitude" : null, + "longitude" : null, + "gridX" : 3, + "gridY" : 3, + "grid" : null +}, { + "id" : 28, + "cityName" : "北京市", + "aqiValue" : 155, + "recordTime" : "2025-06-17T23:34:02.4183691", + "pm25" : 23.0, + "pm10" : 48.0, + "so2" : 11.0, + "no2" : 29.0, + "co" : 1.6, + "o3" : 62.0, + "latitude" : null, + "longitude" : null, + "gridX" : 3, + "gridY" : 4, + "grid" : null +}, { + "id" : 29, + "cityName" : "北京市", + "aqiValue" : 110, + "recordTime" : "2025-06-20T23:34:02.4183691", + "pm25" : 24.0, + "pm10" : 40.0, + "so2" : 12.0, + "no2" : 25.0, + "co" : 1.8, + "o3" : 50.0, + "latitude" : null, + "longitude" : null, + "gridX" : 4, + "gridY" : 0, + "grid" : null +}, { + "id" : 30, + "cityName" : "北京市", + "aqiValue" : 125, + "recordTime" : "2025-06-19T23:34:02.4183691", + "pm25" : 24.0, + "pm10" : 42.0, + "so2" : 12.0, + "no2" : 26.0, + "co" : 1.8, + "o3" : 53.0, + "latitude" : null, + "longitude" : null, + "gridX" : 4, + "gridY" : 1, + "grid" : null +}, { + "id" : 31, + "cityName" : "北京市", + "aqiValue" : 140, + "recordTime" : "2025-06-18T23:34:02.4183691", + "pm25" : 24.0, + "pm10" : 44.0, + "so2" : 12.0, + "no2" : 27.0, + "co" : 1.8, + "o3" : 56.0, + "latitude" : null, + "longitude" : null, + "gridX" : 4, + "gridY" : 2, + "grid" : null +}, { + "id" : 32, + "cityName" : "北京市", + "aqiValue" : 155, + "recordTime" : "2025-06-17T23:34:02.4183691", + "pm25" : 24.0, + "pm10" : 46.0, + "so2" : 12.0, + "no2" : 28.0, + "co" : 1.8, + "o3" : 59.0, + "latitude" : null, + "longitude" : null, + "gridX" : 4, + "gridY" : 3, + "grid" : null +}, { + "id" : 33, + "cityName" : "北京市", + "aqiValue" : 170, + "recordTime" : "2025-06-16T23:34:02.4183691", + "pm25" : 24.0, + "pm10" : 48.0, + "so2" : 12.0, + "no2" : 29.0, + "co" : 1.8, + "o3" : 62.0, + "latitude" : null, + "longitude" : null, + "gridX" : 4, + "gridY" : 4, + "grid" : null +}, { + "id" : 34, + "cityName" : "北京市", + "aqiValue" : 100, + "recordTime" : "2025-05-24T23:34:02.4574586", + "pm25" : 31.0, + "pm10" : 62.0, + "so2" : 13.0, + "no2" : 36.0, + "co" : 1.7, + "o3" : 73.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 0, + "grid" : null +}, { + "id" : 35, + "cityName" : "北京市", + "aqiValue" : 105, + "recordTime" : "2025-05-29T23:34:02.4574586", + "pm25" : 31.0, + "pm10" : 62.0, + "so2" : 13.0, + "no2" : 36.0, + "co" : 1.7, + "o3" : 73.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 1, + "grid" : null +}, { + "id" : 36, + "cityName" : "北京市", + "aqiValue" : 110, + "recordTime" : "2025-06-03T23:34:02.4574586", + "pm25" : 31.0, + "pm10" : 62.0, + "so2" : 13.0, + "no2" : 36.0, + "co" : 1.7, + "o3" : 73.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 2, + "grid" : null +}, { + "id" : 37, + "cityName" : "北京市", + "aqiValue" : 120, + "recordTime" : "2025-04-24T23:34:02.4574586", + "pm25" : 32.0, + "pm10" : 64.0, + "so2" : 14.0, + "no2" : 37.0, + "co" : 1.9, + "o3" : 76.0, + "latitude" : null, + "longitude" : null, + "gridX" : 2, + "gridY" : 0, + "grid" : null +}, { + "id" : 38, + "cityName" : "北京市", + "aqiValue" : 125, + "recordTime" : "2025-04-29T23:34:02.4574586", + "pm25" : 32.0, + "pm10" : 64.0, + "so2" : 14.0, + "no2" : 37.0, + "co" : 1.9, + "o3" : 76.0, + "latitude" : null, + "longitude" : null, + "gridX" : 2, + "gridY" : 1, + "grid" : null +}, { + "id" : 39, + "cityName" : "北京市", + "aqiValue" : 130, + "recordTime" : "2025-05-04T23:34:02.4574586", + "pm25" : 32.0, + "pm10" : 64.0, + "so2" : 14.0, + "no2" : 37.0, + "co" : 1.9, + "o3" : 76.0, + "latitude" : null, + "longitude" : null, + "gridX" : 2, + "gridY" : 2, + "grid" : null +}, { + "id" : 40, + "cityName" : "北京市", + "aqiValue" : 140, + "recordTime" : "2025-03-24T23:34:02.4574586", + "pm25" : 33.0, + "pm10" : 66.0, + "so2" : 15.0, + "no2" : 38.0, + "co" : 2.1, + "o3" : 79.0, + "latitude" : null, + "longitude" : null, + "gridX" : 3, + "gridY" : 0, + "grid" : null +}, { + "id" : 41, + "cityName" : "北京市", + "aqiValue" : 145, + "recordTime" : "2025-03-29T23:34:02.4574586", + "pm25" : 33.0, + "pm10" : 66.0, + "so2" : 15.0, + "no2" : 38.0, + "co" : 2.1, + "o3" : 79.0, + "latitude" : null, + "longitude" : null, + "gridX" : 3, + "gridY" : 1, + "grid" : null +}, { + "id" : 42, + "cityName" : "北京市", + "aqiValue" : 150, + "recordTime" : "2025-04-03T23:34:02.4574586", + "pm25" : 33.0, + "pm10" : 66.0, + "so2" : 15.0, + "no2" : 38.0, + "co" : 2.1, + "o3" : 79.0, + "latitude" : null, + "longitude" : null, + "gridX" : 3, + "gridY" : 2, + "grid" : null +}, { + "id" : 43, + "cityName" : "北京市", + "aqiValue" : 160, + "recordTime" : "2025-02-24T23:34:02.4574586", + "pm25" : 34.0, + "pm10" : 68.0, + "so2" : 16.0, + "no2" : 39.0, + "co" : 2.3, + "o3" : 82.0, + "latitude" : null, + "longitude" : null, + "gridX" : 4, + "gridY" : 0, + "grid" : null +}, { + "id" : 44, + "cityName" : "北京市", + "aqiValue" : 165, + "recordTime" : "2025-03-01T23:34:02.4574586", + "pm25" : 34.0, + "pm10" : 68.0, + "so2" : 16.0, + "no2" : 39.0, + "co" : 2.3, + "o3" : 82.0, + "latitude" : null, + "longitude" : null, + "gridX" : 4, + "gridY" : 1, + "grid" : null +}, { + "id" : 45, + "cityName" : "北京市", + "aqiValue" : 170, + "recordTime" : "2025-03-06T23:34:02.4574586", + "pm25" : 34.0, + "pm10" : 68.0, + "so2" : 16.0, + "no2" : 39.0, + "co" : 2.3, + "o3" : 82.0, + "latitude" : null, + "longitude" : null, + "gridX" : 4, + "gridY" : 2, + "grid" : null +}, { + "id" : 46, + "cityName" : "北京市", + "aqiValue" : 180, + "recordTime" : "2025-01-24T23:34:02.4574586", + "pm25" : 35.0, + "pm10" : 70.0, + "so2" : 17.0, + "no2" : 40.0, + "co" : 2.5, + "o3" : 85.0, + "latitude" : null, + "longitude" : null, + "gridX" : 0, + "gridY" : 0, + "grid" : null +}, { + "id" : 47, + "cityName" : "北京市", + "aqiValue" : 185, + "recordTime" : "2025-01-29T23:34:02.4574586", + "pm25" : 35.0, + "pm10" : 70.0, + "so2" : 17.0, + "no2" : 40.0, + "co" : 2.5, + "o3" : 85.0, + "latitude" : null, + "longitude" : null, + "gridX" : 0, + "gridY" : 1, + "grid" : null +}, { + "id" : 48, + "cityName" : "北京市", + "aqiValue" : 190, + "recordTime" : "2025-02-03T23:34:02.4574586", + "pm25" : 35.0, + "pm10" : 70.0, + "so2" : 17.0, + "no2" : 40.0, + "co" : 2.5, + "o3" : 85.0, + "latitude" : null, + "longitude" : null, + "gridX" : 0, + "gridY" : 2, + "grid" : null +}, { + "id" : 49, + "cityName" : "北京市", + "aqiValue" : 200, + "recordTime" : "2024-12-24T23:34:02.4574586", + "pm25" : 36.0, + "pm10" : 72.0, + "so2" : 18.0, + "no2" : 41.0, + "co" : 2.7, + "o3" : 88.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 0, + "grid" : null +}, { + "id" : 50, + "cityName" : "北京市", + "aqiValue" : 205, + "recordTime" : "2024-12-29T23:34:02.4574586", + "pm25" : 36.0, + "pm10" : 72.0, + "so2" : 18.0, + "no2" : 41.0, + "co" : 2.7, + "o3" : 88.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 1, + "grid" : null +}, { + "id" : 51, + "cityName" : "北京市", + "aqiValue" : 210, + "recordTime" : "2025-01-03T23:34:02.4574586", + "pm25" : 36.0, + "pm10" : 72.0, + "so2" : 18.0, + "no2" : 41.0, + "co" : 2.7, + "o3" : 88.0, + "latitude" : null, + "longitude" : null, + "gridX" : 1, + "gridY" : 2, + "grid" : null } ] \ No newline at end of file diff --git a/ems-backend/json-db/assignments.json b/ems-backend/json-db/assignments.json index 1cc42f4..e874c4b 100644 --- a/ems-backend/json-db/assignments.json +++ b/ems-backend/json-db/assignments.json @@ -1,134 +1,134 @@ -[ { - "id" : 1, - "task" : { - "id" : 4, - "feedback" : { - "id" : 4, - "eventId" : "6b800765-b0aa-457b-bde7-af60bace45f9", - "title" : "SO2", - "description" : "SO2", - "pollutionType" : "SO2", - "severityLevel" : "MEDIUM", - "status" : "PENDING_ASSIGNMENT", - "textAddress" : "东北大学195号", - "gridX" : 0, - "gridY" : 1, - "submitterId" : 6, - "createdAt" : "2025-06-24T23:35:05.9704809", - "updatedAt" : "2025-06-24T23:39:54.0988378", - "user" : { - "id" : 6, - "name" : "公众监督员", - "phone" : "13700137004", - "email" : "public.supervisor@aizhangz.top", - "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.7646754", - "updatedAt" : "2025-06-24T23:33:58.7646754", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "latitude" : 1.0E-6, - "longitude" : 1.0E-6, - "attachments" : [ ], - "task" : null - }, - "assignee" : { - "id" : 57, - "name" : "特定网格员", - "phone" : "14700009999", - "email" : "worker@aizhangz.top", - "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 10, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2703065", - "updatedAt" : "2025-06-24T23:34:02.2703065", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "createdBy" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "status" : "ASSIGNED", - "assignedAt" : "2025-06-24T23:40:35.4504136", - "completedAt" : null, - "createdAt" : null, - "updatedAt" : null, - "title" : "SO2", - "description" : "SO2", - "pollutionType" : "SO2", - "severityLevel" : "MEDIUM", - "textAddress" : "东北大学195号", - "gridX" : 0, - "gridY" : 1, - "latitude" : 1.0E-6, - "longitude" : 1.0E-6, - "history" : [ ], - "assignment" : null, - "submissions" : [ ] - }, - "assigner" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "assignmentTime" : "2025-06-24T23:40:35.4534373", - "deadline" : null, - "status" : "PENDING", - "remarks" : null +[ { + "id" : 1, + "task" : { + "id" : 4, + "feedback" : { + "id" : 4, + "eventId" : "6b800765-b0aa-457b-bde7-af60bace45f9", + "title" : "SO2", + "description" : "SO2", + "pollutionType" : "SO2", + "severityLevel" : "MEDIUM", + "status" : "PENDING_ASSIGNMENT", + "textAddress" : "东北大学195号", + "gridX" : 0, + "gridY" : 1, + "submitterId" : 6, + "createdAt" : "2025-06-24T23:35:05.9704809", + "updatedAt" : "2025-06-24T23:39:54.0988378", + "user" : { + "id" : 6, + "name" : "公众监督员", + "phone" : "13700137004", + "email" : "public.supervisor@aizhangz.top", + "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.7646754", + "updatedAt" : "2025-06-24T23:33:58.7646754", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "latitude" : 1.0E-6, + "longitude" : 1.0E-6, + "attachments" : [ ], + "task" : null + }, + "assignee" : { + "id" : 57, + "name" : "特定网格员", + "phone" : "14700009999", + "email" : "worker@aizhangz.top", + "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 10, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2703065", + "updatedAt" : "2025-06-24T23:34:02.2703065", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "createdBy" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "status" : "ASSIGNED", + "assignedAt" : "2025-06-24T23:40:35.4504136", + "completedAt" : null, + "createdAt" : null, + "updatedAt" : null, + "title" : "SO2", + "description" : "SO2", + "pollutionType" : "SO2", + "severityLevel" : "MEDIUM", + "textAddress" : "东北大学195号", + "gridX" : 0, + "gridY" : 1, + "latitude" : 1.0E-6, + "longitude" : 1.0E-6, + "history" : [ ], + "assignment" : null, + "submissions" : [ ] + }, + "assigner" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "assignmentTime" : "2025-06-24T23:40:35.4534373", + "deadline" : null, + "status" : "PENDING", + "remarks" : null } ] \ No newline at end of file diff --git a/ems-backend/json-db/attachments.json b/ems-backend/json-db/attachments.json index 197971b..b84b3e6 100644 --- a/ems-backend/json-db/attachments.json +++ b/ems-backend/json-db/attachments.json @@ -1,50 +1,50 @@ -[ { - "id" : 1, - "fileName" : "屏幕截图 2025-05-06 153004.png", - "fileType" : "image/png", - "storedFileName" : "4af7ae76-6dbd-4ab4-b103-e04b1ee15ea2.png", - "fileSize" : 8645, - "uploadDate" : null, - "feedback" : { - "id" : 4, - "eventId" : "6b800765-b0aa-457b-bde7-af60bace45f9", - "title" : "SO2", - "description" : "SO2", - "pollutionType" : "SO2", - "severityLevel" : "MEDIUM", - "status" : "AI_REVIEWING", - "textAddress" : "东北大学195号", - "gridX" : 0, - "gridY" : 1, - "submitterId" : 6, - "createdAt" : "2025-06-24T23:35:05.9704809", - "updatedAt" : "2025-06-24T23:35:05.9704809", - "user" : { - "id" : 6, - "name" : "公众监督员", - "phone" : "13700137004", - "email" : "public.supervisor@aizhangz.top", - "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.7646754", - "updatedAt" : "2025-06-24T23:33:58.7646754", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "latitude" : 1.0E-6, - "longitude" : 1.0E-6, - "attachments" : [ ], - "task" : null - }, - "taskSubmission" : null +[ { + "id" : 1, + "fileName" : "屏幕截图 2025-05-06 153004.png", + "fileType" : "image/png", + "storedFileName" : "4af7ae76-6dbd-4ab4-b103-e04b1ee15ea2.png", + "fileSize" : 8645, + "uploadDate" : null, + "feedback" : { + "id" : 4, + "eventId" : "6b800765-b0aa-457b-bde7-af60bace45f9", + "title" : "SO2", + "description" : "SO2", + "pollutionType" : "SO2", + "severityLevel" : "MEDIUM", + "status" : "AI_REVIEWING", + "textAddress" : "东北大学195号", + "gridX" : 0, + "gridY" : 1, + "submitterId" : 6, + "createdAt" : "2025-06-24T23:35:05.9704809", + "updatedAt" : "2025-06-24T23:35:05.9704809", + "user" : { + "id" : 6, + "name" : "公众监督员", + "phone" : "13700137004", + "email" : "public.supervisor@aizhangz.top", + "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.7646754", + "updatedAt" : "2025-06-24T23:33:58.7646754", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "latitude" : 1.0E-6, + "longitude" : 1.0E-6, + "attachments" : [ ], + "task" : null + }, + "taskSubmission" : null } ] \ No newline at end of file diff --git a/ems-backend/json-db/feedbacks.json b/ems-backend/json-db/feedbacks.json index 782a734..3ccd48f 100644 --- a/ems-backend/json-db/feedbacks.json +++ b/ems-backend/json-db/feedbacks.json @@ -1,161 +1,161 @@ -[ { - "id" : 1, - "eventId" : "FB-202301", - "title" : "工业区PM2.5超标", - "description" : "市中心化工厂在夜间排放黄色烟雾,气味刺鼻。", - "pollutionType" : "PM25", - "severityLevel" : "HIGH", - "status" : "PENDING_ASSIGNMENT", - "textAddress" : null, - "gridX" : 5, - "gridY" : 5, - "submitterId" : 6, - "createdAt" : "2025-06-24T23:34:02.4935754", - "updatedAt" : "2025-06-24T23:34:02.4935754", - "user" : { - "id" : 6, - "name" : "公众监督员", - "phone" : "13700137004", - "email" : "public.supervisor@aizhangz.top", - "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.7646754", - "updatedAt" : "2025-06-24T23:33:58.7646754", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "latitude" : 39.9042, - "longitude" : 116.4074, - "attachments" : [ ], - "task" : null -}, { - "id" : 2, - "eventId" : "FB-202302", - "title" : "交通要道NO2浓度过高", - "description" : "主干道车辆拥堵,空气质量差。", - "pollutionType" : "NO2", - "severityLevel" : "MEDIUM", - "status" : "ASSIGNED", - "textAddress" : null, - "gridX" : 6, - "gridY" : 6, - "submitterId" : 6, - "createdAt" : "2025-06-24T23:34:02.4945751", - "updatedAt" : "2025-06-24T23:34:02.4945751", - "user" : { - "id" : 6, - "name" : "公众监督员", - "phone" : "13700137004", - "email" : "public.supervisor@aizhangz.top", - "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.7646754", - "updatedAt" : "2025-06-24T23:33:58.7646754", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "latitude" : 39.915, - "longitude" : 116.4, - "attachments" : [ ], - "task" : null -}, { - "id" : 3, - "eventId" : "FB-202303", - "title" : "未知来源的SO2异味", - "description" : "居民区闻到类似烧煤的刺鼻气味。", - "pollutionType" : "SO2", - "severityLevel" : "LOW", - "status" : "PROCESSED", - "textAddress" : null, - "gridX" : 7, - "gridY" : 7, - "submitterId" : 6, - "createdAt" : "2025-06-24T23:34:02.4945751", - "updatedAt" : "2025-06-24T23:34:02.4945751", - "user" : { - "id" : 6, - "name" : "公众监督员", - "phone" : "13700137004", - "email" : "public.supervisor@aizhangz.top", - "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.7646754", - "updatedAt" : "2025-06-24T23:33:58.7646754", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "latitude" : 39.888, - "longitude" : 116.356, - "attachments" : [ ], - "task" : null -}, { - "id" : 4, - "eventId" : "6b800765-b0aa-457b-bde7-af60bace45f9", - "title" : "SO2", - "description" : "SO2", - "pollutionType" : "SO2", - "severityLevel" : "MEDIUM", - "status" : "ASSIGNED", - "textAddress" : "东北大学195号", - "gridX" : 0, - "gridY" : 1, - "submitterId" : 6, - "createdAt" : "2025-06-24T23:35:05.9704809", - "updatedAt" : "2025-06-24T23:39:54.0988378", - "user" : { - "id" : 6, - "name" : "公众监督员", - "phone" : "13700137004", - "email" : "public.supervisor@aizhangz.top", - "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.7646754", - "updatedAt" : "2025-06-24T23:33:58.7646754", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "latitude" : 1.0E-6, - "longitude" : 1.0E-6, - "attachments" : [ ], - "task" : null +[ { + "id" : 1, + "eventId" : "FB-202301", + "title" : "工业区PM2.5超标", + "description" : "市中心化工厂在夜间排放黄色烟雾,气味刺鼻。", + "pollutionType" : "PM25", + "severityLevel" : "HIGH", + "status" : "PENDING_ASSIGNMENT", + "textAddress" : null, + "gridX" : 5, + "gridY" : 5, + "submitterId" : 6, + "createdAt" : "2025-06-24T23:34:02.4935754", + "updatedAt" : "2025-06-24T23:34:02.4935754", + "user" : { + "id" : 6, + "name" : "公众监督员", + "phone" : "13700137004", + "email" : "public.supervisor@aizhangz.top", + "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.7646754", + "updatedAt" : "2025-06-24T23:33:58.7646754", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "latitude" : 39.9042, + "longitude" : 116.4074, + "attachments" : [ ], + "task" : null +}, { + "id" : 2, + "eventId" : "FB-202302", + "title" : "交通要道NO2浓度过高", + "description" : "主干道车辆拥堵,空气质量差。", + "pollutionType" : "NO2", + "severityLevel" : "MEDIUM", + "status" : "ASSIGNED", + "textAddress" : null, + "gridX" : 6, + "gridY" : 6, + "submitterId" : 6, + "createdAt" : "2025-06-24T23:34:02.4945751", + "updatedAt" : "2025-06-24T23:34:02.4945751", + "user" : { + "id" : 6, + "name" : "公众监督员", + "phone" : "13700137004", + "email" : "public.supervisor@aizhangz.top", + "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.7646754", + "updatedAt" : "2025-06-24T23:33:58.7646754", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "latitude" : 39.915, + "longitude" : 116.4, + "attachments" : [ ], + "task" : null +}, { + "id" : 3, + "eventId" : "FB-202303", + "title" : "未知来源的SO2异味", + "description" : "居民区闻到类似烧煤的刺鼻气味。", + "pollutionType" : "SO2", + "severityLevel" : "LOW", + "status" : "PROCESSED", + "textAddress" : null, + "gridX" : 7, + "gridY" : 7, + "submitterId" : 6, + "createdAt" : "2025-06-24T23:34:02.4945751", + "updatedAt" : "2025-06-24T23:34:02.4945751", + "user" : { + "id" : 6, + "name" : "公众监督员", + "phone" : "13700137004", + "email" : "public.supervisor@aizhangz.top", + "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.7646754", + "updatedAt" : "2025-06-24T23:33:58.7646754", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "latitude" : 39.888, + "longitude" : 116.356, + "attachments" : [ ], + "task" : null +}, { + "id" : 4, + "eventId" : "6b800765-b0aa-457b-bde7-af60bace45f9", + "title" : "SO2", + "description" : "SO2", + "pollutionType" : "SO2", + "severityLevel" : "MEDIUM", + "status" : "ASSIGNED", + "textAddress" : "东北大学195号", + "gridX" : 0, + "gridY" : 1, + "submitterId" : 6, + "createdAt" : "2025-06-24T23:35:05.9704809", + "updatedAt" : "2025-06-24T23:39:54.0988378", + "user" : { + "id" : 6, + "name" : "公众监督员", + "phone" : "13700137004", + "email" : "public.supervisor@aizhangz.top", + "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.7646754", + "updatedAt" : "2025-06-24T23:33:58.7646754", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "latitude" : 1.0E-6, + "longitude" : 1.0E-6, + "attachments" : [ ], + "task" : null } ] \ No newline at end of file diff --git a/ems-backend/json-db/grids.json b/ems-backend/json-db/grids.json index e37eaa8..ae53405 100644 --- a/ems-backend/json-db/grids.json +++ b/ems-backend/json-db/grids.json @@ -1,12801 +1,12801 @@ -[ { - "id" : 1, - "gridX" : 0, - "gridY" : 0, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 2, - "gridX" : 0, - "gridY" : 1, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 3, - "gridX" : 0, - "gridY" : 2, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 4, - "gridX" : 0, - "gridY" : 3, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 5, - "gridX" : 0, - "gridY" : 4, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 6, - "gridX" : 0, - "gridY" : 5, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 7, - "gridX" : 0, - "gridY" : 6, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 8, - "gridX" : 0, - "gridY" : 7, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 9, - "gridX" : 0, - "gridY" : 8, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 10, - "gridX" : 0, - "gridY" : 9, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 11, - "gridX" : 0, - "gridY" : 10, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 12, - "gridX" : 0, - "gridY" : 11, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 13, - "gridX" : 0, - "gridY" : 12, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 14, - "gridX" : 0, - "gridY" : 13, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 15, - "gridX" : 0, - "gridY" : 14, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 16, - "gridX" : 0, - "gridY" : 15, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 17, - "gridX" : 0, - "gridY" : 16, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 18, - "gridX" : 0, - "gridY" : 17, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 19, - "gridX" : 0, - "gridY" : 18, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 20, - "gridX" : 0, - "gridY" : 19, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 21, - "gridX" : 0, - "gridY" : 20, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 22, - "gridX" : 0, - "gridY" : 21, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 23, - "gridX" : 0, - "gridY" : 22, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 24, - "gridX" : 0, - "gridY" : 23, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 25, - "gridX" : 0, - "gridY" : 24, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 26, - "gridX" : 0, - "gridY" : 25, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 27, - "gridX" : 0, - "gridY" : 26, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 28, - "gridX" : 0, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 29, - "gridX" : 0, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 30, - "gridX" : 0, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 31, - "gridX" : 0, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 32, - "gridX" : 0, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 33, - "gridX" : 0, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 34, - "gridX" : 0, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 35, - "gridX" : 0, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 36, - "gridX" : 0, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 37, - "gridX" : 0, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 38, - "gridX" : 0, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 39, - "gridX" : 0, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 40, - "gridX" : 0, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 41, - "gridX" : 1, - "gridY" : 0, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 42, - "gridX" : 1, - "gridY" : 1, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 43, - "gridX" : 1, - "gridY" : 2, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 44, - "gridX" : 1, - "gridY" : 3, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 45, - "gridX" : 1, - "gridY" : 4, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 46, - "gridX" : 1, - "gridY" : 5, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 47, - "gridX" : 1, - "gridY" : 6, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 48, - "gridX" : 1, - "gridY" : 7, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 49, - "gridX" : 1, - "gridY" : 8, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 50, - "gridX" : 1, - "gridY" : 9, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 51, - "gridX" : 1, - "gridY" : 10, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 52, - "gridX" : 1, - "gridY" : 11, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 53, - "gridX" : 1, - "gridY" : 12, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 54, - "gridX" : 1, - "gridY" : 13, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 55, - "gridX" : 1, - "gridY" : 14, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 56, - "gridX" : 1, - "gridY" : 15, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 57, - "gridX" : 1, - "gridY" : 16, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 58, - "gridX" : 1, - "gridY" : 17, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 59, - "gridX" : 1, - "gridY" : 18, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 60, - "gridX" : 1, - "gridY" : 19, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 61, - "gridX" : 1, - "gridY" : 20, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 62, - "gridX" : 1, - "gridY" : 21, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 63, - "gridX" : 1, - "gridY" : 22, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 64, - "gridX" : 1, - "gridY" : 23, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 65, - "gridX" : 1, - "gridY" : 24, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 66, - "gridX" : 1, - "gridY" : 25, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 67, - "gridX" : 1, - "gridY" : 26, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 68, - "gridX" : 1, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 69, - "gridX" : 1, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 70, - "gridX" : 1, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 71, - "gridX" : 1, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 72, - "gridX" : 1, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 73, - "gridX" : 1, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 74, - "gridX" : 1, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 75, - "gridX" : 1, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 76, - "gridX" : 1, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 77, - "gridX" : 1, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 78, - "gridX" : 1, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 79, - "gridX" : 1, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 80, - "gridX" : 1, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 81, - "gridX" : 2, - "gridY" : 0, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 82, - "gridX" : 2, - "gridY" : 1, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 83, - "gridX" : 2, - "gridY" : 2, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 84, - "gridX" : 2, - "gridY" : 3, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 85, - "gridX" : 2, - "gridY" : 4, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 86, - "gridX" : 2, - "gridY" : 5, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 87, - "gridX" : 2, - "gridY" : 6, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 88, - "gridX" : 2, - "gridY" : 7, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 89, - "gridX" : 2, - "gridY" : 8, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 90, - "gridX" : 2, - "gridY" : 9, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 91, - "gridX" : 2, - "gridY" : 10, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 92, - "gridX" : 2, - "gridY" : 11, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 93, - "gridX" : 2, - "gridY" : 12, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 94, - "gridX" : 2, - "gridY" : 13, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 95, - "gridX" : 2, - "gridY" : 14, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 96, - "gridX" : 2, - "gridY" : 15, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 97, - "gridX" : 2, - "gridY" : 16, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 98, - "gridX" : 2, - "gridY" : 17, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 99, - "gridX" : 2, - "gridY" : 18, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 100, - "gridX" : 2, - "gridY" : 19, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 101, - "gridX" : 2, - "gridY" : 20, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 102, - "gridX" : 2, - "gridY" : 21, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 103, - "gridX" : 2, - "gridY" : 22, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 104, - "gridX" : 2, - "gridY" : 23, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 105, - "gridX" : 2, - "gridY" : 24, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 106, - "gridX" : 2, - "gridY" : 25, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 107, - "gridX" : 2, - "gridY" : 26, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 108, - "gridX" : 2, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 109, - "gridX" : 2, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : true -}, { - "id" : 110, - "gridX" : 2, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 111, - "gridX" : 2, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 112, - "gridX" : 2, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 113, - "gridX" : 2, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 114, - "gridX" : 2, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 115, - "gridX" : 2, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 116, - "gridX" : 2, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 117, - "gridX" : 2, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 118, - "gridX" : 2, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 119, - "gridX" : 2, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 120, - "gridX" : 2, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 121, - "gridX" : 3, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 122, - "gridX" : 3, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 123, - "gridX" : 3, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 124, - "gridX" : 3, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 125, - "gridX" : 3, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 126, - "gridX" : 3, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 127, - "gridX" : 3, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 128, - "gridX" : 3, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 129, - "gridX" : 3, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 130, - "gridX" : 3, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 131, - "gridX" : 3, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 132, - "gridX" : 3, - "gridY" : 11, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 133, - "gridX" : 3, - "gridY" : 12, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 134, - "gridX" : 3, - "gridY" : 13, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 135, - "gridX" : 3, - "gridY" : 14, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 136, - "gridX" : 3, - "gridY" : 15, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 137, - "gridX" : 3, - "gridY" : 16, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 138, - "gridX" : 3, - "gridY" : 17, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 139, - "gridX" : 3, - "gridY" : 18, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 140, - "gridX" : 3, - "gridY" : 19, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 141, - "gridX" : 3, - "gridY" : 20, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 142, - "gridX" : 3, - "gridY" : 21, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 143, - "gridX" : 3, - "gridY" : 22, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 144, - "gridX" : 3, - "gridY" : 23, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 145, - "gridX" : 3, - "gridY" : 24, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 146, - "gridX" : 3, - "gridY" : 25, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 147, - "gridX" : 3, - "gridY" : 26, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 148, - "gridX" : 3, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 149, - "gridX" : 3, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 150, - "gridX" : 3, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 151, - "gridX" : 3, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 152, - "gridX" : 3, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 153, - "gridX" : 3, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 154, - "gridX" : 3, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 155, - "gridX" : 3, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 156, - "gridX" : 3, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 157, - "gridX" : 3, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 158, - "gridX" : 3, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 159, - "gridX" : 3, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 160, - "gridX" : 3, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 161, - "gridX" : 4, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 162, - "gridX" : 4, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 163, - "gridX" : 4, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 164, - "gridX" : 4, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 165, - "gridX" : 4, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 166, - "gridX" : 4, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 167, - "gridX" : 4, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 168, - "gridX" : 4, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 169, - "gridX" : 4, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 170, - "gridX" : 4, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 171, - "gridX" : 4, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 172, - "gridX" : 4, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 173, - "gridX" : 4, - "gridY" : 12, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 174, - "gridX" : 4, - "gridY" : 13, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 175, - "gridX" : 4, - "gridY" : 14, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 176, - "gridX" : 4, - "gridY" : 15, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 177, - "gridX" : 4, - "gridY" : 16, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 178, - "gridX" : 4, - "gridY" : 17, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 179, - "gridX" : 4, - "gridY" : 18, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 180, - "gridX" : 4, - "gridY" : 19, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 181, - "gridX" : 4, - "gridY" : 20, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 182, - "gridX" : 4, - "gridY" : 21, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 183, - "gridX" : 4, - "gridY" : 22, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 184, - "gridX" : 4, - "gridY" : 23, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 185, - "gridX" : 4, - "gridY" : 24, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 186, - "gridX" : 4, - "gridY" : 25, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 187, - "gridX" : 4, - "gridY" : 26, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 188, - "gridX" : 4, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 189, - "gridX" : 4, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 190, - "gridX" : 4, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 191, - "gridX" : 4, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 192, - "gridX" : 4, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 193, - "gridX" : 4, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 194, - "gridX" : 4, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 195, - "gridX" : 4, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 196, - "gridX" : 4, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 197, - "gridX" : 4, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 198, - "gridX" : 4, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 199, - "gridX" : 4, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 200, - "gridX" : 4, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 201, - "gridX" : 5, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 202, - "gridX" : 5, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 203, - "gridX" : 5, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 204, - "gridX" : 5, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 205, - "gridX" : 5, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 206, - "gridX" : 5, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 207, - "gridX" : 5, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 208, - "gridX" : 5, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 209, - "gridX" : 5, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 210, - "gridX" : 5, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 211, - "gridX" : 5, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 212, - "gridX" : 5, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 213, - "gridX" : 5, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 214, - "gridX" : 5, - "gridY" : 13, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 215, - "gridX" : 5, - "gridY" : 14, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 216, - "gridX" : 5, - "gridY" : 15, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 217, - "gridX" : 5, - "gridY" : 16, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 218, - "gridX" : 5, - "gridY" : 17, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 219, - "gridX" : 5, - "gridY" : 18, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 220, - "gridX" : 5, - "gridY" : 19, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 221, - "gridX" : 5, - "gridY" : 20, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 222, - "gridX" : 5, - "gridY" : 21, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 223, - "gridX" : 5, - "gridY" : 22, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 224, - "gridX" : 5, - "gridY" : 23, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 225, - "gridX" : 5, - "gridY" : 24, - "cityName" : "苏州市", - "districtName" : "昆山市", - "description" : null, - "isObstacle" : false -}, { - "id" : 226, - "gridX" : 5, - "gridY" : 25, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 227, - "gridX" : 5, - "gridY" : 26, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 228, - "gridX" : 5, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 229, - "gridX" : 5, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 230, - "gridX" : 5, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 231, - "gridX" : 5, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 232, - "gridX" : 5, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 233, - "gridX" : 5, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 234, - "gridX" : 5, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 235, - "gridX" : 5, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 236, - "gridX" : 5, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 237, - "gridX" : 5, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 238, - "gridX" : 5, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 239, - "gridX" : 5, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 240, - "gridX" : 5, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 241, - "gridX" : 6, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 242, - "gridX" : 6, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 243, - "gridX" : 6, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 244, - "gridX" : 6, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 245, - "gridX" : 6, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 246, - "gridX" : 6, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 247, - "gridX" : 6, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 248, - "gridX" : 6, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 249, - "gridX" : 6, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 250, - "gridX" : 6, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 251, - "gridX" : 6, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 252, - "gridX" : 6, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 253, - "gridX" : 6, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 254, - "gridX" : 6, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 255, - "gridX" : 6, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 256, - "gridX" : 6, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 257, - "gridX" : 6, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 258, - "gridX" : 6, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 259, - "gridX" : 6, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 260, - "gridX" : 6, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 261, - "gridX" : 6, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 262, - "gridX" : 6, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 263, - "gridX" : 6, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 264, - "gridX" : 6, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 265, - "gridX" : 6, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 266, - "gridX" : 6, - "gridY" : 25, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 267, - "gridX" : 6, - "gridY" : 26, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 268, - "gridX" : 6, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 269, - "gridX" : 6, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 270, - "gridX" : 6, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 271, - "gridX" : 6, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 272, - "gridX" : 6, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 273, - "gridX" : 6, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 274, - "gridX" : 6, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 275, - "gridX" : 6, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 276, - "gridX" : 6, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 277, - "gridX" : 6, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 278, - "gridX" : 6, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 279, - "gridX" : 6, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 280, - "gridX" : 6, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 281, - "gridX" : 7, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 282, - "gridX" : 7, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 283, - "gridX" : 7, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 284, - "gridX" : 7, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 285, - "gridX" : 7, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 286, - "gridX" : 7, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 287, - "gridX" : 7, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 288, - "gridX" : 7, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 289, - "gridX" : 7, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 290, - "gridX" : 7, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 291, - "gridX" : 7, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 292, - "gridX" : 7, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 293, - "gridX" : 7, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 294, - "gridX" : 7, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 295, - "gridX" : 7, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 296, - "gridX" : 7, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 297, - "gridX" : 7, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 298, - "gridX" : 7, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 299, - "gridX" : 7, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 300, - "gridX" : 7, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 301, - "gridX" : 7, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 302, - "gridX" : 7, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 303, - "gridX" : 7, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 304, - "gridX" : 7, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 305, - "gridX" : 7, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 306, - "gridX" : 7, - "gridY" : 25, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 307, - "gridX" : 7, - "gridY" : 26, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 308, - "gridX" : 7, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 309, - "gridX" : 7, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 310, - "gridX" : 7, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 311, - "gridX" : 7, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 312, - "gridX" : 7, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 313, - "gridX" : 7, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 314, - "gridX" : 7, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 315, - "gridX" : 7, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 316, - "gridX" : 7, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 317, - "gridX" : 7, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 318, - "gridX" : 7, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 319, - "gridX" : 7, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 320, - "gridX" : 7, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 321, - "gridX" : 8, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 322, - "gridX" : 8, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 323, - "gridX" : 8, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 324, - "gridX" : 8, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 325, - "gridX" : 8, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 326, - "gridX" : 8, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 327, - "gridX" : 8, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 328, - "gridX" : 8, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 329, - "gridX" : 8, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 330, - "gridX" : 8, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 331, - "gridX" : 8, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 332, - "gridX" : 8, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 333, - "gridX" : 8, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 334, - "gridX" : 8, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 335, - "gridX" : 8, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 336, - "gridX" : 8, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 337, - "gridX" : 8, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 338, - "gridX" : 8, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 339, - "gridX" : 8, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 340, - "gridX" : 8, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 341, - "gridX" : 8, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 342, - "gridX" : 8, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 343, - "gridX" : 8, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 344, - "gridX" : 8, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 345, - "gridX" : 8, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 346, - "gridX" : 8, - "gridY" : 25, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 347, - "gridX" : 8, - "gridY" : 26, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 348, - "gridX" : 8, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 349, - "gridX" : 8, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 350, - "gridX" : 8, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : true -}, { - "id" : 351, - "gridX" : 8, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 352, - "gridX" : 8, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 353, - "gridX" : 8, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 354, - "gridX" : 8, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 355, - "gridX" : 8, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 356, - "gridX" : 8, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : true -}, { - "id" : 357, - "gridX" : 8, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 358, - "gridX" : 8, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 359, - "gridX" : 8, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 360, - "gridX" : 8, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 361, - "gridX" : 9, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 362, - "gridX" : 9, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 363, - "gridX" : 9, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 364, - "gridX" : 9, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 365, - "gridX" : 9, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 366, - "gridX" : 9, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 367, - "gridX" : 9, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 368, - "gridX" : 9, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 369, - "gridX" : 9, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 370, - "gridX" : 9, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 371, - "gridX" : 9, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 372, - "gridX" : 9, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 373, - "gridX" : 9, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 374, - "gridX" : 9, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 375, - "gridX" : 9, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 376, - "gridX" : 9, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 377, - "gridX" : 9, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 378, - "gridX" : 9, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 379, - "gridX" : 9, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 380, - "gridX" : 9, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 381, - "gridX" : 9, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 382, - "gridX" : 9, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 383, - "gridX" : 9, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 384, - "gridX" : 9, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 385, - "gridX" : 9, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 386, - "gridX" : 9, - "gridY" : 25, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 387, - "gridX" : 9, - "gridY" : 26, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 388, - "gridX" : 9, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 389, - "gridX" : 9, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 390, - "gridX" : 9, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 391, - "gridX" : 9, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 392, - "gridX" : 9, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 393, - "gridX" : 9, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 394, - "gridX" : 9, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 395, - "gridX" : 9, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 396, - "gridX" : 9, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 397, - "gridX" : 9, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 398, - "gridX" : 9, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 399, - "gridX" : 9, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 400, - "gridX" : 9, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 401, - "gridX" : 10, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 402, - "gridX" : 10, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 403, - "gridX" : 10, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 404, - "gridX" : 10, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 405, - "gridX" : 10, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 406, - "gridX" : 10, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 407, - "gridX" : 10, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 408, - "gridX" : 10, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 409, - "gridX" : 10, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 410, - "gridX" : 10, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 411, - "gridX" : 10, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 412, - "gridX" : 10, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 413, - "gridX" : 10, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 414, - "gridX" : 10, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 415, - "gridX" : 10, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 416, - "gridX" : 10, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 417, - "gridX" : 10, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 418, - "gridX" : 10, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 419, - "gridX" : 10, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 420, - "gridX" : 10, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 421, - "gridX" : 10, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 422, - "gridX" : 10, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 423, - "gridX" : 10, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 424, - "gridX" : 10, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 425, - "gridX" : 10, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 426, - "gridX" : 10, - "gridY" : 25, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 427, - "gridX" : 10, - "gridY" : 26, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 428, - "gridX" : 10, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 429, - "gridX" : 10, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 430, - "gridX" : 10, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 431, - "gridX" : 10, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 432, - "gridX" : 10, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : true -}, { - "id" : 433, - "gridX" : 10, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 434, - "gridX" : 10, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 435, - "gridX" : 10, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 436, - "gridX" : 10, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 437, - "gridX" : 10, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 438, - "gridX" : 10, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : true -}, { - "id" : 439, - "gridX" : 10, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 440, - "gridX" : 10, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 441, - "gridX" : 11, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 442, - "gridX" : 11, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 443, - "gridX" : 11, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 444, - "gridX" : 11, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 445, - "gridX" : 11, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 446, - "gridX" : 11, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 447, - "gridX" : 11, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 448, - "gridX" : 11, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 449, - "gridX" : 11, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 450, - "gridX" : 11, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 451, - "gridX" : 11, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 452, - "gridX" : 11, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 453, - "gridX" : 11, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 454, - "gridX" : 11, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 455, - "gridX" : 11, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 456, - "gridX" : 11, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 457, - "gridX" : 11, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 458, - "gridX" : 11, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 459, - "gridX" : 11, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 460, - "gridX" : 11, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 461, - "gridX" : 11, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 462, - "gridX" : 11, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 463, - "gridX" : 11, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 464, - "gridX" : 11, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 465, - "gridX" : 11, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 466, - "gridX" : 11, - "gridY" : 25, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 467, - "gridX" : 11, - "gridY" : 26, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 468, - "gridX" : 11, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 469, - "gridX" : 11, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 470, - "gridX" : 11, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 471, - "gridX" : 11, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 472, - "gridX" : 11, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 473, - "gridX" : 11, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 474, - "gridX" : 11, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 475, - "gridX" : 11, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 476, - "gridX" : 11, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 477, - "gridX" : 11, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 478, - "gridX" : 11, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 479, - "gridX" : 11, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 480, - "gridX" : 11, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 481, - "gridX" : 12, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 482, - "gridX" : 12, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 483, - "gridX" : 12, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 484, - "gridX" : 12, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 485, - "gridX" : 12, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 486, - "gridX" : 12, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 487, - "gridX" : 12, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 488, - "gridX" : 12, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 489, - "gridX" : 12, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 490, - "gridX" : 12, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 491, - "gridX" : 12, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 492, - "gridX" : 12, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 493, - "gridX" : 12, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 494, - "gridX" : 12, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 495, - "gridX" : 12, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 496, - "gridX" : 12, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 497, - "gridX" : 12, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 498, - "gridX" : 12, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 499, - "gridX" : 12, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 500, - "gridX" : 12, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 501, - "gridX" : 12, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 502, - "gridX" : 12, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 503, - "gridX" : 12, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 504, - "gridX" : 12, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 505, - "gridX" : 12, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 506, - "gridX" : 12, - "gridY" : 25, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 507, - "gridX" : 12, - "gridY" : 26, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 508, - "gridX" : 12, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 509, - "gridX" : 12, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 510, - "gridX" : 12, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 511, - "gridX" : 12, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 512, - "gridX" : 12, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 513, - "gridX" : 12, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 514, - "gridX" : 12, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 515, - "gridX" : 12, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 516, - "gridX" : 12, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 517, - "gridX" : 12, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 518, - "gridX" : 12, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 519, - "gridX" : 12, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 520, - "gridX" : 12, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 521, - "gridX" : 13, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 522, - "gridX" : 13, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 523, - "gridX" : 13, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 524, - "gridX" : 13, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 525, - "gridX" : 13, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 526, - "gridX" : 13, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 527, - "gridX" : 13, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 528, - "gridX" : 13, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 529, - "gridX" : 13, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 530, - "gridX" : 13, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 531, - "gridX" : 13, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 532, - "gridX" : 13, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 533, - "gridX" : 13, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 534, - "gridX" : 13, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 535, - "gridX" : 13, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 536, - "gridX" : 13, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 537, - "gridX" : 13, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 538, - "gridX" : 13, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 539, - "gridX" : 13, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 540, - "gridX" : 13, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 541, - "gridX" : 13, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 542, - "gridX" : 13, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 543, - "gridX" : 13, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 544, - "gridX" : 13, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 545, - "gridX" : 13, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 546, - "gridX" : 13, - "gridY" : 25, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 547, - "gridX" : 13, - "gridY" : 26, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 548, - "gridX" : 13, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 549, - "gridX" : 13, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 550, - "gridX" : 13, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 551, - "gridX" : 13, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 552, - "gridX" : 13, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 553, - "gridX" : 13, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 554, - "gridX" : 13, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 555, - "gridX" : 13, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 556, - "gridX" : 13, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 557, - "gridX" : 13, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 558, - "gridX" : 13, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 559, - "gridX" : 13, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 560, - "gridX" : 13, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 561, - "gridX" : 14, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 562, - "gridX" : 14, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 563, - "gridX" : 14, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 564, - "gridX" : 14, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 565, - "gridX" : 14, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 566, - "gridX" : 14, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 567, - "gridX" : 14, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 568, - "gridX" : 14, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 569, - "gridX" : 14, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 570, - "gridX" : 14, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 571, - "gridX" : 14, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 572, - "gridX" : 14, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 573, - "gridX" : 14, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 574, - "gridX" : 14, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 575, - "gridX" : 14, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 576, - "gridX" : 14, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 577, - "gridX" : 14, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 578, - "gridX" : 14, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 579, - "gridX" : 14, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 580, - "gridX" : 14, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 581, - "gridX" : 14, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 582, - "gridX" : 14, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 583, - "gridX" : 14, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 584, - "gridX" : 14, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 585, - "gridX" : 14, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 586, - "gridX" : 14, - "gridY" : 25, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 587, - "gridX" : 14, - "gridY" : 26, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 588, - "gridX" : 14, - "gridY" : 27, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 589, - "gridX" : 14, - "gridY" : 28, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 590, - "gridX" : 14, - "gridY" : 29, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 591, - "gridX" : 14, - "gridY" : 30, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 592, - "gridX" : 14, - "gridY" : 31, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 593, - "gridX" : 14, - "gridY" : 32, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 594, - "gridX" : 14, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 595, - "gridX" : 14, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 596, - "gridX" : 14, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 597, - "gridX" : 14, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 598, - "gridX" : 14, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 599, - "gridX" : 14, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 600, - "gridX" : 14, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 601, - "gridX" : 15, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 602, - "gridX" : 15, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 603, - "gridX" : 15, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 604, - "gridX" : 15, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 605, - "gridX" : 15, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 606, - "gridX" : 15, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 607, - "gridX" : 15, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 608, - "gridX" : 15, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 609, - "gridX" : 15, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 610, - "gridX" : 15, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 611, - "gridX" : 15, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 612, - "gridX" : 15, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 613, - "gridX" : 15, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 614, - "gridX" : 15, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 615, - "gridX" : 15, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 616, - "gridX" : 15, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 617, - "gridX" : 15, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 618, - "gridX" : 15, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 619, - "gridX" : 15, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 620, - "gridX" : 15, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 621, - "gridX" : 15, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 622, - "gridX" : 15, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 623, - "gridX" : 15, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 624, - "gridX" : 15, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 625, - "gridX" : 15, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 626, - "gridX" : 15, - "gridY" : 25, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 627, - "gridX" : 15, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 628, - "gridX" : 15, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 629, - "gridX" : 15, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 630, - "gridX" : 15, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 631, - "gridX" : 15, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 632, - "gridX" : 15, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 633, - "gridX" : 15, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 634, - "gridX" : 15, - "gridY" : 33, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 635, - "gridX" : 15, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : true -}, { - "id" : 636, - "gridX" : 15, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 637, - "gridX" : 15, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : true -}, { - "id" : 638, - "gridX" : 15, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 639, - "gridX" : 15, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 640, - "gridX" : 15, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 641, - "gridX" : 16, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 642, - "gridX" : 16, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 643, - "gridX" : 16, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 644, - "gridX" : 16, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 645, - "gridX" : 16, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 646, - "gridX" : 16, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 647, - "gridX" : 16, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 648, - "gridX" : 16, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 649, - "gridX" : 16, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 650, - "gridX" : 16, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 651, - "gridX" : 16, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 652, - "gridX" : 16, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 653, - "gridX" : 16, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 654, - "gridX" : 16, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 655, - "gridX" : 16, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 656, - "gridX" : 16, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 657, - "gridX" : 16, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 658, - "gridX" : 16, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 659, - "gridX" : 16, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 660, - "gridX" : 16, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 661, - "gridX" : 16, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 662, - "gridX" : 16, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 663, - "gridX" : 16, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 664, - "gridX" : 16, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 665, - "gridX" : 16, - "gridY" : 24, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 666, - "gridX" : 16, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 667, - "gridX" : 16, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 668, - "gridX" : 16, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 669, - "gridX" : 16, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 670, - "gridX" : 16, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 671, - "gridX" : 16, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 672, - "gridX" : 16, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 673, - "gridX" : 16, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 674, - "gridX" : 16, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 675, - "gridX" : 16, - "gridY" : 34, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 676, - "gridX" : 16, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 677, - "gridX" : 16, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 678, - "gridX" : 16, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 679, - "gridX" : 16, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 680, - "gridX" : 16, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 681, - "gridX" : 17, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 682, - "gridX" : 17, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 683, - "gridX" : 17, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 684, - "gridX" : 17, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 685, - "gridX" : 17, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 686, - "gridX" : 17, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 687, - "gridX" : 17, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 688, - "gridX" : 17, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 689, - "gridX" : 17, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 690, - "gridX" : 17, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 691, - "gridX" : 17, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 692, - "gridX" : 17, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 693, - "gridX" : 17, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 694, - "gridX" : 17, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 695, - "gridX" : 17, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 696, - "gridX" : 17, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 697, - "gridX" : 17, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 698, - "gridX" : 17, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 699, - "gridX" : 17, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 700, - "gridX" : 17, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 701, - "gridX" : 17, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 702, - "gridX" : 17, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 703, - "gridX" : 17, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 704, - "gridX" : 17, - "gridY" : 23, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 705, - "gridX" : 17, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 706, - "gridX" : 17, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 707, - "gridX" : 17, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 708, - "gridX" : 17, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 709, - "gridX" : 17, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 710, - "gridX" : 17, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 711, - "gridX" : 17, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 712, - "gridX" : 17, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 713, - "gridX" : 17, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 714, - "gridX" : 17, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 715, - "gridX" : 17, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 716, - "gridX" : 17, - "gridY" : 35, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 717, - "gridX" : 17, - "gridY" : 36, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 718, - "gridX" : 17, - "gridY" : 37, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 719, - "gridX" : 17, - "gridY" : 38, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 720, - "gridX" : 17, - "gridY" : 39, - "cityName" : "南京市", - "districtName" : "江宁区", - "description" : null, - "isObstacle" : false -}, { - "id" : 721, - "gridX" : 18, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 722, - "gridX" : 18, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 723, - "gridX" : 18, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 724, - "gridX" : 18, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 725, - "gridX" : 18, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 726, - "gridX" : 18, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 727, - "gridX" : 18, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 728, - "gridX" : 18, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 729, - "gridX" : 18, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 730, - "gridX" : 18, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 731, - "gridX" : 18, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 732, - "gridX" : 18, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 733, - "gridX" : 18, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 734, - "gridX" : 18, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 735, - "gridX" : 18, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 736, - "gridX" : 18, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 737, - "gridX" : 18, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 738, - "gridX" : 18, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 739, - "gridX" : 18, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 740, - "gridX" : 18, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 741, - "gridX" : 18, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 742, - "gridX" : 18, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 743, - "gridX" : 18, - "gridY" : 22, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 744, - "gridX" : 18, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 745, - "gridX" : 18, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 746, - "gridX" : 18, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 747, - "gridX" : 18, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 748, - "gridX" : 18, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 749, - "gridX" : 18, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 750, - "gridX" : 18, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 751, - "gridX" : 18, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 752, - "gridX" : 18, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 753, - "gridX" : 18, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 754, - "gridX" : 18, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 755, - "gridX" : 18, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 756, - "gridX" : 18, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 757, - "gridX" : 18, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 758, - "gridX" : 18, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 759, - "gridX" : 18, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 760, - "gridX" : 18, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 761, - "gridX" : 19, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 762, - "gridX" : 19, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 763, - "gridX" : 19, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 764, - "gridX" : 19, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 765, - "gridX" : 19, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 766, - "gridX" : 19, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 767, - "gridX" : 19, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 768, - "gridX" : 19, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 769, - "gridX" : 19, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 770, - "gridX" : 19, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 771, - "gridX" : 19, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 772, - "gridX" : 19, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 773, - "gridX" : 19, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 774, - "gridX" : 19, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 775, - "gridX" : 19, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 776, - "gridX" : 19, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 777, - "gridX" : 19, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 778, - "gridX" : 19, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 779, - "gridX" : 19, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 780, - "gridX" : 19, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 781, - "gridX" : 19, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 782, - "gridX" : 19, - "gridY" : 21, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 783, - "gridX" : 19, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 784, - "gridX" : 19, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 785, - "gridX" : 19, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 786, - "gridX" : 19, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 787, - "gridX" : 19, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 788, - "gridX" : 19, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 789, - "gridX" : 19, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 790, - "gridX" : 19, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 791, - "gridX" : 19, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 792, - "gridX" : 19, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 793, - "gridX" : 19, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 794, - "gridX" : 19, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 795, - "gridX" : 19, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 796, - "gridX" : 19, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 797, - "gridX" : 19, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 798, - "gridX" : 19, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 799, - "gridX" : 19, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 800, - "gridX" : 19, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 801, - "gridX" : 20, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 802, - "gridX" : 20, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 803, - "gridX" : 20, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 804, - "gridX" : 20, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 805, - "gridX" : 20, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 806, - "gridX" : 20, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 807, - "gridX" : 20, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 808, - "gridX" : 20, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 809, - "gridX" : 20, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 810, - "gridX" : 20, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 811, - "gridX" : 20, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 812, - "gridX" : 20, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 813, - "gridX" : 20, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 814, - "gridX" : 20, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 815, - "gridX" : 20, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 816, - "gridX" : 20, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 817, - "gridX" : 20, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 818, - "gridX" : 20, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 819, - "gridX" : 20, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 820, - "gridX" : 20, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 821, - "gridX" : 20, - "gridY" : 20, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 822, - "gridX" : 20, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 823, - "gridX" : 20, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 824, - "gridX" : 20, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 825, - "gridX" : 20, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 826, - "gridX" : 20, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 827, - "gridX" : 20, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 828, - "gridX" : 20, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 829, - "gridX" : 20, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 830, - "gridX" : 20, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 831, - "gridX" : 20, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 832, - "gridX" : 20, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 833, - "gridX" : 20, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 834, - "gridX" : 20, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 835, - "gridX" : 20, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 836, - "gridX" : 20, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 837, - "gridX" : 20, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 838, - "gridX" : 20, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 839, - "gridX" : 20, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 840, - "gridX" : 20, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 841, - "gridX" : 21, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 842, - "gridX" : 21, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 843, - "gridX" : 21, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 844, - "gridX" : 21, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 845, - "gridX" : 21, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 846, - "gridX" : 21, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 847, - "gridX" : 21, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 848, - "gridX" : 21, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 849, - "gridX" : 21, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 850, - "gridX" : 21, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 851, - "gridX" : 21, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 852, - "gridX" : 21, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 853, - "gridX" : 21, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 854, - "gridX" : 21, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 855, - "gridX" : 21, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 856, - "gridX" : 21, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 857, - "gridX" : 21, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 858, - "gridX" : 21, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 859, - "gridX" : 21, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 860, - "gridX" : 21, - "gridY" : 19, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 861, - "gridX" : 21, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 862, - "gridX" : 21, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 863, - "gridX" : 21, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 864, - "gridX" : 21, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 865, - "gridX" : 21, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 866, - "gridX" : 21, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 867, - "gridX" : 21, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 868, - "gridX" : 21, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 869, - "gridX" : 21, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 870, - "gridX" : 21, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 871, - "gridX" : 21, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 872, - "gridX" : 21, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 873, - "gridX" : 21, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 874, - "gridX" : 21, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 875, - "gridX" : 21, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 876, - "gridX" : 21, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 877, - "gridX" : 21, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 878, - "gridX" : 21, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 879, - "gridX" : 21, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 880, - "gridX" : 21, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 881, - "gridX" : 22, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 882, - "gridX" : 22, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 883, - "gridX" : 22, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 884, - "gridX" : 22, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 885, - "gridX" : 22, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 886, - "gridX" : 22, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 887, - "gridX" : 22, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 888, - "gridX" : 22, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 889, - "gridX" : 22, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 890, - "gridX" : 22, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 891, - "gridX" : 22, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 892, - "gridX" : 22, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 893, - "gridX" : 22, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 894, - "gridX" : 22, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 895, - "gridX" : 22, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 896, - "gridX" : 22, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 897, - "gridX" : 22, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 898, - "gridX" : 22, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 899, - "gridX" : 22, - "gridY" : 18, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 900, - "gridX" : 22, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 901, - "gridX" : 22, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 902, - "gridX" : 22, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 903, - "gridX" : 22, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 904, - "gridX" : 22, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 905, - "gridX" : 22, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 906, - "gridX" : 22, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 907, - "gridX" : 22, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 908, - "gridX" : 22, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 909, - "gridX" : 22, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 910, - "gridX" : 22, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 911, - "gridX" : 22, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 912, - "gridX" : 22, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 913, - "gridX" : 22, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 914, - "gridX" : 22, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 915, - "gridX" : 22, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 916, - "gridX" : 22, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 917, - "gridX" : 22, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 918, - "gridX" : 22, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 919, - "gridX" : 22, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 920, - "gridX" : 22, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 921, - "gridX" : 23, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 922, - "gridX" : 23, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 923, - "gridX" : 23, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 924, - "gridX" : 23, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 925, - "gridX" : 23, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 926, - "gridX" : 23, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 927, - "gridX" : 23, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 928, - "gridX" : 23, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 929, - "gridX" : 23, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 930, - "gridX" : 23, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 931, - "gridX" : 23, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 932, - "gridX" : 23, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 933, - "gridX" : 23, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 934, - "gridX" : 23, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 935, - "gridX" : 23, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 936, - "gridX" : 23, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 937, - "gridX" : 23, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 938, - "gridX" : 23, - "gridY" : 17, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 939, - "gridX" : 23, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 940, - "gridX" : 23, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 941, - "gridX" : 23, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 942, - "gridX" : 23, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 943, - "gridX" : 23, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 944, - "gridX" : 23, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 945, - "gridX" : 23, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 946, - "gridX" : 23, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 947, - "gridX" : 23, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 948, - "gridX" : 23, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 949, - "gridX" : 23, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 950, - "gridX" : 23, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 951, - "gridX" : 23, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 952, - "gridX" : 23, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 953, - "gridX" : 23, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 954, - "gridX" : 23, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 955, - "gridX" : 23, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 956, - "gridX" : 23, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 957, - "gridX" : 23, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 958, - "gridX" : 23, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 959, - "gridX" : 23, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 960, - "gridX" : 23, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 961, - "gridX" : 24, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 962, - "gridX" : 24, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 963, - "gridX" : 24, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 964, - "gridX" : 24, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 965, - "gridX" : 24, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 966, - "gridX" : 24, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 967, - "gridX" : 24, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 968, - "gridX" : 24, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 969, - "gridX" : 24, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 970, - "gridX" : 24, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 971, - "gridX" : 24, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 972, - "gridX" : 24, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 973, - "gridX" : 24, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 974, - "gridX" : 24, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 975, - "gridX" : 24, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 976, - "gridX" : 24, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 977, - "gridX" : 24, - "gridY" : 16, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 978, - "gridX" : 24, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 979, - "gridX" : 24, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 980, - "gridX" : 24, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 981, - "gridX" : 24, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 982, - "gridX" : 24, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 983, - "gridX" : 24, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 984, - "gridX" : 24, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 985, - "gridX" : 24, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 986, - "gridX" : 24, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 987, - "gridX" : 24, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 988, - "gridX" : 24, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 989, - "gridX" : 24, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 990, - "gridX" : 24, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 991, - "gridX" : 24, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 992, - "gridX" : 24, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 993, - "gridX" : 24, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 994, - "gridX" : 24, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 995, - "gridX" : 24, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 996, - "gridX" : 24, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 997, - "gridX" : 24, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 998, - "gridX" : 24, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 999, - "gridX" : 24, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1000, - "gridX" : 24, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1001, - "gridX" : 25, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1002, - "gridX" : 25, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1003, - "gridX" : 25, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1004, - "gridX" : 25, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1005, - "gridX" : 25, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1006, - "gridX" : 25, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1007, - "gridX" : 25, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1008, - "gridX" : 25, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1009, - "gridX" : 25, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1010, - "gridX" : 25, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1011, - "gridX" : 25, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1012, - "gridX" : 25, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1013, - "gridX" : 25, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1014, - "gridX" : 25, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1015, - "gridX" : 25, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1016, - "gridX" : 25, - "gridY" : 15, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1017, - "gridX" : 25, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1018, - "gridX" : 25, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1019, - "gridX" : 25, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1020, - "gridX" : 25, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1021, - "gridX" : 25, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1022, - "gridX" : 25, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1023, - "gridX" : 25, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1024, - "gridX" : 25, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1025, - "gridX" : 25, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1026, - "gridX" : 25, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1027, - "gridX" : 25, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1028, - "gridX" : 25, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1029, - "gridX" : 25, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1030, - "gridX" : 25, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1031, - "gridX" : 25, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1032, - "gridX" : 25, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1033, - "gridX" : 25, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1034, - "gridX" : 25, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1035, - "gridX" : 25, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1036, - "gridX" : 25, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1037, - "gridX" : 25, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1038, - "gridX" : 25, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1039, - "gridX" : 25, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1040, - "gridX" : 25, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1041, - "gridX" : 26, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1042, - "gridX" : 26, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1043, - "gridX" : 26, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1044, - "gridX" : 26, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1045, - "gridX" : 26, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1046, - "gridX" : 26, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1047, - "gridX" : 26, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1048, - "gridX" : 26, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1049, - "gridX" : 26, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1050, - "gridX" : 26, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1051, - "gridX" : 26, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1052, - "gridX" : 26, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1053, - "gridX" : 26, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1054, - "gridX" : 26, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1055, - "gridX" : 26, - "gridY" : 14, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1056, - "gridX" : 26, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1057, - "gridX" : 26, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1058, - "gridX" : 26, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1059, - "gridX" : 26, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1060, - "gridX" : 26, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1061, - "gridX" : 26, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1062, - "gridX" : 26, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1063, - "gridX" : 26, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1064, - "gridX" : 26, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1065, - "gridX" : 26, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1066, - "gridX" : 26, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1067, - "gridX" : 26, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1068, - "gridX" : 26, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1069, - "gridX" : 26, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1070, - "gridX" : 26, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1071, - "gridX" : 26, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1072, - "gridX" : 26, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1073, - "gridX" : 26, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1074, - "gridX" : 26, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1075, - "gridX" : 26, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1076, - "gridX" : 26, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1077, - "gridX" : 26, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1078, - "gridX" : 26, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1079, - "gridX" : 26, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1080, - "gridX" : 26, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1081, - "gridX" : 27, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1082, - "gridX" : 27, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1083, - "gridX" : 27, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1084, - "gridX" : 27, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1085, - "gridX" : 27, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1086, - "gridX" : 27, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1087, - "gridX" : 27, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1088, - "gridX" : 27, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1089, - "gridX" : 27, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1090, - "gridX" : 27, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1091, - "gridX" : 27, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1092, - "gridX" : 27, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1093, - "gridX" : 27, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1094, - "gridX" : 27, - "gridY" : 13, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1095, - "gridX" : 27, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1096, - "gridX" : 27, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1097, - "gridX" : 27, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1098, - "gridX" : 27, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1099, - "gridX" : 27, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1100, - "gridX" : 27, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1101, - "gridX" : 27, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1102, - "gridX" : 27, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1103, - "gridX" : 27, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1104, - "gridX" : 27, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1105, - "gridX" : 27, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1106, - "gridX" : 27, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1107, - "gridX" : 27, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1108, - "gridX" : 27, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1109, - "gridX" : 27, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1110, - "gridX" : 27, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1111, - "gridX" : 27, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1112, - "gridX" : 27, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1113, - "gridX" : 27, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1114, - "gridX" : 27, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1115, - "gridX" : 27, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1116, - "gridX" : 27, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1117, - "gridX" : 27, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1118, - "gridX" : 27, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1119, - "gridX" : 27, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1120, - "gridX" : 27, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1121, - "gridX" : 28, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1122, - "gridX" : 28, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1123, - "gridX" : 28, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1124, - "gridX" : 28, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1125, - "gridX" : 28, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1126, - "gridX" : 28, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1127, - "gridX" : 28, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1128, - "gridX" : 28, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1129, - "gridX" : 28, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1130, - "gridX" : 28, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1131, - "gridX" : 28, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1132, - "gridX" : 28, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1133, - "gridX" : 28, - "gridY" : 12, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1134, - "gridX" : 28, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1135, - "gridX" : 28, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1136, - "gridX" : 28, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1137, - "gridX" : 28, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1138, - "gridX" : 28, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1139, - "gridX" : 28, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1140, - "gridX" : 28, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1141, - "gridX" : 28, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1142, - "gridX" : 28, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1143, - "gridX" : 28, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1144, - "gridX" : 28, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1145, - "gridX" : 28, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1146, - "gridX" : 28, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1147, - "gridX" : 28, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1148, - "gridX" : 28, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1149, - "gridX" : 28, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1150, - "gridX" : 28, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1151, - "gridX" : 28, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1152, - "gridX" : 28, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1153, - "gridX" : 28, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1154, - "gridX" : 28, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1155, - "gridX" : 28, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1156, - "gridX" : 28, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1157, - "gridX" : 28, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1158, - "gridX" : 28, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1159, - "gridX" : 28, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1160, - "gridX" : 28, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1161, - "gridX" : 29, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1162, - "gridX" : 29, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1163, - "gridX" : 29, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1164, - "gridX" : 29, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1165, - "gridX" : 29, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1166, - "gridX" : 29, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1167, - "gridX" : 29, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1168, - "gridX" : 29, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1169, - "gridX" : 29, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1170, - "gridX" : 29, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1171, - "gridX" : 29, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1172, - "gridX" : 29, - "gridY" : 11, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1173, - "gridX" : 29, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1174, - "gridX" : 29, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1175, - "gridX" : 29, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1176, - "gridX" : 29, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1177, - "gridX" : 29, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1178, - "gridX" : 29, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1179, - "gridX" : 29, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1180, - "gridX" : 29, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1181, - "gridX" : 29, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1182, - "gridX" : 29, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1183, - "gridX" : 29, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1184, - "gridX" : 29, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1185, - "gridX" : 29, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1186, - "gridX" : 29, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1187, - "gridX" : 29, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1188, - "gridX" : 29, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1189, - "gridX" : 29, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1190, - "gridX" : 29, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1191, - "gridX" : 29, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1192, - "gridX" : 29, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1193, - "gridX" : 29, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1194, - "gridX" : 29, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1195, - "gridX" : 29, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1196, - "gridX" : 29, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1197, - "gridX" : 29, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1198, - "gridX" : 29, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1199, - "gridX" : 29, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1200, - "gridX" : 29, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1201, - "gridX" : 30, - "gridY" : 0, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1202, - "gridX" : 30, - "gridY" : 1, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1203, - "gridX" : 30, - "gridY" : 2, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1204, - "gridX" : 30, - "gridY" : 3, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1205, - "gridX" : 30, - "gridY" : 4, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1206, - "gridX" : 30, - "gridY" : 5, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1207, - "gridX" : 30, - "gridY" : 6, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1208, - "gridX" : 30, - "gridY" : 7, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1209, - "gridX" : 30, - "gridY" : 8, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1210, - "gridX" : 30, - "gridY" : 9, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1211, - "gridX" : 30, - "gridY" : 10, - "cityName" : "常州市", - "districtName" : "溧阳市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1212, - "gridX" : 30, - "gridY" : 11, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1213, - "gridX" : 30, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1214, - "gridX" : 30, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1215, - "gridX" : 30, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1216, - "gridX" : 30, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1217, - "gridX" : 30, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1218, - "gridX" : 30, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1219, - "gridX" : 30, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1220, - "gridX" : 30, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1221, - "gridX" : 30, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1222, - "gridX" : 30, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1223, - "gridX" : 30, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1224, - "gridX" : 30, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1225, - "gridX" : 30, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1226, - "gridX" : 30, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1227, - "gridX" : 30, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1228, - "gridX" : 30, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1229, - "gridX" : 30, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1230, - "gridX" : 30, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1231, - "gridX" : 30, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1232, - "gridX" : 30, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1233, - "gridX" : 30, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1234, - "gridX" : 30, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1235, - "gridX" : 30, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1236, - "gridX" : 30, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1237, - "gridX" : 30, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1238, - "gridX" : 30, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1239, - "gridX" : 30, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1240, - "gridX" : 30, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1241, - "gridX" : 31, - "gridY" : 0, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1242, - "gridX" : 31, - "gridY" : 1, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1243, - "gridX" : 31, - "gridY" : 2, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1244, - "gridX" : 31, - "gridY" : 3, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1245, - "gridX" : 31, - "gridY" : 4, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1246, - "gridX" : 31, - "gridY" : 5, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1247, - "gridX" : 31, - "gridY" : 6, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1248, - "gridX" : 31, - "gridY" : 7, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1249, - "gridX" : 31, - "gridY" : 8, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1250, - "gridX" : 31, - "gridY" : 9, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1251, - "gridX" : 31, - "gridY" : 10, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1252, - "gridX" : 31, - "gridY" : 11, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1253, - "gridX" : 31, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1254, - "gridX" : 31, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1255, - "gridX" : 31, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1256, - "gridX" : 31, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1257, - "gridX" : 31, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1258, - "gridX" : 31, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1259, - "gridX" : 31, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1260, - "gridX" : 31, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1261, - "gridX" : 31, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1262, - "gridX" : 31, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1263, - "gridX" : 31, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1264, - "gridX" : 31, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1265, - "gridX" : 31, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1266, - "gridX" : 31, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1267, - "gridX" : 31, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1268, - "gridX" : 31, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1269, - "gridX" : 31, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1270, - "gridX" : 31, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1271, - "gridX" : 31, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1272, - "gridX" : 31, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1273, - "gridX" : 31, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1274, - "gridX" : 31, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1275, - "gridX" : 31, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1276, - "gridX" : 31, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1277, - "gridX" : 31, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1278, - "gridX" : 31, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1279, - "gridX" : 31, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1280, - "gridX" : 31, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1281, - "gridX" : 32, - "gridY" : 0, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1282, - "gridX" : 32, - "gridY" : 1, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1283, - "gridX" : 32, - "gridY" : 2, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1284, - "gridX" : 32, - "gridY" : 3, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1285, - "gridX" : 32, - "gridY" : 4, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1286, - "gridX" : 32, - "gridY" : 5, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1287, - "gridX" : 32, - "gridY" : 6, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1288, - "gridX" : 32, - "gridY" : 7, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1289, - "gridX" : 32, - "gridY" : 8, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1290, - "gridX" : 32, - "gridY" : 9, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1291, - "gridX" : 32, - "gridY" : 10, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1292, - "gridX" : 32, - "gridY" : 11, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1293, - "gridX" : 32, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1294, - "gridX" : 32, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1295, - "gridX" : 32, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1296, - "gridX" : 32, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1297, - "gridX" : 32, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1298, - "gridX" : 32, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1299, - "gridX" : 32, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1300, - "gridX" : 32, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1301, - "gridX" : 32, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1302, - "gridX" : 32, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1303, - "gridX" : 32, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1304, - "gridX" : 32, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1305, - "gridX" : 32, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1306, - "gridX" : 32, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1307, - "gridX" : 32, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1308, - "gridX" : 32, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1309, - "gridX" : 32, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1310, - "gridX" : 32, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1311, - "gridX" : 32, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1312, - "gridX" : 32, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1313, - "gridX" : 32, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1314, - "gridX" : 32, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1315, - "gridX" : 32, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1316, - "gridX" : 32, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1317, - "gridX" : 32, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1318, - "gridX" : 32, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1319, - "gridX" : 32, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1320, - "gridX" : 32, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1321, - "gridX" : 33, - "gridY" : 0, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1322, - "gridX" : 33, - "gridY" : 1, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1323, - "gridX" : 33, - "gridY" : 2, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1324, - "gridX" : 33, - "gridY" : 3, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1325, - "gridX" : 33, - "gridY" : 4, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1326, - "gridX" : 33, - "gridY" : 5, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1327, - "gridX" : 33, - "gridY" : 6, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1328, - "gridX" : 33, - "gridY" : 7, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1329, - "gridX" : 33, - "gridY" : 8, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1330, - "gridX" : 33, - "gridY" : 9, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1331, - "gridX" : 33, - "gridY" : 10, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1332, - "gridX" : 33, - "gridY" : 11, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1333, - "gridX" : 33, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1334, - "gridX" : 33, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1335, - "gridX" : 33, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1336, - "gridX" : 33, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1337, - "gridX" : 33, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1338, - "gridX" : 33, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1339, - "gridX" : 33, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1340, - "gridX" : 33, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1341, - "gridX" : 33, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1342, - "gridX" : 33, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1343, - "gridX" : 33, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1344, - "gridX" : 33, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1345, - "gridX" : 33, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1346, - "gridX" : 33, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1347, - "gridX" : 33, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1348, - "gridX" : 33, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1349, - "gridX" : 33, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1350, - "gridX" : 33, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1351, - "gridX" : 33, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1352, - "gridX" : 33, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1353, - "gridX" : 33, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1354, - "gridX" : 33, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1355, - "gridX" : 33, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1356, - "gridX" : 33, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1357, - "gridX" : 33, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1358, - "gridX" : 33, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1359, - "gridX" : 33, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1360, - "gridX" : 33, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1361, - "gridX" : 34, - "gridY" : 0, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1362, - "gridX" : 34, - "gridY" : 1, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1363, - "gridX" : 34, - "gridY" : 2, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1364, - "gridX" : 34, - "gridY" : 3, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1365, - "gridX" : 34, - "gridY" : 4, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1366, - "gridX" : 34, - "gridY" : 5, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1367, - "gridX" : 34, - "gridY" : 6, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1368, - "gridX" : 34, - "gridY" : 7, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1369, - "gridX" : 34, - "gridY" : 8, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1370, - "gridX" : 34, - "gridY" : 9, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1371, - "gridX" : 34, - "gridY" : 10, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1372, - "gridX" : 34, - "gridY" : 11, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1373, - "gridX" : 34, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1374, - "gridX" : 34, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1375, - "gridX" : 34, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1376, - "gridX" : 34, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1377, - "gridX" : 34, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1378, - "gridX" : 34, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1379, - "gridX" : 34, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1380, - "gridX" : 34, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1381, - "gridX" : 34, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1382, - "gridX" : 34, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1383, - "gridX" : 34, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1384, - "gridX" : 34, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1385, - "gridX" : 34, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1386, - "gridX" : 34, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1387, - "gridX" : 34, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1388, - "gridX" : 34, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1389, - "gridX" : 34, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1390, - "gridX" : 34, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1391, - "gridX" : 34, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1392, - "gridX" : 34, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1393, - "gridX" : 34, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1394, - "gridX" : 34, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1395, - "gridX" : 34, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1396, - "gridX" : 34, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1397, - "gridX" : 34, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1398, - "gridX" : 34, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1399, - "gridX" : 34, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1400, - "gridX" : 34, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1401, - "gridX" : 35, - "gridY" : 0, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1402, - "gridX" : 35, - "gridY" : 1, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1403, - "gridX" : 35, - "gridY" : 2, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1404, - "gridX" : 35, - "gridY" : 3, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1405, - "gridX" : 35, - "gridY" : 4, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1406, - "gridX" : 35, - "gridY" : 5, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1407, - "gridX" : 35, - "gridY" : 6, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1408, - "gridX" : 35, - "gridY" : 7, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1409, - "gridX" : 35, - "gridY" : 8, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1410, - "gridX" : 35, - "gridY" : 9, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1411, - "gridX" : 35, - "gridY" : 10, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1412, - "gridX" : 35, - "gridY" : 11, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1413, - "gridX" : 35, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1414, - "gridX" : 35, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1415, - "gridX" : 35, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1416, - "gridX" : 35, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1417, - "gridX" : 35, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1418, - "gridX" : 35, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1419, - "gridX" : 35, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1420, - "gridX" : 35, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1421, - "gridX" : 35, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1422, - "gridX" : 35, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1423, - "gridX" : 35, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1424, - "gridX" : 35, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1425, - "gridX" : 35, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1426, - "gridX" : 35, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1427, - "gridX" : 35, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1428, - "gridX" : 35, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1429, - "gridX" : 35, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1430, - "gridX" : 35, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1431, - "gridX" : 35, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1432, - "gridX" : 35, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1433, - "gridX" : 35, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1434, - "gridX" : 35, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1435, - "gridX" : 35, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1436, - "gridX" : 35, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1437, - "gridX" : 35, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1438, - "gridX" : 35, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1439, - "gridX" : 35, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1440, - "gridX" : 35, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1441, - "gridX" : 36, - "gridY" : 0, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1442, - "gridX" : 36, - "gridY" : 1, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1443, - "gridX" : 36, - "gridY" : 2, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1444, - "gridX" : 36, - "gridY" : 3, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1445, - "gridX" : 36, - "gridY" : 4, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1446, - "gridX" : 36, - "gridY" : 5, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1447, - "gridX" : 36, - "gridY" : 6, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1448, - "gridX" : 36, - "gridY" : 7, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1449, - "gridX" : 36, - "gridY" : 8, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1450, - "gridX" : 36, - "gridY" : 9, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1451, - "gridX" : 36, - "gridY" : 10, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1452, - "gridX" : 36, - "gridY" : 11, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1453, - "gridX" : 36, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1454, - "gridX" : 36, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1455, - "gridX" : 36, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1456, - "gridX" : 36, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1457, - "gridX" : 36, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1458, - "gridX" : 36, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1459, - "gridX" : 36, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1460, - "gridX" : 36, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1461, - "gridX" : 36, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1462, - "gridX" : 36, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1463, - "gridX" : 36, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1464, - "gridX" : 36, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1465, - "gridX" : 36, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1466, - "gridX" : 36, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1467, - "gridX" : 36, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1468, - "gridX" : 36, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1469, - "gridX" : 36, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1470, - "gridX" : 36, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1471, - "gridX" : 36, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1472, - "gridX" : 36, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1473, - "gridX" : 36, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1474, - "gridX" : 36, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1475, - "gridX" : 36, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1476, - "gridX" : 36, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1477, - "gridX" : 36, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1478, - "gridX" : 36, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1479, - "gridX" : 36, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1480, - "gridX" : 36, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1481, - "gridX" : 37, - "gridY" : 0, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1482, - "gridX" : 37, - "gridY" : 1, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1483, - "gridX" : 37, - "gridY" : 2, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1484, - "gridX" : 37, - "gridY" : 3, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1485, - "gridX" : 37, - "gridY" : 4, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1486, - "gridX" : 37, - "gridY" : 5, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1487, - "gridX" : 37, - "gridY" : 6, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1488, - "gridX" : 37, - "gridY" : 7, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1489, - "gridX" : 37, - "gridY" : 8, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1490, - "gridX" : 37, - "gridY" : 9, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1491, - "gridX" : 37, - "gridY" : 10, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1492, - "gridX" : 37, - "gridY" : 11, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1493, - "gridX" : 37, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1494, - "gridX" : 37, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1495, - "gridX" : 37, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1496, - "gridX" : 37, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1497, - "gridX" : 37, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1498, - "gridX" : 37, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1499, - "gridX" : 37, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1500, - "gridX" : 37, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1501, - "gridX" : 37, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1502, - "gridX" : 37, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1503, - "gridX" : 37, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1504, - "gridX" : 37, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1505, - "gridX" : 37, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1506, - "gridX" : 37, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1507, - "gridX" : 37, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1508, - "gridX" : 37, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1509, - "gridX" : 37, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1510, - "gridX" : 37, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1511, - "gridX" : 37, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1512, - "gridX" : 37, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1513, - "gridX" : 37, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1514, - "gridX" : 37, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1515, - "gridX" : 37, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1516, - "gridX" : 37, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1517, - "gridX" : 37, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1518, - "gridX" : 37, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1519, - "gridX" : 37, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1520, - "gridX" : 37, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1521, - "gridX" : 38, - "gridY" : 0, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1522, - "gridX" : 38, - "gridY" : 1, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1523, - "gridX" : 38, - "gridY" : 2, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1524, - "gridX" : 38, - "gridY" : 3, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1525, - "gridX" : 38, - "gridY" : 4, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1526, - "gridX" : 38, - "gridY" : 5, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1527, - "gridX" : 38, - "gridY" : 6, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1528, - "gridX" : 38, - "gridY" : 7, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1529, - "gridX" : 38, - "gridY" : 8, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1530, - "gridX" : 38, - "gridY" : 9, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1531, - "gridX" : 38, - "gridY" : 10, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1532, - "gridX" : 38, - "gridY" : 11, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1533, - "gridX" : 38, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1534, - "gridX" : 38, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1535, - "gridX" : 38, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1536, - "gridX" : 38, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1537, - "gridX" : 38, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1538, - "gridX" : 38, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1539, - "gridX" : 38, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1540, - "gridX" : 38, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1541, - "gridX" : 38, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1542, - "gridX" : 38, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1543, - "gridX" : 38, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1544, - "gridX" : 38, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1545, - "gridX" : 38, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1546, - "gridX" : 38, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1547, - "gridX" : 38, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1548, - "gridX" : 38, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1549, - "gridX" : 38, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1550, - "gridX" : 38, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1551, - "gridX" : 38, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : true -}, { - "id" : 1552, - "gridX" : 38, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1553, - "gridX" : 38, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1554, - "gridX" : 38, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1555, - "gridX" : 38, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1556, - "gridX" : 38, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1557, - "gridX" : 38, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1558, - "gridX" : 38, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1559, - "gridX" : 38, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1560, - "gridX" : 38, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1561, - "gridX" : 39, - "gridY" : 0, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1562, - "gridX" : 39, - "gridY" : 1, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1563, - "gridX" : 39, - "gridY" : 2, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1564, - "gridX" : 39, - "gridY" : 3, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1565, - "gridX" : 39, - "gridY" : 4, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1566, - "gridX" : 39, - "gridY" : 5, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1567, - "gridX" : 39, - "gridY" : 6, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1568, - "gridX" : 39, - "gridY" : 7, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1569, - "gridX" : 39, - "gridY" : 8, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1570, - "gridX" : 39, - "gridY" : 9, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1571, - "gridX" : 39, - "gridY" : 10, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1572, - "gridX" : 39, - "gridY" : 11, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1573, - "gridX" : 39, - "gridY" : 12, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1574, - "gridX" : 39, - "gridY" : 13, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1575, - "gridX" : 39, - "gridY" : 14, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1576, - "gridX" : 39, - "gridY" : 15, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1577, - "gridX" : 39, - "gridY" : 16, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1578, - "gridX" : 39, - "gridY" : 17, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1579, - "gridX" : 39, - "gridY" : 18, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1580, - "gridX" : 39, - "gridY" : 19, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1581, - "gridX" : 39, - "gridY" : 20, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1582, - "gridX" : 39, - "gridY" : 21, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1583, - "gridX" : 39, - "gridY" : 22, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1584, - "gridX" : 39, - "gridY" : 23, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1585, - "gridX" : 39, - "gridY" : 24, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1586, - "gridX" : 39, - "gridY" : 25, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1587, - "gridX" : 39, - "gridY" : 26, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1588, - "gridX" : 39, - "gridY" : 27, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1589, - "gridX" : 39, - "gridY" : 28, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1590, - "gridX" : 39, - "gridY" : 29, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1591, - "gridX" : 39, - "gridY" : 30, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1592, - "gridX" : 39, - "gridY" : 31, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1593, - "gridX" : 39, - "gridY" : 32, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1594, - "gridX" : 39, - "gridY" : 33, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1595, - "gridX" : 39, - "gridY" : 34, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1596, - "gridX" : 39, - "gridY" : 35, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1597, - "gridX" : 39, - "gridY" : 36, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1598, - "gridX" : 39, - "gridY" : 37, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1599, - "gridX" : 39, - "gridY" : 38, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false -}, { - "id" : 1600, - "gridX" : 39, - "gridY" : 39, - "cityName" : "无锡市", - "districtName" : "宜兴市", - "description" : null, - "isObstacle" : false +[ { + "id" : 1, + "gridX" : 0, + "gridY" : 0, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 2, + "gridX" : 0, + "gridY" : 1, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 3, + "gridX" : 0, + "gridY" : 2, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 4, + "gridX" : 0, + "gridY" : 3, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 5, + "gridX" : 0, + "gridY" : 4, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 6, + "gridX" : 0, + "gridY" : 5, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 7, + "gridX" : 0, + "gridY" : 6, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 8, + "gridX" : 0, + "gridY" : 7, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 9, + "gridX" : 0, + "gridY" : 8, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 10, + "gridX" : 0, + "gridY" : 9, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 11, + "gridX" : 0, + "gridY" : 10, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 12, + "gridX" : 0, + "gridY" : 11, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 13, + "gridX" : 0, + "gridY" : 12, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 14, + "gridX" : 0, + "gridY" : 13, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 15, + "gridX" : 0, + "gridY" : 14, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 16, + "gridX" : 0, + "gridY" : 15, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 17, + "gridX" : 0, + "gridY" : 16, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 18, + "gridX" : 0, + "gridY" : 17, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 19, + "gridX" : 0, + "gridY" : 18, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 20, + "gridX" : 0, + "gridY" : 19, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 21, + "gridX" : 0, + "gridY" : 20, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 22, + "gridX" : 0, + "gridY" : 21, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 23, + "gridX" : 0, + "gridY" : 22, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 24, + "gridX" : 0, + "gridY" : 23, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 25, + "gridX" : 0, + "gridY" : 24, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 26, + "gridX" : 0, + "gridY" : 25, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 27, + "gridX" : 0, + "gridY" : 26, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 28, + "gridX" : 0, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 29, + "gridX" : 0, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 30, + "gridX" : 0, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 31, + "gridX" : 0, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 32, + "gridX" : 0, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 33, + "gridX" : 0, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 34, + "gridX" : 0, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 35, + "gridX" : 0, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 36, + "gridX" : 0, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 37, + "gridX" : 0, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 38, + "gridX" : 0, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 39, + "gridX" : 0, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 40, + "gridX" : 0, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 41, + "gridX" : 1, + "gridY" : 0, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 42, + "gridX" : 1, + "gridY" : 1, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 43, + "gridX" : 1, + "gridY" : 2, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 44, + "gridX" : 1, + "gridY" : 3, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 45, + "gridX" : 1, + "gridY" : 4, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 46, + "gridX" : 1, + "gridY" : 5, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 47, + "gridX" : 1, + "gridY" : 6, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 48, + "gridX" : 1, + "gridY" : 7, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 49, + "gridX" : 1, + "gridY" : 8, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 50, + "gridX" : 1, + "gridY" : 9, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 51, + "gridX" : 1, + "gridY" : 10, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 52, + "gridX" : 1, + "gridY" : 11, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 53, + "gridX" : 1, + "gridY" : 12, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 54, + "gridX" : 1, + "gridY" : 13, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 55, + "gridX" : 1, + "gridY" : 14, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 56, + "gridX" : 1, + "gridY" : 15, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 57, + "gridX" : 1, + "gridY" : 16, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 58, + "gridX" : 1, + "gridY" : 17, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 59, + "gridX" : 1, + "gridY" : 18, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 60, + "gridX" : 1, + "gridY" : 19, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 61, + "gridX" : 1, + "gridY" : 20, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 62, + "gridX" : 1, + "gridY" : 21, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 63, + "gridX" : 1, + "gridY" : 22, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 64, + "gridX" : 1, + "gridY" : 23, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 65, + "gridX" : 1, + "gridY" : 24, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 66, + "gridX" : 1, + "gridY" : 25, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 67, + "gridX" : 1, + "gridY" : 26, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 68, + "gridX" : 1, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 69, + "gridX" : 1, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 70, + "gridX" : 1, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 71, + "gridX" : 1, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 72, + "gridX" : 1, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 73, + "gridX" : 1, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 74, + "gridX" : 1, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 75, + "gridX" : 1, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 76, + "gridX" : 1, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 77, + "gridX" : 1, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 78, + "gridX" : 1, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 79, + "gridX" : 1, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 80, + "gridX" : 1, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 81, + "gridX" : 2, + "gridY" : 0, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 82, + "gridX" : 2, + "gridY" : 1, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 83, + "gridX" : 2, + "gridY" : 2, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 84, + "gridX" : 2, + "gridY" : 3, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 85, + "gridX" : 2, + "gridY" : 4, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 86, + "gridX" : 2, + "gridY" : 5, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 87, + "gridX" : 2, + "gridY" : 6, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 88, + "gridX" : 2, + "gridY" : 7, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 89, + "gridX" : 2, + "gridY" : 8, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 90, + "gridX" : 2, + "gridY" : 9, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 91, + "gridX" : 2, + "gridY" : 10, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 92, + "gridX" : 2, + "gridY" : 11, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 93, + "gridX" : 2, + "gridY" : 12, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 94, + "gridX" : 2, + "gridY" : 13, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 95, + "gridX" : 2, + "gridY" : 14, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 96, + "gridX" : 2, + "gridY" : 15, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 97, + "gridX" : 2, + "gridY" : 16, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 98, + "gridX" : 2, + "gridY" : 17, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 99, + "gridX" : 2, + "gridY" : 18, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 100, + "gridX" : 2, + "gridY" : 19, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 101, + "gridX" : 2, + "gridY" : 20, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 102, + "gridX" : 2, + "gridY" : 21, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 103, + "gridX" : 2, + "gridY" : 22, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 104, + "gridX" : 2, + "gridY" : 23, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 105, + "gridX" : 2, + "gridY" : 24, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 106, + "gridX" : 2, + "gridY" : 25, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 107, + "gridX" : 2, + "gridY" : 26, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 108, + "gridX" : 2, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 109, + "gridX" : 2, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : true +}, { + "id" : 110, + "gridX" : 2, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 111, + "gridX" : 2, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 112, + "gridX" : 2, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 113, + "gridX" : 2, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 114, + "gridX" : 2, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 115, + "gridX" : 2, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 116, + "gridX" : 2, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 117, + "gridX" : 2, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 118, + "gridX" : 2, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 119, + "gridX" : 2, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 120, + "gridX" : 2, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 121, + "gridX" : 3, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 122, + "gridX" : 3, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 123, + "gridX" : 3, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 124, + "gridX" : 3, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 125, + "gridX" : 3, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 126, + "gridX" : 3, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 127, + "gridX" : 3, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 128, + "gridX" : 3, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 129, + "gridX" : 3, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 130, + "gridX" : 3, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 131, + "gridX" : 3, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 132, + "gridX" : 3, + "gridY" : 11, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 133, + "gridX" : 3, + "gridY" : 12, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 134, + "gridX" : 3, + "gridY" : 13, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 135, + "gridX" : 3, + "gridY" : 14, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 136, + "gridX" : 3, + "gridY" : 15, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 137, + "gridX" : 3, + "gridY" : 16, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 138, + "gridX" : 3, + "gridY" : 17, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 139, + "gridX" : 3, + "gridY" : 18, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 140, + "gridX" : 3, + "gridY" : 19, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 141, + "gridX" : 3, + "gridY" : 20, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 142, + "gridX" : 3, + "gridY" : 21, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 143, + "gridX" : 3, + "gridY" : 22, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 144, + "gridX" : 3, + "gridY" : 23, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 145, + "gridX" : 3, + "gridY" : 24, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 146, + "gridX" : 3, + "gridY" : 25, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 147, + "gridX" : 3, + "gridY" : 26, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 148, + "gridX" : 3, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 149, + "gridX" : 3, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 150, + "gridX" : 3, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 151, + "gridX" : 3, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 152, + "gridX" : 3, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 153, + "gridX" : 3, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 154, + "gridX" : 3, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 155, + "gridX" : 3, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 156, + "gridX" : 3, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 157, + "gridX" : 3, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 158, + "gridX" : 3, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 159, + "gridX" : 3, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 160, + "gridX" : 3, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 161, + "gridX" : 4, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 162, + "gridX" : 4, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 163, + "gridX" : 4, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 164, + "gridX" : 4, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 165, + "gridX" : 4, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 166, + "gridX" : 4, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 167, + "gridX" : 4, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 168, + "gridX" : 4, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 169, + "gridX" : 4, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 170, + "gridX" : 4, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 171, + "gridX" : 4, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 172, + "gridX" : 4, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 173, + "gridX" : 4, + "gridY" : 12, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 174, + "gridX" : 4, + "gridY" : 13, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 175, + "gridX" : 4, + "gridY" : 14, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 176, + "gridX" : 4, + "gridY" : 15, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 177, + "gridX" : 4, + "gridY" : 16, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 178, + "gridX" : 4, + "gridY" : 17, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 179, + "gridX" : 4, + "gridY" : 18, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 180, + "gridX" : 4, + "gridY" : 19, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 181, + "gridX" : 4, + "gridY" : 20, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 182, + "gridX" : 4, + "gridY" : 21, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 183, + "gridX" : 4, + "gridY" : 22, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 184, + "gridX" : 4, + "gridY" : 23, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 185, + "gridX" : 4, + "gridY" : 24, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 186, + "gridX" : 4, + "gridY" : 25, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 187, + "gridX" : 4, + "gridY" : 26, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 188, + "gridX" : 4, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 189, + "gridX" : 4, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 190, + "gridX" : 4, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 191, + "gridX" : 4, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 192, + "gridX" : 4, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 193, + "gridX" : 4, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 194, + "gridX" : 4, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 195, + "gridX" : 4, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 196, + "gridX" : 4, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 197, + "gridX" : 4, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 198, + "gridX" : 4, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 199, + "gridX" : 4, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 200, + "gridX" : 4, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 201, + "gridX" : 5, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 202, + "gridX" : 5, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 203, + "gridX" : 5, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 204, + "gridX" : 5, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 205, + "gridX" : 5, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 206, + "gridX" : 5, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 207, + "gridX" : 5, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 208, + "gridX" : 5, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 209, + "gridX" : 5, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 210, + "gridX" : 5, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 211, + "gridX" : 5, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 212, + "gridX" : 5, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 213, + "gridX" : 5, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 214, + "gridX" : 5, + "gridY" : 13, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 215, + "gridX" : 5, + "gridY" : 14, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 216, + "gridX" : 5, + "gridY" : 15, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 217, + "gridX" : 5, + "gridY" : 16, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 218, + "gridX" : 5, + "gridY" : 17, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 219, + "gridX" : 5, + "gridY" : 18, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 220, + "gridX" : 5, + "gridY" : 19, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 221, + "gridX" : 5, + "gridY" : 20, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 222, + "gridX" : 5, + "gridY" : 21, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 223, + "gridX" : 5, + "gridY" : 22, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 224, + "gridX" : 5, + "gridY" : 23, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 225, + "gridX" : 5, + "gridY" : 24, + "cityName" : "苏州市", + "districtName" : "昆山市", + "description" : null, + "isObstacle" : false +}, { + "id" : 226, + "gridX" : 5, + "gridY" : 25, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 227, + "gridX" : 5, + "gridY" : 26, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 228, + "gridX" : 5, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 229, + "gridX" : 5, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 230, + "gridX" : 5, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 231, + "gridX" : 5, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 232, + "gridX" : 5, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 233, + "gridX" : 5, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 234, + "gridX" : 5, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 235, + "gridX" : 5, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 236, + "gridX" : 5, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 237, + "gridX" : 5, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 238, + "gridX" : 5, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 239, + "gridX" : 5, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 240, + "gridX" : 5, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 241, + "gridX" : 6, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 242, + "gridX" : 6, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 243, + "gridX" : 6, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 244, + "gridX" : 6, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 245, + "gridX" : 6, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 246, + "gridX" : 6, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 247, + "gridX" : 6, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 248, + "gridX" : 6, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 249, + "gridX" : 6, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 250, + "gridX" : 6, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 251, + "gridX" : 6, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 252, + "gridX" : 6, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 253, + "gridX" : 6, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 254, + "gridX" : 6, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 255, + "gridX" : 6, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 256, + "gridX" : 6, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 257, + "gridX" : 6, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 258, + "gridX" : 6, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 259, + "gridX" : 6, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 260, + "gridX" : 6, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 261, + "gridX" : 6, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 262, + "gridX" : 6, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 263, + "gridX" : 6, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 264, + "gridX" : 6, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 265, + "gridX" : 6, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 266, + "gridX" : 6, + "gridY" : 25, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 267, + "gridX" : 6, + "gridY" : 26, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 268, + "gridX" : 6, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 269, + "gridX" : 6, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 270, + "gridX" : 6, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 271, + "gridX" : 6, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 272, + "gridX" : 6, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 273, + "gridX" : 6, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 274, + "gridX" : 6, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 275, + "gridX" : 6, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 276, + "gridX" : 6, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 277, + "gridX" : 6, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 278, + "gridX" : 6, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 279, + "gridX" : 6, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 280, + "gridX" : 6, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 281, + "gridX" : 7, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 282, + "gridX" : 7, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 283, + "gridX" : 7, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 284, + "gridX" : 7, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 285, + "gridX" : 7, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 286, + "gridX" : 7, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 287, + "gridX" : 7, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 288, + "gridX" : 7, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 289, + "gridX" : 7, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 290, + "gridX" : 7, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 291, + "gridX" : 7, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 292, + "gridX" : 7, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 293, + "gridX" : 7, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 294, + "gridX" : 7, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 295, + "gridX" : 7, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 296, + "gridX" : 7, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 297, + "gridX" : 7, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 298, + "gridX" : 7, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 299, + "gridX" : 7, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 300, + "gridX" : 7, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 301, + "gridX" : 7, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 302, + "gridX" : 7, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 303, + "gridX" : 7, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 304, + "gridX" : 7, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 305, + "gridX" : 7, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 306, + "gridX" : 7, + "gridY" : 25, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 307, + "gridX" : 7, + "gridY" : 26, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 308, + "gridX" : 7, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 309, + "gridX" : 7, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 310, + "gridX" : 7, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 311, + "gridX" : 7, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 312, + "gridX" : 7, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 313, + "gridX" : 7, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 314, + "gridX" : 7, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 315, + "gridX" : 7, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 316, + "gridX" : 7, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 317, + "gridX" : 7, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 318, + "gridX" : 7, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 319, + "gridX" : 7, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 320, + "gridX" : 7, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 321, + "gridX" : 8, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 322, + "gridX" : 8, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 323, + "gridX" : 8, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 324, + "gridX" : 8, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 325, + "gridX" : 8, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 326, + "gridX" : 8, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 327, + "gridX" : 8, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 328, + "gridX" : 8, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 329, + "gridX" : 8, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 330, + "gridX" : 8, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 331, + "gridX" : 8, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 332, + "gridX" : 8, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 333, + "gridX" : 8, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 334, + "gridX" : 8, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 335, + "gridX" : 8, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 336, + "gridX" : 8, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 337, + "gridX" : 8, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 338, + "gridX" : 8, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 339, + "gridX" : 8, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 340, + "gridX" : 8, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 341, + "gridX" : 8, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 342, + "gridX" : 8, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 343, + "gridX" : 8, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 344, + "gridX" : 8, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 345, + "gridX" : 8, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 346, + "gridX" : 8, + "gridY" : 25, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 347, + "gridX" : 8, + "gridY" : 26, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 348, + "gridX" : 8, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 349, + "gridX" : 8, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 350, + "gridX" : 8, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : true +}, { + "id" : 351, + "gridX" : 8, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 352, + "gridX" : 8, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 353, + "gridX" : 8, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 354, + "gridX" : 8, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 355, + "gridX" : 8, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 356, + "gridX" : 8, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : true +}, { + "id" : 357, + "gridX" : 8, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 358, + "gridX" : 8, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 359, + "gridX" : 8, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 360, + "gridX" : 8, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 361, + "gridX" : 9, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 362, + "gridX" : 9, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 363, + "gridX" : 9, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 364, + "gridX" : 9, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 365, + "gridX" : 9, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 366, + "gridX" : 9, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 367, + "gridX" : 9, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 368, + "gridX" : 9, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 369, + "gridX" : 9, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 370, + "gridX" : 9, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 371, + "gridX" : 9, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 372, + "gridX" : 9, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 373, + "gridX" : 9, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 374, + "gridX" : 9, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 375, + "gridX" : 9, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 376, + "gridX" : 9, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 377, + "gridX" : 9, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 378, + "gridX" : 9, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 379, + "gridX" : 9, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 380, + "gridX" : 9, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 381, + "gridX" : 9, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 382, + "gridX" : 9, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 383, + "gridX" : 9, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 384, + "gridX" : 9, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 385, + "gridX" : 9, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 386, + "gridX" : 9, + "gridY" : 25, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 387, + "gridX" : 9, + "gridY" : 26, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 388, + "gridX" : 9, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 389, + "gridX" : 9, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 390, + "gridX" : 9, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 391, + "gridX" : 9, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 392, + "gridX" : 9, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 393, + "gridX" : 9, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 394, + "gridX" : 9, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 395, + "gridX" : 9, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 396, + "gridX" : 9, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 397, + "gridX" : 9, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 398, + "gridX" : 9, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 399, + "gridX" : 9, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 400, + "gridX" : 9, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 401, + "gridX" : 10, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 402, + "gridX" : 10, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 403, + "gridX" : 10, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 404, + "gridX" : 10, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 405, + "gridX" : 10, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 406, + "gridX" : 10, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 407, + "gridX" : 10, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 408, + "gridX" : 10, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 409, + "gridX" : 10, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 410, + "gridX" : 10, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 411, + "gridX" : 10, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 412, + "gridX" : 10, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 413, + "gridX" : 10, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 414, + "gridX" : 10, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 415, + "gridX" : 10, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 416, + "gridX" : 10, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 417, + "gridX" : 10, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 418, + "gridX" : 10, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 419, + "gridX" : 10, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 420, + "gridX" : 10, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 421, + "gridX" : 10, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 422, + "gridX" : 10, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 423, + "gridX" : 10, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 424, + "gridX" : 10, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 425, + "gridX" : 10, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 426, + "gridX" : 10, + "gridY" : 25, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 427, + "gridX" : 10, + "gridY" : 26, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 428, + "gridX" : 10, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 429, + "gridX" : 10, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 430, + "gridX" : 10, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 431, + "gridX" : 10, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 432, + "gridX" : 10, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : true +}, { + "id" : 433, + "gridX" : 10, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 434, + "gridX" : 10, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 435, + "gridX" : 10, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 436, + "gridX" : 10, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 437, + "gridX" : 10, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 438, + "gridX" : 10, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : true +}, { + "id" : 439, + "gridX" : 10, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 440, + "gridX" : 10, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 441, + "gridX" : 11, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 442, + "gridX" : 11, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 443, + "gridX" : 11, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 444, + "gridX" : 11, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 445, + "gridX" : 11, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 446, + "gridX" : 11, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 447, + "gridX" : 11, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 448, + "gridX" : 11, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 449, + "gridX" : 11, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 450, + "gridX" : 11, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 451, + "gridX" : 11, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 452, + "gridX" : 11, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 453, + "gridX" : 11, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 454, + "gridX" : 11, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 455, + "gridX" : 11, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 456, + "gridX" : 11, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 457, + "gridX" : 11, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 458, + "gridX" : 11, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 459, + "gridX" : 11, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 460, + "gridX" : 11, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 461, + "gridX" : 11, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 462, + "gridX" : 11, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 463, + "gridX" : 11, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 464, + "gridX" : 11, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 465, + "gridX" : 11, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 466, + "gridX" : 11, + "gridY" : 25, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 467, + "gridX" : 11, + "gridY" : 26, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 468, + "gridX" : 11, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 469, + "gridX" : 11, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 470, + "gridX" : 11, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 471, + "gridX" : 11, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 472, + "gridX" : 11, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 473, + "gridX" : 11, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 474, + "gridX" : 11, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 475, + "gridX" : 11, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 476, + "gridX" : 11, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 477, + "gridX" : 11, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 478, + "gridX" : 11, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 479, + "gridX" : 11, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 480, + "gridX" : 11, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 481, + "gridX" : 12, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 482, + "gridX" : 12, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 483, + "gridX" : 12, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 484, + "gridX" : 12, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 485, + "gridX" : 12, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 486, + "gridX" : 12, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 487, + "gridX" : 12, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 488, + "gridX" : 12, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 489, + "gridX" : 12, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 490, + "gridX" : 12, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 491, + "gridX" : 12, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 492, + "gridX" : 12, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 493, + "gridX" : 12, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 494, + "gridX" : 12, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 495, + "gridX" : 12, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 496, + "gridX" : 12, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 497, + "gridX" : 12, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 498, + "gridX" : 12, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 499, + "gridX" : 12, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 500, + "gridX" : 12, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 501, + "gridX" : 12, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 502, + "gridX" : 12, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 503, + "gridX" : 12, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 504, + "gridX" : 12, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 505, + "gridX" : 12, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 506, + "gridX" : 12, + "gridY" : 25, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 507, + "gridX" : 12, + "gridY" : 26, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 508, + "gridX" : 12, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 509, + "gridX" : 12, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 510, + "gridX" : 12, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 511, + "gridX" : 12, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 512, + "gridX" : 12, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 513, + "gridX" : 12, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 514, + "gridX" : 12, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 515, + "gridX" : 12, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 516, + "gridX" : 12, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 517, + "gridX" : 12, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 518, + "gridX" : 12, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 519, + "gridX" : 12, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 520, + "gridX" : 12, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 521, + "gridX" : 13, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 522, + "gridX" : 13, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 523, + "gridX" : 13, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 524, + "gridX" : 13, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 525, + "gridX" : 13, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 526, + "gridX" : 13, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 527, + "gridX" : 13, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 528, + "gridX" : 13, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 529, + "gridX" : 13, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 530, + "gridX" : 13, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 531, + "gridX" : 13, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 532, + "gridX" : 13, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 533, + "gridX" : 13, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 534, + "gridX" : 13, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 535, + "gridX" : 13, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 536, + "gridX" : 13, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 537, + "gridX" : 13, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 538, + "gridX" : 13, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 539, + "gridX" : 13, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 540, + "gridX" : 13, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 541, + "gridX" : 13, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 542, + "gridX" : 13, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 543, + "gridX" : 13, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 544, + "gridX" : 13, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 545, + "gridX" : 13, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 546, + "gridX" : 13, + "gridY" : 25, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 547, + "gridX" : 13, + "gridY" : 26, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 548, + "gridX" : 13, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 549, + "gridX" : 13, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 550, + "gridX" : 13, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 551, + "gridX" : 13, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 552, + "gridX" : 13, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 553, + "gridX" : 13, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 554, + "gridX" : 13, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 555, + "gridX" : 13, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 556, + "gridX" : 13, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 557, + "gridX" : 13, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 558, + "gridX" : 13, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 559, + "gridX" : 13, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 560, + "gridX" : 13, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 561, + "gridX" : 14, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 562, + "gridX" : 14, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 563, + "gridX" : 14, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 564, + "gridX" : 14, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 565, + "gridX" : 14, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 566, + "gridX" : 14, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 567, + "gridX" : 14, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 568, + "gridX" : 14, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 569, + "gridX" : 14, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 570, + "gridX" : 14, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 571, + "gridX" : 14, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 572, + "gridX" : 14, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 573, + "gridX" : 14, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 574, + "gridX" : 14, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 575, + "gridX" : 14, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 576, + "gridX" : 14, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 577, + "gridX" : 14, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 578, + "gridX" : 14, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 579, + "gridX" : 14, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 580, + "gridX" : 14, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 581, + "gridX" : 14, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 582, + "gridX" : 14, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 583, + "gridX" : 14, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 584, + "gridX" : 14, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 585, + "gridX" : 14, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 586, + "gridX" : 14, + "gridY" : 25, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 587, + "gridX" : 14, + "gridY" : 26, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 588, + "gridX" : 14, + "gridY" : 27, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 589, + "gridX" : 14, + "gridY" : 28, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 590, + "gridX" : 14, + "gridY" : 29, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 591, + "gridX" : 14, + "gridY" : 30, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 592, + "gridX" : 14, + "gridY" : 31, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 593, + "gridX" : 14, + "gridY" : 32, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 594, + "gridX" : 14, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 595, + "gridX" : 14, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 596, + "gridX" : 14, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 597, + "gridX" : 14, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 598, + "gridX" : 14, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 599, + "gridX" : 14, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 600, + "gridX" : 14, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 601, + "gridX" : 15, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 602, + "gridX" : 15, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 603, + "gridX" : 15, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 604, + "gridX" : 15, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 605, + "gridX" : 15, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 606, + "gridX" : 15, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 607, + "gridX" : 15, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 608, + "gridX" : 15, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 609, + "gridX" : 15, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 610, + "gridX" : 15, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 611, + "gridX" : 15, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 612, + "gridX" : 15, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 613, + "gridX" : 15, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 614, + "gridX" : 15, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 615, + "gridX" : 15, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 616, + "gridX" : 15, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 617, + "gridX" : 15, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 618, + "gridX" : 15, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 619, + "gridX" : 15, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 620, + "gridX" : 15, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 621, + "gridX" : 15, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 622, + "gridX" : 15, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 623, + "gridX" : 15, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 624, + "gridX" : 15, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 625, + "gridX" : 15, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 626, + "gridX" : 15, + "gridY" : 25, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 627, + "gridX" : 15, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 628, + "gridX" : 15, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 629, + "gridX" : 15, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 630, + "gridX" : 15, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 631, + "gridX" : 15, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 632, + "gridX" : 15, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 633, + "gridX" : 15, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 634, + "gridX" : 15, + "gridY" : 33, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 635, + "gridX" : 15, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : true +}, { + "id" : 636, + "gridX" : 15, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 637, + "gridX" : 15, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : true +}, { + "id" : 638, + "gridX" : 15, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 639, + "gridX" : 15, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 640, + "gridX" : 15, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 641, + "gridX" : 16, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 642, + "gridX" : 16, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 643, + "gridX" : 16, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 644, + "gridX" : 16, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 645, + "gridX" : 16, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 646, + "gridX" : 16, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 647, + "gridX" : 16, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 648, + "gridX" : 16, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 649, + "gridX" : 16, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 650, + "gridX" : 16, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 651, + "gridX" : 16, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 652, + "gridX" : 16, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 653, + "gridX" : 16, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 654, + "gridX" : 16, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 655, + "gridX" : 16, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 656, + "gridX" : 16, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 657, + "gridX" : 16, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 658, + "gridX" : 16, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 659, + "gridX" : 16, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 660, + "gridX" : 16, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 661, + "gridX" : 16, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 662, + "gridX" : 16, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 663, + "gridX" : 16, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 664, + "gridX" : 16, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 665, + "gridX" : 16, + "gridY" : 24, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 666, + "gridX" : 16, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 667, + "gridX" : 16, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 668, + "gridX" : 16, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 669, + "gridX" : 16, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 670, + "gridX" : 16, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 671, + "gridX" : 16, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 672, + "gridX" : 16, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 673, + "gridX" : 16, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 674, + "gridX" : 16, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 675, + "gridX" : 16, + "gridY" : 34, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 676, + "gridX" : 16, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 677, + "gridX" : 16, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 678, + "gridX" : 16, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 679, + "gridX" : 16, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 680, + "gridX" : 16, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 681, + "gridX" : 17, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 682, + "gridX" : 17, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 683, + "gridX" : 17, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 684, + "gridX" : 17, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 685, + "gridX" : 17, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 686, + "gridX" : 17, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 687, + "gridX" : 17, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 688, + "gridX" : 17, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 689, + "gridX" : 17, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 690, + "gridX" : 17, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 691, + "gridX" : 17, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 692, + "gridX" : 17, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 693, + "gridX" : 17, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 694, + "gridX" : 17, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 695, + "gridX" : 17, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 696, + "gridX" : 17, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 697, + "gridX" : 17, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 698, + "gridX" : 17, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 699, + "gridX" : 17, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 700, + "gridX" : 17, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 701, + "gridX" : 17, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 702, + "gridX" : 17, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 703, + "gridX" : 17, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 704, + "gridX" : 17, + "gridY" : 23, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 705, + "gridX" : 17, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 706, + "gridX" : 17, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 707, + "gridX" : 17, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 708, + "gridX" : 17, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 709, + "gridX" : 17, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 710, + "gridX" : 17, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 711, + "gridX" : 17, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 712, + "gridX" : 17, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 713, + "gridX" : 17, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 714, + "gridX" : 17, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 715, + "gridX" : 17, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 716, + "gridX" : 17, + "gridY" : 35, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 717, + "gridX" : 17, + "gridY" : 36, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 718, + "gridX" : 17, + "gridY" : 37, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 719, + "gridX" : 17, + "gridY" : 38, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 720, + "gridX" : 17, + "gridY" : 39, + "cityName" : "南京市", + "districtName" : "江宁区", + "description" : null, + "isObstacle" : false +}, { + "id" : 721, + "gridX" : 18, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 722, + "gridX" : 18, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 723, + "gridX" : 18, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 724, + "gridX" : 18, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 725, + "gridX" : 18, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 726, + "gridX" : 18, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 727, + "gridX" : 18, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 728, + "gridX" : 18, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 729, + "gridX" : 18, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 730, + "gridX" : 18, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 731, + "gridX" : 18, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 732, + "gridX" : 18, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 733, + "gridX" : 18, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 734, + "gridX" : 18, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 735, + "gridX" : 18, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 736, + "gridX" : 18, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 737, + "gridX" : 18, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 738, + "gridX" : 18, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 739, + "gridX" : 18, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 740, + "gridX" : 18, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 741, + "gridX" : 18, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 742, + "gridX" : 18, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 743, + "gridX" : 18, + "gridY" : 22, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 744, + "gridX" : 18, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 745, + "gridX" : 18, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 746, + "gridX" : 18, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 747, + "gridX" : 18, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 748, + "gridX" : 18, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 749, + "gridX" : 18, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 750, + "gridX" : 18, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 751, + "gridX" : 18, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 752, + "gridX" : 18, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 753, + "gridX" : 18, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 754, + "gridX" : 18, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 755, + "gridX" : 18, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 756, + "gridX" : 18, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 757, + "gridX" : 18, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 758, + "gridX" : 18, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 759, + "gridX" : 18, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 760, + "gridX" : 18, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 761, + "gridX" : 19, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 762, + "gridX" : 19, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 763, + "gridX" : 19, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 764, + "gridX" : 19, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 765, + "gridX" : 19, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 766, + "gridX" : 19, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 767, + "gridX" : 19, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 768, + "gridX" : 19, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 769, + "gridX" : 19, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 770, + "gridX" : 19, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 771, + "gridX" : 19, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 772, + "gridX" : 19, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 773, + "gridX" : 19, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 774, + "gridX" : 19, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 775, + "gridX" : 19, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 776, + "gridX" : 19, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 777, + "gridX" : 19, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 778, + "gridX" : 19, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 779, + "gridX" : 19, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 780, + "gridX" : 19, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 781, + "gridX" : 19, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 782, + "gridX" : 19, + "gridY" : 21, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 783, + "gridX" : 19, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 784, + "gridX" : 19, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 785, + "gridX" : 19, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 786, + "gridX" : 19, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 787, + "gridX" : 19, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 788, + "gridX" : 19, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 789, + "gridX" : 19, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 790, + "gridX" : 19, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 791, + "gridX" : 19, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 792, + "gridX" : 19, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 793, + "gridX" : 19, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 794, + "gridX" : 19, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 795, + "gridX" : 19, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 796, + "gridX" : 19, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 797, + "gridX" : 19, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 798, + "gridX" : 19, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 799, + "gridX" : 19, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 800, + "gridX" : 19, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 801, + "gridX" : 20, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 802, + "gridX" : 20, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 803, + "gridX" : 20, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 804, + "gridX" : 20, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 805, + "gridX" : 20, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 806, + "gridX" : 20, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 807, + "gridX" : 20, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 808, + "gridX" : 20, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 809, + "gridX" : 20, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 810, + "gridX" : 20, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 811, + "gridX" : 20, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 812, + "gridX" : 20, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 813, + "gridX" : 20, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 814, + "gridX" : 20, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 815, + "gridX" : 20, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 816, + "gridX" : 20, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 817, + "gridX" : 20, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 818, + "gridX" : 20, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 819, + "gridX" : 20, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 820, + "gridX" : 20, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 821, + "gridX" : 20, + "gridY" : 20, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 822, + "gridX" : 20, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 823, + "gridX" : 20, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 824, + "gridX" : 20, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 825, + "gridX" : 20, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 826, + "gridX" : 20, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 827, + "gridX" : 20, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 828, + "gridX" : 20, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 829, + "gridX" : 20, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 830, + "gridX" : 20, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 831, + "gridX" : 20, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 832, + "gridX" : 20, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 833, + "gridX" : 20, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 834, + "gridX" : 20, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 835, + "gridX" : 20, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 836, + "gridX" : 20, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 837, + "gridX" : 20, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 838, + "gridX" : 20, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 839, + "gridX" : 20, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 840, + "gridX" : 20, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 841, + "gridX" : 21, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 842, + "gridX" : 21, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 843, + "gridX" : 21, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 844, + "gridX" : 21, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 845, + "gridX" : 21, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 846, + "gridX" : 21, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 847, + "gridX" : 21, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 848, + "gridX" : 21, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 849, + "gridX" : 21, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 850, + "gridX" : 21, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 851, + "gridX" : 21, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 852, + "gridX" : 21, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 853, + "gridX" : 21, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 854, + "gridX" : 21, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 855, + "gridX" : 21, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 856, + "gridX" : 21, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 857, + "gridX" : 21, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 858, + "gridX" : 21, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 859, + "gridX" : 21, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 860, + "gridX" : 21, + "gridY" : 19, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 861, + "gridX" : 21, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 862, + "gridX" : 21, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 863, + "gridX" : 21, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 864, + "gridX" : 21, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 865, + "gridX" : 21, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 866, + "gridX" : 21, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 867, + "gridX" : 21, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 868, + "gridX" : 21, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 869, + "gridX" : 21, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 870, + "gridX" : 21, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 871, + "gridX" : 21, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 872, + "gridX" : 21, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 873, + "gridX" : 21, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 874, + "gridX" : 21, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 875, + "gridX" : 21, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 876, + "gridX" : 21, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 877, + "gridX" : 21, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 878, + "gridX" : 21, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 879, + "gridX" : 21, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 880, + "gridX" : 21, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 881, + "gridX" : 22, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 882, + "gridX" : 22, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 883, + "gridX" : 22, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 884, + "gridX" : 22, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 885, + "gridX" : 22, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 886, + "gridX" : 22, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 887, + "gridX" : 22, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 888, + "gridX" : 22, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 889, + "gridX" : 22, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 890, + "gridX" : 22, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 891, + "gridX" : 22, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 892, + "gridX" : 22, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 893, + "gridX" : 22, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 894, + "gridX" : 22, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 895, + "gridX" : 22, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 896, + "gridX" : 22, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 897, + "gridX" : 22, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 898, + "gridX" : 22, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 899, + "gridX" : 22, + "gridY" : 18, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 900, + "gridX" : 22, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 901, + "gridX" : 22, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 902, + "gridX" : 22, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 903, + "gridX" : 22, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 904, + "gridX" : 22, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 905, + "gridX" : 22, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 906, + "gridX" : 22, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 907, + "gridX" : 22, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 908, + "gridX" : 22, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 909, + "gridX" : 22, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 910, + "gridX" : 22, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 911, + "gridX" : 22, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 912, + "gridX" : 22, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 913, + "gridX" : 22, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 914, + "gridX" : 22, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 915, + "gridX" : 22, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 916, + "gridX" : 22, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 917, + "gridX" : 22, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 918, + "gridX" : 22, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 919, + "gridX" : 22, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 920, + "gridX" : 22, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 921, + "gridX" : 23, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 922, + "gridX" : 23, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 923, + "gridX" : 23, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 924, + "gridX" : 23, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 925, + "gridX" : 23, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 926, + "gridX" : 23, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 927, + "gridX" : 23, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 928, + "gridX" : 23, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 929, + "gridX" : 23, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 930, + "gridX" : 23, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 931, + "gridX" : 23, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 932, + "gridX" : 23, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 933, + "gridX" : 23, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 934, + "gridX" : 23, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 935, + "gridX" : 23, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 936, + "gridX" : 23, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 937, + "gridX" : 23, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 938, + "gridX" : 23, + "gridY" : 17, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 939, + "gridX" : 23, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 940, + "gridX" : 23, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 941, + "gridX" : 23, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 942, + "gridX" : 23, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 943, + "gridX" : 23, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 944, + "gridX" : 23, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 945, + "gridX" : 23, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 946, + "gridX" : 23, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 947, + "gridX" : 23, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 948, + "gridX" : 23, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 949, + "gridX" : 23, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 950, + "gridX" : 23, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 951, + "gridX" : 23, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 952, + "gridX" : 23, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 953, + "gridX" : 23, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 954, + "gridX" : 23, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 955, + "gridX" : 23, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 956, + "gridX" : 23, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 957, + "gridX" : 23, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 958, + "gridX" : 23, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 959, + "gridX" : 23, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 960, + "gridX" : 23, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 961, + "gridX" : 24, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 962, + "gridX" : 24, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 963, + "gridX" : 24, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 964, + "gridX" : 24, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 965, + "gridX" : 24, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 966, + "gridX" : 24, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 967, + "gridX" : 24, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 968, + "gridX" : 24, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 969, + "gridX" : 24, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 970, + "gridX" : 24, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 971, + "gridX" : 24, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 972, + "gridX" : 24, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 973, + "gridX" : 24, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 974, + "gridX" : 24, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 975, + "gridX" : 24, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 976, + "gridX" : 24, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 977, + "gridX" : 24, + "gridY" : 16, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 978, + "gridX" : 24, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 979, + "gridX" : 24, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 980, + "gridX" : 24, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 981, + "gridX" : 24, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 982, + "gridX" : 24, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 983, + "gridX" : 24, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 984, + "gridX" : 24, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 985, + "gridX" : 24, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 986, + "gridX" : 24, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 987, + "gridX" : 24, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 988, + "gridX" : 24, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 989, + "gridX" : 24, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 990, + "gridX" : 24, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 991, + "gridX" : 24, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 992, + "gridX" : 24, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 993, + "gridX" : 24, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 994, + "gridX" : 24, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 995, + "gridX" : 24, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 996, + "gridX" : 24, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 997, + "gridX" : 24, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 998, + "gridX" : 24, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 999, + "gridX" : 24, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1000, + "gridX" : 24, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1001, + "gridX" : 25, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1002, + "gridX" : 25, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1003, + "gridX" : 25, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1004, + "gridX" : 25, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1005, + "gridX" : 25, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1006, + "gridX" : 25, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1007, + "gridX" : 25, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1008, + "gridX" : 25, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1009, + "gridX" : 25, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1010, + "gridX" : 25, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1011, + "gridX" : 25, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1012, + "gridX" : 25, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1013, + "gridX" : 25, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1014, + "gridX" : 25, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1015, + "gridX" : 25, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1016, + "gridX" : 25, + "gridY" : 15, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1017, + "gridX" : 25, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1018, + "gridX" : 25, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1019, + "gridX" : 25, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1020, + "gridX" : 25, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1021, + "gridX" : 25, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1022, + "gridX" : 25, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1023, + "gridX" : 25, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1024, + "gridX" : 25, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1025, + "gridX" : 25, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1026, + "gridX" : 25, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1027, + "gridX" : 25, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1028, + "gridX" : 25, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1029, + "gridX" : 25, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1030, + "gridX" : 25, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1031, + "gridX" : 25, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1032, + "gridX" : 25, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1033, + "gridX" : 25, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1034, + "gridX" : 25, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1035, + "gridX" : 25, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1036, + "gridX" : 25, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1037, + "gridX" : 25, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1038, + "gridX" : 25, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1039, + "gridX" : 25, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1040, + "gridX" : 25, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1041, + "gridX" : 26, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1042, + "gridX" : 26, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1043, + "gridX" : 26, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1044, + "gridX" : 26, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1045, + "gridX" : 26, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1046, + "gridX" : 26, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1047, + "gridX" : 26, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1048, + "gridX" : 26, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1049, + "gridX" : 26, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1050, + "gridX" : 26, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1051, + "gridX" : 26, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1052, + "gridX" : 26, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1053, + "gridX" : 26, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1054, + "gridX" : 26, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1055, + "gridX" : 26, + "gridY" : 14, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1056, + "gridX" : 26, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1057, + "gridX" : 26, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1058, + "gridX" : 26, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1059, + "gridX" : 26, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1060, + "gridX" : 26, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1061, + "gridX" : 26, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1062, + "gridX" : 26, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1063, + "gridX" : 26, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1064, + "gridX" : 26, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1065, + "gridX" : 26, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1066, + "gridX" : 26, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1067, + "gridX" : 26, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1068, + "gridX" : 26, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1069, + "gridX" : 26, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1070, + "gridX" : 26, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1071, + "gridX" : 26, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1072, + "gridX" : 26, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1073, + "gridX" : 26, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1074, + "gridX" : 26, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1075, + "gridX" : 26, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1076, + "gridX" : 26, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1077, + "gridX" : 26, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1078, + "gridX" : 26, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1079, + "gridX" : 26, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1080, + "gridX" : 26, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1081, + "gridX" : 27, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1082, + "gridX" : 27, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1083, + "gridX" : 27, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1084, + "gridX" : 27, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1085, + "gridX" : 27, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1086, + "gridX" : 27, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1087, + "gridX" : 27, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1088, + "gridX" : 27, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1089, + "gridX" : 27, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1090, + "gridX" : 27, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1091, + "gridX" : 27, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1092, + "gridX" : 27, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1093, + "gridX" : 27, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1094, + "gridX" : 27, + "gridY" : 13, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1095, + "gridX" : 27, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1096, + "gridX" : 27, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1097, + "gridX" : 27, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1098, + "gridX" : 27, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1099, + "gridX" : 27, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1100, + "gridX" : 27, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1101, + "gridX" : 27, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1102, + "gridX" : 27, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1103, + "gridX" : 27, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1104, + "gridX" : 27, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1105, + "gridX" : 27, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1106, + "gridX" : 27, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1107, + "gridX" : 27, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1108, + "gridX" : 27, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1109, + "gridX" : 27, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1110, + "gridX" : 27, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1111, + "gridX" : 27, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1112, + "gridX" : 27, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1113, + "gridX" : 27, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1114, + "gridX" : 27, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1115, + "gridX" : 27, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1116, + "gridX" : 27, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1117, + "gridX" : 27, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1118, + "gridX" : 27, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1119, + "gridX" : 27, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1120, + "gridX" : 27, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1121, + "gridX" : 28, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1122, + "gridX" : 28, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1123, + "gridX" : 28, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1124, + "gridX" : 28, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1125, + "gridX" : 28, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1126, + "gridX" : 28, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1127, + "gridX" : 28, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1128, + "gridX" : 28, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1129, + "gridX" : 28, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1130, + "gridX" : 28, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1131, + "gridX" : 28, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1132, + "gridX" : 28, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1133, + "gridX" : 28, + "gridY" : 12, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1134, + "gridX" : 28, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1135, + "gridX" : 28, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1136, + "gridX" : 28, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1137, + "gridX" : 28, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1138, + "gridX" : 28, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1139, + "gridX" : 28, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1140, + "gridX" : 28, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1141, + "gridX" : 28, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1142, + "gridX" : 28, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1143, + "gridX" : 28, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1144, + "gridX" : 28, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1145, + "gridX" : 28, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1146, + "gridX" : 28, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1147, + "gridX" : 28, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1148, + "gridX" : 28, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1149, + "gridX" : 28, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1150, + "gridX" : 28, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1151, + "gridX" : 28, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1152, + "gridX" : 28, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1153, + "gridX" : 28, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1154, + "gridX" : 28, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1155, + "gridX" : 28, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1156, + "gridX" : 28, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1157, + "gridX" : 28, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1158, + "gridX" : 28, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1159, + "gridX" : 28, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1160, + "gridX" : 28, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1161, + "gridX" : 29, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1162, + "gridX" : 29, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1163, + "gridX" : 29, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1164, + "gridX" : 29, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1165, + "gridX" : 29, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1166, + "gridX" : 29, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1167, + "gridX" : 29, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1168, + "gridX" : 29, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1169, + "gridX" : 29, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1170, + "gridX" : 29, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1171, + "gridX" : 29, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1172, + "gridX" : 29, + "gridY" : 11, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1173, + "gridX" : 29, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1174, + "gridX" : 29, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1175, + "gridX" : 29, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1176, + "gridX" : 29, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1177, + "gridX" : 29, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1178, + "gridX" : 29, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1179, + "gridX" : 29, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1180, + "gridX" : 29, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1181, + "gridX" : 29, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1182, + "gridX" : 29, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1183, + "gridX" : 29, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1184, + "gridX" : 29, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1185, + "gridX" : 29, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1186, + "gridX" : 29, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1187, + "gridX" : 29, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1188, + "gridX" : 29, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1189, + "gridX" : 29, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1190, + "gridX" : 29, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1191, + "gridX" : 29, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1192, + "gridX" : 29, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1193, + "gridX" : 29, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1194, + "gridX" : 29, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1195, + "gridX" : 29, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1196, + "gridX" : 29, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1197, + "gridX" : 29, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1198, + "gridX" : 29, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1199, + "gridX" : 29, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1200, + "gridX" : 29, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1201, + "gridX" : 30, + "gridY" : 0, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1202, + "gridX" : 30, + "gridY" : 1, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1203, + "gridX" : 30, + "gridY" : 2, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1204, + "gridX" : 30, + "gridY" : 3, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1205, + "gridX" : 30, + "gridY" : 4, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1206, + "gridX" : 30, + "gridY" : 5, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1207, + "gridX" : 30, + "gridY" : 6, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1208, + "gridX" : 30, + "gridY" : 7, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1209, + "gridX" : 30, + "gridY" : 8, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1210, + "gridX" : 30, + "gridY" : 9, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1211, + "gridX" : 30, + "gridY" : 10, + "cityName" : "常州市", + "districtName" : "溧阳市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1212, + "gridX" : 30, + "gridY" : 11, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1213, + "gridX" : 30, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1214, + "gridX" : 30, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1215, + "gridX" : 30, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1216, + "gridX" : 30, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1217, + "gridX" : 30, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1218, + "gridX" : 30, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1219, + "gridX" : 30, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1220, + "gridX" : 30, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1221, + "gridX" : 30, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1222, + "gridX" : 30, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1223, + "gridX" : 30, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1224, + "gridX" : 30, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1225, + "gridX" : 30, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1226, + "gridX" : 30, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1227, + "gridX" : 30, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1228, + "gridX" : 30, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1229, + "gridX" : 30, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1230, + "gridX" : 30, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1231, + "gridX" : 30, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1232, + "gridX" : 30, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1233, + "gridX" : 30, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1234, + "gridX" : 30, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1235, + "gridX" : 30, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1236, + "gridX" : 30, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1237, + "gridX" : 30, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1238, + "gridX" : 30, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1239, + "gridX" : 30, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1240, + "gridX" : 30, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1241, + "gridX" : 31, + "gridY" : 0, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1242, + "gridX" : 31, + "gridY" : 1, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1243, + "gridX" : 31, + "gridY" : 2, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1244, + "gridX" : 31, + "gridY" : 3, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1245, + "gridX" : 31, + "gridY" : 4, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1246, + "gridX" : 31, + "gridY" : 5, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1247, + "gridX" : 31, + "gridY" : 6, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1248, + "gridX" : 31, + "gridY" : 7, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1249, + "gridX" : 31, + "gridY" : 8, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1250, + "gridX" : 31, + "gridY" : 9, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1251, + "gridX" : 31, + "gridY" : 10, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1252, + "gridX" : 31, + "gridY" : 11, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1253, + "gridX" : 31, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1254, + "gridX" : 31, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1255, + "gridX" : 31, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1256, + "gridX" : 31, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1257, + "gridX" : 31, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1258, + "gridX" : 31, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1259, + "gridX" : 31, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1260, + "gridX" : 31, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1261, + "gridX" : 31, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1262, + "gridX" : 31, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1263, + "gridX" : 31, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1264, + "gridX" : 31, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1265, + "gridX" : 31, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1266, + "gridX" : 31, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1267, + "gridX" : 31, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1268, + "gridX" : 31, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1269, + "gridX" : 31, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1270, + "gridX" : 31, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1271, + "gridX" : 31, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1272, + "gridX" : 31, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1273, + "gridX" : 31, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1274, + "gridX" : 31, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1275, + "gridX" : 31, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1276, + "gridX" : 31, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1277, + "gridX" : 31, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1278, + "gridX" : 31, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1279, + "gridX" : 31, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1280, + "gridX" : 31, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1281, + "gridX" : 32, + "gridY" : 0, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1282, + "gridX" : 32, + "gridY" : 1, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1283, + "gridX" : 32, + "gridY" : 2, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1284, + "gridX" : 32, + "gridY" : 3, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1285, + "gridX" : 32, + "gridY" : 4, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1286, + "gridX" : 32, + "gridY" : 5, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1287, + "gridX" : 32, + "gridY" : 6, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1288, + "gridX" : 32, + "gridY" : 7, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1289, + "gridX" : 32, + "gridY" : 8, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1290, + "gridX" : 32, + "gridY" : 9, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1291, + "gridX" : 32, + "gridY" : 10, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1292, + "gridX" : 32, + "gridY" : 11, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1293, + "gridX" : 32, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1294, + "gridX" : 32, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1295, + "gridX" : 32, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1296, + "gridX" : 32, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1297, + "gridX" : 32, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1298, + "gridX" : 32, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1299, + "gridX" : 32, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1300, + "gridX" : 32, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1301, + "gridX" : 32, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1302, + "gridX" : 32, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1303, + "gridX" : 32, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1304, + "gridX" : 32, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1305, + "gridX" : 32, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1306, + "gridX" : 32, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1307, + "gridX" : 32, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1308, + "gridX" : 32, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1309, + "gridX" : 32, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1310, + "gridX" : 32, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1311, + "gridX" : 32, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1312, + "gridX" : 32, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1313, + "gridX" : 32, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1314, + "gridX" : 32, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1315, + "gridX" : 32, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1316, + "gridX" : 32, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1317, + "gridX" : 32, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1318, + "gridX" : 32, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1319, + "gridX" : 32, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1320, + "gridX" : 32, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1321, + "gridX" : 33, + "gridY" : 0, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1322, + "gridX" : 33, + "gridY" : 1, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1323, + "gridX" : 33, + "gridY" : 2, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1324, + "gridX" : 33, + "gridY" : 3, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1325, + "gridX" : 33, + "gridY" : 4, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1326, + "gridX" : 33, + "gridY" : 5, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1327, + "gridX" : 33, + "gridY" : 6, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1328, + "gridX" : 33, + "gridY" : 7, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1329, + "gridX" : 33, + "gridY" : 8, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1330, + "gridX" : 33, + "gridY" : 9, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1331, + "gridX" : 33, + "gridY" : 10, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1332, + "gridX" : 33, + "gridY" : 11, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1333, + "gridX" : 33, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1334, + "gridX" : 33, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1335, + "gridX" : 33, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1336, + "gridX" : 33, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1337, + "gridX" : 33, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1338, + "gridX" : 33, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1339, + "gridX" : 33, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1340, + "gridX" : 33, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1341, + "gridX" : 33, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1342, + "gridX" : 33, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1343, + "gridX" : 33, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1344, + "gridX" : 33, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1345, + "gridX" : 33, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1346, + "gridX" : 33, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1347, + "gridX" : 33, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1348, + "gridX" : 33, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1349, + "gridX" : 33, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1350, + "gridX" : 33, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1351, + "gridX" : 33, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1352, + "gridX" : 33, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1353, + "gridX" : 33, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1354, + "gridX" : 33, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1355, + "gridX" : 33, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1356, + "gridX" : 33, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1357, + "gridX" : 33, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1358, + "gridX" : 33, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1359, + "gridX" : 33, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1360, + "gridX" : 33, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1361, + "gridX" : 34, + "gridY" : 0, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1362, + "gridX" : 34, + "gridY" : 1, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1363, + "gridX" : 34, + "gridY" : 2, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1364, + "gridX" : 34, + "gridY" : 3, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1365, + "gridX" : 34, + "gridY" : 4, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1366, + "gridX" : 34, + "gridY" : 5, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1367, + "gridX" : 34, + "gridY" : 6, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1368, + "gridX" : 34, + "gridY" : 7, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1369, + "gridX" : 34, + "gridY" : 8, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1370, + "gridX" : 34, + "gridY" : 9, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1371, + "gridX" : 34, + "gridY" : 10, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1372, + "gridX" : 34, + "gridY" : 11, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1373, + "gridX" : 34, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1374, + "gridX" : 34, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1375, + "gridX" : 34, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1376, + "gridX" : 34, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1377, + "gridX" : 34, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1378, + "gridX" : 34, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1379, + "gridX" : 34, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1380, + "gridX" : 34, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1381, + "gridX" : 34, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1382, + "gridX" : 34, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1383, + "gridX" : 34, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1384, + "gridX" : 34, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1385, + "gridX" : 34, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1386, + "gridX" : 34, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1387, + "gridX" : 34, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1388, + "gridX" : 34, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1389, + "gridX" : 34, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1390, + "gridX" : 34, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1391, + "gridX" : 34, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1392, + "gridX" : 34, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1393, + "gridX" : 34, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1394, + "gridX" : 34, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1395, + "gridX" : 34, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1396, + "gridX" : 34, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1397, + "gridX" : 34, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1398, + "gridX" : 34, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1399, + "gridX" : 34, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1400, + "gridX" : 34, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1401, + "gridX" : 35, + "gridY" : 0, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1402, + "gridX" : 35, + "gridY" : 1, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1403, + "gridX" : 35, + "gridY" : 2, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1404, + "gridX" : 35, + "gridY" : 3, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1405, + "gridX" : 35, + "gridY" : 4, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1406, + "gridX" : 35, + "gridY" : 5, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1407, + "gridX" : 35, + "gridY" : 6, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1408, + "gridX" : 35, + "gridY" : 7, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1409, + "gridX" : 35, + "gridY" : 8, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1410, + "gridX" : 35, + "gridY" : 9, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1411, + "gridX" : 35, + "gridY" : 10, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1412, + "gridX" : 35, + "gridY" : 11, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1413, + "gridX" : 35, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1414, + "gridX" : 35, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1415, + "gridX" : 35, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1416, + "gridX" : 35, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1417, + "gridX" : 35, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1418, + "gridX" : 35, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1419, + "gridX" : 35, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1420, + "gridX" : 35, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1421, + "gridX" : 35, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1422, + "gridX" : 35, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1423, + "gridX" : 35, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1424, + "gridX" : 35, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1425, + "gridX" : 35, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1426, + "gridX" : 35, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1427, + "gridX" : 35, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1428, + "gridX" : 35, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1429, + "gridX" : 35, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1430, + "gridX" : 35, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1431, + "gridX" : 35, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1432, + "gridX" : 35, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1433, + "gridX" : 35, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1434, + "gridX" : 35, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1435, + "gridX" : 35, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1436, + "gridX" : 35, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1437, + "gridX" : 35, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1438, + "gridX" : 35, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1439, + "gridX" : 35, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1440, + "gridX" : 35, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1441, + "gridX" : 36, + "gridY" : 0, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1442, + "gridX" : 36, + "gridY" : 1, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1443, + "gridX" : 36, + "gridY" : 2, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1444, + "gridX" : 36, + "gridY" : 3, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1445, + "gridX" : 36, + "gridY" : 4, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1446, + "gridX" : 36, + "gridY" : 5, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1447, + "gridX" : 36, + "gridY" : 6, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1448, + "gridX" : 36, + "gridY" : 7, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1449, + "gridX" : 36, + "gridY" : 8, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1450, + "gridX" : 36, + "gridY" : 9, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1451, + "gridX" : 36, + "gridY" : 10, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1452, + "gridX" : 36, + "gridY" : 11, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1453, + "gridX" : 36, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1454, + "gridX" : 36, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1455, + "gridX" : 36, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1456, + "gridX" : 36, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1457, + "gridX" : 36, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1458, + "gridX" : 36, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1459, + "gridX" : 36, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1460, + "gridX" : 36, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1461, + "gridX" : 36, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1462, + "gridX" : 36, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1463, + "gridX" : 36, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1464, + "gridX" : 36, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1465, + "gridX" : 36, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1466, + "gridX" : 36, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1467, + "gridX" : 36, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1468, + "gridX" : 36, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1469, + "gridX" : 36, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1470, + "gridX" : 36, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1471, + "gridX" : 36, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1472, + "gridX" : 36, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1473, + "gridX" : 36, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1474, + "gridX" : 36, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1475, + "gridX" : 36, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1476, + "gridX" : 36, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1477, + "gridX" : 36, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1478, + "gridX" : 36, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1479, + "gridX" : 36, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1480, + "gridX" : 36, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1481, + "gridX" : 37, + "gridY" : 0, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1482, + "gridX" : 37, + "gridY" : 1, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1483, + "gridX" : 37, + "gridY" : 2, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1484, + "gridX" : 37, + "gridY" : 3, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1485, + "gridX" : 37, + "gridY" : 4, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1486, + "gridX" : 37, + "gridY" : 5, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1487, + "gridX" : 37, + "gridY" : 6, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1488, + "gridX" : 37, + "gridY" : 7, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1489, + "gridX" : 37, + "gridY" : 8, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1490, + "gridX" : 37, + "gridY" : 9, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1491, + "gridX" : 37, + "gridY" : 10, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1492, + "gridX" : 37, + "gridY" : 11, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1493, + "gridX" : 37, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1494, + "gridX" : 37, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1495, + "gridX" : 37, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1496, + "gridX" : 37, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1497, + "gridX" : 37, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1498, + "gridX" : 37, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1499, + "gridX" : 37, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1500, + "gridX" : 37, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1501, + "gridX" : 37, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1502, + "gridX" : 37, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1503, + "gridX" : 37, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1504, + "gridX" : 37, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1505, + "gridX" : 37, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1506, + "gridX" : 37, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1507, + "gridX" : 37, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1508, + "gridX" : 37, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1509, + "gridX" : 37, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1510, + "gridX" : 37, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1511, + "gridX" : 37, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1512, + "gridX" : 37, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1513, + "gridX" : 37, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1514, + "gridX" : 37, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1515, + "gridX" : 37, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1516, + "gridX" : 37, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1517, + "gridX" : 37, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1518, + "gridX" : 37, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1519, + "gridX" : 37, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1520, + "gridX" : 37, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1521, + "gridX" : 38, + "gridY" : 0, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1522, + "gridX" : 38, + "gridY" : 1, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1523, + "gridX" : 38, + "gridY" : 2, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1524, + "gridX" : 38, + "gridY" : 3, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1525, + "gridX" : 38, + "gridY" : 4, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1526, + "gridX" : 38, + "gridY" : 5, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1527, + "gridX" : 38, + "gridY" : 6, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1528, + "gridX" : 38, + "gridY" : 7, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1529, + "gridX" : 38, + "gridY" : 8, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1530, + "gridX" : 38, + "gridY" : 9, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1531, + "gridX" : 38, + "gridY" : 10, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1532, + "gridX" : 38, + "gridY" : 11, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1533, + "gridX" : 38, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1534, + "gridX" : 38, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1535, + "gridX" : 38, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1536, + "gridX" : 38, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1537, + "gridX" : 38, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1538, + "gridX" : 38, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1539, + "gridX" : 38, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1540, + "gridX" : 38, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1541, + "gridX" : 38, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1542, + "gridX" : 38, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1543, + "gridX" : 38, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1544, + "gridX" : 38, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1545, + "gridX" : 38, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1546, + "gridX" : 38, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1547, + "gridX" : 38, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1548, + "gridX" : 38, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1549, + "gridX" : 38, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1550, + "gridX" : 38, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1551, + "gridX" : 38, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : true +}, { + "id" : 1552, + "gridX" : 38, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1553, + "gridX" : 38, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1554, + "gridX" : 38, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1555, + "gridX" : 38, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1556, + "gridX" : 38, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1557, + "gridX" : 38, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1558, + "gridX" : 38, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1559, + "gridX" : 38, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1560, + "gridX" : 38, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1561, + "gridX" : 39, + "gridY" : 0, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1562, + "gridX" : 39, + "gridY" : 1, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1563, + "gridX" : 39, + "gridY" : 2, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1564, + "gridX" : 39, + "gridY" : 3, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1565, + "gridX" : 39, + "gridY" : 4, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1566, + "gridX" : 39, + "gridY" : 5, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1567, + "gridX" : 39, + "gridY" : 6, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1568, + "gridX" : 39, + "gridY" : 7, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1569, + "gridX" : 39, + "gridY" : 8, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1570, + "gridX" : 39, + "gridY" : 9, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1571, + "gridX" : 39, + "gridY" : 10, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1572, + "gridX" : 39, + "gridY" : 11, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1573, + "gridX" : 39, + "gridY" : 12, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1574, + "gridX" : 39, + "gridY" : 13, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1575, + "gridX" : 39, + "gridY" : 14, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1576, + "gridX" : 39, + "gridY" : 15, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1577, + "gridX" : 39, + "gridY" : 16, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1578, + "gridX" : 39, + "gridY" : 17, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1579, + "gridX" : 39, + "gridY" : 18, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1580, + "gridX" : 39, + "gridY" : 19, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1581, + "gridX" : 39, + "gridY" : 20, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1582, + "gridX" : 39, + "gridY" : 21, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1583, + "gridX" : 39, + "gridY" : 22, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1584, + "gridX" : 39, + "gridY" : 23, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1585, + "gridX" : 39, + "gridY" : 24, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1586, + "gridX" : 39, + "gridY" : 25, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1587, + "gridX" : 39, + "gridY" : 26, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1588, + "gridX" : 39, + "gridY" : 27, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1589, + "gridX" : 39, + "gridY" : 28, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1590, + "gridX" : 39, + "gridY" : 29, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1591, + "gridX" : 39, + "gridY" : 30, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1592, + "gridX" : 39, + "gridY" : 31, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1593, + "gridX" : 39, + "gridY" : 32, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1594, + "gridX" : 39, + "gridY" : 33, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1595, + "gridX" : 39, + "gridY" : 34, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1596, + "gridX" : 39, + "gridY" : 35, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1597, + "gridX" : 39, + "gridY" : 36, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1598, + "gridX" : 39, + "gridY" : 37, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1599, + "gridX" : 39, + "gridY" : 38, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false +}, { + "id" : 1600, + "gridX" : 39, + "gridY" : 39, + "cityName" : "无锡市", + "districtName" : "宜兴市", + "description" : null, + "isObstacle" : false } ] \ No newline at end of file diff --git a/ems-backend/json-db/map_grids.json b/ems-backend/json-db/map_grids.json index f5ca921..3877032 100644 --- a/ems-backend/json-db/map_grids.json +++ b/ems-backend/json-db/map_grids.json @@ -1,11201 +1,11201 @@ -[ { - "id" : 1, - "x" : 0, - "y" : 0, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 2, - "x" : 0, - "y" : 1, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 3, - "x" : 0, - "y" : 2, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 4, - "x" : 0, - "y" : 3, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 5, - "x" : 0, - "y" : 4, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 6, - "x" : 0, - "y" : 5, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 7, - "x" : 0, - "y" : 6, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 8, - "x" : 0, - "y" : 7, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 9, - "x" : 0, - "y" : 8, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 10, - "x" : 0, - "y" : 9, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 11, - "x" : 0, - "y" : 10, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 12, - "x" : 0, - "y" : 11, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 13, - "x" : 0, - "y" : 12, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 14, - "x" : 0, - "y" : 13, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 15, - "x" : 0, - "y" : 14, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 16, - "x" : 0, - "y" : 15, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 17, - "x" : 0, - "y" : 16, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 18, - "x" : 0, - "y" : 17, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 19, - "x" : 0, - "y" : 18, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 20, - "x" : 0, - "y" : 19, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 21, - "x" : 0, - "y" : 20, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 22, - "x" : 0, - "y" : 21, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 23, - "x" : 0, - "y" : 22, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 24, - "x" : 0, - "y" : 23, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 25, - "x" : 0, - "y" : 24, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 26, - "x" : 0, - "y" : 25, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 27, - "x" : 0, - "y" : 26, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 28, - "x" : 0, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 29, - "x" : 0, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 30, - "x" : 0, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 31, - "x" : 0, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 32, - "x" : 0, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 33, - "x" : 0, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 34, - "x" : 0, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 35, - "x" : 0, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 36, - "x" : 0, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 37, - "x" : 0, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 38, - "x" : 0, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 39, - "x" : 0, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 40, - "x" : 0, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 41, - "x" : 1, - "y" : 0, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 42, - "x" : 1, - "y" : 1, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 43, - "x" : 1, - "y" : 2, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 44, - "x" : 1, - "y" : 3, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 45, - "x" : 1, - "y" : 4, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 46, - "x" : 1, - "y" : 5, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 47, - "x" : 1, - "y" : 6, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 48, - "x" : 1, - "y" : 7, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 49, - "x" : 1, - "y" : 8, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 50, - "x" : 1, - "y" : 9, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 51, - "x" : 1, - "y" : 10, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 52, - "x" : 1, - "y" : 11, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 53, - "x" : 1, - "y" : 12, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 54, - "x" : 1, - "y" : 13, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 55, - "x" : 1, - "y" : 14, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 56, - "x" : 1, - "y" : 15, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 57, - "x" : 1, - "y" : 16, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 58, - "x" : 1, - "y" : 17, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 59, - "x" : 1, - "y" : 18, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 60, - "x" : 1, - "y" : 19, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 61, - "x" : 1, - "y" : 20, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 62, - "x" : 1, - "y" : 21, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 63, - "x" : 1, - "y" : 22, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 64, - "x" : 1, - "y" : 23, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 65, - "x" : 1, - "y" : 24, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 66, - "x" : 1, - "y" : 25, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 67, - "x" : 1, - "y" : 26, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 68, - "x" : 1, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 69, - "x" : 1, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 70, - "x" : 1, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 71, - "x" : 1, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 72, - "x" : 1, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 73, - "x" : 1, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 74, - "x" : 1, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 75, - "x" : 1, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 76, - "x" : 1, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 77, - "x" : 1, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 78, - "x" : 1, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 79, - "x" : 1, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 80, - "x" : 1, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 81, - "x" : 2, - "y" : 0, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 82, - "x" : 2, - "y" : 1, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 83, - "x" : 2, - "y" : 2, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 84, - "x" : 2, - "y" : 3, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 85, - "x" : 2, - "y" : 4, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 86, - "x" : 2, - "y" : 5, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 87, - "x" : 2, - "y" : 6, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 88, - "x" : 2, - "y" : 7, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 89, - "x" : 2, - "y" : 8, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 90, - "x" : 2, - "y" : 9, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 91, - "x" : 2, - "y" : 10, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 92, - "x" : 2, - "y" : 11, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 93, - "x" : 2, - "y" : 12, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 94, - "x" : 2, - "y" : 13, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 95, - "x" : 2, - "y" : 14, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 96, - "x" : 2, - "y" : 15, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 97, - "x" : 2, - "y" : 16, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 98, - "x" : 2, - "y" : 17, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 99, - "x" : 2, - "y" : 18, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 100, - "x" : 2, - "y" : 19, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 101, - "x" : 2, - "y" : 20, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 102, - "x" : 2, - "y" : 21, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 103, - "x" : 2, - "y" : 22, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 104, - "x" : 2, - "y" : 23, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 105, - "x" : 2, - "y" : 24, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 106, - "x" : 2, - "y" : 25, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 107, - "x" : 2, - "y" : 26, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 108, - "x" : 2, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 109, - "x" : 2, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 110, - "x" : 2, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 111, - "x" : 2, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 112, - "x" : 2, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 113, - "x" : 2, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 114, - "x" : 2, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 115, - "x" : 2, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 116, - "x" : 2, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 117, - "x" : 2, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 118, - "x" : 2, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 119, - "x" : 2, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 120, - "x" : 2, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 121, - "x" : 3, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 122, - "x" : 3, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 123, - "x" : 3, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 124, - "x" : 3, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 125, - "x" : 3, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 126, - "x" : 3, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 127, - "x" : 3, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 128, - "x" : 3, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 129, - "x" : 3, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 130, - "x" : 3, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 131, - "x" : 3, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 132, - "x" : 3, - "y" : 11, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 133, - "x" : 3, - "y" : 12, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 134, - "x" : 3, - "y" : 13, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 135, - "x" : 3, - "y" : 14, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 136, - "x" : 3, - "y" : 15, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 137, - "x" : 3, - "y" : 16, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 138, - "x" : 3, - "y" : 17, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 139, - "x" : 3, - "y" : 18, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 140, - "x" : 3, - "y" : 19, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 141, - "x" : 3, - "y" : 20, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 142, - "x" : 3, - "y" : 21, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 143, - "x" : 3, - "y" : 22, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 144, - "x" : 3, - "y" : 23, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 145, - "x" : 3, - "y" : 24, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 146, - "x" : 3, - "y" : 25, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 147, - "x" : 3, - "y" : 26, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 148, - "x" : 3, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 149, - "x" : 3, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 150, - "x" : 3, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 151, - "x" : 3, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 152, - "x" : 3, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 153, - "x" : 3, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 154, - "x" : 3, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 155, - "x" : 3, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 156, - "x" : 3, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 157, - "x" : 3, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 158, - "x" : 3, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 159, - "x" : 3, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 160, - "x" : 3, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 161, - "x" : 4, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 162, - "x" : 4, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 163, - "x" : 4, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 164, - "x" : 4, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 165, - "x" : 4, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 166, - "x" : 4, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 167, - "x" : 4, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 168, - "x" : 4, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 169, - "x" : 4, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 170, - "x" : 4, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 171, - "x" : 4, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 172, - "x" : 4, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 173, - "x" : 4, - "y" : 12, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 174, - "x" : 4, - "y" : 13, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 175, - "x" : 4, - "y" : 14, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 176, - "x" : 4, - "y" : 15, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 177, - "x" : 4, - "y" : 16, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 178, - "x" : 4, - "y" : 17, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 179, - "x" : 4, - "y" : 18, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 180, - "x" : 4, - "y" : 19, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 181, - "x" : 4, - "y" : 20, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 182, - "x" : 4, - "y" : 21, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 183, - "x" : 4, - "y" : 22, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 184, - "x" : 4, - "y" : 23, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 185, - "x" : 4, - "y" : 24, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 186, - "x" : 4, - "y" : 25, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 187, - "x" : 4, - "y" : 26, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 188, - "x" : 4, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 189, - "x" : 4, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 190, - "x" : 4, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 191, - "x" : 4, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 192, - "x" : 4, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 193, - "x" : 4, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 194, - "x" : 4, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 195, - "x" : 4, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 196, - "x" : 4, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 197, - "x" : 4, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 198, - "x" : 4, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 199, - "x" : 4, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 200, - "x" : 4, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 201, - "x" : 5, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 202, - "x" : 5, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 203, - "x" : 5, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 204, - "x" : 5, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 205, - "x" : 5, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 206, - "x" : 5, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 207, - "x" : 5, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 208, - "x" : 5, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 209, - "x" : 5, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 210, - "x" : 5, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 211, - "x" : 5, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 212, - "x" : 5, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 213, - "x" : 5, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 214, - "x" : 5, - "y" : 13, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 215, - "x" : 5, - "y" : 14, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 216, - "x" : 5, - "y" : 15, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 217, - "x" : 5, - "y" : 16, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 218, - "x" : 5, - "y" : 17, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 219, - "x" : 5, - "y" : 18, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 220, - "x" : 5, - "y" : 19, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 221, - "x" : 5, - "y" : 20, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 222, - "x" : 5, - "y" : 21, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 223, - "x" : 5, - "y" : 22, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 224, - "x" : 5, - "y" : 23, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 225, - "x" : 5, - "y" : 24, - "cityName" : "苏州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 226, - "x" : 5, - "y" : 25, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 227, - "x" : 5, - "y" : 26, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 228, - "x" : 5, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 229, - "x" : 5, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 230, - "x" : 5, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 231, - "x" : 5, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 232, - "x" : 5, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 233, - "x" : 5, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 234, - "x" : 5, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 235, - "x" : 5, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 236, - "x" : 5, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 237, - "x" : 5, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 238, - "x" : 5, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 239, - "x" : 5, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 240, - "x" : 5, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 241, - "x" : 6, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 242, - "x" : 6, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 243, - "x" : 6, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 244, - "x" : 6, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 245, - "x" : 6, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 246, - "x" : 6, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 247, - "x" : 6, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 248, - "x" : 6, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 249, - "x" : 6, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 250, - "x" : 6, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 251, - "x" : 6, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 252, - "x" : 6, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 253, - "x" : 6, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 254, - "x" : 6, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 255, - "x" : 6, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 256, - "x" : 6, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 257, - "x" : 6, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 258, - "x" : 6, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 259, - "x" : 6, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 260, - "x" : 6, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 261, - "x" : 6, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 262, - "x" : 6, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 263, - "x" : 6, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 264, - "x" : 6, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 265, - "x" : 6, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 266, - "x" : 6, - "y" : 25, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 267, - "x" : 6, - "y" : 26, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 268, - "x" : 6, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 269, - "x" : 6, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 270, - "x" : 6, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 271, - "x" : 6, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 272, - "x" : 6, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 273, - "x" : 6, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 274, - "x" : 6, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 275, - "x" : 6, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 276, - "x" : 6, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 277, - "x" : 6, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 278, - "x" : 6, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 279, - "x" : 6, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 280, - "x" : 6, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 281, - "x" : 7, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 282, - "x" : 7, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 283, - "x" : 7, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 284, - "x" : 7, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 285, - "x" : 7, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 286, - "x" : 7, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 287, - "x" : 7, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 288, - "x" : 7, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 289, - "x" : 7, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 290, - "x" : 7, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 291, - "x" : 7, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 292, - "x" : 7, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 293, - "x" : 7, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 294, - "x" : 7, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 295, - "x" : 7, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 296, - "x" : 7, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 297, - "x" : 7, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 298, - "x" : 7, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 299, - "x" : 7, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 300, - "x" : 7, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 301, - "x" : 7, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 302, - "x" : 7, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 303, - "x" : 7, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 304, - "x" : 7, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 305, - "x" : 7, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 306, - "x" : 7, - "y" : 25, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 307, - "x" : 7, - "y" : 26, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 308, - "x" : 7, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 309, - "x" : 7, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 310, - "x" : 7, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 311, - "x" : 7, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 312, - "x" : 7, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 313, - "x" : 7, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 314, - "x" : 7, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 315, - "x" : 7, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 316, - "x" : 7, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 317, - "x" : 7, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 318, - "x" : 7, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 319, - "x" : 7, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 320, - "x" : 7, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 321, - "x" : 8, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 322, - "x" : 8, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 323, - "x" : 8, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 324, - "x" : 8, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 325, - "x" : 8, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 326, - "x" : 8, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 327, - "x" : 8, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 328, - "x" : 8, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 329, - "x" : 8, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 330, - "x" : 8, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 331, - "x" : 8, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 332, - "x" : 8, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 333, - "x" : 8, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 334, - "x" : 8, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 335, - "x" : 8, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 336, - "x" : 8, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 337, - "x" : 8, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 338, - "x" : 8, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 339, - "x" : 8, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 340, - "x" : 8, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 341, - "x" : 8, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 342, - "x" : 8, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 343, - "x" : 8, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 344, - "x" : 8, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 345, - "x" : 8, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 346, - "x" : 8, - "y" : 25, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 347, - "x" : 8, - "y" : 26, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 348, - "x" : 8, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 349, - "x" : 8, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 350, - "x" : 8, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 351, - "x" : 8, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 352, - "x" : 8, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 353, - "x" : 8, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 354, - "x" : 8, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 355, - "x" : 8, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 356, - "x" : 8, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 357, - "x" : 8, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 358, - "x" : 8, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 359, - "x" : 8, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 360, - "x" : 8, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 361, - "x" : 9, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 362, - "x" : 9, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 363, - "x" : 9, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 364, - "x" : 9, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 365, - "x" : 9, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 366, - "x" : 9, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 367, - "x" : 9, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 368, - "x" : 9, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 369, - "x" : 9, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 370, - "x" : 9, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 371, - "x" : 9, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 372, - "x" : 9, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 373, - "x" : 9, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 374, - "x" : 9, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 375, - "x" : 9, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 376, - "x" : 9, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 377, - "x" : 9, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 378, - "x" : 9, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 379, - "x" : 9, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 380, - "x" : 9, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 381, - "x" : 9, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 382, - "x" : 9, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 383, - "x" : 9, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 384, - "x" : 9, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 385, - "x" : 9, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 386, - "x" : 9, - "y" : 25, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 387, - "x" : 9, - "y" : 26, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 388, - "x" : 9, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 389, - "x" : 9, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 390, - "x" : 9, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 391, - "x" : 9, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 392, - "x" : 9, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 393, - "x" : 9, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 394, - "x" : 9, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 395, - "x" : 9, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 396, - "x" : 9, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 397, - "x" : 9, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 398, - "x" : 9, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 399, - "x" : 9, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 400, - "x" : 9, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 401, - "x" : 10, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 402, - "x" : 10, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 403, - "x" : 10, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 404, - "x" : 10, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 405, - "x" : 10, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 406, - "x" : 10, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 407, - "x" : 10, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 408, - "x" : 10, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 409, - "x" : 10, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 410, - "x" : 10, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 411, - "x" : 10, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 412, - "x" : 10, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 413, - "x" : 10, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 414, - "x" : 10, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 415, - "x" : 10, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 416, - "x" : 10, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 417, - "x" : 10, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 418, - "x" : 10, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 419, - "x" : 10, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 420, - "x" : 10, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 421, - "x" : 10, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 422, - "x" : 10, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 423, - "x" : 10, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 424, - "x" : 10, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 425, - "x" : 10, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 426, - "x" : 10, - "y" : 25, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 427, - "x" : 10, - "y" : 26, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 428, - "x" : 10, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 429, - "x" : 10, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 430, - "x" : 10, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 431, - "x" : 10, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 432, - "x" : 10, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 433, - "x" : 10, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 434, - "x" : 10, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 435, - "x" : 10, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 436, - "x" : 10, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 437, - "x" : 10, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 438, - "x" : 10, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 439, - "x" : 10, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 440, - "x" : 10, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 441, - "x" : 11, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 442, - "x" : 11, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 443, - "x" : 11, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 444, - "x" : 11, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 445, - "x" : 11, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 446, - "x" : 11, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 447, - "x" : 11, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 448, - "x" : 11, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 449, - "x" : 11, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 450, - "x" : 11, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 451, - "x" : 11, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 452, - "x" : 11, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 453, - "x" : 11, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 454, - "x" : 11, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 455, - "x" : 11, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 456, - "x" : 11, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 457, - "x" : 11, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 458, - "x" : 11, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 459, - "x" : 11, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 460, - "x" : 11, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 461, - "x" : 11, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 462, - "x" : 11, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 463, - "x" : 11, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 464, - "x" : 11, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 465, - "x" : 11, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 466, - "x" : 11, - "y" : 25, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 467, - "x" : 11, - "y" : 26, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 468, - "x" : 11, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 469, - "x" : 11, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 470, - "x" : 11, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 471, - "x" : 11, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 472, - "x" : 11, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 473, - "x" : 11, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 474, - "x" : 11, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 475, - "x" : 11, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 476, - "x" : 11, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 477, - "x" : 11, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 478, - "x" : 11, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 479, - "x" : 11, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 480, - "x" : 11, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 481, - "x" : 12, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 482, - "x" : 12, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 483, - "x" : 12, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 484, - "x" : 12, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 485, - "x" : 12, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 486, - "x" : 12, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 487, - "x" : 12, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 488, - "x" : 12, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 489, - "x" : 12, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 490, - "x" : 12, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 491, - "x" : 12, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 492, - "x" : 12, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 493, - "x" : 12, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 494, - "x" : 12, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 495, - "x" : 12, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 496, - "x" : 12, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 497, - "x" : 12, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 498, - "x" : 12, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 499, - "x" : 12, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 500, - "x" : 12, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 501, - "x" : 12, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 502, - "x" : 12, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 503, - "x" : 12, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 504, - "x" : 12, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 505, - "x" : 12, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 506, - "x" : 12, - "y" : 25, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 507, - "x" : 12, - "y" : 26, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 508, - "x" : 12, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 509, - "x" : 12, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 510, - "x" : 12, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 511, - "x" : 12, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 512, - "x" : 12, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 513, - "x" : 12, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 514, - "x" : 12, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 515, - "x" : 12, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 516, - "x" : 12, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 517, - "x" : 12, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 518, - "x" : 12, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 519, - "x" : 12, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 520, - "x" : 12, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 521, - "x" : 13, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 522, - "x" : 13, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 523, - "x" : 13, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 524, - "x" : 13, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 525, - "x" : 13, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 526, - "x" : 13, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 527, - "x" : 13, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 528, - "x" : 13, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 529, - "x" : 13, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 530, - "x" : 13, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 531, - "x" : 13, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 532, - "x" : 13, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 533, - "x" : 13, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 534, - "x" : 13, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 535, - "x" : 13, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 536, - "x" : 13, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 537, - "x" : 13, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 538, - "x" : 13, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 539, - "x" : 13, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 540, - "x" : 13, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 541, - "x" : 13, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 542, - "x" : 13, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 543, - "x" : 13, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 544, - "x" : 13, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 545, - "x" : 13, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 546, - "x" : 13, - "y" : 25, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 547, - "x" : 13, - "y" : 26, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 548, - "x" : 13, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 549, - "x" : 13, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 550, - "x" : 13, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 551, - "x" : 13, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 552, - "x" : 13, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 553, - "x" : 13, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 554, - "x" : 13, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 555, - "x" : 13, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 556, - "x" : 13, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 557, - "x" : 13, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 558, - "x" : 13, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 559, - "x" : 13, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 560, - "x" : 13, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 561, - "x" : 14, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 562, - "x" : 14, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 563, - "x" : 14, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 564, - "x" : 14, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 565, - "x" : 14, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 566, - "x" : 14, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 567, - "x" : 14, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 568, - "x" : 14, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 569, - "x" : 14, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 570, - "x" : 14, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 571, - "x" : 14, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 572, - "x" : 14, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 573, - "x" : 14, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 574, - "x" : 14, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 575, - "x" : 14, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 576, - "x" : 14, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 577, - "x" : 14, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 578, - "x" : 14, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 579, - "x" : 14, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 580, - "x" : 14, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 581, - "x" : 14, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 582, - "x" : 14, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 583, - "x" : 14, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 584, - "x" : 14, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 585, - "x" : 14, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 586, - "x" : 14, - "y" : 25, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 587, - "x" : 14, - "y" : 26, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 588, - "x" : 14, - "y" : 27, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 589, - "x" : 14, - "y" : 28, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 590, - "x" : 14, - "y" : 29, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 591, - "x" : 14, - "y" : 30, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 592, - "x" : 14, - "y" : 31, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 593, - "x" : 14, - "y" : 32, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 594, - "x" : 14, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 595, - "x" : 14, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 596, - "x" : 14, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 597, - "x" : 14, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 598, - "x" : 14, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 599, - "x" : 14, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 600, - "x" : 14, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 601, - "x" : 15, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 602, - "x" : 15, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 603, - "x" : 15, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 604, - "x" : 15, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 605, - "x" : 15, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 606, - "x" : 15, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 607, - "x" : 15, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 608, - "x" : 15, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 609, - "x" : 15, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 610, - "x" : 15, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 611, - "x" : 15, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 612, - "x" : 15, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 613, - "x" : 15, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 614, - "x" : 15, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 615, - "x" : 15, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 616, - "x" : 15, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 617, - "x" : 15, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 618, - "x" : 15, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 619, - "x" : 15, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 620, - "x" : 15, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 621, - "x" : 15, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 622, - "x" : 15, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 623, - "x" : 15, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 624, - "x" : 15, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 625, - "x" : 15, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 626, - "x" : 15, - "y" : 25, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 627, - "x" : 15, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 628, - "x" : 15, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 629, - "x" : 15, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 630, - "x" : 15, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 631, - "x" : 15, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 632, - "x" : 15, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 633, - "x" : 15, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 634, - "x" : 15, - "y" : 33, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 635, - "x" : 15, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 636, - "x" : 15, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 637, - "x" : 15, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 638, - "x" : 15, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 639, - "x" : 15, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 640, - "x" : 15, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 641, - "x" : 16, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 642, - "x" : 16, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 643, - "x" : 16, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 644, - "x" : 16, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 645, - "x" : 16, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 646, - "x" : 16, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 647, - "x" : 16, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 648, - "x" : 16, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 649, - "x" : 16, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 650, - "x" : 16, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 651, - "x" : 16, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 652, - "x" : 16, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 653, - "x" : 16, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 654, - "x" : 16, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 655, - "x" : 16, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 656, - "x" : 16, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 657, - "x" : 16, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 658, - "x" : 16, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 659, - "x" : 16, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 660, - "x" : 16, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 661, - "x" : 16, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 662, - "x" : 16, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 663, - "x" : 16, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 664, - "x" : 16, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 665, - "x" : 16, - "y" : 24, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 666, - "x" : 16, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 667, - "x" : 16, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 668, - "x" : 16, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 669, - "x" : 16, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 670, - "x" : 16, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 671, - "x" : 16, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 672, - "x" : 16, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 673, - "x" : 16, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 674, - "x" : 16, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 675, - "x" : 16, - "y" : 34, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 676, - "x" : 16, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 677, - "x" : 16, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 678, - "x" : 16, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 679, - "x" : 16, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 680, - "x" : 16, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 681, - "x" : 17, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 682, - "x" : 17, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 683, - "x" : 17, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 684, - "x" : 17, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 685, - "x" : 17, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 686, - "x" : 17, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 687, - "x" : 17, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 688, - "x" : 17, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 689, - "x" : 17, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 690, - "x" : 17, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 691, - "x" : 17, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 692, - "x" : 17, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 693, - "x" : 17, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 694, - "x" : 17, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 695, - "x" : 17, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 696, - "x" : 17, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 697, - "x" : 17, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 698, - "x" : 17, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 699, - "x" : 17, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 700, - "x" : 17, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 701, - "x" : 17, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 702, - "x" : 17, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 703, - "x" : 17, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 704, - "x" : 17, - "y" : 23, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 705, - "x" : 17, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 706, - "x" : 17, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 707, - "x" : 17, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 708, - "x" : 17, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 709, - "x" : 17, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 710, - "x" : 17, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 711, - "x" : 17, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 712, - "x" : 17, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 713, - "x" : 17, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 714, - "x" : 17, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 715, - "x" : 17, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 716, - "x" : 17, - "y" : 35, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 717, - "x" : 17, - "y" : 36, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 718, - "x" : 17, - "y" : 37, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 719, - "x" : 17, - "y" : 38, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 720, - "x" : 17, - "y" : 39, - "cityName" : "南京市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 721, - "x" : 18, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 722, - "x" : 18, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 723, - "x" : 18, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 724, - "x" : 18, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 725, - "x" : 18, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 726, - "x" : 18, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 727, - "x" : 18, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 728, - "x" : 18, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 729, - "x" : 18, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 730, - "x" : 18, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 731, - "x" : 18, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 732, - "x" : 18, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 733, - "x" : 18, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 734, - "x" : 18, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 735, - "x" : 18, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 736, - "x" : 18, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 737, - "x" : 18, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 738, - "x" : 18, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 739, - "x" : 18, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 740, - "x" : 18, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 741, - "x" : 18, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 742, - "x" : 18, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 743, - "x" : 18, - "y" : 22, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 744, - "x" : 18, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 745, - "x" : 18, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 746, - "x" : 18, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 747, - "x" : 18, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 748, - "x" : 18, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 749, - "x" : 18, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 750, - "x" : 18, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 751, - "x" : 18, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 752, - "x" : 18, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 753, - "x" : 18, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 754, - "x" : 18, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 755, - "x" : 18, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 756, - "x" : 18, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 757, - "x" : 18, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 758, - "x" : 18, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 759, - "x" : 18, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 760, - "x" : 18, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 761, - "x" : 19, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 762, - "x" : 19, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 763, - "x" : 19, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 764, - "x" : 19, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 765, - "x" : 19, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 766, - "x" : 19, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 767, - "x" : 19, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 768, - "x" : 19, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 769, - "x" : 19, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 770, - "x" : 19, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 771, - "x" : 19, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 772, - "x" : 19, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 773, - "x" : 19, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 774, - "x" : 19, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 775, - "x" : 19, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 776, - "x" : 19, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 777, - "x" : 19, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 778, - "x" : 19, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 779, - "x" : 19, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 780, - "x" : 19, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 781, - "x" : 19, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 782, - "x" : 19, - "y" : 21, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 783, - "x" : 19, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 784, - "x" : 19, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 785, - "x" : 19, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 786, - "x" : 19, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 787, - "x" : 19, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 788, - "x" : 19, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 789, - "x" : 19, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 790, - "x" : 19, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 791, - "x" : 19, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 792, - "x" : 19, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 793, - "x" : 19, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 794, - "x" : 19, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 795, - "x" : 19, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 796, - "x" : 19, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 797, - "x" : 19, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 798, - "x" : 19, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 799, - "x" : 19, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 800, - "x" : 19, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 801, - "x" : 20, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 802, - "x" : 20, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 803, - "x" : 20, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 804, - "x" : 20, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 805, - "x" : 20, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 806, - "x" : 20, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 807, - "x" : 20, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 808, - "x" : 20, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 809, - "x" : 20, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 810, - "x" : 20, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 811, - "x" : 20, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 812, - "x" : 20, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 813, - "x" : 20, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 814, - "x" : 20, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 815, - "x" : 20, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 816, - "x" : 20, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 817, - "x" : 20, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 818, - "x" : 20, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 819, - "x" : 20, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 820, - "x" : 20, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 821, - "x" : 20, - "y" : 20, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 822, - "x" : 20, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 823, - "x" : 20, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 824, - "x" : 20, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 825, - "x" : 20, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 826, - "x" : 20, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 827, - "x" : 20, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 828, - "x" : 20, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 829, - "x" : 20, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 830, - "x" : 20, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 831, - "x" : 20, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 832, - "x" : 20, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 833, - "x" : 20, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 834, - "x" : 20, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 835, - "x" : 20, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 836, - "x" : 20, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 837, - "x" : 20, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 838, - "x" : 20, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 839, - "x" : 20, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 840, - "x" : 20, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 841, - "x" : 21, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 842, - "x" : 21, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 843, - "x" : 21, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 844, - "x" : 21, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 845, - "x" : 21, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 846, - "x" : 21, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 847, - "x" : 21, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 848, - "x" : 21, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 849, - "x" : 21, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 850, - "x" : 21, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 851, - "x" : 21, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 852, - "x" : 21, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 853, - "x" : 21, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 854, - "x" : 21, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 855, - "x" : 21, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 856, - "x" : 21, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 857, - "x" : 21, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 858, - "x" : 21, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 859, - "x" : 21, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 860, - "x" : 21, - "y" : 19, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 861, - "x" : 21, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 862, - "x" : 21, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 863, - "x" : 21, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 864, - "x" : 21, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 865, - "x" : 21, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 866, - "x" : 21, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 867, - "x" : 21, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 868, - "x" : 21, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 869, - "x" : 21, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 870, - "x" : 21, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 871, - "x" : 21, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 872, - "x" : 21, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 873, - "x" : 21, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 874, - "x" : 21, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 875, - "x" : 21, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 876, - "x" : 21, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 877, - "x" : 21, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 878, - "x" : 21, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 879, - "x" : 21, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 880, - "x" : 21, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 881, - "x" : 22, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 882, - "x" : 22, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 883, - "x" : 22, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 884, - "x" : 22, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 885, - "x" : 22, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 886, - "x" : 22, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 887, - "x" : 22, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 888, - "x" : 22, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 889, - "x" : 22, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 890, - "x" : 22, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 891, - "x" : 22, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 892, - "x" : 22, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 893, - "x" : 22, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 894, - "x" : 22, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 895, - "x" : 22, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 896, - "x" : 22, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 897, - "x" : 22, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 898, - "x" : 22, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 899, - "x" : 22, - "y" : 18, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 900, - "x" : 22, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 901, - "x" : 22, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 902, - "x" : 22, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 903, - "x" : 22, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 904, - "x" : 22, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 905, - "x" : 22, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 906, - "x" : 22, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 907, - "x" : 22, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 908, - "x" : 22, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 909, - "x" : 22, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 910, - "x" : 22, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 911, - "x" : 22, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 912, - "x" : 22, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 913, - "x" : 22, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 914, - "x" : 22, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 915, - "x" : 22, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 916, - "x" : 22, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 917, - "x" : 22, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 918, - "x" : 22, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 919, - "x" : 22, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 920, - "x" : 22, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 921, - "x" : 23, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 922, - "x" : 23, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 923, - "x" : 23, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 924, - "x" : 23, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 925, - "x" : 23, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 926, - "x" : 23, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 927, - "x" : 23, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 928, - "x" : 23, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 929, - "x" : 23, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 930, - "x" : 23, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 931, - "x" : 23, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 932, - "x" : 23, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 933, - "x" : 23, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 934, - "x" : 23, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 935, - "x" : 23, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 936, - "x" : 23, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 937, - "x" : 23, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 938, - "x" : 23, - "y" : 17, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 939, - "x" : 23, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 940, - "x" : 23, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 941, - "x" : 23, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 942, - "x" : 23, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 943, - "x" : 23, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 944, - "x" : 23, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 945, - "x" : 23, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 946, - "x" : 23, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 947, - "x" : 23, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 948, - "x" : 23, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 949, - "x" : 23, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 950, - "x" : 23, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 951, - "x" : 23, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 952, - "x" : 23, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 953, - "x" : 23, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 954, - "x" : 23, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 955, - "x" : 23, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 956, - "x" : 23, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 957, - "x" : 23, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 958, - "x" : 23, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 959, - "x" : 23, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 960, - "x" : 23, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 961, - "x" : 24, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 962, - "x" : 24, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 963, - "x" : 24, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 964, - "x" : 24, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 965, - "x" : 24, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 966, - "x" : 24, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 967, - "x" : 24, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 968, - "x" : 24, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 969, - "x" : 24, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 970, - "x" : 24, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 971, - "x" : 24, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 972, - "x" : 24, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 973, - "x" : 24, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 974, - "x" : 24, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 975, - "x" : 24, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 976, - "x" : 24, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 977, - "x" : 24, - "y" : 16, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 978, - "x" : 24, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 979, - "x" : 24, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 980, - "x" : 24, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 981, - "x" : 24, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 982, - "x" : 24, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 983, - "x" : 24, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 984, - "x" : 24, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 985, - "x" : 24, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 986, - "x" : 24, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 987, - "x" : 24, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 988, - "x" : 24, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 989, - "x" : 24, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 990, - "x" : 24, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 991, - "x" : 24, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 992, - "x" : 24, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 993, - "x" : 24, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 994, - "x" : 24, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 995, - "x" : 24, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 996, - "x" : 24, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 997, - "x" : 24, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 998, - "x" : 24, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 999, - "x" : 24, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1000, - "x" : 24, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1001, - "x" : 25, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1002, - "x" : 25, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1003, - "x" : 25, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1004, - "x" : 25, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1005, - "x" : 25, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1006, - "x" : 25, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1007, - "x" : 25, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1008, - "x" : 25, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1009, - "x" : 25, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1010, - "x" : 25, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1011, - "x" : 25, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1012, - "x" : 25, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1013, - "x" : 25, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1014, - "x" : 25, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1015, - "x" : 25, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1016, - "x" : 25, - "y" : 15, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1017, - "x" : 25, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1018, - "x" : 25, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1019, - "x" : 25, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1020, - "x" : 25, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1021, - "x" : 25, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1022, - "x" : 25, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1023, - "x" : 25, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1024, - "x" : 25, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1025, - "x" : 25, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1026, - "x" : 25, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1027, - "x" : 25, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1028, - "x" : 25, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1029, - "x" : 25, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1030, - "x" : 25, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1031, - "x" : 25, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1032, - "x" : 25, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1033, - "x" : 25, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1034, - "x" : 25, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1035, - "x" : 25, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1036, - "x" : 25, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1037, - "x" : 25, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1038, - "x" : 25, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1039, - "x" : 25, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1040, - "x" : 25, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1041, - "x" : 26, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1042, - "x" : 26, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1043, - "x" : 26, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1044, - "x" : 26, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1045, - "x" : 26, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1046, - "x" : 26, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1047, - "x" : 26, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1048, - "x" : 26, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1049, - "x" : 26, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1050, - "x" : 26, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1051, - "x" : 26, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1052, - "x" : 26, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1053, - "x" : 26, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1054, - "x" : 26, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1055, - "x" : 26, - "y" : 14, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1056, - "x" : 26, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1057, - "x" : 26, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1058, - "x" : 26, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1059, - "x" : 26, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1060, - "x" : 26, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1061, - "x" : 26, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1062, - "x" : 26, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1063, - "x" : 26, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1064, - "x" : 26, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1065, - "x" : 26, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1066, - "x" : 26, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1067, - "x" : 26, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1068, - "x" : 26, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1069, - "x" : 26, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1070, - "x" : 26, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1071, - "x" : 26, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1072, - "x" : 26, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1073, - "x" : 26, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1074, - "x" : 26, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1075, - "x" : 26, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1076, - "x" : 26, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1077, - "x" : 26, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1078, - "x" : 26, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1079, - "x" : 26, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1080, - "x" : 26, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1081, - "x" : 27, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1082, - "x" : 27, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1083, - "x" : 27, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1084, - "x" : 27, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1085, - "x" : 27, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1086, - "x" : 27, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1087, - "x" : 27, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1088, - "x" : 27, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1089, - "x" : 27, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1090, - "x" : 27, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1091, - "x" : 27, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1092, - "x" : 27, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1093, - "x" : 27, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1094, - "x" : 27, - "y" : 13, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1095, - "x" : 27, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1096, - "x" : 27, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1097, - "x" : 27, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1098, - "x" : 27, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1099, - "x" : 27, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1100, - "x" : 27, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1101, - "x" : 27, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1102, - "x" : 27, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1103, - "x" : 27, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1104, - "x" : 27, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1105, - "x" : 27, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1106, - "x" : 27, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1107, - "x" : 27, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1108, - "x" : 27, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1109, - "x" : 27, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1110, - "x" : 27, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1111, - "x" : 27, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1112, - "x" : 27, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1113, - "x" : 27, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1114, - "x" : 27, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1115, - "x" : 27, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1116, - "x" : 27, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1117, - "x" : 27, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1118, - "x" : 27, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1119, - "x" : 27, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1120, - "x" : 27, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1121, - "x" : 28, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1122, - "x" : 28, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1123, - "x" : 28, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1124, - "x" : 28, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1125, - "x" : 28, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1126, - "x" : 28, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1127, - "x" : 28, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1128, - "x" : 28, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1129, - "x" : 28, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1130, - "x" : 28, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1131, - "x" : 28, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1132, - "x" : 28, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1133, - "x" : 28, - "y" : 12, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1134, - "x" : 28, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1135, - "x" : 28, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1136, - "x" : 28, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1137, - "x" : 28, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1138, - "x" : 28, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1139, - "x" : 28, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1140, - "x" : 28, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1141, - "x" : 28, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1142, - "x" : 28, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1143, - "x" : 28, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1144, - "x" : 28, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1145, - "x" : 28, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1146, - "x" : 28, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1147, - "x" : 28, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1148, - "x" : 28, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1149, - "x" : 28, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1150, - "x" : 28, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1151, - "x" : 28, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1152, - "x" : 28, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1153, - "x" : 28, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1154, - "x" : 28, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1155, - "x" : 28, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1156, - "x" : 28, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1157, - "x" : 28, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1158, - "x" : 28, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1159, - "x" : 28, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1160, - "x" : 28, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1161, - "x" : 29, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1162, - "x" : 29, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1163, - "x" : 29, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1164, - "x" : 29, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1165, - "x" : 29, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1166, - "x" : 29, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1167, - "x" : 29, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1168, - "x" : 29, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1169, - "x" : 29, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1170, - "x" : 29, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1171, - "x" : 29, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1172, - "x" : 29, - "y" : 11, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1173, - "x" : 29, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1174, - "x" : 29, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1175, - "x" : 29, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1176, - "x" : 29, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1177, - "x" : 29, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1178, - "x" : 29, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1179, - "x" : 29, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1180, - "x" : 29, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1181, - "x" : 29, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1182, - "x" : 29, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1183, - "x" : 29, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1184, - "x" : 29, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1185, - "x" : 29, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1186, - "x" : 29, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1187, - "x" : 29, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1188, - "x" : 29, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1189, - "x" : 29, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1190, - "x" : 29, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1191, - "x" : 29, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1192, - "x" : 29, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1193, - "x" : 29, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1194, - "x" : 29, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1195, - "x" : 29, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1196, - "x" : 29, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1197, - "x" : 29, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1198, - "x" : 29, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1199, - "x" : 29, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1200, - "x" : 29, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1201, - "x" : 30, - "y" : 0, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1202, - "x" : 30, - "y" : 1, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1203, - "x" : 30, - "y" : 2, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1204, - "x" : 30, - "y" : 3, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1205, - "x" : 30, - "y" : 4, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1206, - "x" : 30, - "y" : 5, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1207, - "x" : 30, - "y" : 6, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1208, - "x" : 30, - "y" : 7, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1209, - "x" : 30, - "y" : 8, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1210, - "x" : 30, - "y" : 9, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1211, - "x" : 30, - "y" : 10, - "cityName" : "常州市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1212, - "x" : 30, - "y" : 11, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1213, - "x" : 30, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1214, - "x" : 30, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1215, - "x" : 30, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1216, - "x" : 30, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1217, - "x" : 30, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1218, - "x" : 30, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1219, - "x" : 30, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1220, - "x" : 30, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1221, - "x" : 30, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1222, - "x" : 30, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1223, - "x" : 30, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1224, - "x" : 30, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1225, - "x" : 30, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1226, - "x" : 30, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1227, - "x" : 30, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1228, - "x" : 30, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1229, - "x" : 30, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1230, - "x" : 30, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1231, - "x" : 30, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1232, - "x" : 30, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1233, - "x" : 30, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1234, - "x" : 30, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1235, - "x" : 30, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1236, - "x" : 30, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1237, - "x" : 30, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1238, - "x" : 30, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1239, - "x" : 30, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1240, - "x" : 30, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1241, - "x" : 31, - "y" : 0, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1242, - "x" : 31, - "y" : 1, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1243, - "x" : 31, - "y" : 2, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1244, - "x" : 31, - "y" : 3, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1245, - "x" : 31, - "y" : 4, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1246, - "x" : 31, - "y" : 5, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1247, - "x" : 31, - "y" : 6, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1248, - "x" : 31, - "y" : 7, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1249, - "x" : 31, - "y" : 8, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1250, - "x" : 31, - "y" : 9, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1251, - "x" : 31, - "y" : 10, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1252, - "x" : 31, - "y" : 11, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1253, - "x" : 31, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1254, - "x" : 31, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1255, - "x" : 31, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1256, - "x" : 31, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1257, - "x" : 31, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1258, - "x" : 31, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1259, - "x" : 31, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1260, - "x" : 31, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1261, - "x" : 31, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1262, - "x" : 31, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1263, - "x" : 31, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1264, - "x" : 31, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1265, - "x" : 31, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1266, - "x" : 31, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1267, - "x" : 31, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1268, - "x" : 31, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1269, - "x" : 31, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1270, - "x" : 31, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1271, - "x" : 31, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1272, - "x" : 31, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1273, - "x" : 31, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1274, - "x" : 31, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1275, - "x" : 31, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1276, - "x" : 31, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1277, - "x" : 31, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1278, - "x" : 31, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1279, - "x" : 31, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1280, - "x" : 31, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1281, - "x" : 32, - "y" : 0, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1282, - "x" : 32, - "y" : 1, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1283, - "x" : 32, - "y" : 2, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1284, - "x" : 32, - "y" : 3, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1285, - "x" : 32, - "y" : 4, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1286, - "x" : 32, - "y" : 5, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1287, - "x" : 32, - "y" : 6, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1288, - "x" : 32, - "y" : 7, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1289, - "x" : 32, - "y" : 8, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1290, - "x" : 32, - "y" : 9, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1291, - "x" : 32, - "y" : 10, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1292, - "x" : 32, - "y" : 11, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1293, - "x" : 32, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1294, - "x" : 32, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1295, - "x" : 32, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1296, - "x" : 32, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1297, - "x" : 32, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1298, - "x" : 32, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1299, - "x" : 32, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1300, - "x" : 32, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1301, - "x" : 32, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1302, - "x" : 32, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1303, - "x" : 32, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1304, - "x" : 32, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1305, - "x" : 32, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1306, - "x" : 32, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1307, - "x" : 32, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1308, - "x" : 32, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1309, - "x" : 32, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1310, - "x" : 32, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1311, - "x" : 32, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1312, - "x" : 32, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1313, - "x" : 32, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1314, - "x" : 32, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1315, - "x" : 32, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1316, - "x" : 32, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1317, - "x" : 32, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1318, - "x" : 32, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1319, - "x" : 32, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1320, - "x" : 32, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1321, - "x" : 33, - "y" : 0, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1322, - "x" : 33, - "y" : 1, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1323, - "x" : 33, - "y" : 2, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1324, - "x" : 33, - "y" : 3, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1325, - "x" : 33, - "y" : 4, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1326, - "x" : 33, - "y" : 5, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1327, - "x" : 33, - "y" : 6, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1328, - "x" : 33, - "y" : 7, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1329, - "x" : 33, - "y" : 8, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1330, - "x" : 33, - "y" : 9, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1331, - "x" : 33, - "y" : 10, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1332, - "x" : 33, - "y" : 11, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1333, - "x" : 33, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1334, - "x" : 33, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1335, - "x" : 33, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1336, - "x" : 33, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1337, - "x" : 33, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1338, - "x" : 33, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1339, - "x" : 33, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1340, - "x" : 33, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1341, - "x" : 33, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1342, - "x" : 33, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1343, - "x" : 33, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1344, - "x" : 33, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1345, - "x" : 33, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1346, - "x" : 33, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1347, - "x" : 33, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1348, - "x" : 33, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1349, - "x" : 33, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1350, - "x" : 33, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1351, - "x" : 33, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1352, - "x" : 33, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1353, - "x" : 33, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1354, - "x" : 33, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1355, - "x" : 33, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1356, - "x" : 33, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1357, - "x" : 33, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1358, - "x" : 33, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1359, - "x" : 33, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1360, - "x" : 33, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1361, - "x" : 34, - "y" : 0, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1362, - "x" : 34, - "y" : 1, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1363, - "x" : 34, - "y" : 2, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1364, - "x" : 34, - "y" : 3, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1365, - "x" : 34, - "y" : 4, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1366, - "x" : 34, - "y" : 5, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1367, - "x" : 34, - "y" : 6, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1368, - "x" : 34, - "y" : 7, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1369, - "x" : 34, - "y" : 8, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1370, - "x" : 34, - "y" : 9, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1371, - "x" : 34, - "y" : 10, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1372, - "x" : 34, - "y" : 11, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1373, - "x" : 34, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1374, - "x" : 34, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1375, - "x" : 34, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1376, - "x" : 34, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1377, - "x" : 34, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1378, - "x" : 34, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1379, - "x" : 34, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1380, - "x" : 34, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1381, - "x" : 34, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1382, - "x" : 34, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1383, - "x" : 34, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1384, - "x" : 34, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1385, - "x" : 34, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1386, - "x" : 34, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1387, - "x" : 34, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1388, - "x" : 34, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1389, - "x" : 34, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1390, - "x" : 34, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1391, - "x" : 34, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1392, - "x" : 34, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1393, - "x" : 34, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1394, - "x" : 34, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1395, - "x" : 34, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1396, - "x" : 34, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1397, - "x" : 34, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1398, - "x" : 34, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1399, - "x" : 34, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1400, - "x" : 34, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1401, - "x" : 35, - "y" : 0, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1402, - "x" : 35, - "y" : 1, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1403, - "x" : 35, - "y" : 2, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1404, - "x" : 35, - "y" : 3, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1405, - "x" : 35, - "y" : 4, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1406, - "x" : 35, - "y" : 5, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1407, - "x" : 35, - "y" : 6, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1408, - "x" : 35, - "y" : 7, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1409, - "x" : 35, - "y" : 8, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1410, - "x" : 35, - "y" : 9, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1411, - "x" : 35, - "y" : 10, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1412, - "x" : 35, - "y" : 11, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1413, - "x" : 35, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1414, - "x" : 35, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1415, - "x" : 35, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1416, - "x" : 35, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1417, - "x" : 35, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1418, - "x" : 35, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1419, - "x" : 35, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1420, - "x" : 35, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1421, - "x" : 35, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1422, - "x" : 35, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1423, - "x" : 35, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1424, - "x" : 35, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1425, - "x" : 35, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1426, - "x" : 35, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1427, - "x" : 35, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1428, - "x" : 35, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1429, - "x" : 35, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1430, - "x" : 35, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1431, - "x" : 35, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1432, - "x" : 35, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1433, - "x" : 35, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1434, - "x" : 35, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1435, - "x" : 35, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1436, - "x" : 35, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1437, - "x" : 35, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1438, - "x" : 35, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1439, - "x" : 35, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1440, - "x" : 35, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1441, - "x" : 36, - "y" : 0, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1442, - "x" : 36, - "y" : 1, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1443, - "x" : 36, - "y" : 2, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1444, - "x" : 36, - "y" : 3, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1445, - "x" : 36, - "y" : 4, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1446, - "x" : 36, - "y" : 5, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1447, - "x" : 36, - "y" : 6, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1448, - "x" : 36, - "y" : 7, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1449, - "x" : 36, - "y" : 8, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1450, - "x" : 36, - "y" : 9, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1451, - "x" : 36, - "y" : 10, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1452, - "x" : 36, - "y" : 11, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1453, - "x" : 36, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1454, - "x" : 36, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1455, - "x" : 36, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1456, - "x" : 36, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1457, - "x" : 36, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1458, - "x" : 36, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1459, - "x" : 36, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1460, - "x" : 36, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1461, - "x" : 36, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1462, - "x" : 36, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1463, - "x" : 36, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1464, - "x" : 36, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1465, - "x" : 36, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1466, - "x" : 36, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1467, - "x" : 36, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1468, - "x" : 36, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1469, - "x" : 36, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1470, - "x" : 36, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1471, - "x" : 36, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1472, - "x" : 36, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1473, - "x" : 36, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1474, - "x" : 36, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1475, - "x" : 36, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1476, - "x" : 36, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1477, - "x" : 36, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1478, - "x" : 36, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1479, - "x" : 36, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1480, - "x" : 36, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1481, - "x" : 37, - "y" : 0, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1482, - "x" : 37, - "y" : 1, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1483, - "x" : 37, - "y" : 2, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1484, - "x" : 37, - "y" : 3, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1485, - "x" : 37, - "y" : 4, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1486, - "x" : 37, - "y" : 5, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1487, - "x" : 37, - "y" : 6, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1488, - "x" : 37, - "y" : 7, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1489, - "x" : 37, - "y" : 8, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1490, - "x" : 37, - "y" : 9, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1491, - "x" : 37, - "y" : 10, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1492, - "x" : 37, - "y" : 11, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1493, - "x" : 37, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1494, - "x" : 37, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1495, - "x" : 37, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1496, - "x" : 37, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1497, - "x" : 37, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1498, - "x" : 37, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1499, - "x" : 37, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1500, - "x" : 37, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1501, - "x" : 37, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1502, - "x" : 37, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1503, - "x" : 37, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1504, - "x" : 37, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1505, - "x" : 37, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1506, - "x" : 37, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1507, - "x" : 37, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1508, - "x" : 37, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1509, - "x" : 37, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1510, - "x" : 37, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1511, - "x" : 37, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1512, - "x" : 37, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1513, - "x" : 37, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1514, - "x" : 37, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1515, - "x" : 37, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1516, - "x" : 37, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1517, - "x" : 37, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1518, - "x" : 37, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1519, - "x" : 37, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1520, - "x" : 37, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1521, - "x" : 38, - "y" : 0, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1522, - "x" : 38, - "y" : 1, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1523, - "x" : 38, - "y" : 2, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1524, - "x" : 38, - "y" : 3, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1525, - "x" : 38, - "y" : 4, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1526, - "x" : 38, - "y" : 5, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1527, - "x" : 38, - "y" : 6, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1528, - "x" : 38, - "y" : 7, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1529, - "x" : 38, - "y" : 8, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1530, - "x" : 38, - "y" : 9, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1531, - "x" : 38, - "y" : 10, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1532, - "x" : 38, - "y" : 11, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1533, - "x" : 38, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1534, - "x" : 38, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1535, - "x" : 38, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1536, - "x" : 38, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1537, - "x" : 38, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1538, - "x" : 38, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1539, - "x" : 38, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1540, - "x" : 38, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1541, - "x" : 38, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1542, - "x" : 38, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1543, - "x" : 38, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1544, - "x" : 38, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1545, - "x" : 38, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1546, - "x" : 38, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1547, - "x" : 38, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1548, - "x" : 38, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1549, - "x" : 38, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1550, - "x" : 38, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1551, - "x" : 38, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : true -}, { - "id" : 1552, - "x" : 38, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1553, - "x" : 38, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1554, - "x" : 38, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1555, - "x" : 38, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1556, - "x" : 38, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1557, - "x" : 38, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1558, - "x" : 38, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1559, - "x" : 38, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1560, - "x" : 38, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1561, - "x" : 39, - "y" : 0, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1562, - "x" : 39, - "y" : 1, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1563, - "x" : 39, - "y" : 2, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1564, - "x" : 39, - "y" : 3, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1565, - "x" : 39, - "y" : 4, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1566, - "x" : 39, - "y" : 5, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1567, - "x" : 39, - "y" : 6, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1568, - "x" : 39, - "y" : 7, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1569, - "x" : 39, - "y" : 8, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1570, - "x" : 39, - "y" : 9, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1571, - "x" : 39, - "y" : 10, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1572, - "x" : 39, - "y" : 11, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1573, - "x" : 39, - "y" : 12, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1574, - "x" : 39, - "y" : 13, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1575, - "x" : 39, - "y" : 14, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1576, - "x" : 39, - "y" : 15, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1577, - "x" : 39, - "y" : 16, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1578, - "x" : 39, - "y" : 17, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1579, - "x" : 39, - "y" : 18, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1580, - "x" : 39, - "y" : 19, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1581, - "x" : 39, - "y" : 20, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1582, - "x" : 39, - "y" : 21, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1583, - "x" : 39, - "y" : 22, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1584, - "x" : 39, - "y" : 23, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1585, - "x" : 39, - "y" : 24, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1586, - "x" : 39, - "y" : 25, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1587, - "x" : 39, - "y" : 26, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1588, - "x" : 39, - "y" : 27, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1589, - "x" : 39, - "y" : 28, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1590, - "x" : 39, - "y" : 29, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1591, - "x" : 39, - "y" : 30, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1592, - "x" : 39, - "y" : 31, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1593, - "x" : 39, - "y" : 32, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1594, - "x" : 39, - "y" : 33, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1595, - "x" : 39, - "y" : 34, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1596, - "x" : 39, - "y" : 35, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1597, - "x" : 39, - "y" : 36, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1598, - "x" : 39, - "y" : 37, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1599, - "x" : 39, - "y" : 38, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false -}, { - "id" : 1600, - "x" : 39, - "y" : 39, - "cityName" : "无锡市", - "terrainType" : null, - "obstacle" : false +[ { + "id" : 1, + "x" : 0, + "y" : 0, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 2, + "x" : 0, + "y" : 1, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 3, + "x" : 0, + "y" : 2, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 4, + "x" : 0, + "y" : 3, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 5, + "x" : 0, + "y" : 4, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 6, + "x" : 0, + "y" : 5, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 7, + "x" : 0, + "y" : 6, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 8, + "x" : 0, + "y" : 7, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 9, + "x" : 0, + "y" : 8, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 10, + "x" : 0, + "y" : 9, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 11, + "x" : 0, + "y" : 10, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 12, + "x" : 0, + "y" : 11, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 13, + "x" : 0, + "y" : 12, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 14, + "x" : 0, + "y" : 13, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 15, + "x" : 0, + "y" : 14, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 16, + "x" : 0, + "y" : 15, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 17, + "x" : 0, + "y" : 16, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 18, + "x" : 0, + "y" : 17, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 19, + "x" : 0, + "y" : 18, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 20, + "x" : 0, + "y" : 19, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 21, + "x" : 0, + "y" : 20, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 22, + "x" : 0, + "y" : 21, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 23, + "x" : 0, + "y" : 22, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 24, + "x" : 0, + "y" : 23, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 25, + "x" : 0, + "y" : 24, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 26, + "x" : 0, + "y" : 25, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 27, + "x" : 0, + "y" : 26, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 28, + "x" : 0, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 29, + "x" : 0, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 30, + "x" : 0, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 31, + "x" : 0, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 32, + "x" : 0, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 33, + "x" : 0, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 34, + "x" : 0, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 35, + "x" : 0, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 36, + "x" : 0, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 37, + "x" : 0, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 38, + "x" : 0, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 39, + "x" : 0, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 40, + "x" : 0, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 41, + "x" : 1, + "y" : 0, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 42, + "x" : 1, + "y" : 1, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 43, + "x" : 1, + "y" : 2, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 44, + "x" : 1, + "y" : 3, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 45, + "x" : 1, + "y" : 4, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 46, + "x" : 1, + "y" : 5, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 47, + "x" : 1, + "y" : 6, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 48, + "x" : 1, + "y" : 7, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 49, + "x" : 1, + "y" : 8, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 50, + "x" : 1, + "y" : 9, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 51, + "x" : 1, + "y" : 10, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 52, + "x" : 1, + "y" : 11, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 53, + "x" : 1, + "y" : 12, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 54, + "x" : 1, + "y" : 13, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 55, + "x" : 1, + "y" : 14, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 56, + "x" : 1, + "y" : 15, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 57, + "x" : 1, + "y" : 16, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 58, + "x" : 1, + "y" : 17, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 59, + "x" : 1, + "y" : 18, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 60, + "x" : 1, + "y" : 19, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 61, + "x" : 1, + "y" : 20, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 62, + "x" : 1, + "y" : 21, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 63, + "x" : 1, + "y" : 22, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 64, + "x" : 1, + "y" : 23, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 65, + "x" : 1, + "y" : 24, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 66, + "x" : 1, + "y" : 25, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 67, + "x" : 1, + "y" : 26, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 68, + "x" : 1, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 69, + "x" : 1, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 70, + "x" : 1, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 71, + "x" : 1, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 72, + "x" : 1, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 73, + "x" : 1, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 74, + "x" : 1, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 75, + "x" : 1, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 76, + "x" : 1, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 77, + "x" : 1, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 78, + "x" : 1, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 79, + "x" : 1, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 80, + "x" : 1, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 81, + "x" : 2, + "y" : 0, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 82, + "x" : 2, + "y" : 1, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 83, + "x" : 2, + "y" : 2, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 84, + "x" : 2, + "y" : 3, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 85, + "x" : 2, + "y" : 4, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 86, + "x" : 2, + "y" : 5, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 87, + "x" : 2, + "y" : 6, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 88, + "x" : 2, + "y" : 7, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 89, + "x" : 2, + "y" : 8, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 90, + "x" : 2, + "y" : 9, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 91, + "x" : 2, + "y" : 10, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 92, + "x" : 2, + "y" : 11, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 93, + "x" : 2, + "y" : 12, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 94, + "x" : 2, + "y" : 13, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 95, + "x" : 2, + "y" : 14, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 96, + "x" : 2, + "y" : 15, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 97, + "x" : 2, + "y" : 16, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 98, + "x" : 2, + "y" : 17, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 99, + "x" : 2, + "y" : 18, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 100, + "x" : 2, + "y" : 19, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 101, + "x" : 2, + "y" : 20, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 102, + "x" : 2, + "y" : 21, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 103, + "x" : 2, + "y" : 22, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 104, + "x" : 2, + "y" : 23, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 105, + "x" : 2, + "y" : 24, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 106, + "x" : 2, + "y" : 25, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 107, + "x" : 2, + "y" : 26, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 108, + "x" : 2, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 109, + "x" : 2, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 110, + "x" : 2, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 111, + "x" : 2, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 112, + "x" : 2, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 113, + "x" : 2, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 114, + "x" : 2, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 115, + "x" : 2, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 116, + "x" : 2, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 117, + "x" : 2, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 118, + "x" : 2, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 119, + "x" : 2, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 120, + "x" : 2, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 121, + "x" : 3, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 122, + "x" : 3, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 123, + "x" : 3, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 124, + "x" : 3, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 125, + "x" : 3, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 126, + "x" : 3, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 127, + "x" : 3, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 128, + "x" : 3, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 129, + "x" : 3, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 130, + "x" : 3, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 131, + "x" : 3, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 132, + "x" : 3, + "y" : 11, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 133, + "x" : 3, + "y" : 12, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 134, + "x" : 3, + "y" : 13, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 135, + "x" : 3, + "y" : 14, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 136, + "x" : 3, + "y" : 15, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 137, + "x" : 3, + "y" : 16, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 138, + "x" : 3, + "y" : 17, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 139, + "x" : 3, + "y" : 18, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 140, + "x" : 3, + "y" : 19, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 141, + "x" : 3, + "y" : 20, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 142, + "x" : 3, + "y" : 21, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 143, + "x" : 3, + "y" : 22, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 144, + "x" : 3, + "y" : 23, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 145, + "x" : 3, + "y" : 24, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 146, + "x" : 3, + "y" : 25, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 147, + "x" : 3, + "y" : 26, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 148, + "x" : 3, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 149, + "x" : 3, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 150, + "x" : 3, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 151, + "x" : 3, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 152, + "x" : 3, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 153, + "x" : 3, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 154, + "x" : 3, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 155, + "x" : 3, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 156, + "x" : 3, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 157, + "x" : 3, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 158, + "x" : 3, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 159, + "x" : 3, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 160, + "x" : 3, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 161, + "x" : 4, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 162, + "x" : 4, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 163, + "x" : 4, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 164, + "x" : 4, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 165, + "x" : 4, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 166, + "x" : 4, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 167, + "x" : 4, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 168, + "x" : 4, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 169, + "x" : 4, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 170, + "x" : 4, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 171, + "x" : 4, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 172, + "x" : 4, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 173, + "x" : 4, + "y" : 12, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 174, + "x" : 4, + "y" : 13, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 175, + "x" : 4, + "y" : 14, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 176, + "x" : 4, + "y" : 15, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 177, + "x" : 4, + "y" : 16, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 178, + "x" : 4, + "y" : 17, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 179, + "x" : 4, + "y" : 18, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 180, + "x" : 4, + "y" : 19, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 181, + "x" : 4, + "y" : 20, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 182, + "x" : 4, + "y" : 21, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 183, + "x" : 4, + "y" : 22, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 184, + "x" : 4, + "y" : 23, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 185, + "x" : 4, + "y" : 24, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 186, + "x" : 4, + "y" : 25, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 187, + "x" : 4, + "y" : 26, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 188, + "x" : 4, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 189, + "x" : 4, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 190, + "x" : 4, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 191, + "x" : 4, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 192, + "x" : 4, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 193, + "x" : 4, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 194, + "x" : 4, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 195, + "x" : 4, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 196, + "x" : 4, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 197, + "x" : 4, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 198, + "x" : 4, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 199, + "x" : 4, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 200, + "x" : 4, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 201, + "x" : 5, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 202, + "x" : 5, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 203, + "x" : 5, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 204, + "x" : 5, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 205, + "x" : 5, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 206, + "x" : 5, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 207, + "x" : 5, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 208, + "x" : 5, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 209, + "x" : 5, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 210, + "x" : 5, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 211, + "x" : 5, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 212, + "x" : 5, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 213, + "x" : 5, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 214, + "x" : 5, + "y" : 13, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 215, + "x" : 5, + "y" : 14, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 216, + "x" : 5, + "y" : 15, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 217, + "x" : 5, + "y" : 16, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 218, + "x" : 5, + "y" : 17, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 219, + "x" : 5, + "y" : 18, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 220, + "x" : 5, + "y" : 19, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 221, + "x" : 5, + "y" : 20, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 222, + "x" : 5, + "y" : 21, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 223, + "x" : 5, + "y" : 22, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 224, + "x" : 5, + "y" : 23, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 225, + "x" : 5, + "y" : 24, + "cityName" : "苏州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 226, + "x" : 5, + "y" : 25, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 227, + "x" : 5, + "y" : 26, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 228, + "x" : 5, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 229, + "x" : 5, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 230, + "x" : 5, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 231, + "x" : 5, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 232, + "x" : 5, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 233, + "x" : 5, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 234, + "x" : 5, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 235, + "x" : 5, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 236, + "x" : 5, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 237, + "x" : 5, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 238, + "x" : 5, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 239, + "x" : 5, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 240, + "x" : 5, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 241, + "x" : 6, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 242, + "x" : 6, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 243, + "x" : 6, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 244, + "x" : 6, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 245, + "x" : 6, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 246, + "x" : 6, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 247, + "x" : 6, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 248, + "x" : 6, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 249, + "x" : 6, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 250, + "x" : 6, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 251, + "x" : 6, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 252, + "x" : 6, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 253, + "x" : 6, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 254, + "x" : 6, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 255, + "x" : 6, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 256, + "x" : 6, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 257, + "x" : 6, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 258, + "x" : 6, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 259, + "x" : 6, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 260, + "x" : 6, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 261, + "x" : 6, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 262, + "x" : 6, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 263, + "x" : 6, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 264, + "x" : 6, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 265, + "x" : 6, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 266, + "x" : 6, + "y" : 25, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 267, + "x" : 6, + "y" : 26, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 268, + "x" : 6, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 269, + "x" : 6, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 270, + "x" : 6, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 271, + "x" : 6, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 272, + "x" : 6, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 273, + "x" : 6, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 274, + "x" : 6, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 275, + "x" : 6, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 276, + "x" : 6, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 277, + "x" : 6, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 278, + "x" : 6, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 279, + "x" : 6, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 280, + "x" : 6, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 281, + "x" : 7, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 282, + "x" : 7, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 283, + "x" : 7, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 284, + "x" : 7, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 285, + "x" : 7, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 286, + "x" : 7, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 287, + "x" : 7, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 288, + "x" : 7, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 289, + "x" : 7, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 290, + "x" : 7, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 291, + "x" : 7, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 292, + "x" : 7, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 293, + "x" : 7, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 294, + "x" : 7, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 295, + "x" : 7, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 296, + "x" : 7, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 297, + "x" : 7, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 298, + "x" : 7, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 299, + "x" : 7, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 300, + "x" : 7, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 301, + "x" : 7, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 302, + "x" : 7, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 303, + "x" : 7, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 304, + "x" : 7, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 305, + "x" : 7, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 306, + "x" : 7, + "y" : 25, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 307, + "x" : 7, + "y" : 26, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 308, + "x" : 7, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 309, + "x" : 7, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 310, + "x" : 7, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 311, + "x" : 7, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 312, + "x" : 7, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 313, + "x" : 7, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 314, + "x" : 7, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 315, + "x" : 7, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 316, + "x" : 7, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 317, + "x" : 7, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 318, + "x" : 7, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 319, + "x" : 7, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 320, + "x" : 7, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 321, + "x" : 8, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 322, + "x" : 8, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 323, + "x" : 8, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 324, + "x" : 8, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 325, + "x" : 8, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 326, + "x" : 8, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 327, + "x" : 8, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 328, + "x" : 8, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 329, + "x" : 8, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 330, + "x" : 8, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 331, + "x" : 8, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 332, + "x" : 8, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 333, + "x" : 8, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 334, + "x" : 8, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 335, + "x" : 8, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 336, + "x" : 8, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 337, + "x" : 8, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 338, + "x" : 8, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 339, + "x" : 8, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 340, + "x" : 8, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 341, + "x" : 8, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 342, + "x" : 8, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 343, + "x" : 8, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 344, + "x" : 8, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 345, + "x" : 8, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 346, + "x" : 8, + "y" : 25, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 347, + "x" : 8, + "y" : 26, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 348, + "x" : 8, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 349, + "x" : 8, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 350, + "x" : 8, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 351, + "x" : 8, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 352, + "x" : 8, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 353, + "x" : 8, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 354, + "x" : 8, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 355, + "x" : 8, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 356, + "x" : 8, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 357, + "x" : 8, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 358, + "x" : 8, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 359, + "x" : 8, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 360, + "x" : 8, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 361, + "x" : 9, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 362, + "x" : 9, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 363, + "x" : 9, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 364, + "x" : 9, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 365, + "x" : 9, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 366, + "x" : 9, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 367, + "x" : 9, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 368, + "x" : 9, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 369, + "x" : 9, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 370, + "x" : 9, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 371, + "x" : 9, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 372, + "x" : 9, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 373, + "x" : 9, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 374, + "x" : 9, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 375, + "x" : 9, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 376, + "x" : 9, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 377, + "x" : 9, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 378, + "x" : 9, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 379, + "x" : 9, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 380, + "x" : 9, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 381, + "x" : 9, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 382, + "x" : 9, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 383, + "x" : 9, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 384, + "x" : 9, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 385, + "x" : 9, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 386, + "x" : 9, + "y" : 25, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 387, + "x" : 9, + "y" : 26, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 388, + "x" : 9, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 389, + "x" : 9, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 390, + "x" : 9, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 391, + "x" : 9, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 392, + "x" : 9, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 393, + "x" : 9, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 394, + "x" : 9, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 395, + "x" : 9, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 396, + "x" : 9, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 397, + "x" : 9, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 398, + "x" : 9, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 399, + "x" : 9, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 400, + "x" : 9, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 401, + "x" : 10, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 402, + "x" : 10, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 403, + "x" : 10, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 404, + "x" : 10, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 405, + "x" : 10, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 406, + "x" : 10, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 407, + "x" : 10, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 408, + "x" : 10, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 409, + "x" : 10, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 410, + "x" : 10, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 411, + "x" : 10, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 412, + "x" : 10, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 413, + "x" : 10, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 414, + "x" : 10, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 415, + "x" : 10, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 416, + "x" : 10, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 417, + "x" : 10, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 418, + "x" : 10, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 419, + "x" : 10, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 420, + "x" : 10, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 421, + "x" : 10, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 422, + "x" : 10, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 423, + "x" : 10, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 424, + "x" : 10, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 425, + "x" : 10, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 426, + "x" : 10, + "y" : 25, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 427, + "x" : 10, + "y" : 26, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 428, + "x" : 10, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 429, + "x" : 10, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 430, + "x" : 10, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 431, + "x" : 10, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 432, + "x" : 10, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 433, + "x" : 10, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 434, + "x" : 10, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 435, + "x" : 10, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 436, + "x" : 10, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 437, + "x" : 10, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 438, + "x" : 10, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 439, + "x" : 10, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 440, + "x" : 10, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 441, + "x" : 11, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 442, + "x" : 11, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 443, + "x" : 11, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 444, + "x" : 11, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 445, + "x" : 11, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 446, + "x" : 11, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 447, + "x" : 11, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 448, + "x" : 11, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 449, + "x" : 11, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 450, + "x" : 11, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 451, + "x" : 11, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 452, + "x" : 11, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 453, + "x" : 11, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 454, + "x" : 11, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 455, + "x" : 11, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 456, + "x" : 11, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 457, + "x" : 11, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 458, + "x" : 11, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 459, + "x" : 11, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 460, + "x" : 11, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 461, + "x" : 11, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 462, + "x" : 11, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 463, + "x" : 11, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 464, + "x" : 11, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 465, + "x" : 11, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 466, + "x" : 11, + "y" : 25, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 467, + "x" : 11, + "y" : 26, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 468, + "x" : 11, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 469, + "x" : 11, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 470, + "x" : 11, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 471, + "x" : 11, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 472, + "x" : 11, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 473, + "x" : 11, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 474, + "x" : 11, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 475, + "x" : 11, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 476, + "x" : 11, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 477, + "x" : 11, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 478, + "x" : 11, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 479, + "x" : 11, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 480, + "x" : 11, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 481, + "x" : 12, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 482, + "x" : 12, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 483, + "x" : 12, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 484, + "x" : 12, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 485, + "x" : 12, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 486, + "x" : 12, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 487, + "x" : 12, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 488, + "x" : 12, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 489, + "x" : 12, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 490, + "x" : 12, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 491, + "x" : 12, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 492, + "x" : 12, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 493, + "x" : 12, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 494, + "x" : 12, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 495, + "x" : 12, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 496, + "x" : 12, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 497, + "x" : 12, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 498, + "x" : 12, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 499, + "x" : 12, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 500, + "x" : 12, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 501, + "x" : 12, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 502, + "x" : 12, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 503, + "x" : 12, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 504, + "x" : 12, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 505, + "x" : 12, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 506, + "x" : 12, + "y" : 25, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 507, + "x" : 12, + "y" : 26, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 508, + "x" : 12, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 509, + "x" : 12, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 510, + "x" : 12, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 511, + "x" : 12, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 512, + "x" : 12, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 513, + "x" : 12, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 514, + "x" : 12, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 515, + "x" : 12, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 516, + "x" : 12, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 517, + "x" : 12, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 518, + "x" : 12, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 519, + "x" : 12, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 520, + "x" : 12, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 521, + "x" : 13, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 522, + "x" : 13, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 523, + "x" : 13, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 524, + "x" : 13, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 525, + "x" : 13, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 526, + "x" : 13, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 527, + "x" : 13, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 528, + "x" : 13, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 529, + "x" : 13, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 530, + "x" : 13, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 531, + "x" : 13, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 532, + "x" : 13, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 533, + "x" : 13, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 534, + "x" : 13, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 535, + "x" : 13, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 536, + "x" : 13, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 537, + "x" : 13, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 538, + "x" : 13, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 539, + "x" : 13, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 540, + "x" : 13, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 541, + "x" : 13, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 542, + "x" : 13, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 543, + "x" : 13, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 544, + "x" : 13, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 545, + "x" : 13, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 546, + "x" : 13, + "y" : 25, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 547, + "x" : 13, + "y" : 26, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 548, + "x" : 13, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 549, + "x" : 13, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 550, + "x" : 13, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 551, + "x" : 13, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 552, + "x" : 13, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 553, + "x" : 13, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 554, + "x" : 13, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 555, + "x" : 13, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 556, + "x" : 13, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 557, + "x" : 13, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 558, + "x" : 13, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 559, + "x" : 13, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 560, + "x" : 13, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 561, + "x" : 14, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 562, + "x" : 14, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 563, + "x" : 14, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 564, + "x" : 14, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 565, + "x" : 14, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 566, + "x" : 14, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 567, + "x" : 14, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 568, + "x" : 14, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 569, + "x" : 14, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 570, + "x" : 14, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 571, + "x" : 14, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 572, + "x" : 14, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 573, + "x" : 14, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 574, + "x" : 14, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 575, + "x" : 14, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 576, + "x" : 14, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 577, + "x" : 14, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 578, + "x" : 14, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 579, + "x" : 14, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 580, + "x" : 14, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 581, + "x" : 14, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 582, + "x" : 14, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 583, + "x" : 14, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 584, + "x" : 14, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 585, + "x" : 14, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 586, + "x" : 14, + "y" : 25, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 587, + "x" : 14, + "y" : 26, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 588, + "x" : 14, + "y" : 27, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 589, + "x" : 14, + "y" : 28, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 590, + "x" : 14, + "y" : 29, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 591, + "x" : 14, + "y" : 30, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 592, + "x" : 14, + "y" : 31, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 593, + "x" : 14, + "y" : 32, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 594, + "x" : 14, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 595, + "x" : 14, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 596, + "x" : 14, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 597, + "x" : 14, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 598, + "x" : 14, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 599, + "x" : 14, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 600, + "x" : 14, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 601, + "x" : 15, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 602, + "x" : 15, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 603, + "x" : 15, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 604, + "x" : 15, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 605, + "x" : 15, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 606, + "x" : 15, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 607, + "x" : 15, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 608, + "x" : 15, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 609, + "x" : 15, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 610, + "x" : 15, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 611, + "x" : 15, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 612, + "x" : 15, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 613, + "x" : 15, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 614, + "x" : 15, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 615, + "x" : 15, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 616, + "x" : 15, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 617, + "x" : 15, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 618, + "x" : 15, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 619, + "x" : 15, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 620, + "x" : 15, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 621, + "x" : 15, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 622, + "x" : 15, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 623, + "x" : 15, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 624, + "x" : 15, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 625, + "x" : 15, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 626, + "x" : 15, + "y" : 25, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 627, + "x" : 15, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 628, + "x" : 15, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 629, + "x" : 15, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 630, + "x" : 15, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 631, + "x" : 15, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 632, + "x" : 15, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 633, + "x" : 15, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 634, + "x" : 15, + "y" : 33, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 635, + "x" : 15, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 636, + "x" : 15, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 637, + "x" : 15, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 638, + "x" : 15, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 639, + "x" : 15, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 640, + "x" : 15, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 641, + "x" : 16, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 642, + "x" : 16, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 643, + "x" : 16, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 644, + "x" : 16, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 645, + "x" : 16, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 646, + "x" : 16, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 647, + "x" : 16, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 648, + "x" : 16, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 649, + "x" : 16, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 650, + "x" : 16, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 651, + "x" : 16, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 652, + "x" : 16, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 653, + "x" : 16, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 654, + "x" : 16, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 655, + "x" : 16, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 656, + "x" : 16, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 657, + "x" : 16, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 658, + "x" : 16, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 659, + "x" : 16, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 660, + "x" : 16, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 661, + "x" : 16, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 662, + "x" : 16, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 663, + "x" : 16, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 664, + "x" : 16, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 665, + "x" : 16, + "y" : 24, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 666, + "x" : 16, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 667, + "x" : 16, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 668, + "x" : 16, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 669, + "x" : 16, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 670, + "x" : 16, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 671, + "x" : 16, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 672, + "x" : 16, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 673, + "x" : 16, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 674, + "x" : 16, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 675, + "x" : 16, + "y" : 34, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 676, + "x" : 16, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 677, + "x" : 16, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 678, + "x" : 16, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 679, + "x" : 16, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 680, + "x" : 16, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 681, + "x" : 17, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 682, + "x" : 17, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 683, + "x" : 17, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 684, + "x" : 17, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 685, + "x" : 17, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 686, + "x" : 17, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 687, + "x" : 17, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 688, + "x" : 17, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 689, + "x" : 17, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 690, + "x" : 17, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 691, + "x" : 17, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 692, + "x" : 17, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 693, + "x" : 17, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 694, + "x" : 17, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 695, + "x" : 17, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 696, + "x" : 17, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 697, + "x" : 17, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 698, + "x" : 17, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 699, + "x" : 17, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 700, + "x" : 17, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 701, + "x" : 17, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 702, + "x" : 17, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 703, + "x" : 17, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 704, + "x" : 17, + "y" : 23, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 705, + "x" : 17, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 706, + "x" : 17, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 707, + "x" : 17, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 708, + "x" : 17, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 709, + "x" : 17, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 710, + "x" : 17, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 711, + "x" : 17, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 712, + "x" : 17, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 713, + "x" : 17, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 714, + "x" : 17, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 715, + "x" : 17, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 716, + "x" : 17, + "y" : 35, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 717, + "x" : 17, + "y" : 36, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 718, + "x" : 17, + "y" : 37, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 719, + "x" : 17, + "y" : 38, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 720, + "x" : 17, + "y" : 39, + "cityName" : "南京市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 721, + "x" : 18, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 722, + "x" : 18, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 723, + "x" : 18, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 724, + "x" : 18, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 725, + "x" : 18, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 726, + "x" : 18, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 727, + "x" : 18, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 728, + "x" : 18, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 729, + "x" : 18, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 730, + "x" : 18, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 731, + "x" : 18, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 732, + "x" : 18, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 733, + "x" : 18, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 734, + "x" : 18, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 735, + "x" : 18, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 736, + "x" : 18, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 737, + "x" : 18, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 738, + "x" : 18, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 739, + "x" : 18, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 740, + "x" : 18, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 741, + "x" : 18, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 742, + "x" : 18, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 743, + "x" : 18, + "y" : 22, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 744, + "x" : 18, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 745, + "x" : 18, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 746, + "x" : 18, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 747, + "x" : 18, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 748, + "x" : 18, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 749, + "x" : 18, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 750, + "x" : 18, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 751, + "x" : 18, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 752, + "x" : 18, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 753, + "x" : 18, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 754, + "x" : 18, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 755, + "x" : 18, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 756, + "x" : 18, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 757, + "x" : 18, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 758, + "x" : 18, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 759, + "x" : 18, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 760, + "x" : 18, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 761, + "x" : 19, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 762, + "x" : 19, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 763, + "x" : 19, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 764, + "x" : 19, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 765, + "x" : 19, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 766, + "x" : 19, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 767, + "x" : 19, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 768, + "x" : 19, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 769, + "x" : 19, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 770, + "x" : 19, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 771, + "x" : 19, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 772, + "x" : 19, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 773, + "x" : 19, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 774, + "x" : 19, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 775, + "x" : 19, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 776, + "x" : 19, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 777, + "x" : 19, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 778, + "x" : 19, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 779, + "x" : 19, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 780, + "x" : 19, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 781, + "x" : 19, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 782, + "x" : 19, + "y" : 21, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 783, + "x" : 19, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 784, + "x" : 19, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 785, + "x" : 19, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 786, + "x" : 19, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 787, + "x" : 19, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 788, + "x" : 19, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 789, + "x" : 19, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 790, + "x" : 19, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 791, + "x" : 19, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 792, + "x" : 19, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 793, + "x" : 19, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 794, + "x" : 19, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 795, + "x" : 19, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 796, + "x" : 19, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 797, + "x" : 19, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 798, + "x" : 19, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 799, + "x" : 19, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 800, + "x" : 19, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 801, + "x" : 20, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 802, + "x" : 20, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 803, + "x" : 20, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 804, + "x" : 20, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 805, + "x" : 20, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 806, + "x" : 20, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 807, + "x" : 20, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 808, + "x" : 20, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 809, + "x" : 20, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 810, + "x" : 20, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 811, + "x" : 20, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 812, + "x" : 20, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 813, + "x" : 20, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 814, + "x" : 20, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 815, + "x" : 20, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 816, + "x" : 20, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 817, + "x" : 20, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 818, + "x" : 20, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 819, + "x" : 20, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 820, + "x" : 20, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 821, + "x" : 20, + "y" : 20, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 822, + "x" : 20, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 823, + "x" : 20, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 824, + "x" : 20, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 825, + "x" : 20, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 826, + "x" : 20, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 827, + "x" : 20, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 828, + "x" : 20, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 829, + "x" : 20, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 830, + "x" : 20, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 831, + "x" : 20, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 832, + "x" : 20, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 833, + "x" : 20, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 834, + "x" : 20, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 835, + "x" : 20, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 836, + "x" : 20, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 837, + "x" : 20, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 838, + "x" : 20, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 839, + "x" : 20, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 840, + "x" : 20, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 841, + "x" : 21, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 842, + "x" : 21, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 843, + "x" : 21, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 844, + "x" : 21, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 845, + "x" : 21, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 846, + "x" : 21, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 847, + "x" : 21, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 848, + "x" : 21, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 849, + "x" : 21, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 850, + "x" : 21, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 851, + "x" : 21, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 852, + "x" : 21, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 853, + "x" : 21, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 854, + "x" : 21, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 855, + "x" : 21, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 856, + "x" : 21, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 857, + "x" : 21, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 858, + "x" : 21, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 859, + "x" : 21, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 860, + "x" : 21, + "y" : 19, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 861, + "x" : 21, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 862, + "x" : 21, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 863, + "x" : 21, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 864, + "x" : 21, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 865, + "x" : 21, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 866, + "x" : 21, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 867, + "x" : 21, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 868, + "x" : 21, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 869, + "x" : 21, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 870, + "x" : 21, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 871, + "x" : 21, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 872, + "x" : 21, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 873, + "x" : 21, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 874, + "x" : 21, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 875, + "x" : 21, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 876, + "x" : 21, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 877, + "x" : 21, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 878, + "x" : 21, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 879, + "x" : 21, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 880, + "x" : 21, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 881, + "x" : 22, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 882, + "x" : 22, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 883, + "x" : 22, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 884, + "x" : 22, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 885, + "x" : 22, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 886, + "x" : 22, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 887, + "x" : 22, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 888, + "x" : 22, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 889, + "x" : 22, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 890, + "x" : 22, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 891, + "x" : 22, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 892, + "x" : 22, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 893, + "x" : 22, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 894, + "x" : 22, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 895, + "x" : 22, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 896, + "x" : 22, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 897, + "x" : 22, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 898, + "x" : 22, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 899, + "x" : 22, + "y" : 18, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 900, + "x" : 22, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 901, + "x" : 22, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 902, + "x" : 22, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 903, + "x" : 22, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 904, + "x" : 22, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 905, + "x" : 22, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 906, + "x" : 22, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 907, + "x" : 22, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 908, + "x" : 22, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 909, + "x" : 22, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 910, + "x" : 22, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 911, + "x" : 22, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 912, + "x" : 22, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 913, + "x" : 22, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 914, + "x" : 22, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 915, + "x" : 22, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 916, + "x" : 22, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 917, + "x" : 22, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 918, + "x" : 22, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 919, + "x" : 22, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 920, + "x" : 22, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 921, + "x" : 23, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 922, + "x" : 23, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 923, + "x" : 23, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 924, + "x" : 23, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 925, + "x" : 23, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 926, + "x" : 23, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 927, + "x" : 23, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 928, + "x" : 23, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 929, + "x" : 23, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 930, + "x" : 23, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 931, + "x" : 23, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 932, + "x" : 23, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 933, + "x" : 23, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 934, + "x" : 23, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 935, + "x" : 23, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 936, + "x" : 23, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 937, + "x" : 23, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 938, + "x" : 23, + "y" : 17, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 939, + "x" : 23, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 940, + "x" : 23, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 941, + "x" : 23, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 942, + "x" : 23, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 943, + "x" : 23, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 944, + "x" : 23, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 945, + "x" : 23, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 946, + "x" : 23, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 947, + "x" : 23, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 948, + "x" : 23, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 949, + "x" : 23, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 950, + "x" : 23, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 951, + "x" : 23, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 952, + "x" : 23, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 953, + "x" : 23, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 954, + "x" : 23, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 955, + "x" : 23, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 956, + "x" : 23, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 957, + "x" : 23, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 958, + "x" : 23, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 959, + "x" : 23, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 960, + "x" : 23, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 961, + "x" : 24, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 962, + "x" : 24, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 963, + "x" : 24, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 964, + "x" : 24, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 965, + "x" : 24, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 966, + "x" : 24, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 967, + "x" : 24, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 968, + "x" : 24, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 969, + "x" : 24, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 970, + "x" : 24, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 971, + "x" : 24, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 972, + "x" : 24, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 973, + "x" : 24, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 974, + "x" : 24, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 975, + "x" : 24, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 976, + "x" : 24, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 977, + "x" : 24, + "y" : 16, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 978, + "x" : 24, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 979, + "x" : 24, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 980, + "x" : 24, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 981, + "x" : 24, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 982, + "x" : 24, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 983, + "x" : 24, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 984, + "x" : 24, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 985, + "x" : 24, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 986, + "x" : 24, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 987, + "x" : 24, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 988, + "x" : 24, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 989, + "x" : 24, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 990, + "x" : 24, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 991, + "x" : 24, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 992, + "x" : 24, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 993, + "x" : 24, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 994, + "x" : 24, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 995, + "x" : 24, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 996, + "x" : 24, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 997, + "x" : 24, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 998, + "x" : 24, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 999, + "x" : 24, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1000, + "x" : 24, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1001, + "x" : 25, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1002, + "x" : 25, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1003, + "x" : 25, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1004, + "x" : 25, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1005, + "x" : 25, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1006, + "x" : 25, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1007, + "x" : 25, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1008, + "x" : 25, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1009, + "x" : 25, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1010, + "x" : 25, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1011, + "x" : 25, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1012, + "x" : 25, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1013, + "x" : 25, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1014, + "x" : 25, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1015, + "x" : 25, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1016, + "x" : 25, + "y" : 15, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1017, + "x" : 25, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1018, + "x" : 25, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1019, + "x" : 25, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1020, + "x" : 25, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1021, + "x" : 25, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1022, + "x" : 25, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1023, + "x" : 25, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1024, + "x" : 25, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1025, + "x" : 25, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1026, + "x" : 25, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1027, + "x" : 25, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1028, + "x" : 25, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1029, + "x" : 25, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1030, + "x" : 25, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1031, + "x" : 25, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1032, + "x" : 25, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1033, + "x" : 25, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1034, + "x" : 25, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1035, + "x" : 25, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1036, + "x" : 25, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1037, + "x" : 25, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1038, + "x" : 25, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1039, + "x" : 25, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1040, + "x" : 25, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1041, + "x" : 26, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1042, + "x" : 26, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1043, + "x" : 26, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1044, + "x" : 26, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1045, + "x" : 26, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1046, + "x" : 26, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1047, + "x" : 26, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1048, + "x" : 26, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1049, + "x" : 26, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1050, + "x" : 26, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1051, + "x" : 26, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1052, + "x" : 26, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1053, + "x" : 26, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1054, + "x" : 26, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1055, + "x" : 26, + "y" : 14, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1056, + "x" : 26, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1057, + "x" : 26, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1058, + "x" : 26, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1059, + "x" : 26, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1060, + "x" : 26, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1061, + "x" : 26, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1062, + "x" : 26, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1063, + "x" : 26, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1064, + "x" : 26, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1065, + "x" : 26, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1066, + "x" : 26, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1067, + "x" : 26, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1068, + "x" : 26, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1069, + "x" : 26, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1070, + "x" : 26, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1071, + "x" : 26, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1072, + "x" : 26, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1073, + "x" : 26, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1074, + "x" : 26, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1075, + "x" : 26, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1076, + "x" : 26, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1077, + "x" : 26, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1078, + "x" : 26, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1079, + "x" : 26, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1080, + "x" : 26, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1081, + "x" : 27, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1082, + "x" : 27, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1083, + "x" : 27, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1084, + "x" : 27, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1085, + "x" : 27, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1086, + "x" : 27, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1087, + "x" : 27, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1088, + "x" : 27, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1089, + "x" : 27, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1090, + "x" : 27, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1091, + "x" : 27, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1092, + "x" : 27, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1093, + "x" : 27, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1094, + "x" : 27, + "y" : 13, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1095, + "x" : 27, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1096, + "x" : 27, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1097, + "x" : 27, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1098, + "x" : 27, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1099, + "x" : 27, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1100, + "x" : 27, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1101, + "x" : 27, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1102, + "x" : 27, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1103, + "x" : 27, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1104, + "x" : 27, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1105, + "x" : 27, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1106, + "x" : 27, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1107, + "x" : 27, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1108, + "x" : 27, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1109, + "x" : 27, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1110, + "x" : 27, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1111, + "x" : 27, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1112, + "x" : 27, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1113, + "x" : 27, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1114, + "x" : 27, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1115, + "x" : 27, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1116, + "x" : 27, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1117, + "x" : 27, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1118, + "x" : 27, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1119, + "x" : 27, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1120, + "x" : 27, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1121, + "x" : 28, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1122, + "x" : 28, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1123, + "x" : 28, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1124, + "x" : 28, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1125, + "x" : 28, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1126, + "x" : 28, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1127, + "x" : 28, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1128, + "x" : 28, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1129, + "x" : 28, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1130, + "x" : 28, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1131, + "x" : 28, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1132, + "x" : 28, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1133, + "x" : 28, + "y" : 12, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1134, + "x" : 28, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1135, + "x" : 28, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1136, + "x" : 28, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1137, + "x" : 28, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1138, + "x" : 28, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1139, + "x" : 28, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1140, + "x" : 28, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1141, + "x" : 28, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1142, + "x" : 28, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1143, + "x" : 28, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1144, + "x" : 28, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1145, + "x" : 28, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1146, + "x" : 28, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1147, + "x" : 28, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1148, + "x" : 28, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1149, + "x" : 28, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1150, + "x" : 28, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1151, + "x" : 28, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1152, + "x" : 28, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1153, + "x" : 28, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1154, + "x" : 28, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1155, + "x" : 28, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1156, + "x" : 28, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1157, + "x" : 28, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1158, + "x" : 28, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1159, + "x" : 28, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1160, + "x" : 28, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1161, + "x" : 29, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1162, + "x" : 29, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1163, + "x" : 29, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1164, + "x" : 29, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1165, + "x" : 29, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1166, + "x" : 29, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1167, + "x" : 29, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1168, + "x" : 29, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1169, + "x" : 29, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1170, + "x" : 29, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1171, + "x" : 29, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1172, + "x" : 29, + "y" : 11, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1173, + "x" : 29, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1174, + "x" : 29, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1175, + "x" : 29, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1176, + "x" : 29, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1177, + "x" : 29, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1178, + "x" : 29, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1179, + "x" : 29, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1180, + "x" : 29, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1181, + "x" : 29, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1182, + "x" : 29, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1183, + "x" : 29, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1184, + "x" : 29, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1185, + "x" : 29, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1186, + "x" : 29, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1187, + "x" : 29, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1188, + "x" : 29, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1189, + "x" : 29, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1190, + "x" : 29, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1191, + "x" : 29, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1192, + "x" : 29, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1193, + "x" : 29, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1194, + "x" : 29, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1195, + "x" : 29, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1196, + "x" : 29, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1197, + "x" : 29, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1198, + "x" : 29, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1199, + "x" : 29, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1200, + "x" : 29, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1201, + "x" : 30, + "y" : 0, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1202, + "x" : 30, + "y" : 1, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1203, + "x" : 30, + "y" : 2, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1204, + "x" : 30, + "y" : 3, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1205, + "x" : 30, + "y" : 4, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1206, + "x" : 30, + "y" : 5, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1207, + "x" : 30, + "y" : 6, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1208, + "x" : 30, + "y" : 7, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1209, + "x" : 30, + "y" : 8, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1210, + "x" : 30, + "y" : 9, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1211, + "x" : 30, + "y" : 10, + "cityName" : "常州市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1212, + "x" : 30, + "y" : 11, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1213, + "x" : 30, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1214, + "x" : 30, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1215, + "x" : 30, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1216, + "x" : 30, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1217, + "x" : 30, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1218, + "x" : 30, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1219, + "x" : 30, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1220, + "x" : 30, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1221, + "x" : 30, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1222, + "x" : 30, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1223, + "x" : 30, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1224, + "x" : 30, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1225, + "x" : 30, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1226, + "x" : 30, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1227, + "x" : 30, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1228, + "x" : 30, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1229, + "x" : 30, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1230, + "x" : 30, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1231, + "x" : 30, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1232, + "x" : 30, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1233, + "x" : 30, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1234, + "x" : 30, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1235, + "x" : 30, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1236, + "x" : 30, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1237, + "x" : 30, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1238, + "x" : 30, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1239, + "x" : 30, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1240, + "x" : 30, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1241, + "x" : 31, + "y" : 0, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1242, + "x" : 31, + "y" : 1, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1243, + "x" : 31, + "y" : 2, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1244, + "x" : 31, + "y" : 3, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1245, + "x" : 31, + "y" : 4, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1246, + "x" : 31, + "y" : 5, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1247, + "x" : 31, + "y" : 6, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1248, + "x" : 31, + "y" : 7, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1249, + "x" : 31, + "y" : 8, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1250, + "x" : 31, + "y" : 9, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1251, + "x" : 31, + "y" : 10, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1252, + "x" : 31, + "y" : 11, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1253, + "x" : 31, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1254, + "x" : 31, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1255, + "x" : 31, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1256, + "x" : 31, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1257, + "x" : 31, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1258, + "x" : 31, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1259, + "x" : 31, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1260, + "x" : 31, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1261, + "x" : 31, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1262, + "x" : 31, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1263, + "x" : 31, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1264, + "x" : 31, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1265, + "x" : 31, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1266, + "x" : 31, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1267, + "x" : 31, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1268, + "x" : 31, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1269, + "x" : 31, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1270, + "x" : 31, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1271, + "x" : 31, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1272, + "x" : 31, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1273, + "x" : 31, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1274, + "x" : 31, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1275, + "x" : 31, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1276, + "x" : 31, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1277, + "x" : 31, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1278, + "x" : 31, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1279, + "x" : 31, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1280, + "x" : 31, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1281, + "x" : 32, + "y" : 0, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1282, + "x" : 32, + "y" : 1, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1283, + "x" : 32, + "y" : 2, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1284, + "x" : 32, + "y" : 3, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1285, + "x" : 32, + "y" : 4, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1286, + "x" : 32, + "y" : 5, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1287, + "x" : 32, + "y" : 6, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1288, + "x" : 32, + "y" : 7, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1289, + "x" : 32, + "y" : 8, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1290, + "x" : 32, + "y" : 9, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1291, + "x" : 32, + "y" : 10, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1292, + "x" : 32, + "y" : 11, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1293, + "x" : 32, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1294, + "x" : 32, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1295, + "x" : 32, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1296, + "x" : 32, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1297, + "x" : 32, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1298, + "x" : 32, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1299, + "x" : 32, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1300, + "x" : 32, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1301, + "x" : 32, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1302, + "x" : 32, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1303, + "x" : 32, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1304, + "x" : 32, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1305, + "x" : 32, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1306, + "x" : 32, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1307, + "x" : 32, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1308, + "x" : 32, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1309, + "x" : 32, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1310, + "x" : 32, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1311, + "x" : 32, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1312, + "x" : 32, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1313, + "x" : 32, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1314, + "x" : 32, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1315, + "x" : 32, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1316, + "x" : 32, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1317, + "x" : 32, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1318, + "x" : 32, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1319, + "x" : 32, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1320, + "x" : 32, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1321, + "x" : 33, + "y" : 0, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1322, + "x" : 33, + "y" : 1, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1323, + "x" : 33, + "y" : 2, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1324, + "x" : 33, + "y" : 3, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1325, + "x" : 33, + "y" : 4, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1326, + "x" : 33, + "y" : 5, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1327, + "x" : 33, + "y" : 6, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1328, + "x" : 33, + "y" : 7, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1329, + "x" : 33, + "y" : 8, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1330, + "x" : 33, + "y" : 9, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1331, + "x" : 33, + "y" : 10, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1332, + "x" : 33, + "y" : 11, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1333, + "x" : 33, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1334, + "x" : 33, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1335, + "x" : 33, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1336, + "x" : 33, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1337, + "x" : 33, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1338, + "x" : 33, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1339, + "x" : 33, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1340, + "x" : 33, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1341, + "x" : 33, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1342, + "x" : 33, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1343, + "x" : 33, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1344, + "x" : 33, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1345, + "x" : 33, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1346, + "x" : 33, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1347, + "x" : 33, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1348, + "x" : 33, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1349, + "x" : 33, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1350, + "x" : 33, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1351, + "x" : 33, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1352, + "x" : 33, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1353, + "x" : 33, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1354, + "x" : 33, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1355, + "x" : 33, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1356, + "x" : 33, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1357, + "x" : 33, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1358, + "x" : 33, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1359, + "x" : 33, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1360, + "x" : 33, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1361, + "x" : 34, + "y" : 0, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1362, + "x" : 34, + "y" : 1, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1363, + "x" : 34, + "y" : 2, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1364, + "x" : 34, + "y" : 3, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1365, + "x" : 34, + "y" : 4, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1366, + "x" : 34, + "y" : 5, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1367, + "x" : 34, + "y" : 6, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1368, + "x" : 34, + "y" : 7, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1369, + "x" : 34, + "y" : 8, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1370, + "x" : 34, + "y" : 9, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1371, + "x" : 34, + "y" : 10, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1372, + "x" : 34, + "y" : 11, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1373, + "x" : 34, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1374, + "x" : 34, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1375, + "x" : 34, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1376, + "x" : 34, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1377, + "x" : 34, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1378, + "x" : 34, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1379, + "x" : 34, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1380, + "x" : 34, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1381, + "x" : 34, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1382, + "x" : 34, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1383, + "x" : 34, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1384, + "x" : 34, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1385, + "x" : 34, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1386, + "x" : 34, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1387, + "x" : 34, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1388, + "x" : 34, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1389, + "x" : 34, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1390, + "x" : 34, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1391, + "x" : 34, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1392, + "x" : 34, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1393, + "x" : 34, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1394, + "x" : 34, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1395, + "x" : 34, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1396, + "x" : 34, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1397, + "x" : 34, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1398, + "x" : 34, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1399, + "x" : 34, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1400, + "x" : 34, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1401, + "x" : 35, + "y" : 0, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1402, + "x" : 35, + "y" : 1, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1403, + "x" : 35, + "y" : 2, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1404, + "x" : 35, + "y" : 3, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1405, + "x" : 35, + "y" : 4, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1406, + "x" : 35, + "y" : 5, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1407, + "x" : 35, + "y" : 6, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1408, + "x" : 35, + "y" : 7, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1409, + "x" : 35, + "y" : 8, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1410, + "x" : 35, + "y" : 9, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1411, + "x" : 35, + "y" : 10, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1412, + "x" : 35, + "y" : 11, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1413, + "x" : 35, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1414, + "x" : 35, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1415, + "x" : 35, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1416, + "x" : 35, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1417, + "x" : 35, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1418, + "x" : 35, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1419, + "x" : 35, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1420, + "x" : 35, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1421, + "x" : 35, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1422, + "x" : 35, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1423, + "x" : 35, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1424, + "x" : 35, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1425, + "x" : 35, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1426, + "x" : 35, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1427, + "x" : 35, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1428, + "x" : 35, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1429, + "x" : 35, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1430, + "x" : 35, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1431, + "x" : 35, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1432, + "x" : 35, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1433, + "x" : 35, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1434, + "x" : 35, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1435, + "x" : 35, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1436, + "x" : 35, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1437, + "x" : 35, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1438, + "x" : 35, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1439, + "x" : 35, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1440, + "x" : 35, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1441, + "x" : 36, + "y" : 0, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1442, + "x" : 36, + "y" : 1, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1443, + "x" : 36, + "y" : 2, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1444, + "x" : 36, + "y" : 3, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1445, + "x" : 36, + "y" : 4, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1446, + "x" : 36, + "y" : 5, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1447, + "x" : 36, + "y" : 6, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1448, + "x" : 36, + "y" : 7, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1449, + "x" : 36, + "y" : 8, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1450, + "x" : 36, + "y" : 9, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1451, + "x" : 36, + "y" : 10, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1452, + "x" : 36, + "y" : 11, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1453, + "x" : 36, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1454, + "x" : 36, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1455, + "x" : 36, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1456, + "x" : 36, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1457, + "x" : 36, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1458, + "x" : 36, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1459, + "x" : 36, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1460, + "x" : 36, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1461, + "x" : 36, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1462, + "x" : 36, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1463, + "x" : 36, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1464, + "x" : 36, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1465, + "x" : 36, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1466, + "x" : 36, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1467, + "x" : 36, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1468, + "x" : 36, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1469, + "x" : 36, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1470, + "x" : 36, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1471, + "x" : 36, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1472, + "x" : 36, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1473, + "x" : 36, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1474, + "x" : 36, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1475, + "x" : 36, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1476, + "x" : 36, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1477, + "x" : 36, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1478, + "x" : 36, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1479, + "x" : 36, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1480, + "x" : 36, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1481, + "x" : 37, + "y" : 0, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1482, + "x" : 37, + "y" : 1, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1483, + "x" : 37, + "y" : 2, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1484, + "x" : 37, + "y" : 3, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1485, + "x" : 37, + "y" : 4, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1486, + "x" : 37, + "y" : 5, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1487, + "x" : 37, + "y" : 6, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1488, + "x" : 37, + "y" : 7, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1489, + "x" : 37, + "y" : 8, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1490, + "x" : 37, + "y" : 9, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1491, + "x" : 37, + "y" : 10, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1492, + "x" : 37, + "y" : 11, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1493, + "x" : 37, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1494, + "x" : 37, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1495, + "x" : 37, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1496, + "x" : 37, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1497, + "x" : 37, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1498, + "x" : 37, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1499, + "x" : 37, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1500, + "x" : 37, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1501, + "x" : 37, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1502, + "x" : 37, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1503, + "x" : 37, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1504, + "x" : 37, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1505, + "x" : 37, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1506, + "x" : 37, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1507, + "x" : 37, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1508, + "x" : 37, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1509, + "x" : 37, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1510, + "x" : 37, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1511, + "x" : 37, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1512, + "x" : 37, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1513, + "x" : 37, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1514, + "x" : 37, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1515, + "x" : 37, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1516, + "x" : 37, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1517, + "x" : 37, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1518, + "x" : 37, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1519, + "x" : 37, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1520, + "x" : 37, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1521, + "x" : 38, + "y" : 0, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1522, + "x" : 38, + "y" : 1, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1523, + "x" : 38, + "y" : 2, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1524, + "x" : 38, + "y" : 3, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1525, + "x" : 38, + "y" : 4, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1526, + "x" : 38, + "y" : 5, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1527, + "x" : 38, + "y" : 6, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1528, + "x" : 38, + "y" : 7, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1529, + "x" : 38, + "y" : 8, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1530, + "x" : 38, + "y" : 9, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1531, + "x" : 38, + "y" : 10, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1532, + "x" : 38, + "y" : 11, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1533, + "x" : 38, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1534, + "x" : 38, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1535, + "x" : 38, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1536, + "x" : 38, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1537, + "x" : 38, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1538, + "x" : 38, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1539, + "x" : 38, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1540, + "x" : 38, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1541, + "x" : 38, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1542, + "x" : 38, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1543, + "x" : 38, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1544, + "x" : 38, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1545, + "x" : 38, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1546, + "x" : 38, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1547, + "x" : 38, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1548, + "x" : 38, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1549, + "x" : 38, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1550, + "x" : 38, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1551, + "x" : 38, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : true +}, { + "id" : 1552, + "x" : 38, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1553, + "x" : 38, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1554, + "x" : 38, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1555, + "x" : 38, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1556, + "x" : 38, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1557, + "x" : 38, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1558, + "x" : 38, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1559, + "x" : 38, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1560, + "x" : 38, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1561, + "x" : 39, + "y" : 0, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1562, + "x" : 39, + "y" : 1, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1563, + "x" : 39, + "y" : 2, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1564, + "x" : 39, + "y" : 3, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1565, + "x" : 39, + "y" : 4, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1566, + "x" : 39, + "y" : 5, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1567, + "x" : 39, + "y" : 6, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1568, + "x" : 39, + "y" : 7, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1569, + "x" : 39, + "y" : 8, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1570, + "x" : 39, + "y" : 9, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1571, + "x" : 39, + "y" : 10, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1572, + "x" : 39, + "y" : 11, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1573, + "x" : 39, + "y" : 12, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1574, + "x" : 39, + "y" : 13, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1575, + "x" : 39, + "y" : 14, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1576, + "x" : 39, + "y" : 15, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1577, + "x" : 39, + "y" : 16, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1578, + "x" : 39, + "y" : 17, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1579, + "x" : 39, + "y" : 18, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1580, + "x" : 39, + "y" : 19, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1581, + "x" : 39, + "y" : 20, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1582, + "x" : 39, + "y" : 21, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1583, + "x" : 39, + "y" : 22, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1584, + "x" : 39, + "y" : 23, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1585, + "x" : 39, + "y" : 24, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1586, + "x" : 39, + "y" : 25, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1587, + "x" : 39, + "y" : 26, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1588, + "x" : 39, + "y" : 27, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1589, + "x" : 39, + "y" : 28, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1590, + "x" : 39, + "y" : 29, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1591, + "x" : 39, + "y" : 30, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1592, + "x" : 39, + "y" : 31, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1593, + "x" : 39, + "y" : 32, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1594, + "x" : 39, + "y" : 33, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1595, + "x" : 39, + "y" : 34, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1596, + "x" : 39, + "y" : 35, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1597, + "x" : 39, + "y" : 36, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1598, + "x" : 39, + "y" : 37, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1599, + "x" : 39, + "y" : 38, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false +}, { + "id" : 1600, + "x" : 39, + "y" : 39, + "cityName" : "无锡市", + "terrainType" : null, + "obstacle" : false } ] \ No newline at end of file diff --git a/ems-backend/json-db/operation_logs.json b/ems-backend/json-db/operation_logs.json index 1ae2681..fa244ce 100644 --- a/ems-backend/json-db/operation_logs.json +++ b/ems-backend/json-db/operation_logs.json @@ -1,811 +1,811 @@ -[ { - "id" : 1, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "1", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:34:09.7292392" -}, { - "id" : 2, - "user" : { - "id" : 6, - "name" : "公众监督员", - "phone" : "13700137004", - "email" : "public.supervisor@aizhangz.top", - "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.7646754", - "updatedAt" : "2025-06-24T23:33:58.7646754", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录失败", - "targetId" : "6", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:34:29.3306051" -}, { - "id" : 3, - "user" : { - "id" : 6, - "name" : "公众监督员", - "phone" : "13700137004", - "email" : "public.supervisor@aizhangz.top", - "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.7646754", - "updatedAt" : "2025-06-24T23:33:58.7646754", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "6", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:34:37.314075" -}, { - "id" : 4, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "1", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:35:10.1312074" -}, { - "id" : 5, - "user" : { - "id" : 58, - "name" : "尹子轩", - "phone" : "14132111111", - "email" : "13@aizhangz.top", - "password" : "$2a$10$a1c.05bruVOjqBNqg1ZnBurewQpcQaQjj3AiSk/rZuIzfgdWWjTE2", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:37:55.5868193", - "updatedAt" : "2025-06-24T23:37:55.5868193", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "CREATE", - "description" : "新用户注册成功: 13@aizhangz.top", - "targetId" : "58", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:37:55.6624432" -}, { - "id" : 6, - "user" : { - "id" : 58, - "name" : "尹子轩", - "phone" : "14132111111", - "email" : "13@aizhangz.top", - "password" : "$2a$10$a1c.05bruVOjqBNqg1ZnBurewQpcQaQjj3AiSk/rZuIzfgdWWjTE2", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:37:55.5868193", - "updatedAt" : "2025-06-24T23:37:55.5868193", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "58", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:38:07.7292368" -}, { - "id" : 7, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "1", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:39:13.4246305" -}, { - "id" : 8, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "APPROVE_FEEDBACK", - "description" : "反馈已批准,待分配。备注: 管理员已批准该反馈,准备分配任务。", - "targetId" : "4", - "targetType" : "Feedback", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:39:54.0991964" -}, { - "id" : 9, - "user" : { - "id" : 9, - "name" : "网格员3", - "phone" : "14700000002", - "email" : "worker3@example.com", - "password" : "$2a$10$DozEFhdm56LYCGAj/Op60OWgp7pN22R9vB6HvDfJJUsEV/bfThhIq", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 2, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.0083253", - "updatedAt" : "2025-06-24T23:33:59.0083253", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "9", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:40:02.5480048" -}, { - "id" : 10, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "1", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:40:21.0490939" -}, { - "id" : 11, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "ASSIGN_TASK", - "description" : "任务(ID: 4) 已分配给网格员 特定网格员 (ID: 57)", - "targetId" : "4", - "targetType" : "Task", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:40:35.4594763" -}, { - "id" : 12, - "user" : { - "id" : 9, - "name" : "网格员3", - "phone" : "14700000002", - "email" : "worker3@example.com", - "password" : "$2a$10$DozEFhdm56LYCGAj/Op60OWgp7pN22R9vB6HvDfJJUsEV/bfThhIq", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 2, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.0083253", - "updatedAt" : "2025-06-24T23:33:59.0083253", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "9", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:40:39.6578426" -}, { - "id" : 13, - "user" : { - "id" : 57, - "name" : "特定网格员", - "phone" : "14700009999", - "email" : "worker@aizhangz.top", - "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 10, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2703065", - "updatedAt" : "2025-06-24T23:34:02.2703065", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "57", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:41:38.1601197" -}, { - "id" : 14, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "1", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:42:31.957322" -}, { - "id" : 15, - "user" : { - "id" : 58, - "name" : "尹子轩", - "phone" : "14132111111", - "email" : "13@aizhangz.top", - "password" : "$2a$10$sU7TJ05g7rRHB4m2x5rOOOMviWbIr7X4Eb2vn6U7Xto5Azo/NT.v.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "INACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:37:55.5868193", - "updatedAt" : "2025-06-24T23:37:55.5868193", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "UPDATE", - "description" : "用户密码重置成功: 13@aizhangz.top", - "targetId" : "58", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:43:25.1639234" -}, { - "id" : 16, - "user" : { - "id" : 58, - "name" : "尹子轩", - "phone" : "14132111111", - "email" : "13@aizhangz.top", - "password" : "$2a$10$sU7TJ05g7rRHB4m2x5rOOOMviWbIr7X4Eb2vn6U7Xto5Azo/NT.v.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "INACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:37:55.5868193", - "updatedAt" : "2025-06-24T23:37:55.5868193", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录失败", - "targetId" : "58", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:43:39.6007499" -}, { - "id" : 17, - "user" : { - "id" : 58, - "name" : "尹子轩", - "phone" : "14132111111", - "email" : "13@aizhangz.top", - "password" : "$2a$10$sU7TJ05g7rRHB4m2x5rOOOMviWbIr7X4Eb2vn6U7Xto5Azo/NT.v.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "INACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:37:55.5868193", - "updatedAt" : "2025-06-24T23:37:55.5868193", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录失败", - "targetId" : "58", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:43:46.6059216" -}, { - "id" : 18, - "user" : { - "id" : 58, - "name" : "尹子轩", - "phone" : "14132111111", - "email" : "13@aizhangz.top", - "password" : "$2a$10$sU7TJ05g7rRHB4m2x5rOOOMviWbIr7X4Eb2vn6U7Xto5Azo/NT.v.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "INACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:37:55.5868193", - "updatedAt" : "2025-06-24T23:37:55.5868193", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录失败", - "targetId" : "58", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:43:59.543322" -}, { - "id" : 19, - "user" : { - "id" : 57, - "name" : "特定网格员", - "phone" : "14700009999", - "email" : "worker@aizhangz.top", - "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 10, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2703065", - "updatedAt" : "2025-06-24T23:34:02.2703065", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "57", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-24T23:46:15.7644452" -}, { - "id" : 20, - "user" : { - "id" : 57, - "name" : "特定网格员", - "phone" : "14700009999", - "email" : "worker@aizhangz.top", - "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 10, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2703065", - "updatedAt" : "2025-06-24T23:34:02.2703065", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "57", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-06-27T23:57:30.9731674" -}, { - "id" : 21, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "1", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-10-25T19:09:54.3566996" -}, { - "id" : 22, - "user" : { - "id" : 9, - "name" : "网格员3", - "phone" : "14700000002", - "email" : "worker3@example.com", - "password" : "$2a$10$DozEFhdm56LYCGAj/Op60OWgp7pN22R9vB6HvDfJJUsEV/bfThhIq", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 2, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.0083253", - "updatedAt" : "2025-06-24T23:33:59.0083253", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "9", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-10-25T19:11:31.9871387" -}, { - "id" : 23, - "user" : { - "id" : 2, - "name" : "决策者", - "phone" : "13900139000", - "email" : "decision@example.com", - "password" : "$2a$10$RyY5edaCBC2FdiXsdkz5Ae/bHs4FOQDMMg4U/1lRbNyGoXqDmKWIa", - "gender" : null, - "role" : "DECISION_MAKER", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.4177379", - "updatedAt" : "2025-06-24T23:33:58.4177379", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "2", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-10-25T19:11:44.6822258" -}, { - "id" : 24, - "user" : { - "id" : 57, - "name" : "特定网格员", - "phone" : "14700009999", - "email" : "worker@aizhangz.top", - "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 10, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2703065", - "updatedAt" : "2025-06-24T23:34:02.2703065", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录失败", - "targetId" : "57", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-10-25T19:12:28.9176313" -}, { - "id" : 25, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "1", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-10-25T19:13:39.6709067" -}, { - "id" : 26, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "1", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-10-25T19:14:22.7981559" -}, { - "id" : 27, - "user" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "operationType" : "LOGIN", - "description" : "用户登录成功", - "targetId" : "1", - "targetType" : "UserAccount", - "ipAddress" : "0:0:0:0:0:0:0:1", - "createdAt" : "2025-10-25T19:14:34.822513" +[ { + "id" : 1, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "1", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:34:09.7292392" +}, { + "id" : 2, + "user" : { + "id" : 6, + "name" : "公众监督员", + "phone" : "13700137004", + "email" : "public.supervisor@aizhangz.top", + "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.7646754", + "updatedAt" : "2025-06-24T23:33:58.7646754", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录失败", + "targetId" : "6", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:34:29.3306051" +}, { + "id" : 3, + "user" : { + "id" : 6, + "name" : "公众监督员", + "phone" : "13700137004", + "email" : "public.supervisor@aizhangz.top", + "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.7646754", + "updatedAt" : "2025-06-24T23:33:58.7646754", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "6", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:34:37.314075" +}, { + "id" : 4, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "1", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:35:10.1312074" +}, { + "id" : 5, + "user" : { + "id" : 58, + "name" : "尹子轩", + "phone" : "14132111111", + "email" : "13@aizhangz.top", + "password" : "$2a$10$a1c.05bruVOjqBNqg1ZnBurewQpcQaQjj3AiSk/rZuIzfgdWWjTE2", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:37:55.5868193", + "updatedAt" : "2025-06-24T23:37:55.5868193", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "CREATE", + "description" : "新用户注册成功: 13@aizhangz.top", + "targetId" : "58", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:37:55.6624432" +}, { + "id" : 6, + "user" : { + "id" : 58, + "name" : "尹子轩", + "phone" : "14132111111", + "email" : "13@aizhangz.top", + "password" : "$2a$10$a1c.05bruVOjqBNqg1ZnBurewQpcQaQjj3AiSk/rZuIzfgdWWjTE2", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:37:55.5868193", + "updatedAt" : "2025-06-24T23:37:55.5868193", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "58", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:38:07.7292368" +}, { + "id" : 7, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "1", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:39:13.4246305" +}, { + "id" : 8, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "APPROVE_FEEDBACK", + "description" : "反馈已批准,待分配。备注: 管理员已批准该反馈,准备分配任务。", + "targetId" : "4", + "targetType" : "Feedback", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:39:54.0991964" +}, { + "id" : 9, + "user" : { + "id" : 9, + "name" : "网格员3", + "phone" : "14700000002", + "email" : "worker3@example.com", + "password" : "$2a$10$DozEFhdm56LYCGAj/Op60OWgp7pN22R9vB6HvDfJJUsEV/bfThhIq", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 2, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.0083253", + "updatedAt" : "2025-06-24T23:33:59.0083253", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "9", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:40:02.5480048" +}, { + "id" : 10, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "1", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:40:21.0490939" +}, { + "id" : 11, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "ASSIGN_TASK", + "description" : "任务(ID: 4) 已分配给网格员 特定网格员 (ID: 57)", + "targetId" : "4", + "targetType" : "Task", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:40:35.4594763" +}, { + "id" : 12, + "user" : { + "id" : 9, + "name" : "网格员3", + "phone" : "14700000002", + "email" : "worker3@example.com", + "password" : "$2a$10$DozEFhdm56LYCGAj/Op60OWgp7pN22R9vB6HvDfJJUsEV/bfThhIq", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 2, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.0083253", + "updatedAt" : "2025-06-24T23:33:59.0083253", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "9", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:40:39.6578426" +}, { + "id" : 13, + "user" : { + "id" : 57, + "name" : "特定网格员", + "phone" : "14700009999", + "email" : "worker@aizhangz.top", + "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 10, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2703065", + "updatedAt" : "2025-06-24T23:34:02.2703065", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "57", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:41:38.1601197" +}, { + "id" : 14, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "1", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:42:31.957322" +}, { + "id" : 15, + "user" : { + "id" : 58, + "name" : "尹子轩", + "phone" : "14132111111", + "email" : "13@aizhangz.top", + "password" : "$2a$10$sU7TJ05g7rRHB4m2x5rOOOMviWbIr7X4Eb2vn6U7Xto5Azo/NT.v.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "INACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:37:55.5868193", + "updatedAt" : "2025-06-24T23:37:55.5868193", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "UPDATE", + "description" : "用户密码重置成功: 13@aizhangz.top", + "targetId" : "58", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:43:25.1639234" +}, { + "id" : 16, + "user" : { + "id" : 58, + "name" : "尹子轩", + "phone" : "14132111111", + "email" : "13@aizhangz.top", + "password" : "$2a$10$sU7TJ05g7rRHB4m2x5rOOOMviWbIr7X4Eb2vn6U7Xto5Azo/NT.v.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "INACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:37:55.5868193", + "updatedAt" : "2025-06-24T23:37:55.5868193", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录失败", + "targetId" : "58", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:43:39.6007499" +}, { + "id" : 17, + "user" : { + "id" : 58, + "name" : "尹子轩", + "phone" : "14132111111", + "email" : "13@aizhangz.top", + "password" : "$2a$10$sU7TJ05g7rRHB4m2x5rOOOMviWbIr7X4Eb2vn6U7Xto5Azo/NT.v.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "INACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:37:55.5868193", + "updatedAt" : "2025-06-24T23:37:55.5868193", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录失败", + "targetId" : "58", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:43:46.6059216" +}, { + "id" : 18, + "user" : { + "id" : 58, + "name" : "尹子轩", + "phone" : "14132111111", + "email" : "13@aizhangz.top", + "password" : "$2a$10$sU7TJ05g7rRHB4m2x5rOOOMviWbIr7X4Eb2vn6U7Xto5Azo/NT.v.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "INACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:37:55.5868193", + "updatedAt" : "2025-06-24T23:37:55.5868193", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录失败", + "targetId" : "58", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:43:59.543322" +}, { + "id" : 19, + "user" : { + "id" : 57, + "name" : "特定网格员", + "phone" : "14700009999", + "email" : "worker@aizhangz.top", + "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 10, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2703065", + "updatedAt" : "2025-06-24T23:34:02.2703065", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "57", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-24T23:46:15.7644452" +}, { + "id" : 20, + "user" : { + "id" : 57, + "name" : "特定网格员", + "phone" : "14700009999", + "email" : "worker@aizhangz.top", + "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 10, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2703065", + "updatedAt" : "2025-06-24T23:34:02.2703065", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "57", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-06-27T23:57:30.9731674" +}, { + "id" : 21, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "1", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-10-25T19:09:54.3566996" +}, { + "id" : 22, + "user" : { + "id" : 9, + "name" : "网格员3", + "phone" : "14700000002", + "email" : "worker3@example.com", + "password" : "$2a$10$DozEFhdm56LYCGAj/Op60OWgp7pN22R9vB6HvDfJJUsEV/bfThhIq", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 2, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.0083253", + "updatedAt" : "2025-06-24T23:33:59.0083253", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "9", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-10-25T19:11:31.9871387" +}, { + "id" : 23, + "user" : { + "id" : 2, + "name" : "决策者", + "phone" : "13900139000", + "email" : "decision@example.com", + "password" : "$2a$10$RyY5edaCBC2FdiXsdkz5Ae/bHs4FOQDMMg4U/1lRbNyGoXqDmKWIa", + "gender" : null, + "role" : "DECISION_MAKER", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.4177379", + "updatedAt" : "2025-06-24T23:33:58.4177379", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "2", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-10-25T19:11:44.6822258" +}, { + "id" : 24, + "user" : { + "id" : 57, + "name" : "特定网格员", + "phone" : "14700009999", + "email" : "worker@aizhangz.top", + "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 10, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2703065", + "updatedAt" : "2025-06-24T23:34:02.2703065", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录失败", + "targetId" : "57", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-10-25T19:12:28.9176313" +}, { + "id" : 25, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "1", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-10-25T19:13:39.6709067" +}, { + "id" : 26, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "1", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-10-25T19:14:22.7981559" +}, { + "id" : 27, + "user" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "operationType" : "LOGIN", + "description" : "用户登录成功", + "targetId" : "1", + "targetType" : "UserAccount", + "ipAddress" : "0:0:0:0:0:0:0:1", + "createdAt" : "2025-10-25T19:14:34.822513" } ] \ No newline at end of file diff --git a/ems-backend/json-db/pollutant_thresholds.json b/ems-backend/json-db/pollutant_thresholds.json index 62f9dd0..e10b775 100644 --- a/ems-backend/json-db/pollutant_thresholds.json +++ b/ems-backend/json-db/pollutant_thresholds.json @@ -1,36 +1,36 @@ -[ { - "id" : 1, - "pollutionType" : "PM25", - "pollutantName" : "PM25", - "threshold" : 75.0, - "unit" : "µg/m³", - "description" : "细颗粒物" -}, { - "id" : 2, - "pollutionType" : "O3", - "pollutantName" : "O3", - "threshold" : 160.0, - "unit" : "µg/m³", - "description" : "臭氧" -}, { - "id" : 3, - "pollutionType" : "NO2", - "pollutantName" : "NO2", - "threshold" : 100.0, - "unit" : "µg/m³", - "description" : "二氧化氮" -}, { - "id" : 4, - "pollutionType" : "SO2", - "pollutantName" : "SO2", - "threshold" : 150.0, - "unit" : "µg/m³", - "description" : "二氧化硫" -}, { - "id" : 5, - "pollutionType" : "OTHER", - "pollutantName" : "OTHER", - "threshold" : 100.0, - "unit" : "unit", - "description" : "其他污染物" +[ { + "id" : 1, + "pollutionType" : "PM25", + "pollutantName" : "PM25", + "threshold" : 75.0, + "unit" : "µg/m³", + "description" : "细颗粒物" +}, { + "id" : 2, + "pollutionType" : "O3", + "pollutantName" : "O3", + "threshold" : 160.0, + "unit" : "µg/m³", + "description" : "臭氧" +}, { + "id" : 3, + "pollutionType" : "NO2", + "pollutantName" : "NO2", + "threshold" : 100.0, + "unit" : "µg/m³", + "description" : "二氧化氮" +}, { + "id" : 4, + "pollutionType" : "SO2", + "pollutantName" : "SO2", + "threshold" : 150.0, + "unit" : "µg/m³", + "description" : "二氧化硫" +}, { + "id" : 5, + "pollutionType" : "OTHER", + "pollutantName" : "OTHER", + "threshold" : 100.0, + "unit" : "unit", + "description" : "其他污染物" } ] \ No newline at end of file diff --git a/ems-backend/json-db/tasks.json b/ems-backend/json-db/tasks.json index 285719f..a37d7b5 100644 --- a/ems-backend/json-db/tasks.json +++ b/ems-backend/json-db/tasks.json @@ -1,297 +1,297 @@ -[ { - "id" : 1, - "feedback" : null, - "assignee" : { - "id" : 57, - "name" : "特定网格员", - "phone" : "14700009999", - "email" : "worker@aizhangz.top", - "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 10, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2703065", - "updatedAt" : "2025-06-24T23:34:02.2703065", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "createdBy" : { - "id" : 5, - "name" : "特定主管", - "phone" : "13700137003", - "email" : "supervisor@aizhangz.top", - "password" : "$2a$10$t4pY9dOVge5xJygEasVI/enJPqWZwTVOE9wTSKs6VN8DQ4Dd8D8Zu", - "gender" : null, - "role" : "SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.6886435", - "updatedAt" : "2025-06-24T23:33:58.6886435", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "status" : "IN_PROGRESS", - "assignedAt" : "2025-06-22T23:34:02.5085803", - "completedAt" : null, - "createdAt" : null, - "updatedAt" : null, - "title" : "调查PM2.5超标问题", - "description" : "前往工业区,使用便携式检测仪检测空气质量,并记录排放情况。", - "pollutionType" : null, - "severityLevel" : "HIGH", - "textAddress" : null, - "gridX" : 2, - "gridY" : 3, - "latitude" : 39.9042, - "longitude" : 116.4074, - "history" : [ ], - "assignment" : null, - "submissions" : [ ] -}, { - "id" : 2, - "feedback" : null, - "assignee" : { - "id" : 57, - "name" : "特定网格员", - "phone" : "14700009999", - "email" : "worker@aizhangz.top", - "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 10, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2703065", - "updatedAt" : "2025-06-24T23:34:02.2703065", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "createdBy" : { - "id" : 5, - "name" : "特定主管", - "phone" : "13700137003", - "email" : "supervisor@aizhangz.top", - "password" : "$2a$10$t4pY9dOVge5xJygEasVI/enJPqWZwTVOE9wTSKs6VN8DQ4Dd8D8Zu", - "gender" : null, - "role" : "SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.6886435", - "updatedAt" : "2025-06-24T23:33:58.6886435", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "status" : "COMPLETED", - "assignedAt" : "2025-06-20T23:34:02.5085803", - "completedAt" : "2025-06-23T23:34:02.5085803", - "createdAt" : null, - "updatedAt" : null, - "title" : "疏导交通并监测NO2", - "description" : "在交通要道关键节点进行监测,并与交管部门协调。", - "pollutionType" : null, - "severityLevel" : "MEDIUM", - "textAddress" : null, - "gridX" : 4, - "gridY" : 5, - "latitude" : 39.915, - "longitude" : 116.4, - "history" : [ ], - "assignment" : null, - "submissions" : [ ] -}, { - "id" : 3, - "feedback" : null, - "assignee" : { - "id" : 57, - "name" : "特定网格员", - "phone" : "14700009999", - "email" : "worker@aizhangz.top", - "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 10, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2703065", - "updatedAt" : "2025-06-24T23:34:02.2703065", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "createdBy" : { - "id" : 5, - "name" : "特定主管", - "phone" : "13700137003", - "email" : "supervisor@aizhangz.top", - "password" : "$2a$10$t4pY9dOVge5xJygEasVI/enJPqWZwTVOE9wTSKs6VN8DQ4Dd8D8Zu", - "gender" : null, - "role" : "SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.6886435", - "updatedAt" : "2025-06-24T23:33:58.6886435", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "status" : "ASSIGNED", - "assignedAt" : "2025-06-24T23:34:02.5085803", - "completedAt" : null, - "createdAt" : null, - "updatedAt" : null, - "title" : "排查SO2异味源", - "description" : "在相关居民区进行走访和气味溯源。", - "pollutionType" : null, - "severityLevel" : "LOW", - "textAddress" : null, - "gridX" : 6, - "gridY" : 7, - "latitude" : 39.888, - "longitude" : 116.356, - "history" : [ ], - "assignment" : null, - "submissions" : [ ] -}, { - "id" : 4, - "feedback" : { - "id" : 4, - "eventId" : "6b800765-b0aa-457b-bde7-af60bace45f9", - "title" : "SO2", - "description" : "SO2", - "pollutionType" : "SO2", - "severityLevel" : "MEDIUM", - "status" : "PENDING_ASSIGNMENT", - "textAddress" : "东北大学195号", - "gridX" : 0, - "gridY" : 1, - "submitterId" : 6, - "createdAt" : "2025-06-24T23:35:05.9704809", - "updatedAt" : "2025-06-24T23:39:54.0988378", - "user" : { - "id" : 6, - "name" : "公众监督员", - "phone" : "13700137004", - "email" : "public.supervisor@aizhangz.top", - "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.7646754", - "updatedAt" : "2025-06-24T23:33:58.7646754", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "latitude" : 1.0E-6, - "longitude" : 1.0E-6, - "attachments" : [ ], - "task" : null - }, - "assignee" : { - "id" : 57, - "name" : "特定网格员", - "phone" : "14700009999", - "email" : "worker@aizhangz.top", - "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 10, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2703065", - "updatedAt" : "2025-06-24T23:34:02.2703065", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "createdBy" : { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null - }, - "status" : "ASSIGNED", - "assignedAt" : "2025-06-24T23:40:35.4504136", - "completedAt" : null, - "createdAt" : null, - "updatedAt" : null, - "title" : "SO2", - "description" : "SO2", - "pollutionType" : "SO2", - "severityLevel" : "MEDIUM", - "textAddress" : "东北大学195号", - "gridX" : 0, - "gridY" : 1, - "latitude" : 1.0E-6, - "longitude" : 1.0E-6, - "history" : [ ], - "assignment" : null, - "submissions" : [ ] +[ { + "id" : 1, + "feedback" : null, + "assignee" : { + "id" : 57, + "name" : "特定网格员", + "phone" : "14700009999", + "email" : "worker@aizhangz.top", + "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 10, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2703065", + "updatedAt" : "2025-06-24T23:34:02.2703065", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "createdBy" : { + "id" : 5, + "name" : "特定主管", + "phone" : "13700137003", + "email" : "supervisor@aizhangz.top", + "password" : "$2a$10$t4pY9dOVge5xJygEasVI/enJPqWZwTVOE9wTSKs6VN8DQ4Dd8D8Zu", + "gender" : null, + "role" : "SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.6886435", + "updatedAt" : "2025-06-24T23:33:58.6886435", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "status" : "IN_PROGRESS", + "assignedAt" : "2025-06-22T23:34:02.5085803", + "completedAt" : null, + "createdAt" : null, + "updatedAt" : null, + "title" : "调查PM2.5超标问题", + "description" : "前往工业区,使用便携式检测仪检测空气质量,并记录排放情况。", + "pollutionType" : null, + "severityLevel" : "HIGH", + "textAddress" : null, + "gridX" : 2, + "gridY" : 3, + "latitude" : 39.9042, + "longitude" : 116.4074, + "history" : [ ], + "assignment" : null, + "submissions" : [ ] +}, { + "id" : 2, + "feedback" : null, + "assignee" : { + "id" : 57, + "name" : "特定网格员", + "phone" : "14700009999", + "email" : "worker@aizhangz.top", + "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 10, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2703065", + "updatedAt" : "2025-06-24T23:34:02.2703065", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "createdBy" : { + "id" : 5, + "name" : "特定主管", + "phone" : "13700137003", + "email" : "supervisor@aizhangz.top", + "password" : "$2a$10$t4pY9dOVge5xJygEasVI/enJPqWZwTVOE9wTSKs6VN8DQ4Dd8D8Zu", + "gender" : null, + "role" : "SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.6886435", + "updatedAt" : "2025-06-24T23:33:58.6886435", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "status" : "COMPLETED", + "assignedAt" : "2025-06-20T23:34:02.5085803", + "completedAt" : "2025-06-23T23:34:02.5085803", + "createdAt" : null, + "updatedAt" : null, + "title" : "疏导交通并监测NO2", + "description" : "在交通要道关键节点进行监测,并与交管部门协调。", + "pollutionType" : null, + "severityLevel" : "MEDIUM", + "textAddress" : null, + "gridX" : 4, + "gridY" : 5, + "latitude" : 39.915, + "longitude" : 116.4, + "history" : [ ], + "assignment" : null, + "submissions" : [ ] +}, { + "id" : 3, + "feedback" : null, + "assignee" : { + "id" : 57, + "name" : "特定网格员", + "phone" : "14700009999", + "email" : "worker@aizhangz.top", + "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 10, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2703065", + "updatedAt" : "2025-06-24T23:34:02.2703065", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "createdBy" : { + "id" : 5, + "name" : "特定主管", + "phone" : "13700137003", + "email" : "supervisor@aizhangz.top", + "password" : "$2a$10$t4pY9dOVge5xJygEasVI/enJPqWZwTVOE9wTSKs6VN8DQ4Dd8D8Zu", + "gender" : null, + "role" : "SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.6886435", + "updatedAt" : "2025-06-24T23:33:58.6886435", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "status" : "ASSIGNED", + "assignedAt" : "2025-06-24T23:34:02.5085803", + "completedAt" : null, + "createdAt" : null, + "updatedAt" : null, + "title" : "排查SO2异味源", + "description" : "在相关居民区进行走访和气味溯源。", + "pollutionType" : null, + "severityLevel" : "LOW", + "textAddress" : null, + "gridX" : 6, + "gridY" : 7, + "latitude" : 39.888, + "longitude" : 116.356, + "history" : [ ], + "assignment" : null, + "submissions" : [ ] +}, { + "id" : 4, + "feedback" : { + "id" : 4, + "eventId" : "6b800765-b0aa-457b-bde7-af60bace45f9", + "title" : "SO2", + "description" : "SO2", + "pollutionType" : "SO2", + "severityLevel" : "MEDIUM", + "status" : "PENDING_ASSIGNMENT", + "textAddress" : "东北大学195号", + "gridX" : 0, + "gridY" : 1, + "submitterId" : 6, + "createdAt" : "2025-06-24T23:35:05.9704809", + "updatedAt" : "2025-06-24T23:39:54.0988378", + "user" : { + "id" : 6, + "name" : "公众监督员", + "phone" : "13700137004", + "email" : "public.supervisor@aizhangz.top", + "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.7646754", + "updatedAt" : "2025-06-24T23:33:58.7646754", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "latitude" : 1.0E-6, + "longitude" : 1.0E-6, + "attachments" : [ ], + "task" : null + }, + "assignee" : { + "id" : 57, + "name" : "特定网格员", + "phone" : "14700009999", + "email" : "worker@aizhangz.top", + "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 10, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2703065", + "updatedAt" : "2025-06-24T23:34:02.2703065", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "createdBy" : { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null + }, + "status" : "ASSIGNED", + "assignedAt" : "2025-06-24T23:40:35.4504136", + "completedAt" : null, + "createdAt" : null, + "updatedAt" : null, + "title" : "SO2", + "description" : "SO2", + "pollutionType" : "SO2", + "severityLevel" : "MEDIUM", + "textAddress" : "东北大学195号", + "gridX" : 0, + "gridY" : 1, + "latitude" : 1.0E-6, + "longitude" : 1.0E-6, + "history" : [ ], + "assignment" : null, + "submissions" : [ ] } ] \ No newline at end of file diff --git a/ems-backend/json-db/users.json b/ems-backend/json-db/users.json index 790e59e..f0b5024 100644 --- a/ems-backend/json-db/users.json +++ b/ems-backend/json-db/users.json @@ -1,1219 +1,1219 @@ -[ { - "id" : 1, - "name" : "系统管理员", - "phone" : "13800138000", - "email" : "admin@example.com", - "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", - "gender" : null, - "role" : "ADMIN", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.3224414", - "updatedAt" : "2025-06-24T23:33:58.323441", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 2, - "name" : "决策者", - "phone" : "13900139000", - "email" : "decision@example.com", - "password" : "$2a$10$RyY5edaCBC2FdiXsdkz5Ae/bHs4FOQDMMg4U/1lRbNyGoXqDmKWIa", - "gender" : null, - "role" : "DECISION_MAKER", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.4177379", - "updatedAt" : "2025-06-24T23:33:58.4177379", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 3, - "name" : "主管A", - "phone" : "13700137001", - "email" : "supervisor1@example.com", - "password" : "$2a$10$nXywR.Efh.pbxlnWwQ0mAuP0CfTcSmglrUr2700AiyMSJdtyobzvS", - "gender" : null, - "role" : "SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.5024495", - "updatedAt" : "2025-06-24T23:33:58.5024495", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 4, - "name" : "主管B", - "phone" : "13700137002", - "email" : "supervisor2@example.com", - "password" : "$2a$10$ehIPTqNZZemcnrnHRQ3Q9OkD3caQWsDX/euRyokJ4zszCn2BMtz3e", - "gender" : null, - "role" : "SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : "无锡市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.5834793", - "updatedAt" : "2025-06-24T23:33:58.5834793", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 5, - "name" : "特定主管", - "phone" : "13700137003", - "email" : "supervisor@aizhangz.top", - "password" : "$2a$10$t4pY9dOVge5xJygEasVI/enJPqWZwTVOE9wTSKs6VN8DQ4Dd8D8Zu", - "gender" : null, - "role" : "SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.6886435", - "updatedAt" : "2025-06-24T23:33:58.6886435", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 6, - "name" : "公众监督员", - "phone" : "13700137004", - "email" : "public.supervisor@aizhangz.top", - "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "ACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.7646754", - "updatedAt" : "2025-06-24T23:33:58.7646754", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 7, - "name" : "网格员1", - "phone" : "14700000000", - "email" : "worker1@example.com", - "password" : "$2a$10$9Lo1AiaOChZOE1FQeE2xnOyPKAPCk7aamxUV7qZVLFqiFOZx5fX4y", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 0, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.8577284", - "updatedAt" : "2025-06-24T23:33:58.8577284", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 8, - "name" : "网格员2", - "phone" : "14700000001", - "email" : "worker2@example.com", - "password" : "$2a$10$VFVoEoRt8e.R4Xy9Y7.S9.GgrrZRpEcBIkHroYVRNmUqV0.JWzghi", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 1, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:58.9332873", - "updatedAt" : "2025-06-24T23:33:58.9332873", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 9, - "name" : "网格员3", - "phone" : "14700000002", - "email" : "worker3@example.com", - "password" : "$2a$10$DozEFhdm56LYCGAj/Op60OWgp7pN22R9vB6HvDfJJUsEV/bfThhIq", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 2, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.0083253", - "updatedAt" : "2025-06-24T23:33:59.0083253", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 10, - "name" : "网格员4", - "phone" : "14700000003", - "email" : "worker4@example.com", - "password" : "$2a$10$0kJ8kYvmd4s2Y/8ilUl0AelVH/LU1/W3shYIbRbbEkZQ3Wn.qqLG2", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 3, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.0803462", - "updatedAt" : "2025-06-24T23:33:59.0803462", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 11, - "name" : "网格员5", - "phone" : "14700000004", - "email" : "worker5@example.com", - "password" : "$2a$10$eEVrwYa7OuNxzSegyt6LluyeuJwTsP3lIqqDmbov61bmBzkTqoNG.", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 4, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.1494003", - "updatedAt" : "2025-06-24T23:33:59.1494003", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 12, - "name" : "网格员6", - "phone" : "14700000005", - "email" : "worker6@example.com", - "password" : "$2a$10$rL8zLjsc.dVj36U1nypd/OTMCC9xcSwzCQi4QZqU/5Le37ubwkT9W", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 5, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.2191889", - "updatedAt" : "2025-06-24T23:33:59.2191889", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 13, - "name" : "网格员7", - "phone" : "14700000006", - "email" : "worker7@example.com", - "password" : "$2a$10$CTTWgZPlXQWIxCheDFGT/emrzrUfnjhKv10Re9K7Z/mtN.xBcpth2", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 6, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.2872282", - "updatedAt" : "2025-06-24T23:33:59.2872282", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 14, - "name" : "网格员8", - "phone" : "14700000007", - "email" : "worker8@example.com", - "password" : "$2a$10$UmzNjd2eH6dY2wrQzN30ueQhOoALZg7iRjqHsnME1cvfSMJr8V3le", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 7, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.3592613", - "updatedAt" : "2025-06-24T23:33:59.3592613", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 15, - "name" : "网格员9", - "phone" : "14700000008", - "email" : "worker9@example.com", - "password" : "$2a$10$pmdW7NhP0gH5mwivvMap7u3CzVrJtqpXzzZ4OIGuv.TkiXiQ2HW4W", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 8, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.4294304", - "updatedAt" : "2025-06-24T23:33:59.4294304", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 16, - "name" : "网格员10", - "phone" : "14700000009", - "email" : "worker10@example.com", - "password" : "$2a$10$UHxT2bxL5py7dYMWHS/um.NPuYeqFJQ57udniWTWi46VqSjvt6usO", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 9, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.4994751", - "updatedAt" : "2025-06-24T23:33:59.4994751", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 17, - "name" : "网格员11", - "phone" : "14700000010", - "email" : "worker11@example.com", - "password" : "$2a$10$Y36TiARhwHfLHyzMBZPqNemV0D2.StGH6lfuYEYWoyBg3QRlfp51e", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.5704841", - "updatedAt" : "2025-06-24T23:33:59.5704841", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 18, - "name" : "网格员12", - "phone" : "14700000011", - "email" : "worker12@example.com", - "password" : "$2a$10$S5M2iDV6Eesd2H6GTN7Fiu3ckd1olbdKYfMsJniLv3ygOHF/W5H7C", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 11, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.6395125", - "updatedAt" : "2025-06-24T23:33:59.6395125", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 19, - "name" : "网格员13", - "phone" : "14700000012", - "email" : "worker13@example.com", - "password" : "$2a$10$.XzAatIZ4rX6DCiTjpGNp.VshqvtKOtrn.7YVjAEU3wW172wSwGFi", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 12, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.7105843", - "updatedAt" : "2025-06-24T23:33:59.7105843", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 20, - "name" : "网格员14", - "phone" : "14700000013", - "email" : "worker14@example.com", - "password" : "$2a$10$uWswmWHGotzkxqOQkvE/cO5n1HnLgnDqlKWl5xSSnPYaLtp/lm.HK", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 13, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.7815956", - "updatedAt" : "2025-06-24T23:33:59.7815956", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 21, - "name" : "网格员15", - "phone" : "14700000014", - "email" : "worker15@example.com", - "password" : "$2a$10$9dsvQaI10C2ww/6eLySs9uV39dGow7WfsJSs.iDbS.LPCUQ5yWNWu", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 14, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.8546451", - "updatedAt" : "2025-06-24T23:33:59.8546451", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 22, - "name" : "网格员16", - "phone" : "14700000015", - "email" : "worker16@example.com", - "password" : "$2a$10$5uv88bzqKweWvM8r4/Gl7eIireGSfj7GrFQbtnKjuxWesVAkOhiuW", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 15, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.9226816", - "updatedAt" : "2025-06-24T23:33:59.9226816", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 23, - "name" : "网格员17", - "phone" : "14700000016", - "email" : "worker17@example.com", - "password" : "$2a$10$yHInFotFXyLQf3NqpQXxnug2deUQiaPA7DxD.d9XAdiDL6P45JTri", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 16, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:33:59.9917248", - "updatedAt" : "2025-06-24T23:33:59.9917248", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 24, - "name" : "网格员18", - "phone" : "14700000017", - "email" : "worker18@example.com", - "password" : "$2a$10$0Q5mHfQ8f35CNe78RjwLfu4mStRl3J8bShEq.M4JYXdWY.Qm5p6hi", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 17, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.0627365", - "updatedAt" : "2025-06-24T23:34:00.0627365", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 25, - "name" : "网格员19", - "phone" : "14700000018", - "email" : "worker19@example.com", - "password" : "$2a$10$Y3CfjeONo1zk41ZbKoNXiOd3qS670hRf9DNY7ziLwaYVPW8YfRLzO", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 18, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.1336032", - "updatedAt" : "2025-06-24T23:34:00.1336032", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 26, - "name" : "网格员20", - "phone" : "14700000019", - "email" : "worker20@example.com", - "password" : "$2a$10$AaKL9rgO4ftm6p.X42OdbeTifLb5iobNsgPGBK.47DkzLPJZTypG2", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 19, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.1996359", - "updatedAt" : "2025-06-24T23:34:00.1996359", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 27, - "name" : "网格员21", - "phone" : "14700000020", - "email" : "worker21@example.com", - "password" : "$2a$10$G0t8mU4eal0UoMSj31sL5e6yhEP6529LZ/Z7kf6YOdULAnfgLEmMW", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 20, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.2677049", - "updatedAt" : "2025-06-24T23:34:00.2677049", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 28, - "name" : "网格员22", - "phone" : "14700000021", - "email" : "worker22@example.com", - "password" : "$2a$10$0pXSAxy7Oq.unoRrtBaTH.amN8OzV.cmQT1Xpc/gaO45slPLPhyMK", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 21, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.3337498", - "updatedAt" : "2025-06-24T23:34:00.3337498", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 29, - "name" : "网格员23", - "phone" : "14700000022", - "email" : "worker23@example.com", - "password" : "$2a$10$llqQfX4gSowFsRTXfmkLw.ulvbZXf7XFkvjBDA3t.P0zhcC/M4QJ2", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 22, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.4012568", - "updatedAt" : "2025-06-24T23:34:00.4012568", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 30, - "name" : "网格员24", - "phone" : "14700000023", - "email" : "worker24@example.com", - "password" : "$2a$10$6sT3hDE.699A1zKWS2w4N.hdQC.gdtBH2Ve9R3KbYngCe6z1TeahS", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 23, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.4662713", - "updatedAt" : "2025-06-24T23:34:00.4662713", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 31, - "name" : "网格员25", - "phone" : "14700000024", - "email" : "worker25@example.com", - "password" : "$2a$10$fLLC3j.Vi1v6Uq7PCmMHE.kqn43Vkx3X3kjCAgVXHjgDl/KODjq4S", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 24, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.5322698", - "updatedAt" : "2025-06-24T23:34:00.5322698", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 32, - "name" : "网格员26", - "phone" : "14700000025", - "email" : "worker26@example.com", - "password" : "$2a$10$YtIJTChC7Aa4ZIxnMkXDj.5FHedT9cQr/wwUM.crzQAcW0FqcwIlm", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 25, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.5993025", - "updatedAt" : "2025-06-24T23:34:00.5993025", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 33, - "name" : "网格员27", - "phone" : "14700000026", - "email" : "worker27@example.com", - "password" : "$2a$10$LM2t73zTo9j5CRFUh2Im/OQhWY2DKMOZl38MQnjRo2Gu9iug3av9G", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 26, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.6653442", - "updatedAt" : "2025-06-24T23:34:00.6653442", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 34, - "name" : "网格员28", - "phone" : "14700000027", - "email" : "worker28@example.com", - "password" : "$2a$10$OxiEKnNIkiWjTBJvthjmcuCHRoTj4OROSTNWxX2FjTbBIDOOFPpg2", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 27, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.7325102", - "updatedAt" : "2025-06-24T23:34:00.7325102", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 35, - "name" : "网格员29", - "phone" : "14700000028", - "email" : "worker29@example.com", - "password" : "$2a$10$STgsWesP0ixMUXqERdONducldxgKF.pluNyXOZiC/4UYDB4Mr4jom", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 28, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.7987418", - "updatedAt" : "2025-06-24T23:34:00.7987418", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 36, - "name" : "网格员30", - "phone" : "14700000029", - "email" : "worker30@example.com", - "password" : "$2a$10$M9BguQYxLYocEw3r3hL10eYX25J4wLj0BY2Efou/Txdifu1UAWbVC", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 29, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.8662659", - "updatedAt" : "2025-06-24T23:34:00.8662659", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 37, - "name" : "网格员31", - "phone" : "14700000030", - "email" : "worker31@example.com", - "password" : "$2a$10$ze6O9VGywYelRYaUwAb4JuGtRhGwjw6T9YnzQCh/ZWBeazbAGKZyO", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 30, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:00.9333477", - "updatedAt" : "2025-06-24T23:34:00.9333477", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 38, - "name" : "网格员32", - "phone" : "14700000031", - "email" : "worker32@example.com", - "password" : "$2a$10$7BErW3Pzrmd.CfKSnRVuneRdlZHwMKwZ46A9cUFjt9l4Bf/85mCMO", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 31, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.0003922", - "updatedAt" : "2025-06-24T23:34:01.0003922", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 39, - "name" : "网格员33", - "phone" : "14700000032", - "email" : "worker33@example.com", - "password" : "$2a$10$b9JE5cOaOEexXfr1mBmcou9wltFQIxb8dlcpve2mLIdRJwJ0lltZ.", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 32, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.0674152", - "updatedAt" : "2025-06-24T23:34:01.0674152", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 40, - "name" : "网格员34", - "phone" : "14700000033", - "email" : "worker34@example.com", - "password" : "$2a$10$QmcYQls.hVDvTTAs0QxghujdYYzNeBvebyp3UCSES9vuTaav53wDm", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 33, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.1334493", - "updatedAt" : "2025-06-24T23:34:01.1334493", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 41, - "name" : "网格员35", - "phone" : "14700000034", - "email" : "worker35@example.com", - "password" : "$2a$10$j/wNZMrAuoGJ/Hkld9.i8O5F.Z4LG8eEXt5FmlSLkSoJ48I0dR8IW", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 34, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.2005301", - "updatedAt" : "2025-06-24T23:34:01.2005301", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 42, - "name" : "网格员36", - "phone" : "14700000035", - "email" : "worker36@example.com", - "password" : "$2a$10$XdDx4HvHDg7/nQFEP7QyA.cOPTuSNJOf5Az2rHBr9910R5BUSpoL2", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 35, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.2684515", - "updatedAt" : "2025-06-24T23:34:01.2684515", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 43, - "name" : "网格员37", - "phone" : "14700000036", - "email" : "worker37@example.com", - "password" : "$2a$10$XlObJBvrJeeX1cOZw0Bco.0xmjIyiTCtkm3ilqDswBYm6pb12N2su", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 36, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.3365813", - "updatedAt" : "2025-06-24T23:34:01.3365813", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 44, - "name" : "网格员38", - "phone" : "14700000037", - "email" : "worker38@example.com", - "password" : "$2a$10$IwEDVFKTsk/uk7J8f85pXukSrUdEVKC5SageZbv4Lp1IMLZmYxwKa", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 37, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.4032453", - "updatedAt" : "2025-06-24T23:34:01.4032453", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 45, - "name" : "网格员39", - "phone" : "14700000038", - "email" : "worker39@example.com", - "password" : "$2a$10$Za5ei5NRDkdsrpGC57zkv.bJDLN5yRUX6NWhUR2Fx/FwqWDqT7wwe", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 38, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.4702661", - "updatedAt" : "2025-06-24T23:34:01.4702661", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 46, - "name" : "网格员40", - "phone" : "14700000039", - "email" : "worker40@example.com", - "password" : "$2a$10$bQZMyL7mwWcYmy1Jn1NiyuokUoy8Zsu5nAgTREn.sYvsOd2x4a.pW", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 0, - "gridY" : 39, - "region" : "南京市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.5382918", - "updatedAt" : "2025-06-24T23:34:01.5382918", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 47, - "name" : "网格员41", - "phone" : "14700000040", - "email" : "worker41@example.com", - "password" : "$2a$10$eQ7duczjQ3TGy1UPDZCacOklMzYC12XC3tAX4o/pVzndxTgWUV.HK", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 1, - "gridY" : 0, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.6063272", - "updatedAt" : "2025-06-24T23:34:01.6063272", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 48, - "name" : "网格员42", - "phone" : "14700000041", - "email" : "worker42@example.com", - "password" : "$2a$10$sowqGyxq/00pJ/qKdS3iduuzaezF3uNvu0la2x4jrrTBeHL18dSx6", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 1, - "gridY" : 1, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.6723079", - "updatedAt" : "2025-06-24T23:34:01.6723079", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 49, - "name" : "网格员43", - "phone" : "14700000042", - "email" : "worker43@example.com", - "password" : "$2a$10$3TD4Ak3ERDAbutckB7.7i.PCl4nU8Fysdh8iQW6UVkA8IGDNV8Uiq", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 1, - "gridY" : 2, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.7404348", - "updatedAt" : "2025-06-24T23:34:01.7404348", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 50, - "name" : "网格员44", - "phone" : "14700000043", - "email" : "worker44@example.com", - "password" : "$2a$10$REyNx01Nw3OgCbZ.ZIOEIu/tNXqKwshIUO23bfc75v/vECVV/E1Ae", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 1, - "gridY" : 3, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.8064891", - "updatedAt" : "2025-06-24T23:34:01.8064891", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 51, - "name" : "网格员45", - "phone" : "14700000044", - "email" : "worker45@example.com", - "password" : "$2a$10$Gm1j7Q48Tnfqd2A0zb27pOF2lAEd8eYPSBJ7jk0LhsM9xsJWsstBa", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 1, - "gridY" : 4, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.8726235", - "updatedAt" : "2025-06-24T23:34:01.8726235", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 52, - "name" : "网格员46", - "phone" : "14700000045", - "email" : "worker46@example.com", - "password" : "$2a$10$6t2SVjD4LIHyTvgmhTN75eGdZ6lfQP6D9MjxXeASE..ZRIJG0Rcg.", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 1, - "gridY" : 5, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:01.940435", - "updatedAt" : "2025-06-24T23:34:01.940435", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 53, - "name" : "网格员47", - "phone" : "14700000046", - "email" : "worker47@example.com", - "password" : "$2a$10$wZ4zSHLWpBw23OxXRwrY8eaVMuxtNH/cBLIAAFvi3Hx8XCoUU7Vvu", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 1, - "gridY" : 6, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.0064624", - "updatedAt" : "2025-06-24T23:34:02.0064624", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 54, - "name" : "网格员48", - "phone" : "14700000047", - "email" : "worker48@example.com", - "password" : "$2a$10$9cBlSj7.3FMxWhvtKtj1K.anWSwefEFHzLIs.VIJxFs6BZc5tbm7S", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 1, - "gridY" : 7, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.0724775", - "updatedAt" : "2025-06-24T23:34:02.0724775", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 55, - "name" : "网格员49", - "phone" : "14700000048", - "email" : "worker49@example.com", - "password" : "$2a$10$8t3YYuz5q4R3h7fvdLJMlOpwl9NHNp3KsaPufIblSamKvbNdtL/ZO", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 1, - "gridY" : 8, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.13851", - "updatedAt" : "2025-06-24T23:34:02.13851", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 56, - "name" : "网格员50", - "phone" : "14700000049", - "email" : "worker50@example.com", - "password" : "$2a$10$MLCdmr.J1PBnTBtFxgbxZu6E30r8MGjmirrL9fH2wrMSNmnBKGPw.", - "gender" : "FEMALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 1, - "gridY" : 9, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2045297", - "updatedAt" : "2025-06-24T23:34:02.2045297", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 57, - "name" : "特定网格员", - "phone" : "14700009999", - "email" : "worker@aizhangz.top", - "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", - "gender" : "MALE", - "role" : "GRID_WORKER", - "status" : "ACTIVE", - "gridX" : 10, - "gridY" : 10, - "region" : "苏州市", - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:34:02.2703065", - "updatedAt" : "2025-06-24T23:34:02.2703065", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null -}, { - "id" : 58, - "name" : "尹子轩", - "phone" : "14132111111", - "email" : "13@aizhangz.top", - "password" : "$2a$10$sU7TJ05g7rRHB4m2x5rOOOMviWbIr7X4Eb2vn6U7Xto5Azo/NT.v.", - "gender" : null, - "role" : "PUBLIC_SUPERVISOR", - "status" : "INACTIVE", - "gridX" : null, - "gridY" : null, - "region" : null, - "level" : null, - "skills" : null, - "createdAt" : "2025-06-24T23:37:55.5868193", - "updatedAt" : "2025-06-24T23:37:55.5868193", - "enabled" : true, - "currentLatitude" : null, - "currentLongitude" : null, - "failedLoginAttempts" : 0, - "lockoutEndTime" : null +[ { + "id" : 1, + "name" : "系统管理员", + "phone" : "13800138000", + "email" : "admin@example.com", + "password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK", + "gender" : null, + "role" : "ADMIN", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.3224414", + "updatedAt" : "2025-06-24T23:33:58.323441", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 2, + "name" : "决策者", + "phone" : "13900139000", + "email" : "decision@example.com", + "password" : "$2a$10$RyY5edaCBC2FdiXsdkz5Ae/bHs4FOQDMMg4U/1lRbNyGoXqDmKWIa", + "gender" : null, + "role" : "DECISION_MAKER", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.4177379", + "updatedAt" : "2025-06-24T23:33:58.4177379", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 3, + "name" : "主管A", + "phone" : "13700137001", + "email" : "supervisor1@example.com", + "password" : "$2a$10$nXywR.Efh.pbxlnWwQ0mAuP0CfTcSmglrUr2700AiyMSJdtyobzvS", + "gender" : null, + "role" : "SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.5024495", + "updatedAt" : "2025-06-24T23:33:58.5024495", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 4, + "name" : "主管B", + "phone" : "13700137002", + "email" : "supervisor2@example.com", + "password" : "$2a$10$ehIPTqNZZemcnrnHRQ3Q9OkD3caQWsDX/euRyokJ4zszCn2BMtz3e", + "gender" : null, + "role" : "SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : "无锡市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.5834793", + "updatedAt" : "2025-06-24T23:33:58.5834793", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 5, + "name" : "特定主管", + "phone" : "13700137003", + "email" : "supervisor@aizhangz.top", + "password" : "$2a$10$t4pY9dOVge5xJygEasVI/enJPqWZwTVOE9wTSKs6VN8DQ4Dd8D8Zu", + "gender" : null, + "role" : "SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.6886435", + "updatedAt" : "2025-06-24T23:33:58.6886435", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 6, + "name" : "公众监督员", + "phone" : "13700137004", + "email" : "public.supervisor@aizhangz.top", + "password" : "$2a$10$tg2qMBxN/grBjChEMOzGI.lPFjBD5H7nwwnIQX59DHJ81AfRUjUI.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "ACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.7646754", + "updatedAt" : "2025-06-24T23:33:58.7646754", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 7, + "name" : "网格员1", + "phone" : "14700000000", + "email" : "worker1@example.com", + "password" : "$2a$10$9Lo1AiaOChZOE1FQeE2xnOyPKAPCk7aamxUV7qZVLFqiFOZx5fX4y", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 0, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.8577284", + "updatedAt" : "2025-06-24T23:33:58.8577284", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 8, + "name" : "网格员2", + "phone" : "14700000001", + "email" : "worker2@example.com", + "password" : "$2a$10$VFVoEoRt8e.R4Xy9Y7.S9.GgrrZRpEcBIkHroYVRNmUqV0.JWzghi", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 1, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:58.9332873", + "updatedAt" : "2025-06-24T23:33:58.9332873", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 9, + "name" : "网格员3", + "phone" : "14700000002", + "email" : "worker3@example.com", + "password" : "$2a$10$DozEFhdm56LYCGAj/Op60OWgp7pN22R9vB6HvDfJJUsEV/bfThhIq", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 2, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.0083253", + "updatedAt" : "2025-06-24T23:33:59.0083253", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 10, + "name" : "网格员4", + "phone" : "14700000003", + "email" : "worker4@example.com", + "password" : "$2a$10$0kJ8kYvmd4s2Y/8ilUl0AelVH/LU1/W3shYIbRbbEkZQ3Wn.qqLG2", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 3, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.0803462", + "updatedAt" : "2025-06-24T23:33:59.0803462", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 11, + "name" : "网格员5", + "phone" : "14700000004", + "email" : "worker5@example.com", + "password" : "$2a$10$eEVrwYa7OuNxzSegyt6LluyeuJwTsP3lIqqDmbov61bmBzkTqoNG.", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 4, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.1494003", + "updatedAt" : "2025-06-24T23:33:59.1494003", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 12, + "name" : "网格员6", + "phone" : "14700000005", + "email" : "worker6@example.com", + "password" : "$2a$10$rL8zLjsc.dVj36U1nypd/OTMCC9xcSwzCQi4QZqU/5Le37ubwkT9W", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 5, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.2191889", + "updatedAt" : "2025-06-24T23:33:59.2191889", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 13, + "name" : "网格员7", + "phone" : "14700000006", + "email" : "worker7@example.com", + "password" : "$2a$10$CTTWgZPlXQWIxCheDFGT/emrzrUfnjhKv10Re9K7Z/mtN.xBcpth2", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 6, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.2872282", + "updatedAt" : "2025-06-24T23:33:59.2872282", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 14, + "name" : "网格员8", + "phone" : "14700000007", + "email" : "worker8@example.com", + "password" : "$2a$10$UmzNjd2eH6dY2wrQzN30ueQhOoALZg7iRjqHsnME1cvfSMJr8V3le", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 7, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.3592613", + "updatedAt" : "2025-06-24T23:33:59.3592613", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 15, + "name" : "网格员9", + "phone" : "14700000008", + "email" : "worker9@example.com", + "password" : "$2a$10$pmdW7NhP0gH5mwivvMap7u3CzVrJtqpXzzZ4OIGuv.TkiXiQ2HW4W", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 8, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.4294304", + "updatedAt" : "2025-06-24T23:33:59.4294304", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 16, + "name" : "网格员10", + "phone" : "14700000009", + "email" : "worker10@example.com", + "password" : "$2a$10$UHxT2bxL5py7dYMWHS/um.NPuYeqFJQ57udniWTWi46VqSjvt6usO", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 9, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.4994751", + "updatedAt" : "2025-06-24T23:33:59.4994751", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 17, + "name" : "网格员11", + "phone" : "14700000010", + "email" : "worker11@example.com", + "password" : "$2a$10$Y36TiARhwHfLHyzMBZPqNemV0D2.StGH6lfuYEYWoyBg3QRlfp51e", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.5704841", + "updatedAt" : "2025-06-24T23:33:59.5704841", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 18, + "name" : "网格员12", + "phone" : "14700000011", + "email" : "worker12@example.com", + "password" : "$2a$10$S5M2iDV6Eesd2H6GTN7Fiu3ckd1olbdKYfMsJniLv3ygOHF/W5H7C", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 11, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.6395125", + "updatedAt" : "2025-06-24T23:33:59.6395125", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 19, + "name" : "网格员13", + "phone" : "14700000012", + "email" : "worker13@example.com", + "password" : "$2a$10$.XzAatIZ4rX6DCiTjpGNp.VshqvtKOtrn.7YVjAEU3wW172wSwGFi", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 12, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.7105843", + "updatedAt" : "2025-06-24T23:33:59.7105843", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 20, + "name" : "网格员14", + "phone" : "14700000013", + "email" : "worker14@example.com", + "password" : "$2a$10$uWswmWHGotzkxqOQkvE/cO5n1HnLgnDqlKWl5xSSnPYaLtp/lm.HK", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 13, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.7815956", + "updatedAt" : "2025-06-24T23:33:59.7815956", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 21, + "name" : "网格员15", + "phone" : "14700000014", + "email" : "worker15@example.com", + "password" : "$2a$10$9dsvQaI10C2ww/6eLySs9uV39dGow7WfsJSs.iDbS.LPCUQ5yWNWu", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 14, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.8546451", + "updatedAt" : "2025-06-24T23:33:59.8546451", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 22, + "name" : "网格员16", + "phone" : "14700000015", + "email" : "worker16@example.com", + "password" : "$2a$10$5uv88bzqKweWvM8r4/Gl7eIireGSfj7GrFQbtnKjuxWesVAkOhiuW", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 15, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.9226816", + "updatedAt" : "2025-06-24T23:33:59.9226816", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 23, + "name" : "网格员17", + "phone" : "14700000016", + "email" : "worker17@example.com", + "password" : "$2a$10$yHInFotFXyLQf3NqpQXxnug2deUQiaPA7DxD.d9XAdiDL6P45JTri", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 16, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:33:59.9917248", + "updatedAt" : "2025-06-24T23:33:59.9917248", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 24, + "name" : "网格员18", + "phone" : "14700000017", + "email" : "worker18@example.com", + "password" : "$2a$10$0Q5mHfQ8f35CNe78RjwLfu4mStRl3J8bShEq.M4JYXdWY.Qm5p6hi", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 17, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.0627365", + "updatedAt" : "2025-06-24T23:34:00.0627365", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 25, + "name" : "网格员19", + "phone" : "14700000018", + "email" : "worker19@example.com", + "password" : "$2a$10$Y3CfjeONo1zk41ZbKoNXiOd3qS670hRf9DNY7ziLwaYVPW8YfRLzO", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 18, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.1336032", + "updatedAt" : "2025-06-24T23:34:00.1336032", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 26, + "name" : "网格员20", + "phone" : "14700000019", + "email" : "worker20@example.com", + "password" : "$2a$10$AaKL9rgO4ftm6p.X42OdbeTifLb5iobNsgPGBK.47DkzLPJZTypG2", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 19, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.1996359", + "updatedAt" : "2025-06-24T23:34:00.1996359", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 27, + "name" : "网格员21", + "phone" : "14700000020", + "email" : "worker21@example.com", + "password" : "$2a$10$G0t8mU4eal0UoMSj31sL5e6yhEP6529LZ/Z7kf6YOdULAnfgLEmMW", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 20, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.2677049", + "updatedAt" : "2025-06-24T23:34:00.2677049", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 28, + "name" : "网格员22", + "phone" : "14700000021", + "email" : "worker22@example.com", + "password" : "$2a$10$0pXSAxy7Oq.unoRrtBaTH.amN8OzV.cmQT1Xpc/gaO45slPLPhyMK", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 21, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.3337498", + "updatedAt" : "2025-06-24T23:34:00.3337498", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 29, + "name" : "网格员23", + "phone" : "14700000022", + "email" : "worker23@example.com", + "password" : "$2a$10$llqQfX4gSowFsRTXfmkLw.ulvbZXf7XFkvjBDA3t.P0zhcC/M4QJ2", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 22, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.4012568", + "updatedAt" : "2025-06-24T23:34:00.4012568", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 30, + "name" : "网格员24", + "phone" : "14700000023", + "email" : "worker24@example.com", + "password" : "$2a$10$6sT3hDE.699A1zKWS2w4N.hdQC.gdtBH2Ve9R3KbYngCe6z1TeahS", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 23, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.4662713", + "updatedAt" : "2025-06-24T23:34:00.4662713", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 31, + "name" : "网格员25", + "phone" : "14700000024", + "email" : "worker25@example.com", + "password" : "$2a$10$fLLC3j.Vi1v6Uq7PCmMHE.kqn43Vkx3X3kjCAgVXHjgDl/KODjq4S", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 24, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.5322698", + "updatedAt" : "2025-06-24T23:34:00.5322698", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 32, + "name" : "网格员26", + "phone" : "14700000025", + "email" : "worker26@example.com", + "password" : "$2a$10$YtIJTChC7Aa4ZIxnMkXDj.5FHedT9cQr/wwUM.crzQAcW0FqcwIlm", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 25, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.5993025", + "updatedAt" : "2025-06-24T23:34:00.5993025", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 33, + "name" : "网格员27", + "phone" : "14700000026", + "email" : "worker27@example.com", + "password" : "$2a$10$LM2t73zTo9j5CRFUh2Im/OQhWY2DKMOZl38MQnjRo2Gu9iug3av9G", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 26, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.6653442", + "updatedAt" : "2025-06-24T23:34:00.6653442", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 34, + "name" : "网格员28", + "phone" : "14700000027", + "email" : "worker28@example.com", + "password" : "$2a$10$OxiEKnNIkiWjTBJvthjmcuCHRoTj4OROSTNWxX2FjTbBIDOOFPpg2", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 27, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.7325102", + "updatedAt" : "2025-06-24T23:34:00.7325102", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 35, + "name" : "网格员29", + "phone" : "14700000028", + "email" : "worker29@example.com", + "password" : "$2a$10$STgsWesP0ixMUXqERdONducldxgKF.pluNyXOZiC/4UYDB4Mr4jom", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 28, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.7987418", + "updatedAt" : "2025-06-24T23:34:00.7987418", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 36, + "name" : "网格员30", + "phone" : "14700000029", + "email" : "worker30@example.com", + "password" : "$2a$10$M9BguQYxLYocEw3r3hL10eYX25J4wLj0BY2Efou/Txdifu1UAWbVC", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 29, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.8662659", + "updatedAt" : "2025-06-24T23:34:00.8662659", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 37, + "name" : "网格员31", + "phone" : "14700000030", + "email" : "worker31@example.com", + "password" : "$2a$10$ze6O9VGywYelRYaUwAb4JuGtRhGwjw6T9YnzQCh/ZWBeazbAGKZyO", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 30, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:00.9333477", + "updatedAt" : "2025-06-24T23:34:00.9333477", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 38, + "name" : "网格员32", + "phone" : "14700000031", + "email" : "worker32@example.com", + "password" : "$2a$10$7BErW3Pzrmd.CfKSnRVuneRdlZHwMKwZ46A9cUFjt9l4Bf/85mCMO", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 31, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.0003922", + "updatedAt" : "2025-06-24T23:34:01.0003922", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 39, + "name" : "网格员33", + "phone" : "14700000032", + "email" : "worker33@example.com", + "password" : "$2a$10$b9JE5cOaOEexXfr1mBmcou9wltFQIxb8dlcpve2mLIdRJwJ0lltZ.", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 32, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.0674152", + "updatedAt" : "2025-06-24T23:34:01.0674152", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 40, + "name" : "网格员34", + "phone" : "14700000033", + "email" : "worker34@example.com", + "password" : "$2a$10$QmcYQls.hVDvTTAs0QxghujdYYzNeBvebyp3UCSES9vuTaav53wDm", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 33, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.1334493", + "updatedAt" : "2025-06-24T23:34:01.1334493", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 41, + "name" : "网格员35", + "phone" : "14700000034", + "email" : "worker35@example.com", + "password" : "$2a$10$j/wNZMrAuoGJ/Hkld9.i8O5F.Z4LG8eEXt5FmlSLkSoJ48I0dR8IW", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 34, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.2005301", + "updatedAt" : "2025-06-24T23:34:01.2005301", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 42, + "name" : "网格员36", + "phone" : "14700000035", + "email" : "worker36@example.com", + "password" : "$2a$10$XdDx4HvHDg7/nQFEP7QyA.cOPTuSNJOf5Az2rHBr9910R5BUSpoL2", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 35, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.2684515", + "updatedAt" : "2025-06-24T23:34:01.2684515", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 43, + "name" : "网格员37", + "phone" : "14700000036", + "email" : "worker37@example.com", + "password" : "$2a$10$XlObJBvrJeeX1cOZw0Bco.0xmjIyiTCtkm3ilqDswBYm6pb12N2su", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 36, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.3365813", + "updatedAt" : "2025-06-24T23:34:01.3365813", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 44, + "name" : "网格员38", + "phone" : "14700000037", + "email" : "worker38@example.com", + "password" : "$2a$10$IwEDVFKTsk/uk7J8f85pXukSrUdEVKC5SageZbv4Lp1IMLZmYxwKa", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 37, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.4032453", + "updatedAt" : "2025-06-24T23:34:01.4032453", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 45, + "name" : "网格员39", + "phone" : "14700000038", + "email" : "worker39@example.com", + "password" : "$2a$10$Za5ei5NRDkdsrpGC57zkv.bJDLN5yRUX6NWhUR2Fx/FwqWDqT7wwe", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 38, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.4702661", + "updatedAt" : "2025-06-24T23:34:01.4702661", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 46, + "name" : "网格员40", + "phone" : "14700000039", + "email" : "worker40@example.com", + "password" : "$2a$10$bQZMyL7mwWcYmy1Jn1NiyuokUoy8Zsu5nAgTREn.sYvsOd2x4a.pW", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 0, + "gridY" : 39, + "region" : "南京市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.5382918", + "updatedAt" : "2025-06-24T23:34:01.5382918", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 47, + "name" : "网格员41", + "phone" : "14700000040", + "email" : "worker41@example.com", + "password" : "$2a$10$eQ7duczjQ3TGy1UPDZCacOklMzYC12XC3tAX4o/pVzndxTgWUV.HK", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 1, + "gridY" : 0, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.6063272", + "updatedAt" : "2025-06-24T23:34:01.6063272", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 48, + "name" : "网格员42", + "phone" : "14700000041", + "email" : "worker42@example.com", + "password" : "$2a$10$sowqGyxq/00pJ/qKdS3iduuzaezF3uNvu0la2x4jrrTBeHL18dSx6", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 1, + "gridY" : 1, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.6723079", + "updatedAt" : "2025-06-24T23:34:01.6723079", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 49, + "name" : "网格员43", + "phone" : "14700000042", + "email" : "worker43@example.com", + "password" : "$2a$10$3TD4Ak3ERDAbutckB7.7i.PCl4nU8Fysdh8iQW6UVkA8IGDNV8Uiq", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 1, + "gridY" : 2, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.7404348", + "updatedAt" : "2025-06-24T23:34:01.7404348", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 50, + "name" : "网格员44", + "phone" : "14700000043", + "email" : "worker44@example.com", + "password" : "$2a$10$REyNx01Nw3OgCbZ.ZIOEIu/tNXqKwshIUO23bfc75v/vECVV/E1Ae", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 1, + "gridY" : 3, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.8064891", + "updatedAt" : "2025-06-24T23:34:01.8064891", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 51, + "name" : "网格员45", + "phone" : "14700000044", + "email" : "worker45@example.com", + "password" : "$2a$10$Gm1j7Q48Tnfqd2A0zb27pOF2lAEd8eYPSBJ7jk0LhsM9xsJWsstBa", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 1, + "gridY" : 4, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.8726235", + "updatedAt" : "2025-06-24T23:34:01.8726235", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 52, + "name" : "网格员46", + "phone" : "14700000045", + "email" : "worker46@example.com", + "password" : "$2a$10$6t2SVjD4LIHyTvgmhTN75eGdZ6lfQP6D9MjxXeASE..ZRIJG0Rcg.", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 1, + "gridY" : 5, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:01.940435", + "updatedAt" : "2025-06-24T23:34:01.940435", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 53, + "name" : "网格员47", + "phone" : "14700000046", + "email" : "worker47@example.com", + "password" : "$2a$10$wZ4zSHLWpBw23OxXRwrY8eaVMuxtNH/cBLIAAFvi3Hx8XCoUU7Vvu", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 1, + "gridY" : 6, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.0064624", + "updatedAt" : "2025-06-24T23:34:02.0064624", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 54, + "name" : "网格员48", + "phone" : "14700000047", + "email" : "worker48@example.com", + "password" : "$2a$10$9cBlSj7.3FMxWhvtKtj1K.anWSwefEFHzLIs.VIJxFs6BZc5tbm7S", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 1, + "gridY" : 7, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.0724775", + "updatedAt" : "2025-06-24T23:34:02.0724775", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 55, + "name" : "网格员49", + "phone" : "14700000048", + "email" : "worker49@example.com", + "password" : "$2a$10$8t3YYuz5q4R3h7fvdLJMlOpwl9NHNp3KsaPufIblSamKvbNdtL/ZO", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 1, + "gridY" : 8, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.13851", + "updatedAt" : "2025-06-24T23:34:02.13851", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 56, + "name" : "网格员50", + "phone" : "14700000049", + "email" : "worker50@example.com", + "password" : "$2a$10$MLCdmr.J1PBnTBtFxgbxZu6E30r8MGjmirrL9fH2wrMSNmnBKGPw.", + "gender" : "FEMALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 1, + "gridY" : 9, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2045297", + "updatedAt" : "2025-06-24T23:34:02.2045297", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 57, + "name" : "特定网格员", + "phone" : "14700009999", + "email" : "worker@aizhangz.top", + "password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i", + "gender" : "MALE", + "role" : "GRID_WORKER", + "status" : "ACTIVE", + "gridX" : 10, + "gridY" : 10, + "region" : "苏州市", + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:34:02.2703065", + "updatedAt" : "2025-06-24T23:34:02.2703065", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null +}, { + "id" : 58, + "name" : "尹子轩", + "phone" : "14132111111", + "email" : "13@aizhangz.top", + "password" : "$2a$10$sU7TJ05g7rRHB4m2x5rOOOMviWbIr7X4Eb2vn6U7Xto5Azo/NT.v.", + "gender" : null, + "role" : "PUBLIC_SUPERVISOR", + "status" : "INACTIVE", + "gridX" : null, + "gridY" : null, + "region" : null, + "level" : null, + "skills" : null, + "createdAt" : "2025-06-24T23:37:55.5868193", + "updatedAt" : "2025-06-24T23:37:55.5868193", + "enabled" : true, + "currentLatitude" : null, + "currentLongitude" : null, + "failedLoginAttempts" : 0, + "lockoutEndTime" : null } ] \ No newline at end of file diff --git a/ems-backend/mvnw.cmd b/ems-backend/mvnw.cmd index 249bdf3..b150b91 100644 --- a/ems-backend/mvnw.cmd +++ b/ems-backend/mvnw.cmd @@ -1,149 +1,149 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.2 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" -if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" -} -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/ems-backend/pom.xml b/ems-backend/pom.xml index 4bf97dc..edf983a 100644 --- a/ems-backend/pom.xml +++ b/ems-backend/pom.xml @@ -1,141 +1,141 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.0 - - - com.dne - ems-backend - 0.0.1-SNAPSHOT - ems-backend - ems-backend - - - - - - - - - - - - - - - 17 - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-websocket - - - - org.springframework.boot - spring-boot-starter-mail - - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - io.jsonwebtoken - jjwt-api - 0.11.5 - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - runtime - - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.3.0 - - - - - com.google.guava - guava - 32.1.2-jre - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - - - + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.0 + + + com.dne + ems-backend + 0.0.1-SNAPSHOT + ems-backend + ems-backend + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-websocket + + + + org.springframework.boot + spring-boot-starter-mail + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + + + com.google.guava + guava + 32.1.2-jre + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + + diff --git a/ems-backend/src/main/java/com/dne/ems/EmsBackendApplication.java b/ems-backend/src/main/java/com/dne/ems/EmsBackendApplication.java index aba27c3..bad9eee 100644 --- a/ems-backend/src/main/java/com/dne/ems/EmsBackendApplication.java +++ b/ems-backend/src/main/java/com/dne/ems/EmsBackendApplication.java @@ -1,36 +1,36 @@ -package com.dne.ems; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.scheduling.annotation.EnableAsync; - -/** - * EMS系统主启动类 - * - *

核心配置: - *

    - *
  • @SpringBootApplication - 启用Spring Boot自动配置,排除数据库相关配置
  • - *
  • @ComponentScan - 扫描com.dne.ems包下的组件
  • - *
  • @EnableAsync - 启用异步处理
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2025-06 - */ -@SpringBootApplication(exclude = { - DataSourceAutoConfiguration.class, - HibernateJpaAutoConfiguration.class -}) -@ComponentScan(basePackages = "com.dne.ems") -@EnableAsync -public class EmsBackendApplication { - - public static void main(String[] args) { - SpringApplication.run(EmsBackendApplication.class, args); - } - -} +package com.dne.ems; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableAsync; + +/** + * EMS系统主启动类 + * + *

核心配置: + *

    + *
  • @SpringBootApplication - 启用Spring Boot自动配置,排除数据库相关配置
  • + *
  • @ComponentScan - 扫描com.dne.ems包下的组件
  • + *
  • @EnableAsync - 启用异步处理
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2025-06 + */ +@SpringBootApplication(exclude = { + DataSourceAutoConfiguration.class, + HibernateJpaAutoConfiguration.class +}) +@ComponentScan(basePackages = "com.dne.ems") +@EnableAsync +public class EmsBackendApplication { + + public static void main(String[] args) { + SpringApplication.run(EmsBackendApplication.class, args); + } + +} diff --git a/ems-backend/src/main/java/com/dne/ems/config/DataInitializer.java b/ems-backend/src/main/java/com/dne/ems/config/DataInitializer.java index 8c973be..ff7f388 100644 --- a/ems-backend/src/main/java/com/dne/ems/config/DataInitializer.java +++ b/ems-backend/src/main/java/com/dne/ems/config/DataInitializer.java @@ -1,612 +1,612 @@ -package com.dne.ems.config; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; -import java.util.Random; -import java.util.Set; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.CommandLineRunner; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Component; - -import com.dne.ems.dto.CityBlueprint; -import com.dne.ems.model.AqiRecord; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.Grid; -import com.dne.ems.model.MapGrid; -import com.dne.ems.model.Task; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.model.enums.TaskStatus; -import com.dne.ems.model.enums.UserStatus; -import com.dne.ems.repository.AqiRecordRepository; -import com.dne.ems.repository.AssignmentRepository; -import com.dne.ems.repository.FeedbackRepository; -import com.dne.ems.repository.GridRepository; -import com.dne.ems.repository.MapGridRepository; -import com.dne.ems.repository.TaskRepository; -import com.dne.ems.repository.UserAccountRepository; - -import lombok.RequiredArgsConstructor; - -/** - * Initializes the database with seed data upon application startup. - * This component is crucial for setting up a baseline state for the application, - * especially in development and testing environments. It populates grids, user accounts, - * and other essential data. - */ -@Component -@RequiredArgsConstructor -public class DataInitializer implements CommandLineRunner { - - private final GridRepository gridRepository; - private final UserAccountRepository userAccountRepository; - private final PasswordEncoder passwordEncoder; - private final AqiRecordRepository aqiRecordRepository; - private final MapGridRepository mapGridRepository; - private final TaskRepository taskRepository; - private final AssignmentRepository assignmentRepository; - private final FeedbackRepository feedbackRepository; - - private static final Logger log = LoggerFactory.getLogger(DataInitializer.class); - - @Override - public void run(String... args) { - log.info("Starting data initialization..."); - try { - initializeGridData(); - initializeMapGridData(); // Ensure MapGrid is synchronized - initializeUsers(); - initializeAqiData(); - initializeFeedbackData(); - initializeTaskData(); - } catch (Exception e) { - log.error("Error during data initialization", e); - } - log.info("Data initialization finished."); - } - - private void initializeGridData() { - if (gridRepository.count() > 0) { - log.info("Grid data already exists. Skipping initialization."); - return; - } - log.info("Creating initial grid data..."); - - final int MAP_SIZE = 40; - final double OBSTACLE_CHANCE = 0.05; // 5% chance for a cell to be an obstacle - - Grid[][] map = new Grid[MAP_SIZE][MAP_SIZE]; - Random random = new Random(); - List gridsToSave = new ArrayList<>(); - - // 1. Define the 4 cities - List cityBlueprints = List.of( - new CityBlueprint("常州市", "溧阳市", 0, 0, 0, 0), - new CityBlueprint("无锡市", "宜兴市", 0, 0, 0, 0), - new CityBlueprint("苏州市", "昆山市", 0, 0, 0, 0), - new CityBlueprint("南京市", "江宁区", 0, 0, 0, 0) - ); - - // 2. Select 4 unique random seeds (capitals) for the cities - Queue expansionQueue = new LinkedList<>(); - Set usedSeeds = new HashSet<>(); - - for (CityBlueprint city : cityBlueprints) { - Point seed; - do { - seed = new Point(random.nextInt(MAP_SIZE), random.nextInt(MAP_SIZE)); - } while (usedSeeds.contains(seed)); - - usedSeeds.add(seed); - - Grid grid = new Grid(); - grid.setGridX(seed.x); - grid.setGridY(seed.y); - grid.setCityName(city.getCityName()); - grid.setDistrictName(city.getDistrictName()); - grid.setIsObstacle(false); // Capitals are never obstacles - - map[seed.x][seed.y] = grid; - expansionQueue.add(seed); - } - - // 3. Expand all cities simultaneously using Breadth-First Search (BFS) - // This ensures every cell belongs to the closest city, creating Voronoi-like regions - while (!expansionQueue.isEmpty()) { - Point p = expansionQueue.poll(); - Grid centerGrid = map[p.x][p.y]; - - // Use shuffled neighbors to make borders more irregular and interesting - for (Point neighbor : p.getShuffledNeighbors(MAP_SIZE, MAP_SIZE, random)) { - if (map[neighbor.x][neighbor.y] == null) { // If the cell is unclaimed - Grid neighborGrid = new Grid(); - neighborGrid.setGridX(neighbor.x); - neighborGrid.setGridY(neighbor.y); - neighborGrid.setCityName(centerGrid.getCityName()); - neighborGrid.setDistrictName(centerGrid.getDistrictName()); - neighborGrid.setIsObstacle(false); // Set obstacle status later - - map[neighbor.x][neighbor.y] = neighborGrid; - expansionQueue.add(neighbor); - } - } - } - - // 4. Identify border cells to keep them clear of obstacles - Set borderCells = new HashSet<>(); - for (int x = 0; x < MAP_SIZE; x++) { - for (int y = 0; y < MAP_SIZE; y++) { - Grid currentGrid = map[x][y]; - if (currentGrid == null) { - // This should never happen as we ensure every cell is assigned to a city - log.error("发现未分配城市的格子 at ({}, {}), 这是一个错误", x, y); - continue; - } - - Point currentPoint = new Point(x, y); - for (Point neighbor : currentPoint.getNeighbors(MAP_SIZE, MAP_SIZE)) { - Grid neighborGrid = map[neighbor.x][neighbor.y]; - if (neighborGrid != null && !currentGrid.getCityName().equals(neighborGrid.getCityName())) { - borderCells.add(currentPoint); - break; // Found one neighbor from another city, this is a border cell - } - } - } - } - - // 5. Add random obstacles, avoiding borders and capitals - for (int x = 0; x < MAP_SIZE; x++) { - for (int y = 0; y < MAP_SIZE; y++) { - Grid grid = map[x][y]; - if (grid != null) { - boolean isSeed = usedSeeds.contains(new Point(x, y)); - boolean isBorder = borderCells.contains(new Point(x, y)); - - // Don't make capitals or border cells obstacles - if (!isSeed && !isBorder && random.nextDouble() < OBSTACLE_CHANCE) { - grid.setIsObstacle(true); - } - gridsToSave.add(grid); - } else { - // This should never happen - log an error if it does - log.error("发现空格子 at ({}, {}), 这是一个算法错误", x, y); - } - } - } - - gridRepository.saveAll(gridsToSave); - log.info("{} grid cells created.", gridsToSave.size()); - } - - /** - * Synchronizes the grid data with the map_grid table for A* pathfinding. - */ - private void initializeMapGridData() { - if (mapGridRepository.count() > 0) { - log.info("MapGrid data already exists. Skipping synchronization."); - return; - } - log.info("Synchronizing data to MapGrid for pathfinding..."); - - List grids = gridRepository.findAll(); - List mapGrids = grids.stream() - .map(grid -> MapGrid.builder() - .x(grid.getGridX()) - .y(grid.getGridY()) - .isObstacle(grid.getIsObstacle()) - .cityName(grid.getCityName()) - .build()) - .collect(Collectors.toList()); - - mapGridRepository.saveAll(mapGrids); - log.info("Synchronized {} grid cells to MapGrid.", mapGrids.size()); - } - - private void initializeUsers() { - if (userAccountRepository.count() > 0) { - log.info("User data already exists. Skipping initialization."); - return; - } - log.info("Creating initial user accounts..."); - - // Create Admin - UserAccount admin = new UserAccount(); - admin.setName("系统管理员"); - admin.setEmail("admin@example.com"); - admin.setPhone("13800138000"); - admin.setPassword(passwordEncoder.encode("password")); - admin.setRole(Role.ADMIN); - admin.setStatus(UserStatus.ACTIVE); - - // Create Decision Maker - UserAccount decisionMaker = new UserAccount(); - decisionMaker.setName("决策者"); - decisionMaker.setEmail("decision@example.com"); - decisionMaker.setPhone("13900139000"); - decisionMaker.setPassword(passwordEncoder.encode("password")); - decisionMaker.setRole(Role.DECISION_MAKER); - decisionMaker.setStatus(UserStatus.ACTIVE); - - // Create Supervisors - UserAccount supervisor1 = new UserAccount(); - supervisor1.setName("主管A"); - supervisor1.setEmail("supervisor1@example.com"); - supervisor1.setPhone("13700137001"); - supervisor1.setPassword(passwordEncoder.encode("password")); - supervisor1.setRole(Role.SUPERVISOR); - supervisor1.setStatus(UserStatus.ACTIVE); - supervisor1.setRegion("南京市"); - - UserAccount supervisor2 = new UserAccount(); - supervisor2.setName("主管B"); - supervisor2.setEmail("supervisor2@example.com"); - supervisor2.setPhone("13700137002"); - supervisor2.setPassword(passwordEncoder.encode("password")); - supervisor2.setRole(Role.SUPERVISOR); - supervisor2.setStatus(UserStatus.ACTIVE); - supervisor2.setRegion("无锡市"); - - // 添加特定的主管用户,用于任务初始化 - UserAccount specialSupervisor = new UserAccount(); - specialSupervisor.setName("特定主管"); - specialSupervisor.setEmail("supervisor@aizhangz.top"); - specialSupervisor.setPhone("13700137003"); - specialSupervisor.setPassword(passwordEncoder.encode("password")); - specialSupervisor.setRole(Role.SUPERVISOR); - specialSupervisor.setStatus(UserStatus.ACTIVE); - specialSupervisor.setRegion("苏州市"); - - // 添加公众监督员,用于反馈初始化 - UserAccount publicSupervisor = new UserAccount(); - publicSupervisor.setName("公众监督员"); - publicSupervisor.setEmail("public.supervisor@aizhangz.top"); - publicSupervisor.setPhone("13700137004"); - publicSupervisor.setPassword(passwordEncoder.encode("password")); - publicSupervisor.setRole(Role.PUBLIC_SUPERVISOR); - publicSupervisor.setStatus(UserStatus.ACTIVE); - - userAccountRepository.saveAll(List.of(admin, decisionMaker, supervisor1, supervisor2, specialSupervisor, publicSupervisor)); - - // Create Grid Workers - List workers = new ArrayList<>(); - List availableGrids = gridRepository.findByIsObstacle(false); - - int workerCount = 0; - for (Grid grid : availableGrids) { - if (workerCount >= 50) break; // Create up to 50 workers - - UserAccount worker = new UserAccount(); - worker.setName("网格员" + (workerCount + 1)); - worker.setPhone("1470000" + String.format("%04d", workerCount)); - worker.setEmail("worker" + (workerCount + 1) + "@example.com"); - worker.setPassword(passwordEncoder.encode("password")); - worker.setRole(Role.GRID_WORKER); - worker.setStatus(UserStatus.ACTIVE); - worker.setGridX(grid.getGridX()); - worker.setGridY(grid.getGridY()); - worker.setRegion(grid.getCityName()); - worker.setGender(workerCount % 2 == 0 ? com.dne.ems.model.enums.Gender.MALE : com.dne.ems.model.enums.Gender.FEMALE); - workers.add(worker); - workerCount++; - } - - // 添加特定的网格员,用于任务初始化 - UserAccount specialWorker = new UserAccount(); - specialWorker.setName("特定网格员"); - specialWorker.setPhone("14700009999"); - specialWorker.setEmail("worker@aizhangz.top"); - specialWorker.setPassword(passwordEncoder.encode("password")); - specialWorker.setRole(Role.GRID_WORKER); - specialWorker.setStatus(UserStatus.ACTIVE); - specialWorker.setGridX(10); - specialWorker.setGridY(10); - specialWorker.setRegion("苏州市"); - specialWorker.setGender(com.dne.ems.model.enums.Gender.MALE); - workers.add(specialWorker); - - userAccountRepository.saveAll(workers); - - log.info("Created Admin, DecisionMaker, 2 Supervisors, and {} Grid Workers.", workers.size()); - } - - private void initializeAqiData() { - if (aqiRecordRepository.count() > 0) { - log.info("AQI data already exists. Skipping initialization."); - return; - } - log.info("Creating initial AQI data..."); - - // 清空现有数据 - aqiRecordRepository.deleteAll(); - - List records = new ArrayList<>(); - - // 获取网格数据,用于关联 - List grids = gridRepository.findAll(); - if (grids.isEmpty()) { - log.warn("没有找到网格数据,无法完全初始化AQI数据"); - // 即使没有网格数据,也继续初始化基本的 AQI 数据 - } - - // 为不同城市选择不同的网格 - // Map> gridsByCity = grids.stream() - // .collect(Collectors.groupingBy(Grid::getCityName)); - - // 基本 AQI 数据 - 不同级别 - // Good (0-50) - AqiRecord record1 = new AqiRecord("北京市", 40, LocalDateTime.now().minusDays(1), 10.5, 20.3, 5.1, 15.2, 0.8, 30.1); - record1.setGridX(0); - record1.setGridY(0); - records.add(record1); - - AqiRecord record2 = new AqiRecord("上海市", 35, LocalDateTime.now().minusDays(2), 9.2, 18.5, 4.8, 14.0, 0.7, 28.5); - record2.setGridX(0); - record2.setGridY(1); - records.add(record2); - - // Moderate (51-100) - AqiRecord record3 = new AqiRecord("广州市", 75, LocalDateTime.now().minusDays(3), 25.3, 45.7, 10.2, 30.5, 1.5, 60.2); - record3.setGridX(1); - record3.setGridY(0); - records.add(record3); - - AqiRecord record4 = new AqiRecord("深圳市", 85, LocalDateTime.now().minusDays(4), 28.6, 52.1, 12.3, 35.8, 1.8, 68.5); - record4.setGridX(1); - record4.setGridY(1); - records.add(record4); - - // Unhealthy for Sensitive Groups (101-150) - AqiRecord record5 = new AqiRecord("成都市", 130, LocalDateTime.now().minusDays(5), 45.2, 85.3, 20.5, 55.8, 3.2, 90.5); - record5.setGridX(2); - record5.setGridY(0); - records.add(record5); - - // Unhealthy (151-200) - AqiRecord record6 = new AqiRecord("武汉市", 175, LocalDateTime.now().minusDays(6), 60.5, 110.8, 30.2, 70.5, 4.5, 120.3); - record6.setGridX(2); - record6.setGridY(1); - records.add(record6); - - // Very Unhealthy (201-300) - AqiRecord record7 = new AqiRecord("西安市", 250, LocalDateTime.now().minusDays(7), 90.3, 160.5, 45.8, 95.2, 6.8, 150.5); - record7.setGridX(3); - record7.setGridY(0); - records.add(record7); - - // Hazardous (301+) - AqiRecord record8 = new AqiRecord("兰州市", 320, LocalDateTime.now().minusDays(8), 120.5, 200.8, 60.2, 120.5, 8.5, 180.3); - record8.setGridX(3); - record8.setGridY(1); - records.add(record8); - - // 保存基本数据 - List savedRecords = aqiRecordRepository.saveAll(records); - log.info("已保存基本 AQI 数据: {} 条记录", savedRecords.size()); - records.clear(); - - // 添加带有网格位置的数据 - 用于热力图 - // 直接使用网格坐标,不关联 Grid 实体 - for (int x = 0; x < 5; x++) { - for (int y = 0; y < 5; y++) { - int aqiValue = 50 + (x + y) * 15; // 不同 AQI 值 - LocalDateTime recordTime = LocalDateTime.now().minusDays(x + y); - - AqiRecord record = new AqiRecord("北京市", aqiValue, recordTime, - 20.0 + x, 40.0 + y * 2, 8.0 + x, 25.0 + y, 1.0 + x * 0.2, 50.0 + y * 3); - record.setGridX(x); - record.setGridY(y); - records.add(record); - } - } - - // 保存热力图数据 - savedRecords = aqiRecordRepository.saveAll(records); - log.info("已保存热力图 AQI 数据: {} 条记录", savedRecords.size()); - records.clear(); - - // 添加不同月份的数据 - 用于趋势图 - for (int i = 1; i <= 6; i++) { - for (int j = 0; j < 3; j++) { // 每个月添加多条记录 - LocalDateTime recordTime = LocalDateTime.now().minusMonths(i).plusDays(j * 5); - int aqiValue = 80 + i * 20 + j * 5; // 确保有些超过 100 的值 - - AqiRecord record = new AqiRecord("北京市", aqiValue, recordTime, - 30.0 + i, 60.0 + i * 2, 12.0 + i, 35.0 + i, 1.5 + i * 0.2, 70.0 + i * 3); - // 为趋势数据也设置网格坐标 - record.setGridX(i % 5); - record.setGridY(j % 5); - records.add(record); - } - } - - // 保存趋势图数据 - savedRecords = aqiRecordRepository.saveAll(records); - log.info("已保存趋势图 AQI 数据: {} 条记录", savedRecords.size()); - - log.info("Created {} AQI records.", records.size()); - aqiRecordRepository.saveAll(records); - } - - private void initializeFeedbackData() { - if (feedbackRepository.count() > 0) { - log.info("Feedback 数据已存在,跳过初始化"); - return; - } - - // 清除现有数据并重新初始化 - // 必须先清除子表数据,再清除父表数据,以避免外键约束冲突 - assignmentRepository.deleteAll(); - taskRepository.deleteAll(); - feedbackRepository.deleteAll(); - log.info("已清除现有的 Assignment, Task, 和 Feedback 数据,开始重新初始化..."); - - // 使用PUBLIC_SUPERVISOR角色的用户作为反馈提交者 - UserAccount reporter = userAccountRepository.findByEmail("public.supervisor@aizhangz.top").orElse(null); - if (reporter == null) { - log.warn("无法找到公众监督员 'public.supervisor@aizhangz.top',跳过 Feedback 初始化"); - return; - } - - // 创建反馈数据 - List feedbacks = new ArrayList<>(); - - Feedback feedback1 = new Feedback(); - feedback1.setEventId("FB-202301"); - feedback1.setTitle("工业区PM2.5超标"); - feedback1.setDescription("市中心化工厂在夜间排放黄色烟雾,气味刺鼻。"); - feedback1.setLatitude(39.9042); - feedback1.setLongitude(116.4074); - feedback1.setGridX(5); - feedback1.setGridY(5); - feedback1.setPollutionType(PollutionType.PM25); - feedback1.setSeverityLevel(SeverityLevel.HIGH); - feedback1.setStatus(FeedbackStatus.PENDING_ASSIGNMENT); - feedback1.setUser(reporter); - feedback1.setSubmitterId(reporter.getId()); - feedbacks.add(feedback1); - - Feedback feedback2 = new Feedback(); - feedback2.setEventId("FB-202302"); - feedback2.setTitle("交通要道NO2浓度过高"); - feedback2.setDescription("主干道车辆拥堵,空气质量差。"); - feedback2.setLatitude(39.915); - feedback2.setLongitude(116.400); - feedback2.setGridX(6); - feedback2.setGridY(6); - feedback2.setPollutionType(PollutionType.NO2); - feedback2.setSeverityLevel(SeverityLevel.MEDIUM); - feedback2.setStatus(FeedbackStatus.ASSIGNED); - feedback2.setUser(reporter); - feedback2.setSubmitterId(reporter.getId()); - feedbacks.add(feedback2); - - Feedback feedback3 = new Feedback(); - feedback3.setEventId("FB-202303"); - feedback3.setTitle("未知来源的SO2异味"); - feedback3.setDescription("居民区闻到类似烧煤的刺鼻气味。"); - feedback3.setLatitude(39.888); - feedback3.setLongitude(116.356); - feedback3.setGridX(7); - feedback3.setGridY(7); - feedback3.setPollutionType(PollutionType.SO2); - feedback3.setSeverityLevel(SeverityLevel.LOW); - feedback3.setStatus(FeedbackStatus.PROCESSED); - feedback3.setUser(reporter); - feedback3.setSubmitterId(reporter.getId()); - feedbacks.add(feedback3); - - feedbackRepository.saveAll(feedbacks); - log.info("反馈数据初始化完成,共创建 {} 条记录", feedbacks.size()); - } - - private void initializeTaskData() { - if (taskRepository.count() > 0) { - log.info("Task 数据已存在,跳过初始化"); - return; - } - log.info("开始初始化 Task 模拟数据..."); - - UserAccount worker = userAccountRepository.findByEmail("worker@aizhangz.top").orElse(null); - if (worker == null) { - log.warn("无法找到网格员 'worker@aizhangz.top',跳过 Task 初始化"); - return; - } - - UserAccount creator = userAccountRepository.findByEmail("supervisor@aizhangz.top").orElse(null); - if (creator == null) { - log.warn("无法找到主管 'supervisor@aizhangz.top',跳过 Task 初始化"); - return; - } - - List tasks = new ArrayList<>(); - - Task task1 = new Task(); - task1.setTitle("调查PM2.5超标问题"); - task1.setDescription("前往工业区,使用便携式检测仪检测空气质量,并记录排放情况。"); - task1.setAssignee(worker); - task1.setCreatedBy(creator); - task1.setStatus(TaskStatus.IN_PROGRESS); - task1.setAssignedAt(LocalDateTime.now().minusDays(2)); - task1.setLatitude(39.9042); - task1.setLongitude(116.4074); - task1.setGridX(2); - task1.setGridY(3); - task1.setSeverityLevel(SeverityLevel.HIGH); - tasks.add(task1); - - Task task2 = new Task(); - task2.setTitle("疏导交通并监测NO2"); - task2.setDescription("在交通要道关键节点进行监测,并与交管部门协调。"); - task2.setAssignee(worker); - task2.setCreatedBy(creator); - task2.setStatus(TaskStatus.COMPLETED); - task2.setCompletedAt(LocalDateTime.now().minusDays(1)); - task2.setAssignedAt(LocalDateTime.now().minusDays(4)); - task2.setLatitude(39.915); - task2.setLongitude(116.400); - task2.setGridX(4); - task2.setGridY(5); - task2.setSeverityLevel(SeverityLevel.MEDIUM); - tasks.add(task2); - - Task task3 = new Task(); - task3.setTitle("排查SO2异味源"); - task3.setDescription("在相关居民区进行走访和气味溯源。"); - task3.setAssignee(worker); - task3.setCreatedBy(creator); - task3.setStatus(TaskStatus.ASSIGNED); - task3.setAssignedAt(LocalDateTime.now()); - task3.setLatitude(39.888); - task3.setLongitude(116.356); - task3.setGridX(6); - task3.setGridY(7); - task3.setSeverityLevel(SeverityLevel.LOW); - tasks.add(task3); - - taskRepository.saveAll(tasks); - log.info("Task 数据初始化完成,共创建 {} 条记录", tasks.size()); - } - - // Helper class for coordinates - private static class Point { - int x, y; - // Point class implementation... - Point(int x, int y) { this.x = x; this.y = y; } - @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Point point = (Point) o; - return x == point.x && y == point.y; - } - @Override public int hashCode() { - return java.util.Objects.hash(x, y); - } - - List getShuffledNeighbors(int width, int height, Random rand) { - List neighbors = getNeighbors(width, height); - Collections.shuffle(neighbors, rand); - return neighbors; - } - - List getNeighbors(int width, int height) { - List neighbors = new ArrayList<>(); - if (x > 0) neighbors.add(new Point(x - 1, y)); - if (x < width - 1) neighbors.add(new Point(x + 1, y)); - if (y > 0) neighbors.add(new Point(x, y - 1)); - if (y < height - 1) neighbors.add(new Point(x, y + 1)); - return neighbors; - } - } +package com.dne.ems.config; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import com.dne.ems.dto.CityBlueprint; +import com.dne.ems.model.AqiRecord; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.Grid; +import com.dne.ems.model.MapGrid; +import com.dne.ems.model.Task; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.model.enums.TaskStatus; +import com.dne.ems.model.enums.UserStatus; +import com.dne.ems.repository.AqiRecordRepository; +import com.dne.ems.repository.AssignmentRepository; +import com.dne.ems.repository.FeedbackRepository; +import com.dne.ems.repository.GridRepository; +import com.dne.ems.repository.MapGridRepository; +import com.dne.ems.repository.TaskRepository; +import com.dne.ems.repository.UserAccountRepository; + +import lombok.RequiredArgsConstructor; + +/** + * Initializes the database with seed data upon application startup. + * This component is crucial for setting up a baseline state for the application, + * especially in development and testing environments. It populates grids, user accounts, + * and other essential data. + */ +@Component +@RequiredArgsConstructor +public class DataInitializer implements CommandLineRunner { + + private final GridRepository gridRepository; + private final UserAccountRepository userAccountRepository; + private final PasswordEncoder passwordEncoder; + private final AqiRecordRepository aqiRecordRepository; + private final MapGridRepository mapGridRepository; + private final TaskRepository taskRepository; + private final AssignmentRepository assignmentRepository; + private final FeedbackRepository feedbackRepository; + + private static final Logger log = LoggerFactory.getLogger(DataInitializer.class); + + @Override + public void run(String... args) { + log.info("Starting data initialization..."); + try { + initializeGridData(); + initializeMapGridData(); // Ensure MapGrid is synchronized + initializeUsers(); + initializeAqiData(); + initializeFeedbackData(); + initializeTaskData(); + } catch (Exception e) { + log.error("Error during data initialization", e); + } + log.info("Data initialization finished."); + } + + private void initializeGridData() { + if (gridRepository.count() > 0) { + log.info("Grid data already exists. Skipping initialization."); + return; + } + log.info("Creating initial grid data..."); + + final int MAP_SIZE = 40; + final double OBSTACLE_CHANCE = 0.05; // 5% chance for a cell to be an obstacle + + Grid[][] map = new Grid[MAP_SIZE][MAP_SIZE]; + Random random = new Random(); + List gridsToSave = new ArrayList<>(); + + // 1. Define the 4 cities + List cityBlueprints = List.of( + new CityBlueprint("常州市", "溧阳市", 0, 0, 0, 0), + new CityBlueprint("无锡市", "宜兴市", 0, 0, 0, 0), + new CityBlueprint("苏州市", "昆山市", 0, 0, 0, 0), + new CityBlueprint("南京市", "江宁区", 0, 0, 0, 0) + ); + + // 2. Select 4 unique random seeds (capitals) for the cities + Queue expansionQueue = new LinkedList<>(); + Set usedSeeds = new HashSet<>(); + + for (CityBlueprint city : cityBlueprints) { + Point seed; + do { + seed = new Point(random.nextInt(MAP_SIZE), random.nextInt(MAP_SIZE)); + } while (usedSeeds.contains(seed)); + + usedSeeds.add(seed); + + Grid grid = new Grid(); + grid.setGridX(seed.x); + grid.setGridY(seed.y); + grid.setCityName(city.getCityName()); + grid.setDistrictName(city.getDistrictName()); + grid.setIsObstacle(false); // Capitals are never obstacles + + map[seed.x][seed.y] = grid; + expansionQueue.add(seed); + } + + // 3. Expand all cities simultaneously using Breadth-First Search (BFS) + // This ensures every cell belongs to the closest city, creating Voronoi-like regions + while (!expansionQueue.isEmpty()) { + Point p = expansionQueue.poll(); + Grid centerGrid = map[p.x][p.y]; + + // Use shuffled neighbors to make borders more irregular and interesting + for (Point neighbor : p.getShuffledNeighbors(MAP_SIZE, MAP_SIZE, random)) { + if (map[neighbor.x][neighbor.y] == null) { // If the cell is unclaimed + Grid neighborGrid = new Grid(); + neighborGrid.setGridX(neighbor.x); + neighborGrid.setGridY(neighbor.y); + neighborGrid.setCityName(centerGrid.getCityName()); + neighborGrid.setDistrictName(centerGrid.getDistrictName()); + neighborGrid.setIsObstacle(false); // Set obstacle status later + + map[neighbor.x][neighbor.y] = neighborGrid; + expansionQueue.add(neighbor); + } + } + } + + // 4. Identify border cells to keep them clear of obstacles + Set borderCells = new HashSet<>(); + for (int x = 0; x < MAP_SIZE; x++) { + for (int y = 0; y < MAP_SIZE; y++) { + Grid currentGrid = map[x][y]; + if (currentGrid == null) { + // This should never happen as we ensure every cell is assigned to a city + log.error("发现未分配城市的格子 at ({}, {}), 这是一个错误", x, y); + continue; + } + + Point currentPoint = new Point(x, y); + for (Point neighbor : currentPoint.getNeighbors(MAP_SIZE, MAP_SIZE)) { + Grid neighborGrid = map[neighbor.x][neighbor.y]; + if (neighborGrid != null && !currentGrid.getCityName().equals(neighborGrid.getCityName())) { + borderCells.add(currentPoint); + break; // Found one neighbor from another city, this is a border cell + } + } + } + } + + // 5. Add random obstacles, avoiding borders and capitals + for (int x = 0; x < MAP_SIZE; x++) { + for (int y = 0; y < MAP_SIZE; y++) { + Grid grid = map[x][y]; + if (grid != null) { + boolean isSeed = usedSeeds.contains(new Point(x, y)); + boolean isBorder = borderCells.contains(new Point(x, y)); + + // Don't make capitals or border cells obstacles + if (!isSeed && !isBorder && random.nextDouble() < OBSTACLE_CHANCE) { + grid.setIsObstacle(true); + } + gridsToSave.add(grid); + } else { + // This should never happen - log an error if it does + log.error("发现空格子 at ({}, {}), 这是一个算法错误", x, y); + } + } + } + + gridRepository.saveAll(gridsToSave); + log.info("{} grid cells created.", gridsToSave.size()); + } + + /** + * Synchronizes the grid data with the map_grid table for A* pathfinding. + */ + private void initializeMapGridData() { + if (mapGridRepository.count() > 0) { + log.info("MapGrid data already exists. Skipping synchronization."); + return; + } + log.info("Synchronizing data to MapGrid for pathfinding..."); + + List grids = gridRepository.findAll(); + List mapGrids = grids.stream() + .map(grid -> MapGrid.builder() + .x(grid.getGridX()) + .y(grid.getGridY()) + .isObstacle(grid.getIsObstacle()) + .cityName(grid.getCityName()) + .build()) + .collect(Collectors.toList()); + + mapGridRepository.saveAll(mapGrids); + log.info("Synchronized {} grid cells to MapGrid.", mapGrids.size()); + } + + private void initializeUsers() { + if (userAccountRepository.count() > 0) { + log.info("User data already exists. Skipping initialization."); + return; + } + log.info("Creating initial user accounts..."); + + // Create Admin + UserAccount admin = new UserAccount(); + admin.setName("系统管理员"); + admin.setEmail("admin@example.com"); + admin.setPhone("13800138000"); + admin.setPassword(passwordEncoder.encode("password")); + admin.setRole(Role.ADMIN); + admin.setStatus(UserStatus.ACTIVE); + + // Create Decision Maker + UserAccount decisionMaker = new UserAccount(); + decisionMaker.setName("决策者"); + decisionMaker.setEmail("decision@example.com"); + decisionMaker.setPhone("13900139000"); + decisionMaker.setPassword(passwordEncoder.encode("password")); + decisionMaker.setRole(Role.DECISION_MAKER); + decisionMaker.setStatus(UserStatus.ACTIVE); + + // Create Supervisors + UserAccount supervisor1 = new UserAccount(); + supervisor1.setName("主管A"); + supervisor1.setEmail("supervisor1@example.com"); + supervisor1.setPhone("13700137001"); + supervisor1.setPassword(passwordEncoder.encode("password")); + supervisor1.setRole(Role.SUPERVISOR); + supervisor1.setStatus(UserStatus.ACTIVE); + supervisor1.setRegion("南京市"); + + UserAccount supervisor2 = new UserAccount(); + supervisor2.setName("主管B"); + supervisor2.setEmail("supervisor2@example.com"); + supervisor2.setPhone("13700137002"); + supervisor2.setPassword(passwordEncoder.encode("password")); + supervisor2.setRole(Role.SUPERVISOR); + supervisor2.setStatus(UserStatus.ACTIVE); + supervisor2.setRegion("无锡市"); + + // 添加特定的主管用户,用于任务初始化 + UserAccount specialSupervisor = new UserAccount(); + specialSupervisor.setName("特定主管"); + specialSupervisor.setEmail("supervisor@aizhangz.top"); + specialSupervisor.setPhone("13700137003"); + specialSupervisor.setPassword(passwordEncoder.encode("password")); + specialSupervisor.setRole(Role.SUPERVISOR); + specialSupervisor.setStatus(UserStatus.ACTIVE); + specialSupervisor.setRegion("苏州市"); + + // 添加公众监督员,用于反馈初始化 + UserAccount publicSupervisor = new UserAccount(); + publicSupervisor.setName("公众监督员"); + publicSupervisor.setEmail("public.supervisor@aizhangz.top"); + publicSupervisor.setPhone("13700137004"); + publicSupervisor.setPassword(passwordEncoder.encode("password")); + publicSupervisor.setRole(Role.PUBLIC_SUPERVISOR); + publicSupervisor.setStatus(UserStatus.ACTIVE); + + userAccountRepository.saveAll(List.of(admin, decisionMaker, supervisor1, supervisor2, specialSupervisor, publicSupervisor)); + + // Create Grid Workers + List workers = new ArrayList<>(); + List availableGrids = gridRepository.findByIsObstacle(false); + + int workerCount = 0; + for (Grid grid : availableGrids) { + if (workerCount >= 50) break; // Create up to 50 workers + + UserAccount worker = new UserAccount(); + worker.setName("网格员" + (workerCount + 1)); + worker.setPhone("1470000" + String.format("%04d", workerCount)); + worker.setEmail("worker" + (workerCount + 1) + "@example.com"); + worker.setPassword(passwordEncoder.encode("password")); + worker.setRole(Role.GRID_WORKER); + worker.setStatus(UserStatus.ACTIVE); + worker.setGridX(grid.getGridX()); + worker.setGridY(grid.getGridY()); + worker.setRegion(grid.getCityName()); + worker.setGender(workerCount % 2 == 0 ? com.dne.ems.model.enums.Gender.MALE : com.dne.ems.model.enums.Gender.FEMALE); + workers.add(worker); + workerCount++; + } + + // 添加特定的网格员,用于任务初始化 + UserAccount specialWorker = new UserAccount(); + specialWorker.setName("特定网格员"); + specialWorker.setPhone("14700009999"); + specialWorker.setEmail("worker@aizhangz.top"); + specialWorker.setPassword(passwordEncoder.encode("password")); + specialWorker.setRole(Role.GRID_WORKER); + specialWorker.setStatus(UserStatus.ACTIVE); + specialWorker.setGridX(10); + specialWorker.setGridY(10); + specialWorker.setRegion("苏州市"); + specialWorker.setGender(com.dne.ems.model.enums.Gender.MALE); + workers.add(specialWorker); + + userAccountRepository.saveAll(workers); + + log.info("Created Admin, DecisionMaker, 2 Supervisors, and {} Grid Workers.", workers.size()); + } + + private void initializeAqiData() { + if (aqiRecordRepository.count() > 0) { + log.info("AQI data already exists. Skipping initialization."); + return; + } + log.info("Creating initial AQI data..."); + + // 清空现有数据 + aqiRecordRepository.deleteAll(); + + List records = new ArrayList<>(); + + // 获取网格数据,用于关联 + List grids = gridRepository.findAll(); + if (grids.isEmpty()) { + log.warn("没有找到网格数据,无法完全初始化AQI数据"); + // 即使没有网格数据,也继续初始化基本的 AQI 数据 + } + + // 为不同城市选择不同的网格 + // Map> gridsByCity = grids.stream() + // .collect(Collectors.groupingBy(Grid::getCityName)); + + // 基本 AQI 数据 - 不同级别 + // Good (0-50) + AqiRecord record1 = new AqiRecord("北京市", 40, LocalDateTime.now().minusDays(1), 10.5, 20.3, 5.1, 15.2, 0.8, 30.1); + record1.setGridX(0); + record1.setGridY(0); + records.add(record1); + + AqiRecord record2 = new AqiRecord("上海市", 35, LocalDateTime.now().minusDays(2), 9.2, 18.5, 4.8, 14.0, 0.7, 28.5); + record2.setGridX(0); + record2.setGridY(1); + records.add(record2); + + // Moderate (51-100) + AqiRecord record3 = new AqiRecord("广州市", 75, LocalDateTime.now().minusDays(3), 25.3, 45.7, 10.2, 30.5, 1.5, 60.2); + record3.setGridX(1); + record3.setGridY(0); + records.add(record3); + + AqiRecord record4 = new AqiRecord("深圳市", 85, LocalDateTime.now().minusDays(4), 28.6, 52.1, 12.3, 35.8, 1.8, 68.5); + record4.setGridX(1); + record4.setGridY(1); + records.add(record4); + + // Unhealthy for Sensitive Groups (101-150) + AqiRecord record5 = new AqiRecord("成都市", 130, LocalDateTime.now().minusDays(5), 45.2, 85.3, 20.5, 55.8, 3.2, 90.5); + record5.setGridX(2); + record5.setGridY(0); + records.add(record5); + + // Unhealthy (151-200) + AqiRecord record6 = new AqiRecord("武汉市", 175, LocalDateTime.now().minusDays(6), 60.5, 110.8, 30.2, 70.5, 4.5, 120.3); + record6.setGridX(2); + record6.setGridY(1); + records.add(record6); + + // Very Unhealthy (201-300) + AqiRecord record7 = new AqiRecord("西安市", 250, LocalDateTime.now().minusDays(7), 90.3, 160.5, 45.8, 95.2, 6.8, 150.5); + record7.setGridX(3); + record7.setGridY(0); + records.add(record7); + + // Hazardous (301+) + AqiRecord record8 = new AqiRecord("兰州市", 320, LocalDateTime.now().minusDays(8), 120.5, 200.8, 60.2, 120.5, 8.5, 180.3); + record8.setGridX(3); + record8.setGridY(1); + records.add(record8); + + // 保存基本数据 + List savedRecords = aqiRecordRepository.saveAll(records); + log.info("已保存基本 AQI 数据: {} 条记录", savedRecords.size()); + records.clear(); + + // 添加带有网格位置的数据 - 用于热力图 + // 直接使用网格坐标,不关联 Grid 实体 + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + int aqiValue = 50 + (x + y) * 15; // 不同 AQI 值 + LocalDateTime recordTime = LocalDateTime.now().minusDays(x + y); + + AqiRecord record = new AqiRecord("北京市", aqiValue, recordTime, + 20.0 + x, 40.0 + y * 2, 8.0 + x, 25.0 + y, 1.0 + x * 0.2, 50.0 + y * 3); + record.setGridX(x); + record.setGridY(y); + records.add(record); + } + } + + // 保存热力图数据 + savedRecords = aqiRecordRepository.saveAll(records); + log.info("已保存热力图 AQI 数据: {} 条记录", savedRecords.size()); + records.clear(); + + // 添加不同月份的数据 - 用于趋势图 + for (int i = 1; i <= 6; i++) { + for (int j = 0; j < 3; j++) { // 每个月添加多条记录 + LocalDateTime recordTime = LocalDateTime.now().minusMonths(i).plusDays(j * 5); + int aqiValue = 80 + i * 20 + j * 5; // 确保有些超过 100 的值 + + AqiRecord record = new AqiRecord("北京市", aqiValue, recordTime, + 30.0 + i, 60.0 + i * 2, 12.0 + i, 35.0 + i, 1.5 + i * 0.2, 70.0 + i * 3); + // 为趋势数据也设置网格坐标 + record.setGridX(i % 5); + record.setGridY(j % 5); + records.add(record); + } + } + + // 保存趋势图数据 + savedRecords = aqiRecordRepository.saveAll(records); + log.info("已保存趋势图 AQI 数据: {} 条记录", savedRecords.size()); + + log.info("Created {} AQI records.", records.size()); + aqiRecordRepository.saveAll(records); + } + + private void initializeFeedbackData() { + if (feedbackRepository.count() > 0) { + log.info("Feedback 数据已存在,跳过初始化"); + return; + } + + // 清除现有数据并重新初始化 + // 必须先清除子表数据,再清除父表数据,以避免外键约束冲突 + assignmentRepository.deleteAll(); + taskRepository.deleteAll(); + feedbackRepository.deleteAll(); + log.info("已清除现有的 Assignment, Task, 和 Feedback 数据,开始重新初始化..."); + + // 使用PUBLIC_SUPERVISOR角色的用户作为反馈提交者 + UserAccount reporter = userAccountRepository.findByEmail("public.supervisor@aizhangz.top").orElse(null); + if (reporter == null) { + log.warn("无法找到公众监督员 'public.supervisor@aizhangz.top',跳过 Feedback 初始化"); + return; + } + + // 创建反馈数据 + List feedbacks = new ArrayList<>(); + + Feedback feedback1 = new Feedback(); + feedback1.setEventId("FB-202301"); + feedback1.setTitle("工业区PM2.5超标"); + feedback1.setDescription("市中心化工厂在夜间排放黄色烟雾,气味刺鼻。"); + feedback1.setLatitude(39.9042); + feedback1.setLongitude(116.4074); + feedback1.setGridX(5); + feedback1.setGridY(5); + feedback1.setPollutionType(PollutionType.PM25); + feedback1.setSeverityLevel(SeverityLevel.HIGH); + feedback1.setStatus(FeedbackStatus.PENDING_ASSIGNMENT); + feedback1.setUser(reporter); + feedback1.setSubmitterId(reporter.getId()); + feedbacks.add(feedback1); + + Feedback feedback2 = new Feedback(); + feedback2.setEventId("FB-202302"); + feedback2.setTitle("交通要道NO2浓度过高"); + feedback2.setDescription("主干道车辆拥堵,空气质量差。"); + feedback2.setLatitude(39.915); + feedback2.setLongitude(116.400); + feedback2.setGridX(6); + feedback2.setGridY(6); + feedback2.setPollutionType(PollutionType.NO2); + feedback2.setSeverityLevel(SeverityLevel.MEDIUM); + feedback2.setStatus(FeedbackStatus.ASSIGNED); + feedback2.setUser(reporter); + feedback2.setSubmitterId(reporter.getId()); + feedbacks.add(feedback2); + + Feedback feedback3 = new Feedback(); + feedback3.setEventId("FB-202303"); + feedback3.setTitle("未知来源的SO2异味"); + feedback3.setDescription("居民区闻到类似烧煤的刺鼻气味。"); + feedback3.setLatitude(39.888); + feedback3.setLongitude(116.356); + feedback3.setGridX(7); + feedback3.setGridY(7); + feedback3.setPollutionType(PollutionType.SO2); + feedback3.setSeverityLevel(SeverityLevel.LOW); + feedback3.setStatus(FeedbackStatus.PROCESSED); + feedback3.setUser(reporter); + feedback3.setSubmitterId(reporter.getId()); + feedbacks.add(feedback3); + + feedbackRepository.saveAll(feedbacks); + log.info("反馈数据初始化完成,共创建 {} 条记录", feedbacks.size()); + } + + private void initializeTaskData() { + if (taskRepository.count() > 0) { + log.info("Task 数据已存在,跳过初始化"); + return; + } + log.info("开始初始化 Task 模拟数据..."); + + UserAccount worker = userAccountRepository.findByEmail("worker@aizhangz.top").orElse(null); + if (worker == null) { + log.warn("无法找到网格员 'worker@aizhangz.top',跳过 Task 初始化"); + return; + } + + UserAccount creator = userAccountRepository.findByEmail("supervisor@aizhangz.top").orElse(null); + if (creator == null) { + log.warn("无法找到主管 'supervisor@aizhangz.top',跳过 Task 初始化"); + return; + } + + List tasks = new ArrayList<>(); + + Task task1 = new Task(); + task1.setTitle("调查PM2.5超标问题"); + task1.setDescription("前往工业区,使用便携式检测仪检测空气质量,并记录排放情况。"); + task1.setAssignee(worker); + task1.setCreatedBy(creator); + task1.setStatus(TaskStatus.IN_PROGRESS); + task1.setAssignedAt(LocalDateTime.now().minusDays(2)); + task1.setLatitude(39.9042); + task1.setLongitude(116.4074); + task1.setGridX(2); + task1.setGridY(3); + task1.setSeverityLevel(SeverityLevel.HIGH); + tasks.add(task1); + + Task task2 = new Task(); + task2.setTitle("疏导交通并监测NO2"); + task2.setDescription("在交通要道关键节点进行监测,并与交管部门协调。"); + task2.setAssignee(worker); + task2.setCreatedBy(creator); + task2.setStatus(TaskStatus.COMPLETED); + task2.setCompletedAt(LocalDateTime.now().minusDays(1)); + task2.setAssignedAt(LocalDateTime.now().minusDays(4)); + task2.setLatitude(39.915); + task2.setLongitude(116.400); + task2.setGridX(4); + task2.setGridY(5); + task2.setSeverityLevel(SeverityLevel.MEDIUM); + tasks.add(task2); + + Task task3 = new Task(); + task3.setTitle("排查SO2异味源"); + task3.setDescription("在相关居民区进行走访和气味溯源。"); + task3.setAssignee(worker); + task3.setCreatedBy(creator); + task3.setStatus(TaskStatus.ASSIGNED); + task3.setAssignedAt(LocalDateTime.now()); + task3.setLatitude(39.888); + task3.setLongitude(116.356); + task3.setGridX(6); + task3.setGridY(7); + task3.setSeverityLevel(SeverityLevel.LOW); + tasks.add(task3); + + taskRepository.saveAll(tasks); + log.info("Task 数据初始化完成,共创建 {} 条记录", tasks.size()); + } + + // Helper class for coordinates + private static class Point { + int x, y; + // Point class implementation... + Point(int x, int y) { this.x = x; this.y = y; } + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Point point = (Point) o; + return x == point.x && y == point.y; + } + @Override public int hashCode() { + return java.util.Objects.hash(x, y); + } + + List getShuffledNeighbors(int width, int height, Random rand) { + List neighbors = getNeighbors(width, height); + Collections.shuffle(neighbors, rand); + return neighbors; + } + + List getNeighbors(int width, int height) { + List neighbors = new ArrayList<>(); + if (x > 0) neighbors.add(new Point(x - 1, y)); + if (x < width - 1) neighbors.add(new Point(x + 1, y)); + if (y > 0) neighbors.add(new Point(x, y - 1)); + if (y < height - 1) neighbors.add(new Point(x, y + 1)); + return neighbors; + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/config/EmptyJpaAnnotations.java b/ems-backend/src/main/java/com/dne/ems/config/EmptyJpaAnnotations.java index 335f308..e16caaa 100644 --- a/ems-backend/src/main/java/com/dne/ems/config/EmptyJpaAnnotations.java +++ b/ems-backend/src/main/java/com/dne/ems/config/EmptyJpaAnnotations.java @@ -1,134 +1,134 @@ -package com.dne.ems.config; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * 这个类提供了空的JPA注解实现,用于在移除JPA依赖后保持代码兼容性。 - * 这些注解不会有任何实际效果,仅用于编译通过。 - */ -public class EmptyJpaAnnotations { - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.TYPE}) - public @interface Entity { - } - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.TYPE}) - public @interface Table { - String name() default ""; - String schema() default ""; - String catalog() default ""; - UniqueConstraint[] uniqueConstraints() default {}; - } - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.FIELD, ElementType.METHOD}) - public @interface Column { - String name() default ""; - boolean unique() default false; - boolean nullable() default true; - boolean insertable() default true; - boolean updatable() default true; - String columnDefinition() default ""; - String table() default ""; - int length() default 255; - int precision() default 0; - int scale() default 0; - } - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.FIELD, ElementType.METHOD}) - public @interface Id { - } - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.FIELD, ElementType.METHOD}) - public @interface GeneratedValue { - GenerationType strategy() default GenerationType.AUTO; - String generator() default ""; - } - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.FIELD, ElementType.METHOD}) - public @interface ManyToOne { - Class targetEntity() default void.class; - FetchType fetch() default FetchType.EAGER; - boolean optional() default true; - String mappedBy() default ""; - CascadeType[] cascade() default {}; - } - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.FIELD, ElementType.METHOD}) - public @interface OneToMany { - Class targetEntity() default void.class; - String mappedBy() default ""; - CascadeType[] cascade() default {}; - FetchType fetch() default FetchType.LAZY; - boolean orphanRemoval() default false; - } - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.FIELD, ElementType.METHOD}) - public @interface OneToOne { - Class targetEntity() default void.class; - String mappedBy() default ""; - CascadeType[] cascade() default {}; - FetchType fetch() default FetchType.EAGER; - boolean optional() default true; - boolean orphanRemoval() default false; - } - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.FIELD, ElementType.METHOD}) - public @interface ManyToMany { - Class targetEntity() default void.class; - String mappedBy() default ""; - CascadeType[] cascade() default {}; - FetchType fetch() default FetchType.LAZY; - } - - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.FIELD, ElementType.METHOD}) - public @interface JoinColumn { - String name() default ""; - String referencedColumnName() default ""; - boolean unique() default false; - boolean nullable() default true; - boolean insertable() default true; - boolean updatable() default true; - String columnDefinition() default ""; - String table() default ""; - } - - @Retention(RetentionPolicy.RUNTIME) - public @interface UniqueConstraint { - String name() default ""; - String[] columnNames(); - } - - public enum GenerationType { - TABLE, - SEQUENCE, - IDENTITY, - AUTO - } - - public enum FetchType { - LAZY, - EAGER - } - - public enum CascadeType { - ALL, - PERSIST, - MERGE, - REMOVE, - REFRESH, - DETACH - } +package com.dne.ems.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 这个类提供了空的JPA注解实现,用于在移除JPA依赖后保持代码兼容性。 + * 这些注解不会有任何实际效果,仅用于编译通过。 + */ +public class EmptyJpaAnnotations { + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE}) + public @interface Entity { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE}) + public @interface Table { + String name() default ""; + String schema() default ""; + String catalog() default ""; + UniqueConstraint[] uniqueConstraints() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.METHOD}) + public @interface Column { + String name() default ""; + boolean unique() default false; + boolean nullable() default true; + boolean insertable() default true; + boolean updatable() default true; + String columnDefinition() default ""; + String table() default ""; + int length() default 255; + int precision() default 0; + int scale() default 0; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.METHOD}) + public @interface Id { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.METHOD}) + public @interface GeneratedValue { + GenerationType strategy() default GenerationType.AUTO; + String generator() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.METHOD}) + public @interface ManyToOne { + Class targetEntity() default void.class; + FetchType fetch() default FetchType.EAGER; + boolean optional() default true; + String mappedBy() default ""; + CascadeType[] cascade() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.METHOD}) + public @interface OneToMany { + Class targetEntity() default void.class; + String mappedBy() default ""; + CascadeType[] cascade() default {}; + FetchType fetch() default FetchType.LAZY; + boolean orphanRemoval() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.METHOD}) + public @interface OneToOne { + Class targetEntity() default void.class; + String mappedBy() default ""; + CascadeType[] cascade() default {}; + FetchType fetch() default FetchType.EAGER; + boolean optional() default true; + boolean orphanRemoval() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.METHOD}) + public @interface ManyToMany { + Class targetEntity() default void.class; + String mappedBy() default ""; + CascadeType[] cascade() default {}; + FetchType fetch() default FetchType.LAZY; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.METHOD}) + public @interface JoinColumn { + String name() default ""; + String referencedColumnName() default ""; + boolean unique() default false; + boolean nullable() default true; + boolean insertable() default true; + boolean updatable() default true; + String columnDefinition() default ""; + String table() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface UniqueConstraint { + String name() default ""; + String[] columnNames(); + } + + public enum GenerationType { + TABLE, + SEQUENCE, + IDENTITY, + AUTO + } + + public enum FetchType { + LAZY, + EAGER + } + + public enum CascadeType { + ALL, + PERSIST, + MERGE, + REMOVE, + REFRESH, + DETACH + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/config/FileStorageProperties.java b/ems-backend/src/main/java/com/dne/ems/config/FileStorageProperties.java index 3db8328..3161d65 100644 --- a/ems-backend/src/main/java/com/dne/ems/config/FileStorageProperties.java +++ b/ems-backend/src/main/java/com/dne/ems/config/FileStorageProperties.java @@ -1,37 +1,37 @@ -package com.dne.ems.config; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -import lombok.Data; - -/** - * 文件存储配置属性类,用于管理文件上传存储的相关配置 - * - *

该配置类通过Spring Boot的@ConfigurationProperties注解, - * 自动绑定application配置文件中以"file"为前缀的属性。 - * 主要用于指定上传文件的存储目录。 - * - *

使用示例:在application.yml中配置 - *

- * file:
- *   upload-dir: /path/to/uploads
- * 
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Component -@ConfigurationProperties(prefix = "file") -@Data -public class FileStorageProperties { - - /** - * The directory where uploaded files will be stored. - * This path should be configured in the application.yml file. - * Example: file.upload-dir=/path/to/your/upload-dir - */ - private String uploadDir; - +package com.dne.ems.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import lombok.Data; + +/** + * 文件存储配置属性类,用于管理文件上传存储的相关配置 + * + *

该配置类通过Spring Boot的@ConfigurationProperties注解, + * 自动绑定application配置文件中以"file"为前缀的属性。 + * 主要用于指定上传文件的存储目录。 + * + *

使用示例:在application.yml中配置 + *

+ * file:
+ *   upload-dir: /path/to/uploads
+ * 
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Component +@ConfigurationProperties(prefix = "file") +@Data +public class FileStorageProperties { + + /** + * The directory where uploaded files will be stored. + * This path should be configured in the application.yml file. + * Example: file.upload-dir=/path/to/your/upload-dir + */ + private String uploadDir; + } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/config/OpenApiConfig.java b/ems-backend/src/main/java/com/dne/ems/config/OpenApiConfig.java index 9215c06..c84fc51 100644 --- a/ems-backend/src/main/java/com/dne/ems/config/OpenApiConfig.java +++ b/ems-backend/src/main/java/com/dne/ems/config/OpenApiConfig.java @@ -1,40 +1,40 @@ -package com.dne.ems.config; - -import org.springframework.context.annotation.Configuration; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; -import io.swagger.v3.oas.annotations.info.Info; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.security.SecurityScheme; - -/** - * OpenAPI配置类,用于配置Swagger API文档 - * - *

该配置类使用SpringDoc OpenAPI实现,提供以下功能: - *

    - *
  • 配置API文档的基本信息(标题、版本等)
  • - *
  • 配置JWT Bearer Token认证方案
  • - *
  • 为所有API端点添加安全要求
  • - *
- * - *

通过该配置,可以在/swagger-ui.html访问API文档界面 - * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Configuration -@OpenAPIDefinition( - info = @Info(title = "EMS API", version = "v1"), - security = @SecurityRequirement(name = "bearerAuth") -) -@SecurityScheme( - name = "bearerAuth", - type = SecuritySchemeType.HTTP, - scheme = "bearer", - bearerFormat = "JWT" -) -public class OpenApiConfig { - // 配置类,无需实现内容 +package com.dne.ems.config; + +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; + +/** + * OpenAPI配置类,用于配置Swagger API文档 + * + *

该配置类使用SpringDoc OpenAPI实现,提供以下功能: + *

    + *
  • 配置API文档的基本信息(标题、版本等)
  • + *
  • 配置JWT Bearer Token认证方案
  • + *
  • 为所有API端点添加安全要求
  • + *
+ * + *

通过该配置,可以在/swagger-ui.html访问API文档界面 + * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Configuration +@OpenAPIDefinition( + info = @Info(title = "EMS API", version = "v1"), + security = @SecurityRequirement(name = "bearerAuth") +) +@SecurityScheme( + name = "bearerAuth", + type = SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT" +) +public class OpenApiConfig { + // 配置类,无需实现内容 } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/config/RestTemplateConfig.java b/ems-backend/src/main/java/com/dne/ems/config/RestTemplateConfig.java index 3e149ec..7af97d5 100644 --- a/ems-backend/src/main/java/com/dne/ems/config/RestTemplateConfig.java +++ b/ems-backend/src/main/java/com/dne/ems/config/RestTemplateConfig.java @@ -1,36 +1,36 @@ -package com.dne.ems.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestTemplate; - -/** - * RestTemplate配置类,提供HTTP客户端功能 - * - *

该配置类创建并配置RestTemplate Bean,用于执行HTTP请求。 - * RestTemplate是Spring提供的用于与RESTful服务交互的核心类, - * 支持各种HTTP方法(GET、POST、PUT、DELETE等)。 - * - *

主要用途: - *

    - *
  • 与外部API进行集成
  • - *
  • 执行微服务间的通信
  • - *
  • 获取外部数据源的信息
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Configuration -public class RestTemplateConfig { - - /** - * 创建并配置 RestTemplate Bean - * @return 配置好的 RestTemplate 实例 - */ - @Bean - public RestTemplate restTemplate() { - return new RestTemplate(); - } +package com.dne.ems.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * RestTemplate配置类,提供HTTP客户端功能 + * + *

该配置类创建并配置RestTemplate Bean,用于执行HTTP请求。 + * RestTemplate是Spring提供的用于与RESTful服务交互的核心类, + * 支持各种HTTP方法(GET、POST、PUT、DELETE等)。 + * + *

主要用途: + *

    + *
  • 与外部API进行集成
  • + *
  • 执行微服务间的通信
  • + *
  • 获取外部数据源的信息
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Configuration +public class RestTemplateConfig { + + /** + * 创建并配置 RestTemplate Bean + * @return 配置好的 RestTemplate 实例 + */ + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/config/WebClientConfig.java b/ems-backend/src/main/java/com/dne/ems/config/WebClientConfig.java index c856151..c913a8a 100644 --- a/ems-backend/src/main/java/com/dne/ems/config/WebClientConfig.java +++ b/ems-backend/src/main/java/com/dne/ems/config/WebClientConfig.java @@ -1,33 +1,33 @@ -package com.dne.ems.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * WebClient配置类,提供响应式HTTP客户端功能 - * - *

该配置类创建并配置WebClient Bean,用于执行响应式HTTP请求。 - * WebClient是Spring WebFlux提供的非阻塞、响应式的HTTP客户端, - * 支持异步请求处理和响应式流。 - * - *

主要优势: - *

    - *
  • 非阻塞I/O,提高系统吞吐量
  • - *
  • 支持响应式编程模型
  • - *
  • 提供流式处理能力
  • - *
  • 适用于高并发场景
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient() { - return WebClient.builder().build(); - } +package com.dne.ems.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * WebClient配置类,提供响应式HTTP客户端功能 + * + *

该配置类创建并配置WebClient Bean,用于执行响应式HTTP请求。 + * WebClient是Spring WebFlux提供的非阻塞、响应式的HTTP客户端, + * 支持异步请求处理和响应式流。 + * + *

主要优势: + *

    + *
  • 非阻塞I/O,提高系统吞吐量
  • + *
  • 支持响应式编程模型
  • + *
  • 提供流式处理能力
  • + *
  • 适用于高并发场景
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/config/converter/StringListConverter.java b/ems-backend/src/main/java/com/dne/ems/config/converter/StringListConverter.java index a1155ae..33379c6 100644 --- a/ems-backend/src/main/java/com/dne/ems/config/converter/StringListConverter.java +++ b/ems-backend/src/main/java/com/dne/ems/config/converter/StringListConverter.java @@ -1,51 +1,51 @@ -package com.dne.ems.config.converter; - -import java.util.Collections; -import java.util.List; - -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import jakarta.persistence.AttributeConverter; -import jakarta.persistence.Converter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Converter -@Component -@Slf4j -@RequiredArgsConstructor -public class StringListConverter implements AttributeConverter, String> { - - private final ObjectMapper objectMapper; - - @Override - public String convertToDatabaseColumn(List attribute) { - if (attribute == null || attribute.isEmpty()) { - return null; - } - try { - return objectMapper.writeValueAsString(attribute); - } catch (JsonProcessingException e) { - log.error("Could not convert list to JSON string", e); - throw new IllegalArgumentException("Error converting list to JSON", e); - } - } - - @Override - public List convertToEntityAttribute(String dbData) { - if (!StringUtils.hasText(dbData)) { - return Collections.emptyList(); - } - try { - return objectMapper.readValue(dbData, new TypeReference<>() {}); - } catch (JsonProcessingException e) { - log.warn("Could not deserialize JSON string to list: '{}'. Returning empty list.", dbData, e); - return Collections.emptyList(); - } - } +package com.dne.ems.config.converter; + +import java.util.Collections; +import java.util.List; + +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Converter +@Component +@Slf4j +@RequiredArgsConstructor +public class StringListConverter implements AttributeConverter, String> { + + private final ObjectMapper objectMapper; + + @Override + public String convertToDatabaseColumn(List attribute) { + if (attribute == null || attribute.isEmpty()) { + return null; + } + try { + return objectMapper.writeValueAsString(attribute); + } catch (JsonProcessingException e) { + log.error("Could not convert list to JSON string", e); + throw new IllegalArgumentException("Error converting list to JSON", e); + } + } + + @Override + public List convertToEntityAttribute(String dbData) { + if (!StringUtils.hasText(dbData)) { + return Collections.emptyList(); + } + try { + return objectMapper.readValue(dbData, new TypeReference<>() {}); + } catch (JsonProcessingException e) { + log.warn("Could not deserialize JSON string to list: '{}'. Returning empty list.", dbData, e); + return Collections.emptyList(); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/AuthController.java b/ems-backend/src/main/java/com/dne/ems/controller/AuthController.java index 4f74352..a5562c0 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/AuthController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/AuthController.java @@ -1,134 +1,134 @@ -package com.dne.ems.controller; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.dto.JwtAuthenticationResponse; -import com.dne.ems.dto.LoginRequest; -import com.dne.ems.dto.PasswordResetDto; -import com.dne.ems.dto.PasswordResetRequest; -import com.dne.ems.dto.PasswordResetWithCodeDto; -import com.dne.ems.dto.SignUpRequest; -import com.dne.ems.service.AuthService; -import com.dne.ems.service.OperationLogService; -import com.dne.ems.service.VerificationCodeService; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@RestController -@RequestMapping("/api/auth") -@RequiredArgsConstructor -@Slf4j -public class AuthController { - - private final AuthService authService; - private final VerificationCodeService verificationCodeService; - @SuppressWarnings("unused") - private final OperationLogService operationLogService; - - /** - * 用户注册接口 - * @param request 包含用户注册信息的请求体 - * @return 成功创建返回201状态码 - */ - @PostMapping("/signup") - public ResponseEntity signUp(@Valid @RequestBody SignUpRequest request) { - authService.registerUser(request); - return ResponseEntity.status(HttpStatus.CREATED).build(); - } - - /** - * 用户登录认证接口 - * @param request 包含用户凭证的登录请求体 - * @return 包含JWT令牌的认证响应实体 - */ - @PostMapping("/login") - public ResponseEntity signIn(@Valid @RequestBody LoginRequest request) { - return ResponseEntity.ok(authService.signIn(request)); - } - - /** - * 用户登出接口 - * @return 成功则返回200 OK - */ - @PostMapping("/logout") - public ResponseEntity logout() { - authService.logout(); - return ResponseEntity.ok().build(); - } - - /** - * 请求发送账号注册验证码到指定邮箱。 - * - * @param email 接收验证码的邮箱地址 - * @return 成功则返回200 OK - */ - @PostMapping("/send-verification-code") - public ResponseEntity sendVerificationCode(@RequestParam @NotBlank @Email String email) { - verificationCodeService.sendVerificationCode(email); - return ResponseEntity.ok().build(); - } - - /** - * 请求发送密码重置验证码到指定邮箱。 - * - * @param email 接收验证码的邮箱地址 - * @return 成功则返回200 OK - */ - @PostMapping("/send-password-reset-code") - public ResponseEntity sendPasswordResetCode(@RequestParam @NotBlank @Email String email) { - authService.requestPasswordReset(email); - return ResponseEntity.ok().build(); - } - - /** - * 请求密码重置(将发送包含验证码的邮件) - * - * @param request 包含邮箱的请求 - * @return 成功则返回200 OK - * @deprecated 使用 {@link #sendPasswordResetCode(String)} 代替 - */ - @PostMapping("/request-password-reset") - @Deprecated - public ResponseEntity requestPasswordReset(@Valid @RequestBody PasswordResetRequest request) { - authService.requestPasswordReset(request.email()); - return ResponseEntity.ok().build(); - } - - /** - * 使用验证码重置密码 - * - * @param request 包含邮箱、验证码和新密码的请求 - * @return 成功则返回200 OK - */ - @PostMapping("/reset-password-with-code") - public ResponseEntity resetPasswordWithCode(@Valid @RequestBody PasswordResetWithCodeDto request) { - authService.resetPasswordWithCode(request.email(), request.code(), request.newPassword()); - return ResponseEntity.ok().build(); - } - - /** - * 使用令牌重置密码(旧版API,保留以兼容) - * 这将使用令牌和新密码进行重置。 - * @param request 包含令牌和新密码的请求 - * @return 成功则返回200 OK - * @deprecated 使用基于验证码的 {@link #resetPasswordWithCode(PasswordResetWithCodeDto)} 替代 - */ - @PostMapping("/reset-password") - @Deprecated - public ResponseEntity resetPassword(@Valid @RequestBody PasswordResetDto request) { - authService.resetPassword(request.token(), request.newPassword()); - return ResponseEntity.ok().build(); - } - - // Internal DTOs have been moved to the com.dne.ems.dto package +package com.dne.ems.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.dto.JwtAuthenticationResponse; +import com.dne.ems.dto.LoginRequest; +import com.dne.ems.dto.PasswordResetDto; +import com.dne.ems.dto.PasswordResetRequest; +import com.dne.ems.dto.PasswordResetWithCodeDto; +import com.dne.ems.dto.SignUpRequest; +import com.dne.ems.service.AuthService; +import com.dne.ems.service.OperationLogService; +import com.dne.ems.service.VerificationCodeService; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +@Slf4j +public class AuthController { + + private final AuthService authService; + private final VerificationCodeService verificationCodeService; + @SuppressWarnings("unused") + private final OperationLogService operationLogService; + + /** + * 用户注册接口 + * @param request 包含用户注册信息的请求体 + * @return 成功创建返回201状态码 + */ + @PostMapping("/signup") + public ResponseEntity signUp(@Valid @RequestBody SignUpRequest request) { + authService.registerUser(request); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + /** + * 用户登录认证接口 + * @param request 包含用户凭证的登录请求体 + * @return 包含JWT令牌的认证响应实体 + */ + @PostMapping("/login") + public ResponseEntity signIn(@Valid @RequestBody LoginRequest request) { + return ResponseEntity.ok(authService.signIn(request)); + } + + /** + * 用户登出接口 + * @return 成功则返回200 OK + */ + @PostMapping("/logout") + public ResponseEntity logout() { + authService.logout(); + return ResponseEntity.ok().build(); + } + + /** + * 请求发送账号注册验证码到指定邮箱。 + * + * @param email 接收验证码的邮箱地址 + * @return 成功则返回200 OK + */ + @PostMapping("/send-verification-code") + public ResponseEntity sendVerificationCode(@RequestParam @NotBlank @Email String email) { + verificationCodeService.sendVerificationCode(email); + return ResponseEntity.ok().build(); + } + + /** + * 请求发送密码重置验证码到指定邮箱。 + * + * @param email 接收验证码的邮箱地址 + * @return 成功则返回200 OK + */ + @PostMapping("/send-password-reset-code") + public ResponseEntity sendPasswordResetCode(@RequestParam @NotBlank @Email String email) { + authService.requestPasswordReset(email); + return ResponseEntity.ok().build(); + } + + /** + * 请求密码重置(将发送包含验证码的邮件) + * + * @param request 包含邮箱的请求 + * @return 成功则返回200 OK + * @deprecated 使用 {@link #sendPasswordResetCode(String)} 代替 + */ + @PostMapping("/request-password-reset") + @Deprecated + public ResponseEntity requestPasswordReset(@Valid @RequestBody PasswordResetRequest request) { + authService.requestPasswordReset(request.email()); + return ResponseEntity.ok().build(); + } + + /** + * 使用验证码重置密码 + * + * @param request 包含邮箱、验证码和新密码的请求 + * @return 成功则返回200 OK + */ + @PostMapping("/reset-password-with-code") + public ResponseEntity resetPasswordWithCode(@Valid @RequestBody PasswordResetWithCodeDto request) { + authService.resetPasswordWithCode(request.email(), request.code(), request.newPassword()); + return ResponseEntity.ok().build(); + } + + /** + * 使用令牌重置密码(旧版API,保留以兼容) + * 这将使用令牌和新密码进行重置。 + * @param request 包含令牌和新密码的请求 + * @return 成功则返回200 OK + * @deprecated 使用基于验证码的 {@link #resetPasswordWithCode(PasswordResetWithCodeDto)} 替代 + */ + @PostMapping("/reset-password") + @Deprecated + public ResponseEntity resetPassword(@Valid @RequestBody PasswordResetDto request) { + authService.resetPassword(request.token(), request.newPassword()); + return ResponseEntity.ok().build(); + } + + // Internal DTOs have been moved to the com.dne.ems.dto package } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/DashboardController.java b/ems-backend/src/main/java/com/dne/ems/controller/DashboardController.java index c4abf90..ab602c9 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/DashboardController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/DashboardController.java @@ -1,237 +1,237 @@ -package com.dne.ems.controller; - -import java.util.List; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.dto.AqiDistributionDTO; -import com.dne.ems.dto.AqiHeatmapPointDTO; -import com.dne.ems.dto.DashboardStatsDTO; -import com.dne.ems.dto.GridCoverageDTO; -import com.dne.ems.dto.HeatmapPointDTO; -import com.dne.ems.dto.PollutantThresholdDTO; -import com.dne.ems.dto.PollutionStatsDTO; -import com.dne.ems.dto.TaskStatsDTO; -import com.dne.ems.dto.TrendDataPointDTO; -import com.dne.ems.service.DashboardService; - -import lombok.RequiredArgsConstructor; - -/** - * 仪表盘数据控制器,提供各类环境监测数据的统计和可视化接口 - * - *

主要功能包括: - *

    - *
  • 环境质量指标统计
  • - *
  • AQI数据分布和热力图
  • - *
  • 污染物阈值管理
  • - *
  • 任务完成情况统计
  • - *
  • 各类趋势分析数据
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api/dashboard") -@RequiredArgsConstructor -public class DashboardController { - - private final DashboardService dashboardService; - private static final Logger log = LoggerFactory.getLogger(DashboardController.class); - - /** - * 获取仪表盘核心统计数据 - * - * @return 包含各类核心统计数据的响应实体 - * @see DashboardStatsDTO - */ - @GetMapping("/stats") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity getDashboardStats() { - DashboardStatsDTO stats = dashboardService.getDashboardStats(); - return ResponseEntity.ok(stats); - } - - /** - * 获取AQI等级分布数据 - * - * @return 包含各AQI等级分布数据的响应实体 - * @see AqiDistributionDTO - */ - @GetMapping("/reports/aqi-distribution") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity> getAqiDistribution() { - List distribution = dashboardService.getAqiDistribution(); - return ResponseEntity.ok(distribution); - } - - /** - * 获取月度超标趋势数据 - * - * @return 包含月度超标趋势数据的响应实体 - * @see TrendDataPointDTO - */ - @GetMapping("/reports/monthly-exceedance-trend") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity> getMonthlyExceedanceTrend() { - List trend = dashboardService.getMonthlyExceedanceTrend(); - return ResponseEntity.ok(trend); - } - - /** - * 获取网格覆盖情况数据 - * - * @return 包含各城市网格覆盖情况的响应实体 - * @see GridCoverageDTO - */ - @GetMapping("/reports/grid-coverage") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity> getGridCoverage() { - List coverage = dashboardService.getGridCoverageByCity(); - return ResponseEntity.ok(coverage); - } - - /** - * 获取反馈热力图数据 - * - * @return 包含热力图坐标点数据的响应实体 - * @throws ResourceNotFoundException 如果未找到任何热力图数据 - * @see HeatmapPointDTO - */ - @GetMapping("/map/heatmap") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity> getHeatmapData() { - List heatmapData = dashboardService.getHeatmapData(); - if (heatmapData == null || heatmapData.isEmpty()) { - log.warn("反馈热力图数据为空"); - } else { - log.info("成功获取反馈热力图数据,共 {} 个数据点", heatmapData.size()); - } - return ResponseEntity.ok(heatmapData); - } - - /** - * 获取污染物统计报告数据 - * - * @return 包含各类污染物统计数据的响应实体 - * @see PollutionStatsDTO - */ - @GetMapping("/reports/pollution-stats") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity> getPollutionStats() { - List stats = dashboardService.getPollutionStats(); - return ResponseEntity.ok(stats); - } - - /** - * 获取任务完成情况统计数据 - * - * @return 包含任务完成率等统计数据的响应实体 - * @see TaskStatsDTO - */ - @GetMapping("/reports/task-completion-stats") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity getTaskCompletionStats() { - TaskStatsDTO stats = dashboardService.getTaskCompletionStats(); - return ResponseEntity.ok(stats); - } - - /** - * 获取AQI热力图数据 - * - * @return 包含AQI热力图坐标点数据的响应实体 - * @throws ResourceNotFoundException 如果未找到任何AQI数据 - * @see AqiHeatmapPointDTO - */ - @GetMapping("/map/aqi-heatmap") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity> getAqiHeatmapData() { - List heatmapData = dashboardService.getAqiHeatmapData(); - if (heatmapData == null || heatmapData.isEmpty()) { - log.warn("AQI热力图数据为空"); - } else { - log.info("成功获取AQI热力图数据,共 {} 个数据点", heatmapData.size()); - } - return ResponseEntity.ok(heatmapData); - } - - /** - * 获取所有污染物阈值设置 - * - * @return 包含所有污染物阈值设置的响应实体 - * @see PollutantThresholdDTO - */ - @GetMapping("/thresholds") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity> getPollutantThresholds() { - log.info("接收到获取所有污染物阈值设置的请求"); - List thresholds = dashboardService.getPollutantThresholds(); - log.info("返回 {} 个污染物阈值设置", thresholds.size()); - return ResponseEntity.ok(thresholds); - } - - /** - * 获取指定污染物的阈值设置 - * - * @param pollutantName 污染物名称 - * @return 包含指定污染物阈值设置的响应实体 - * @throws ResourceNotFoundException 如果未找到该污染物的阈值设置 - * @see PollutantThresholdDTO - */ - @GetMapping("/thresholds/{pollutantName}") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity getPollutantThreshold(@PathVariable String pollutantName) { - log.info("接收到获取污染物 {} 阈值设置的请求", pollutantName); - PollutantThresholdDTO threshold = dashboardService.getPollutantThreshold(pollutantName); - if (threshold == null) { - log.warn("未找到污染物 {} 的阈值设置", pollutantName); - return ResponseEntity.notFound().build(); - } - log.info("返回污染物 {} 的阈值设置: {}", pollutantName, threshold); - return ResponseEntity.ok(threshold); - } - - /** - * 保存污染物阈值设置 - * - * @param thresholdDTO 包含阈值设置的数据传输对象 - * @return 包含已保存阈值设置的响应实体 - * @throws IllegalArgumentException 如果阈值设置无效 - * @see PollutantThresholdDTO - */ - @PostMapping("/thresholds") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity savePollutantThreshold(@RequestBody PollutantThresholdDTO thresholdDTO) { - log.info("接收到保存污染物阈值设置的请求: {}", thresholdDTO); - PollutantThresholdDTO saved = dashboardService.savePollutantThreshold(thresholdDTO); - log.info("污染物阈值设置已保存: {}", saved); - return ResponseEntity.ok(saved); - } - - /** - * 获取各类污染物的月度趋势数据 - * - * @return 包含各类污染物月度趋势数据的响应实体 - * @see TrendDataPointDTO - */ - @GetMapping("/reports/pollutant-monthly-trends") - @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") - public ResponseEntity>> getPollutantMonthlyTrends() { - log.info("接收到获取每种污染物月度趋势数据的请求"); - Map> trends = dashboardService.getPollutantMonthlyTrends(); - log.info("返回 {} 种污染物的趋势数据", trends.size()); - return ResponseEntity.ok(trends); - } +package com.dne.ems.controller; + +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.dto.AqiDistributionDTO; +import com.dne.ems.dto.AqiHeatmapPointDTO; +import com.dne.ems.dto.DashboardStatsDTO; +import com.dne.ems.dto.GridCoverageDTO; +import com.dne.ems.dto.HeatmapPointDTO; +import com.dne.ems.dto.PollutantThresholdDTO; +import com.dne.ems.dto.PollutionStatsDTO; +import com.dne.ems.dto.TaskStatsDTO; +import com.dne.ems.dto.TrendDataPointDTO; +import com.dne.ems.service.DashboardService; + +import lombok.RequiredArgsConstructor; + +/** + * 仪表盘数据控制器,提供各类环境监测数据的统计和可视化接口 + * + *

主要功能包括: + *

    + *
  • 环境质量指标统计
  • + *
  • AQI数据分布和热力图
  • + *
  • 污染物阈值管理
  • + *
  • 任务完成情况统计
  • + *
  • 各类趋势分析数据
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api/dashboard") +@RequiredArgsConstructor +public class DashboardController { + + private final DashboardService dashboardService; + private static final Logger log = LoggerFactory.getLogger(DashboardController.class); + + /** + * 获取仪表盘核心统计数据 + * + * @return 包含各类核心统计数据的响应实体 + * @see DashboardStatsDTO + */ + @GetMapping("/stats") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity getDashboardStats() { + DashboardStatsDTO stats = dashboardService.getDashboardStats(); + return ResponseEntity.ok(stats); + } + + /** + * 获取AQI等级分布数据 + * + * @return 包含各AQI等级分布数据的响应实体 + * @see AqiDistributionDTO + */ + @GetMapping("/reports/aqi-distribution") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity> getAqiDistribution() { + List distribution = dashboardService.getAqiDistribution(); + return ResponseEntity.ok(distribution); + } + + /** + * 获取月度超标趋势数据 + * + * @return 包含月度超标趋势数据的响应实体 + * @see TrendDataPointDTO + */ + @GetMapping("/reports/monthly-exceedance-trend") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity> getMonthlyExceedanceTrend() { + List trend = dashboardService.getMonthlyExceedanceTrend(); + return ResponseEntity.ok(trend); + } + + /** + * 获取网格覆盖情况数据 + * + * @return 包含各城市网格覆盖情况的响应实体 + * @see GridCoverageDTO + */ + @GetMapping("/reports/grid-coverage") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity> getGridCoverage() { + List coverage = dashboardService.getGridCoverageByCity(); + return ResponseEntity.ok(coverage); + } + + /** + * 获取反馈热力图数据 + * + * @return 包含热力图坐标点数据的响应实体 + * @throws ResourceNotFoundException 如果未找到任何热力图数据 + * @see HeatmapPointDTO + */ + @GetMapping("/map/heatmap") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity> getHeatmapData() { + List heatmapData = dashboardService.getHeatmapData(); + if (heatmapData == null || heatmapData.isEmpty()) { + log.warn("反馈热力图数据为空"); + } else { + log.info("成功获取反馈热力图数据,共 {} 个数据点", heatmapData.size()); + } + return ResponseEntity.ok(heatmapData); + } + + /** + * 获取污染物统计报告数据 + * + * @return 包含各类污染物统计数据的响应实体 + * @see PollutionStatsDTO + */ + @GetMapping("/reports/pollution-stats") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity> getPollutionStats() { + List stats = dashboardService.getPollutionStats(); + return ResponseEntity.ok(stats); + } + + /** + * 获取任务完成情况统计数据 + * + * @return 包含任务完成率等统计数据的响应实体 + * @see TaskStatsDTO + */ + @GetMapping("/reports/task-completion-stats") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity getTaskCompletionStats() { + TaskStatsDTO stats = dashboardService.getTaskCompletionStats(); + return ResponseEntity.ok(stats); + } + + /** + * 获取AQI热力图数据 + * + * @return 包含AQI热力图坐标点数据的响应实体 + * @throws ResourceNotFoundException 如果未找到任何AQI数据 + * @see AqiHeatmapPointDTO + */ + @GetMapping("/map/aqi-heatmap") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity> getAqiHeatmapData() { + List heatmapData = dashboardService.getAqiHeatmapData(); + if (heatmapData == null || heatmapData.isEmpty()) { + log.warn("AQI热力图数据为空"); + } else { + log.info("成功获取AQI热力图数据,共 {} 个数据点", heatmapData.size()); + } + return ResponseEntity.ok(heatmapData); + } + + /** + * 获取所有污染物阈值设置 + * + * @return 包含所有污染物阈值设置的响应实体 + * @see PollutantThresholdDTO + */ + @GetMapping("/thresholds") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity> getPollutantThresholds() { + log.info("接收到获取所有污染物阈值设置的请求"); + List thresholds = dashboardService.getPollutantThresholds(); + log.info("返回 {} 个污染物阈值设置", thresholds.size()); + return ResponseEntity.ok(thresholds); + } + + /** + * 获取指定污染物的阈值设置 + * + * @param pollutantName 污染物名称 + * @return 包含指定污染物阈值设置的响应实体 + * @throws ResourceNotFoundException 如果未找到该污染物的阈值设置 + * @see PollutantThresholdDTO + */ + @GetMapping("/thresholds/{pollutantName}") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity getPollutantThreshold(@PathVariable String pollutantName) { + log.info("接收到获取污染物 {} 阈值设置的请求", pollutantName); + PollutantThresholdDTO threshold = dashboardService.getPollutantThreshold(pollutantName); + if (threshold == null) { + log.warn("未找到污染物 {} 的阈值设置", pollutantName); + return ResponseEntity.notFound().build(); + } + log.info("返回污染物 {} 的阈值设置: {}", pollutantName, threshold); + return ResponseEntity.ok(threshold); + } + + /** + * 保存污染物阈值设置 + * + * @param thresholdDTO 包含阈值设置的数据传输对象 + * @return 包含已保存阈值设置的响应实体 + * @throws IllegalArgumentException 如果阈值设置无效 + * @see PollutantThresholdDTO + */ + @PostMapping("/thresholds") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity savePollutantThreshold(@RequestBody PollutantThresholdDTO thresholdDTO) { + log.info("接收到保存污染物阈值设置的请求: {}", thresholdDTO); + PollutantThresholdDTO saved = dashboardService.savePollutantThreshold(thresholdDTO); + log.info("污染物阈值设置已保存: {}", saved); + return ResponseEntity.ok(saved); + } + + /** + * 获取各类污染物的月度趋势数据 + * + * @return 包含各类污染物月度趋势数据的响应实体 + * @see TrendDataPointDTO + */ + @GetMapping("/reports/pollutant-monthly-trends") + @PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')") + public ResponseEntity>> getPollutantMonthlyTrends() { + log.info("接收到获取每种污染物月度趋势数据的请求"); + Map> trends = dashboardService.getPollutantMonthlyTrends(); + log.info("返回 {} 种污染物的趋势数据", trends.size()); + return ResponseEntity.ok(trends); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/FeedbackController.java b/ems-backend/src/main/java/com/dne/ems/controller/FeedbackController.java index 368a30d..18c27f7 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/FeedbackController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/FeedbackController.java @@ -1,213 +1,213 @@ -package com.dne.ems.controller; - -import java.time.LocalDate; - - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import com.dne.ems.dto.FeedbackResponseDTO; -import com.dne.ems.dto.FeedbackStatsResponse; -import com.dne.ems.dto.FeedbackSubmissionRequest; -import com.dne.ems.dto.ProcessFeedbackRequest; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.FeedbackService; - -import io.swagger.v3.oas.annotations.Operation; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -/** - * 公众反馈控制器,处理环境问题反馈的提交、查询和统计 - * - *

主要功能包括: - *

    - *
  • 公众环境问题反馈提交(支持认证用户和匿名用户)
  • - *
  • 反馈信息查询与多条件过滤
  • - *
  • 反馈数据统计分析
  • - *
  • 反馈状态流转管理
  • - *
- * - *

权限控制: - *

    - *
  • 提交接口:所有认证用户
  • - *
  • 查询接口:管理员、主管、决策者
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024-03 - * @see com.dne.ems.dto.FeedbackSubmissionRequest 反馈提交请求DTO - * @see com.dne.ems.model.enums.FeedbackStatus 反馈状态枚举 - */ -@RestController -@RequestMapping("/api/feedback") -@RequiredArgsConstructor -public class FeedbackController { - - private final FeedbackService feedbackService; - - /** - * 提交反馈(JSON格式,仅用于测试) - * - *

此接口用于测试目的,使用application/json格式提交反馈数据, - * 比multipart/form-data格式更便于使用curl等工具测试。 - * 正式接口请使用 {@link #submitFeedback(FeedbackSubmissionRequest, MultipartFile[], CustomUserDetails)} - * - * @param request 包含反馈信息的请求体 - * @param userDetails 当前认证用户信息 - * @return 创建成功的反馈实体和201状态码 - * @throws IllegalArgumentException 如果请求参数无效 - * @see FeedbackSubmissionRequest - */ - @PostMapping(value = "/submit-json") - @PreAuthorize("isAuthenticated()") - public ResponseEntity submitFeedbackJson( - @Valid @RequestBody FeedbackSubmissionRequest request, - @AuthenticationPrincipal CustomUserDetails userDetails) { - - Feedback createdFeedback = feedbackService.submitFeedback(request, null); // No files for this endpoint - return new ResponseEntity<>(createdFeedback, HttpStatus.CREATED); - } - - /** - * 提交反馈(正式接口) - * - *

使用multipart/form-data格式提交反馈,支持附件上传 - * - * @param request 包含反馈信息的请求体 - * @param files 可选的上传附件 - * @param userDetails 当前认证用户信息 - * @return 创建成功的反馈实体和201状态码 - * @throws IllegalArgumentException 如果请求参数无效 - * @throws FileStorageException 如果文件上传失败 - * @see FeedbackSubmissionRequest - */ - @PostMapping(value = "/submit", consumes = {"multipart/form-data"}) - @PreAuthorize("isAuthenticated()") - public ResponseEntity submitFeedback( - @Valid @RequestPart("feedback") FeedbackSubmissionRequest request, - @RequestPart(value = "files", required = false) MultipartFile[] files, - @AuthenticationPrincipal CustomUserDetails userDetails) { - - Feedback createdFeedback = feedbackService.submitFeedback(request, files); - return new ResponseEntity<>(createdFeedback, HttpStatus.CREATED); - } - - /** - * 获取所有反馈(分页+多条件过滤) - * - *

支持以下过滤条件组合查询: - *

    - *
  • 状态:PENDING_REVIEW(待审核)/PROCESSED(已处理)/REJECTED(已拒绝)
  • - *
  • 污染类型:AIR/WATER/SOIL/NOISE等
  • - *
  • 严重程度:LOW/MEDIUM/HIGH/CRITICAL
  • - *
  • 地理位置:城市/区县
  • - *
  • 时间范围:开始日期至结束日期
  • - *
  • 关键词:标题/描述模糊匹配
  • - *
- * - * @param userDetails 当前认证用户信息 - * @param status 反馈状态过滤条件 - * @param pollutionType 污染类型过滤条件 - * @param severityLevel 严重程度过滤条件 - * @param cityName 城市名称过滤条件 - * @param districtName 区县名称过滤条件 - * @param startDate 开始日期过滤条件(ISO格式:yyyy-MM-dd) - * @param endDate 结束日期过滤条件(ISO格式:yyyy-MM-dd) - * @param keyword 关键词过滤条件(模糊匹配) - * @param pageable 分页参数(页号从0开始) - * @return 符合条件的反馈列表(分页) - * @see com.dne.ems.model.Feedback 反馈实体 - * @see com.dne.ems.model.enums.FeedbackStatus 反馈状态枚举 - * @see com.dne.ems.model.enums.PollutionType 污染类型枚举 - * @see com.dne.ems.model.enums.SeverityLevel 严重程度枚举 - */ - @GetMapping - @PreAuthorize("isAuthenticated()") - @Operation(summary = "Get all feedback", description = "Retrieves a paginated list of all feedback submissions with filtering options.") - public ResponseEntity> getAllFeedback( - @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestParam(required = false) FeedbackStatus status, - @RequestParam(required = false) PollutionType pollutionType, - @RequestParam(required = false) SeverityLevel severityLevel, - @RequestParam(required = false) String cityName, - @RequestParam(required = false) String districtName, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, - @RequestParam(required = false) String keyword, - Pageable pageable) { - - Page feedbackPage = feedbackService.getFeedback( - userDetails, status, pollutionType, severityLevel, cityName, districtName, - startDate, endDate, keyword, pageable); - - return ResponseEntity.ok(feedbackPage); - } - - /** - * 根据ID获取反馈详情 - * - * @param id 反馈ID - * @return 反馈详情实体 - * @throws ResourceNotFoundException 如果反馈不存在 - * @see Feedback - */ - @GetMapping("/{id}") - @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR', 'DECISION_MAKER')") - @Operation(summary = "Get feedback by ID", description = "Retrieves feedback details by ID.") - public ResponseEntity getFeedbackById(@PathVariable Long id) { - FeedbackResponseDTO feedback = feedbackService.getFeedbackDetail(id); - return ResponseEntity.ok(feedback); - } - - /** - * 获取反馈统计数据 - * - * @param userDetails 当前认证用户信息 - * @return 包含各类反馈统计数据的响应实体 - * @see FeedbackStatsResponse - */ - @GetMapping("/stats") - @PreAuthorize("isAuthenticated()") - @Operation(summary = "Get feedback statistics", description = "Retrieves statistics about feedback status counts.") - public ResponseEntity getFeedbackStats(@AuthenticationPrincipal CustomUserDetails userDetails) { - FeedbackStatsResponse stats = feedbackService.getFeedbackStats(userDetails); - return ResponseEntity.ok(stats); - } - - /** - * 处理反馈 (例如, 批准) - * - * @param id 反馈ID - * @param request 包含新状态和备注的请求体 - * @return 更新后的反馈详情 - */ - @PostMapping("/{id}/process") - @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") - @Operation(summary = "Process a feedback entry", description = "Processes a feedback, e.g., approving it to be pending assignment.") - public ResponseEntity processFeedback( - @PathVariable Long id, - @Valid @RequestBody ProcessFeedbackRequest request) { - FeedbackResponseDTO updatedFeedback = feedbackService.processFeedback(id, request); - return ResponseEntity.ok(updatedFeedback); - } +package com.dne.ems.controller; + +import java.time.LocalDate; + + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.dne.ems.dto.FeedbackResponseDTO; +import com.dne.ems.dto.FeedbackStatsResponse; +import com.dne.ems.dto.FeedbackSubmissionRequest; +import com.dne.ems.dto.ProcessFeedbackRequest; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.FeedbackService; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +/** + * 公众反馈控制器,处理环境问题反馈的提交、查询和统计 + * + *

主要功能包括: + *

    + *
  • 公众环境问题反馈提交(支持认证用户和匿名用户)
  • + *
  • 反馈信息查询与多条件过滤
  • + *
  • 反馈数据统计分析
  • + *
  • 反馈状态流转管理
  • + *
+ * + *

权限控制: + *

    + *
  • 提交接口:所有认证用户
  • + *
  • 查询接口:管理员、主管、决策者
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024-03 + * @see com.dne.ems.dto.FeedbackSubmissionRequest 反馈提交请求DTO + * @see com.dne.ems.model.enums.FeedbackStatus 反馈状态枚举 + */ +@RestController +@RequestMapping("/api/feedback") +@RequiredArgsConstructor +public class FeedbackController { + + private final FeedbackService feedbackService; + + /** + * 提交反馈(JSON格式,仅用于测试) + * + *

此接口用于测试目的,使用application/json格式提交反馈数据, + * 比multipart/form-data格式更便于使用curl等工具测试。 + * 正式接口请使用 {@link #submitFeedback(FeedbackSubmissionRequest, MultipartFile[], CustomUserDetails)} + * + * @param request 包含反馈信息的请求体 + * @param userDetails 当前认证用户信息 + * @return 创建成功的反馈实体和201状态码 + * @throws IllegalArgumentException 如果请求参数无效 + * @see FeedbackSubmissionRequest + */ + @PostMapping(value = "/submit-json") + @PreAuthorize("isAuthenticated()") + public ResponseEntity submitFeedbackJson( + @Valid @RequestBody FeedbackSubmissionRequest request, + @AuthenticationPrincipal CustomUserDetails userDetails) { + + Feedback createdFeedback = feedbackService.submitFeedback(request, null); // No files for this endpoint + return new ResponseEntity<>(createdFeedback, HttpStatus.CREATED); + } + + /** + * 提交反馈(正式接口) + * + *

使用multipart/form-data格式提交反馈,支持附件上传 + * + * @param request 包含反馈信息的请求体 + * @param files 可选的上传附件 + * @param userDetails 当前认证用户信息 + * @return 创建成功的反馈实体和201状态码 + * @throws IllegalArgumentException 如果请求参数无效 + * @throws FileStorageException 如果文件上传失败 + * @see FeedbackSubmissionRequest + */ + @PostMapping(value = "/submit", consumes = {"multipart/form-data"}) + @PreAuthorize("isAuthenticated()") + public ResponseEntity submitFeedback( + @Valid @RequestPart("feedback") FeedbackSubmissionRequest request, + @RequestPart(value = "files", required = false) MultipartFile[] files, + @AuthenticationPrincipal CustomUserDetails userDetails) { + + Feedback createdFeedback = feedbackService.submitFeedback(request, files); + return new ResponseEntity<>(createdFeedback, HttpStatus.CREATED); + } + + /** + * 获取所有反馈(分页+多条件过滤) + * + *

支持以下过滤条件组合查询: + *

    + *
  • 状态:PENDING_REVIEW(待审核)/PROCESSED(已处理)/REJECTED(已拒绝)
  • + *
  • 污染类型:AIR/WATER/SOIL/NOISE等
  • + *
  • 严重程度:LOW/MEDIUM/HIGH/CRITICAL
  • + *
  • 地理位置:城市/区县
  • + *
  • 时间范围:开始日期至结束日期
  • + *
  • 关键词:标题/描述模糊匹配
  • + *
+ * + * @param userDetails 当前认证用户信息 + * @param status 反馈状态过滤条件 + * @param pollutionType 污染类型过滤条件 + * @param severityLevel 严重程度过滤条件 + * @param cityName 城市名称过滤条件 + * @param districtName 区县名称过滤条件 + * @param startDate 开始日期过滤条件(ISO格式:yyyy-MM-dd) + * @param endDate 结束日期过滤条件(ISO格式:yyyy-MM-dd) + * @param keyword 关键词过滤条件(模糊匹配) + * @param pageable 分页参数(页号从0开始) + * @return 符合条件的反馈列表(分页) + * @see com.dne.ems.model.Feedback 反馈实体 + * @see com.dne.ems.model.enums.FeedbackStatus 反馈状态枚举 + * @see com.dne.ems.model.enums.PollutionType 污染类型枚举 + * @see com.dne.ems.model.enums.SeverityLevel 严重程度枚举 + */ + @GetMapping + @PreAuthorize("isAuthenticated()") + @Operation(summary = "Get all feedback", description = "Retrieves a paginated list of all feedback submissions with filtering options.") + public ResponseEntity> getAllFeedback( + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestParam(required = false) FeedbackStatus status, + @RequestParam(required = false) PollutionType pollutionType, + @RequestParam(required = false) SeverityLevel severityLevel, + @RequestParam(required = false) String cityName, + @RequestParam(required = false) String districtName, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + @RequestParam(required = false) String keyword, + Pageable pageable) { + + Page feedbackPage = feedbackService.getFeedback( + userDetails, status, pollutionType, severityLevel, cityName, districtName, + startDate, endDate, keyword, pageable); + + return ResponseEntity.ok(feedbackPage); + } + + /** + * 根据ID获取反馈详情 + * + * @param id 反馈ID + * @return 反馈详情实体 + * @throws ResourceNotFoundException 如果反馈不存在 + * @see Feedback + */ + @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR', 'DECISION_MAKER')") + @Operation(summary = "Get feedback by ID", description = "Retrieves feedback details by ID.") + public ResponseEntity getFeedbackById(@PathVariable Long id) { + FeedbackResponseDTO feedback = feedbackService.getFeedbackDetail(id); + return ResponseEntity.ok(feedback); + } + + /** + * 获取反馈统计数据 + * + * @param userDetails 当前认证用户信息 + * @return 包含各类反馈统计数据的响应实体 + * @see FeedbackStatsResponse + */ + @GetMapping("/stats") + @PreAuthorize("isAuthenticated()") + @Operation(summary = "Get feedback statistics", description = "Retrieves statistics about feedback status counts.") + public ResponseEntity getFeedbackStats(@AuthenticationPrincipal CustomUserDetails userDetails) { + FeedbackStatsResponse stats = feedbackService.getFeedbackStats(userDetails); + return ResponseEntity.ok(stats); + } + + /** + * 处理反馈 (例如, 批准) + * + * @param id 反馈ID + * @param request 包含新状态和备注的请求体 + * @return 更新后的反馈详情 + */ + @PostMapping("/{id}/process") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") + @Operation(summary = "Process a feedback entry", description = "Processes a feedback, e.g., approving it to be pending assignment.") + public ResponseEntity processFeedback( + @PathVariable Long id, + @Valid @RequestBody ProcessFeedbackRequest request) { + FeedbackResponseDTO updatedFeedback = feedbackService.processFeedback(id, request); + return ResponseEntity.ok(updatedFeedback); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/FileController.java b/ems-backend/src/main/java/com/dne/ems/controller/FileController.java index 0ae24c1..ee6f52f 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/FileController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/FileController.java @@ -1,103 +1,103 @@ -package com.dne.ems.controller; - -import java.io.IOException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.service.FileStorageService; - -import jakarta.servlet.http.HttpServletRequest; - -/** - * 文件控制器,处理文件下载和预览请求 - * - *

主要功能包括: - *

    - *
  • 文件下载
  • - *
  • 文件在线预览
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api") -public class FileController { - - private static final Logger logger = LoggerFactory.getLogger(FileController.class); - - @Autowired - private FileStorageService fileStorageService; - - /** - * 下载文件 - * - * @param filename 文件名(包含扩展名) - * @param request HTTP请求对象 - * @return 包含文件资源的响应实体 - * @throws FileNotFoundException 如果文件不存在 - * @throws FileStorageException 如果文件读取失败 - */ - @GetMapping("/files/{filename:.+}") - public ResponseEntity downloadFile(@PathVariable String filename, HttpServletRequest request) { - Resource resource = fileStorageService.loadFileAsResource(filename); - - String contentType = null; - try { - contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath()); - } catch (IOException ex) { - logger.info("Could not determine file type."); - } - - // Fallback to the default content type if type could not be determined - if (contentType == null) { - contentType = "application/octet-stream"; - } - - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType(contentType)) - .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"") - .body(resource); - } - - /** - * 在线预览文件 - * - * @param fileName 文件名(包含扩展名) - * @param request HTTP请求对象 - * @return 包含文件资源的响应实体 - * @throws FileNotFoundException 如果文件不存在 - * @throws FileStorageException 如果文件读取失败 - */ - @GetMapping("/view/{fileName:.+}") - public ResponseEntity viewFile(@PathVariable String fileName, HttpServletRequest request) { - Resource resource = fileStorageService.loadFileAsResource(fileName); - - String contentType = null; - try { - contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath()); - } catch (IOException ex) { - // log error - } - - if(contentType == null) { - contentType = "application/octet-stream"; - } - - return ResponseEntity.ok() - .contentType(MediaType.parseMediaType(contentType)) - .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"") - .body(resource); - } +package com.dne.ems.controller; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.service.FileStorageService; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 文件控制器,处理文件下载和预览请求 + * + *

主要功能包括: + *

    + *
  • 文件下载
  • + *
  • 文件在线预览
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api") +public class FileController { + + private static final Logger logger = LoggerFactory.getLogger(FileController.class); + + @Autowired + private FileStorageService fileStorageService; + + /** + * 下载文件 + * + * @param filename 文件名(包含扩展名) + * @param request HTTP请求对象 + * @return 包含文件资源的响应实体 + * @throws FileNotFoundException 如果文件不存在 + * @throws FileStorageException 如果文件读取失败 + */ + @GetMapping("/files/{filename:.+}") + public ResponseEntity downloadFile(@PathVariable String filename, HttpServletRequest request) { + Resource resource = fileStorageService.loadFileAsResource(filename); + + String contentType = null; + try { + contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath()); + } catch (IOException ex) { + logger.info("Could not determine file type."); + } + + // Fallback to the default content type if type could not be determined + if (contentType == null) { + contentType = "application/octet-stream"; + } + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(contentType)) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"") + .body(resource); + } + + /** + * 在线预览文件 + * + * @param fileName 文件名(包含扩展名) + * @param request HTTP请求对象 + * @return 包含文件资源的响应实体 + * @throws FileNotFoundException 如果文件不存在 + * @throws FileStorageException 如果文件读取失败 + */ + @GetMapping("/view/{fileName:.+}") + public ResponseEntity viewFile(@PathVariable String fileName, HttpServletRequest request) { + Resource resource = fileStorageService.loadFileAsResource(fileName); + + String contentType = null; + try { + contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath()); + } catch (IOException ex) { + // log error + } + + if(contentType == null) { + contentType = "application/octet-stream"; + } + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(contentType)) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"") + .body(resource); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/GridController.java b/ems-backend/src/main/java/com/dne/ems/controller/GridController.java index 44cd7b2..e5bb909 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/GridController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/GridController.java @@ -1,343 +1,343 @@ -package com.dne.ems.controller; - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.dto.GridCoverageDTO; -import com.dne.ems.dto.GridUpdateRequest; -import com.dne.ems.model.Grid; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.Role; -import com.dne.ems.repository.GridRepository; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.service.GridService; -import com.dne.ems.service.OperationLogService; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -/** - * 网格管理控制器,负责网格数据的查询、更新和网格员分配管理 - * - *

主要功能包括: - *

    - *
  • 网格数据查询与分页
  • - *
  • 网格信息更新
  • - *
  • 网格员分配与解除分配
  • - *
  • 网格覆盖率统计
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api/grids") -@RequiredArgsConstructor -public class GridController { - - private final GridService gridService; - private final GridRepository gridRepository; - private final UserAccountRepository userAccountRepository; - private final OperationLogService operationLogService; - - // 添加日志记录器 - private static final Logger logger = LoggerFactory.getLogger(GridController.class); - - /** - * 获取网格列表(可分页和过滤) - * - * @return 符合条件的网格分页列表 - * @see Grid - */ - @GetMapping - @PreAuthorize("hasRole('ADMIN') or hasRole('DECISION_MAKER') or hasRole('GRID_WORKER')") - public ResponseEntity> getGrids() { - List grids = gridRepository.findAll(); - return ResponseEntity.ok(grids); - } - - /** - * 更新网格信息 - * - * @param id 要更新的网格ID - * @param request 包含更新数据的请求体 - * @return 更新后的网格实体 - * @throws ResourceNotFoundException 如果网格不存在 - * @see GridUpdateRequest - */ - @Operation(summary = "Update a grid by ID") - @PatchMapping("/{id}") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity updateGrid( - @Parameter(description = "Grid ID") @PathVariable Long id, - @Valid @RequestBody GridUpdateRequest request - ) { - Grid updatedGrid = gridService.updateGrid(id, request); - return ResponseEntity.ok(updatedGrid); - } - - /** - * 获取网格覆盖率统计数据 - * - * @return 各城市网格覆盖率数据列表 - * @see GridCoverageDTO - */ - @GetMapping("/coverage") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity> getGridCoverage() { - List coverage = gridRepository.getGridCoverageByCity(); - return ResponseEntity.ok(coverage); - } - - /** - * 分配网格员到指定网格 - * - * @param gridId 目标网格ID - * @param request 包含用户ID的请求体 - * @return 操作结果 - * @throws ResourceNotFoundException 如果网格或用户不存在 - * @throws IllegalArgumentException 如果用户不是网格员角色 - */ - @PostMapping("/{gridId}/assign") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity assignGridWorker( - @PathVariable Long gridId, - @RequestBody Map request) { - Long userId = request.get("userId"); - if (userId == null) { - return ResponseEntity.badRequest().body("用户ID不能为空"); - } - - Optional gridOpt = gridRepository.findById(gridId); - Optional userOpt = userAccountRepository.findById(userId); - - if (gridOpt.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - if (userOpt.isEmpty()) { - return ResponseEntity.badRequest().body("用户不存在"); - } - - Grid grid = gridOpt.get(); - UserAccount user = userOpt.get(); - - if (user.getRole() != Role.GRID_WORKER) { - return ResponseEntity.badRequest().body("只能分配网格员角色的用户"); - } - - user.setGridX(grid.getGridX()); - user.setGridY(grid.getGridY()); - userAccountRepository.save(user); - - // 记录操作日志 - operationLogService.recordOperation( - com.dne.ems.model.enums.OperationType.UPDATE, - "将网格员 " + user.getName() + " (ID: " + userId + ") 分配到网格 (ID: " + gridId + ")", - userId.toString(), - "UserAccount" - ); - - return ResponseEntity.ok().build(); - } - - /** - * 从网格中移除网格员 - * - * @param gridId 目标网格ID - * @return 操作结果 - * @throws ResourceNotFoundException 如果网格不存在 - */ - @PostMapping("/{gridId}/unassign") - @PreAuthorize("hasRole('ADMIN')") - public ResponseEntity removeGridWorker(@PathVariable Long gridId) { - Optional gridOpt = gridRepository.findById(gridId); - - if (gridOpt.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - Grid grid = gridOpt.get(); - - // 使用新添加的方法查找指定网格坐标的网格员 - List workers = userAccountRepository.findGridWorkersByCoordinates( - grid.getGridX(), grid.getGridY()); - - for (UserAccount worker : workers) { - worker.setGridX(-1); - worker.setGridY(-1); - userAccountRepository.save(worker); - } - - return ResponseEntity.ok().build(); - } - - /** - * 通过网格坐标分配网格员,仅限ADMIN角色使用。 - * - * @param gridX 网格X坐标 - * @param gridY 网格Y坐标 - * @param request 包含用户ID的请求体 - * @return 操作结果响应 - */ - @PostMapping("/coordinates/{gridX}/{gridY}/assign") - @PreAuthorize("hasAnyRole('ADMIN', 'GRID_WORKER')") - public ResponseEntity assignGridWorkerByCoordinates( - @PathVariable Integer gridX, - @PathVariable Integer gridY, - @RequestBody Map request) { - logger.info("开始通过坐标分配网格员: gridX={}, gridY={}, request={}", gridX, gridY, request); - - Long userId = request.get("userId"); - if (userId == null) { - logger.warn("分配网格员失败: 用户ID为空"); - return ResponseEntity.badRequest().body("用户ID不能为空"); - } - - try { - // 通过坐标查找网格 - logger.debug("查询网格坐标: gridX={}, gridY={}", gridX, gridY); - Optional gridOpt = gridRepository.findByGridXAndGridY(gridX, gridY); - - if (gridOpt.isEmpty()) { - logger.warn("分配网格员失败: 未找到网格, gridX={}, gridY={}", gridX, gridY); - - // 尝试查询所有网格,看看是否有数据 - List allGrids = gridRepository.findAll(); - logger.debug("数据库中共有 {} 个网格", allGrids.size()); - if (!allGrids.isEmpty()) { - Grid firstGrid = allGrids.get(0); - logger.debug("第一个网格: id={}, gridX={}, gridY={}, cityName={}", - firstGrid.getId(), firstGrid.getGridX(), firstGrid.getGridY(), firstGrid.getCityName()); - } - - return ResponseEntity.status(404).body("未找到指定坐标的网格"); - } - - logger.debug("查询用户: userId={}", userId); - Optional userOpt = userAccountRepository.findById(userId); - - if (userOpt.isEmpty()) { - logger.warn("分配网格员失败: 未找到用户, userId={}", userId); - return ResponseEntity.badRequest().body("用户不存在"); - } - - Grid grid = gridOpt.get(); - UserAccount user = userOpt.get(); - - logger.debug("网格信息: id={}, gridX={}, gridY={}", grid.getId(), grid.getGridX(), grid.getGridY()); - logger.debug("用户信息: id={}, name={}, role={}", user.getId(), user.getName(), user.getRole()); - - if (user.getRole() != Role.GRID_WORKER) { - logger.warn("分配网格员失败: 用户角色不是网格员, userId={}, role={}", userId, user.getRole()); - return ResponseEntity.badRequest().body("只能分配网格员角色的用户"); - } - - // 先检查是否有其他网格员已分配到此网格 - logger.debug("检查是否有其他网格员已分配到此网格: gridX={}, gridY={}", gridX, gridY); - List existingWorkers = userAccountRepository.findGridWorkersByCoordinates(gridX, gridY); - logger.debug("已分配的网格员数量: {}", existingWorkers.size()); - - for (UserAccount existingWorker : existingWorkers) { - if (!existingWorker.getId().equals(userId)) { - logger.info("移除已存在的网格员: workerId={}, name={}", existingWorker.getId(), existingWorker.getName()); - existingWorker.setGridX(-1); - existingWorker.setGridY(-1); - userAccountRepository.save(existingWorker); - } - } - - // 分配新网格员 - logger.info("分配网格员: userId={}, name={}, gridX={}, gridY={}", user.getId(), user.getName(), grid.getGridX(), grid.getGridY()); - user.setGridX(grid.getGridX()); - user.setGridY(grid.getGridY()); - userAccountRepository.save(user); - - // 记录操作日志 - operationLogService.recordOperation( - com.dne.ems.model.enums.OperationType.UPDATE, - "将网格员 " + user.getName() + " (ID: " + userId + ") 分配到网格 (X: " + gridX + ", Y: " + gridY + ")", - userId.toString(), - "UserAccount" - ); - - logger.info("成功将用户 {} 分配到网格 (X: {}, Y: {})", user.getName(), gridX, gridY); - return ResponseEntity.ok().build(); - - } catch (Exception e) { - logger.error("通过坐标分配网格员时发生异常", e); - return ResponseEntity.status(500).body("服务器内部错误"); - } - } - - /** - * 通过网格坐标移除网格员,仅限ADMIN角色使用。 - * - * @param gridX 网格X坐标 - * @param gridY 网格Y坐标 - * @return 操作结果响应 - */ - @PostMapping("/coordinates/{gridX}/{gridY}/unassign") - @PreAuthorize("hasAnyRole('ADMIN', 'GRID_WORKER')") - public ResponseEntity removeGridWorkerByCoordinates( - @PathVariable Integer gridX, - @PathVariable Integer gridY) { - logger.info("开始通过坐标移除网格员: gridX={}, gridY={}", gridX, gridY); - - try { - // 查找指定坐标的网格员 - logger.debug("查询指定坐标的网格员: gridX={}, gridY={}", gridX, gridY); - List workers = userAccountRepository.findGridWorkersByCoordinates(gridX, gridY); - logger.debug("找到网格员数量: {}", workers.size()); - - if (workers.isEmpty()) { - logger.warn("移除网格员失败: 未找到网格员, gridX={}, gridY={}", gridX, gridY); - return ResponseEntity.status(404).body("未找到指定坐标的网格员"); - } - - // 移除所有网格员的分配 - for (UserAccount worker : workers) { - logger.info("移除网格员分配: workerId={}, name={}, 原坐标: gridX={}, gridY={}", - worker.getId(), worker.getName(), worker.getGridX(), worker.getGridY()); - - try { - worker.setGridX(-1); - worker.setGridY(-1); - UserAccount savedWorker = userAccountRepository.save(worker); - logger.info("网格员移除成功: workerId={}, 新坐标: gridX={}, gridY={}", - savedWorker.getId(), savedWorker.getGridX(), savedWorker.getGridY()); - - // 验证保存是否成功 - UserAccount verifiedWorker = userAccountRepository.findById(worker.getId()).orElse(null); - if (verifiedWorker != null) { - logger.debug("验证移除结果: gridX={}, gridY={}", verifiedWorker.getGridX(), verifiedWorker.getGridY()); - } - } catch (Exception e) { - logger.error("保存用户数据时发生异常", e); - return ResponseEntity.status(500).body("保存用户数据失败: " + e.getMessage()); - } - } - - return ResponseEntity.ok().build(); - } catch (Exception e) { - logger.error("移除网格员时发生异常", e); - return ResponseEntity.status(500).body("移除网格员失败: " + e.getMessage()); - } - } +package com.dne.ems.controller; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.dto.GridCoverageDTO; +import com.dne.ems.dto.GridUpdateRequest; +import com.dne.ems.model.Grid; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.Role; +import com.dne.ems.repository.GridRepository; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.service.GridService; +import com.dne.ems.service.OperationLogService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +/** + * 网格管理控制器,负责网格数据的查询、更新和网格员分配管理 + * + *

主要功能包括: + *

    + *
  • 网格数据查询与分页
  • + *
  • 网格信息更新
  • + *
  • 网格员分配与解除分配
  • + *
  • 网格覆盖率统计
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api/grids") +@RequiredArgsConstructor +public class GridController { + + private final GridService gridService; + private final GridRepository gridRepository; + private final UserAccountRepository userAccountRepository; + private final OperationLogService operationLogService; + + // 添加日志记录器 + private static final Logger logger = LoggerFactory.getLogger(GridController.class); + + /** + * 获取网格列表(可分页和过滤) + * + * @return 符合条件的网格分页列表 + * @see Grid + */ + @GetMapping + @PreAuthorize("hasRole('ADMIN') or hasRole('DECISION_MAKER') or hasRole('GRID_WORKER')") + public ResponseEntity> getGrids() { + List grids = gridRepository.findAll(); + return ResponseEntity.ok(grids); + } + + /** + * 更新网格信息 + * + * @param id 要更新的网格ID + * @param request 包含更新数据的请求体 + * @return 更新后的网格实体 + * @throws ResourceNotFoundException 如果网格不存在 + * @see GridUpdateRequest + */ + @Operation(summary = "Update a grid by ID") + @PatchMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity updateGrid( + @Parameter(description = "Grid ID") @PathVariable Long id, + @Valid @RequestBody GridUpdateRequest request + ) { + Grid updatedGrid = gridService.updateGrid(id, request); + return ResponseEntity.ok(updatedGrid); + } + + /** + * 获取网格覆盖率统计数据 + * + * @return 各城市网格覆盖率数据列表 + * @see GridCoverageDTO + */ + @GetMapping("/coverage") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> getGridCoverage() { + List coverage = gridRepository.getGridCoverageByCity(); + return ResponseEntity.ok(coverage); + } + + /** + * 分配网格员到指定网格 + * + * @param gridId 目标网格ID + * @param request 包含用户ID的请求体 + * @return 操作结果 + * @throws ResourceNotFoundException 如果网格或用户不存在 + * @throws IllegalArgumentException 如果用户不是网格员角色 + */ + @PostMapping("/{gridId}/assign") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity assignGridWorker( + @PathVariable Long gridId, + @RequestBody Map request) { + Long userId = request.get("userId"); + if (userId == null) { + return ResponseEntity.badRequest().body("用户ID不能为空"); + } + + Optional gridOpt = gridRepository.findById(gridId); + Optional userOpt = userAccountRepository.findById(userId); + + if (gridOpt.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + if (userOpt.isEmpty()) { + return ResponseEntity.badRequest().body("用户不存在"); + } + + Grid grid = gridOpt.get(); + UserAccount user = userOpt.get(); + + if (user.getRole() != Role.GRID_WORKER) { + return ResponseEntity.badRequest().body("只能分配网格员角色的用户"); + } + + user.setGridX(grid.getGridX()); + user.setGridY(grid.getGridY()); + userAccountRepository.save(user); + + // 记录操作日志 + operationLogService.recordOperation( + com.dne.ems.model.enums.OperationType.UPDATE, + "将网格员 " + user.getName() + " (ID: " + userId + ") 分配到网格 (ID: " + gridId + ")", + userId.toString(), + "UserAccount" + ); + + return ResponseEntity.ok().build(); + } + + /** + * 从网格中移除网格员 + * + * @param gridId 目标网格ID + * @return 操作结果 + * @throws ResourceNotFoundException 如果网格不存在 + */ + @PostMapping("/{gridId}/unassign") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity removeGridWorker(@PathVariable Long gridId) { + Optional gridOpt = gridRepository.findById(gridId); + + if (gridOpt.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + Grid grid = gridOpt.get(); + + // 使用新添加的方法查找指定网格坐标的网格员 + List workers = userAccountRepository.findGridWorkersByCoordinates( + grid.getGridX(), grid.getGridY()); + + for (UserAccount worker : workers) { + worker.setGridX(-1); + worker.setGridY(-1); + userAccountRepository.save(worker); + } + + return ResponseEntity.ok().build(); + } + + /** + * 通过网格坐标分配网格员,仅限ADMIN角色使用。 + * + * @param gridX 网格X坐标 + * @param gridY 网格Y坐标 + * @param request 包含用户ID的请求体 + * @return 操作结果响应 + */ + @PostMapping("/coordinates/{gridX}/{gridY}/assign") + @PreAuthorize("hasAnyRole('ADMIN', 'GRID_WORKER')") + public ResponseEntity assignGridWorkerByCoordinates( + @PathVariable Integer gridX, + @PathVariable Integer gridY, + @RequestBody Map request) { + logger.info("开始通过坐标分配网格员: gridX={}, gridY={}, request={}", gridX, gridY, request); + + Long userId = request.get("userId"); + if (userId == null) { + logger.warn("分配网格员失败: 用户ID为空"); + return ResponseEntity.badRequest().body("用户ID不能为空"); + } + + try { + // 通过坐标查找网格 + logger.debug("查询网格坐标: gridX={}, gridY={}", gridX, gridY); + Optional gridOpt = gridRepository.findByGridXAndGridY(gridX, gridY); + + if (gridOpt.isEmpty()) { + logger.warn("分配网格员失败: 未找到网格, gridX={}, gridY={}", gridX, gridY); + + // 尝试查询所有网格,看看是否有数据 + List allGrids = gridRepository.findAll(); + logger.debug("数据库中共有 {} 个网格", allGrids.size()); + if (!allGrids.isEmpty()) { + Grid firstGrid = allGrids.get(0); + logger.debug("第一个网格: id={}, gridX={}, gridY={}, cityName={}", + firstGrid.getId(), firstGrid.getGridX(), firstGrid.getGridY(), firstGrid.getCityName()); + } + + return ResponseEntity.status(404).body("未找到指定坐标的网格"); + } + + logger.debug("查询用户: userId={}", userId); + Optional userOpt = userAccountRepository.findById(userId); + + if (userOpt.isEmpty()) { + logger.warn("分配网格员失败: 未找到用户, userId={}", userId); + return ResponseEntity.badRequest().body("用户不存在"); + } + + Grid grid = gridOpt.get(); + UserAccount user = userOpt.get(); + + logger.debug("网格信息: id={}, gridX={}, gridY={}", grid.getId(), grid.getGridX(), grid.getGridY()); + logger.debug("用户信息: id={}, name={}, role={}", user.getId(), user.getName(), user.getRole()); + + if (user.getRole() != Role.GRID_WORKER) { + logger.warn("分配网格员失败: 用户角色不是网格员, userId={}, role={}", userId, user.getRole()); + return ResponseEntity.badRequest().body("只能分配网格员角色的用户"); + } + + // 先检查是否有其他网格员已分配到此网格 + logger.debug("检查是否有其他网格员已分配到此网格: gridX={}, gridY={}", gridX, gridY); + List existingWorkers = userAccountRepository.findGridWorkersByCoordinates(gridX, gridY); + logger.debug("已分配的网格员数量: {}", existingWorkers.size()); + + for (UserAccount existingWorker : existingWorkers) { + if (!existingWorker.getId().equals(userId)) { + logger.info("移除已存在的网格员: workerId={}, name={}", existingWorker.getId(), existingWorker.getName()); + existingWorker.setGridX(-1); + existingWorker.setGridY(-1); + userAccountRepository.save(existingWorker); + } + } + + // 分配新网格员 + logger.info("分配网格员: userId={}, name={}, gridX={}, gridY={}", user.getId(), user.getName(), grid.getGridX(), grid.getGridY()); + user.setGridX(grid.getGridX()); + user.setGridY(grid.getGridY()); + userAccountRepository.save(user); + + // 记录操作日志 + operationLogService.recordOperation( + com.dne.ems.model.enums.OperationType.UPDATE, + "将网格员 " + user.getName() + " (ID: " + userId + ") 分配到网格 (X: " + gridX + ", Y: " + gridY + ")", + userId.toString(), + "UserAccount" + ); + + logger.info("成功将用户 {} 分配到网格 (X: {}, Y: {})", user.getName(), gridX, gridY); + return ResponseEntity.ok().build(); + + } catch (Exception e) { + logger.error("通过坐标分配网格员时发生异常", e); + return ResponseEntity.status(500).body("服务器内部错误"); + } + } + + /** + * 通过网格坐标移除网格员,仅限ADMIN角色使用。 + * + * @param gridX 网格X坐标 + * @param gridY 网格Y坐标 + * @return 操作结果响应 + */ + @PostMapping("/coordinates/{gridX}/{gridY}/unassign") + @PreAuthorize("hasAnyRole('ADMIN', 'GRID_WORKER')") + public ResponseEntity removeGridWorkerByCoordinates( + @PathVariable Integer gridX, + @PathVariable Integer gridY) { + logger.info("开始通过坐标移除网格员: gridX={}, gridY={}", gridX, gridY); + + try { + // 查找指定坐标的网格员 + logger.debug("查询指定坐标的网格员: gridX={}, gridY={}", gridX, gridY); + List workers = userAccountRepository.findGridWorkersByCoordinates(gridX, gridY); + logger.debug("找到网格员数量: {}", workers.size()); + + if (workers.isEmpty()) { + logger.warn("移除网格员失败: 未找到网格员, gridX={}, gridY={}", gridX, gridY); + return ResponseEntity.status(404).body("未找到指定坐标的网格员"); + } + + // 移除所有网格员的分配 + for (UserAccount worker : workers) { + logger.info("移除网格员分配: workerId={}, name={}, 原坐标: gridX={}, gridY={}", + worker.getId(), worker.getName(), worker.getGridX(), worker.getGridY()); + + try { + worker.setGridX(-1); + worker.setGridY(-1); + UserAccount savedWorker = userAccountRepository.save(worker); + logger.info("网格员移除成功: workerId={}, 新坐标: gridX={}, gridY={}", + savedWorker.getId(), savedWorker.getGridX(), savedWorker.getGridY()); + + // 验证保存是否成功 + UserAccount verifiedWorker = userAccountRepository.findById(worker.getId()).orElse(null); + if (verifiedWorker != null) { + logger.debug("验证移除结果: gridX={}, gridY={}", verifiedWorker.getGridX(), verifiedWorker.getGridY()); + } + } catch (Exception e) { + logger.error("保存用户数据时发生异常", e); + return ResponseEntity.status(500).body("保存用户数据失败: " + e.getMessage()); + } + } + + return ResponseEntity.ok().build(); + } catch (Exception e) { + logger.error("移除网格员时发生异常", e); + return ResponseEntity.status(500).body("移除网格员失败: " + e.getMessage()); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/GridWorkerTaskController.java b/ems-backend/src/main/java/com/dne/ems/controller/GridWorkerTaskController.java index 2d02387..1af02d1 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/GridWorkerTaskController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/GridWorkerTaskController.java @@ -1,136 +1,136 @@ -package com.dne.ems.controller; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import com.dne.ems.dto.TaskDetailDTO; -import com.dne.ems.dto.TaskSubmissionRequest; -import com.dne.ems.dto.TaskSummaryDTO; -import com.dne.ems.model.enums.TaskStatus; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.GridWorkerTaskService; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -import java.util.List; -import org.springframework.http.MediaType; - -/** - * 网格员任务控制器,处理网格员的任务管理操作 - * - *

主要功能包括: - *

    - *
  • 获取分配的任务列表
  • - *
  • 接受任务
  • - *
  • 提交任务完成情况
  • - *
  • 查看任务详情
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api/worker") -@RequiredArgsConstructor -@PreAuthorize("hasRole('GRID_WORKER')") -@Tag(name = "Grid Worker Tasks", description = "Endpoints for grid workers to manage their assigned tasks") -public class GridWorkerTaskController { - - private final GridWorkerTaskService gridWorkerTaskService; - - /** - * 获取当前网格员分配的任务列表 - * - * @param userDetails 当前认证用户信息 - * @param status 任务状态过滤条件(可选) - * @param pageable 分页参数 - * @return 任务摘要列表 - * @see TaskSummaryDTO - */ - @GetMapping - @PreAuthorize("hasAuthority('ROLE_GRID_WORKER')") - @Operation(summary = "Get my assigned tasks", description = "Retrieves a paginated list of tasks assigned to the currently authenticated grid worker.") - public ResponseEntity> getMyTasks( - @AuthenticationPrincipal CustomUserDetails userDetails, - @Parameter(description = "Filter tasks by status") @RequestParam(required = false) TaskStatus status, - Pageable pageable - ) { - Page tasks = gridWorkerTaskService.getAssignedTasks(userDetails.getId(), status, pageable); - return ResponseEntity.ok(tasks); - } - - /** - * 接受指定任务 - * - * @param taskId 任务ID - * @param userDetails 当前认证用户信息 - * @return 更新后的任务摘要 - * @throws ResourceNotFoundException 如果任务不存在 - * @throws InvalidOperationException 如果任务无法被接受 - * @see TaskSummaryDTO - */ - @PostMapping("/{taskId}/accept") - public ResponseEntity acceptTask( - @PathVariable Long taskId, - @AuthenticationPrincipal CustomUserDetails userDetails) { - TaskSummaryDTO updatedTask = gridWorkerTaskService.acceptTask(taskId, userDetails.getId()); - return ResponseEntity.ok(updatedTask); - } - - /** - * 提交任务完成情况 - * - * @param taskId 任务ID - * @param comments 任务完成评论 - * @param files 任务完成附件 - * @param userDetails 当前认证用户信息 - * @return 更新后的任务摘要 - * @throws ResourceNotFoundException 如果任务不存在 - * @throws InvalidOperationException 如果任务无法被提交 - * @see TaskSubmissionRequest - * @see TaskSummaryDTO - */ - @PostMapping(value = "/{taskId}/submit", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity submitTask( - @PathVariable Long taskId, - @RequestPart("comments") @Valid String comments, - @RequestPart(value = "files", required = false) List files, - @AuthenticationPrincipal CustomUserDetails userDetails) { - TaskSubmissionRequest request = new TaskSubmissionRequest(comments); - TaskSummaryDTO updatedTask = gridWorkerTaskService.submitTaskCompletion(taskId, userDetails.getId(), request, files); - return ResponseEntity.ok(updatedTask); - } - /** - * 获取任务详情 - * - * @param taskId 任务ID - * @param userDetails 当前认证用户信息 - * @return 任务详情 - * @throws ResourceNotFoundException 如果任务不存在 - * @see TaskDetailDTO - */ - @GetMapping("/{taskId}") - public ResponseEntity getTaskDetails( - @PathVariable Long taskId, - @AuthenticationPrincipal CustomUserDetails userDetails) { - TaskDetailDTO taskDetails = gridWorkerTaskService.getTaskDetails(taskId, userDetails.getId()); - return ResponseEntity.ok(taskDetails); - } +package com.dne.ems.controller; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.dne.ems.dto.TaskDetailDTO; +import com.dne.ems.dto.TaskSubmissionRequest; +import com.dne.ems.dto.TaskSummaryDTO; +import com.dne.ems.model.enums.TaskStatus; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.GridWorkerTaskService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import org.springframework.http.MediaType; + +/** + * 网格员任务控制器,处理网格员的任务管理操作 + * + *

主要功能包括: + *

    + *
  • 获取分配的任务列表
  • + *
  • 接受任务
  • + *
  • 提交任务完成情况
  • + *
  • 查看任务详情
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api/worker") +@RequiredArgsConstructor +@PreAuthorize("hasRole('GRID_WORKER')") +@Tag(name = "Grid Worker Tasks", description = "Endpoints for grid workers to manage their assigned tasks") +public class GridWorkerTaskController { + + private final GridWorkerTaskService gridWorkerTaskService; + + /** + * 获取当前网格员分配的任务列表 + * + * @param userDetails 当前认证用户信息 + * @param status 任务状态过滤条件(可选) + * @param pageable 分页参数 + * @return 任务摘要列表 + * @see TaskSummaryDTO + */ + @GetMapping + @PreAuthorize("hasAuthority('ROLE_GRID_WORKER')") + @Operation(summary = "Get my assigned tasks", description = "Retrieves a paginated list of tasks assigned to the currently authenticated grid worker.") + public ResponseEntity> getMyTasks( + @AuthenticationPrincipal CustomUserDetails userDetails, + @Parameter(description = "Filter tasks by status") @RequestParam(required = false) TaskStatus status, + Pageable pageable + ) { + Page tasks = gridWorkerTaskService.getAssignedTasks(userDetails.getId(), status, pageable); + return ResponseEntity.ok(tasks); + } + + /** + * 接受指定任务 + * + * @param taskId 任务ID + * @param userDetails 当前认证用户信息 + * @return 更新后的任务摘要 + * @throws ResourceNotFoundException 如果任务不存在 + * @throws InvalidOperationException 如果任务无法被接受 + * @see TaskSummaryDTO + */ + @PostMapping("/{taskId}/accept") + public ResponseEntity acceptTask( + @PathVariable Long taskId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + TaskSummaryDTO updatedTask = gridWorkerTaskService.acceptTask(taskId, userDetails.getId()); + return ResponseEntity.ok(updatedTask); + } + + /** + * 提交任务完成情况 + * + * @param taskId 任务ID + * @param comments 任务完成评论 + * @param files 任务完成附件 + * @param userDetails 当前认证用户信息 + * @return 更新后的任务摘要 + * @throws ResourceNotFoundException 如果任务不存在 + * @throws InvalidOperationException 如果任务无法被提交 + * @see TaskSubmissionRequest + * @see TaskSummaryDTO + */ + @PostMapping(value = "/{taskId}/submit", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity submitTask( + @PathVariable Long taskId, + @RequestPart("comments") @Valid String comments, + @RequestPart(value = "files", required = false) List files, + @AuthenticationPrincipal CustomUserDetails userDetails) { + TaskSubmissionRequest request = new TaskSubmissionRequest(comments); + TaskSummaryDTO updatedTask = gridWorkerTaskService.submitTaskCompletion(taskId, userDetails.getId(), request, files); + return ResponseEntity.ok(updatedTask); + } + /** + * 获取任务详情 + * + * @param taskId 任务ID + * @param userDetails 当前认证用户信息 + * @return 任务详情 + * @throws ResourceNotFoundException 如果任务不存在 + * @see TaskDetailDTO + */ + @GetMapping("/{taskId}") + public ResponseEntity getTaskDetails( + @PathVariable Long taskId, + @AuthenticationPrincipal CustomUserDetails userDetails) { + TaskDetailDTO taskDetails = gridWorkerTaskService.getTaskDetails(taskId, userDetails.getId()); + return ResponseEntity.ok(taskDetails); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/MapController.java b/ems-backend/src/main/java/com/dne/ems/controller/MapController.java index f27a556..c15f20f 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/MapController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/MapController.java @@ -1,106 +1,106 @@ -package com.dne.ems.controller; - -import java.util.List; - -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.model.MapGrid; -import com.dne.ems.repository.MapGridRepository; - -import lombok.RequiredArgsConstructor; - -/** - * 地图控制器,处理地图网格数据的操作 - * - *

主要功能包括: - *

    - *
  • 获取完整地图网格数据
  • - *
  • 创建或更新网格单元
  • - *
  • 初始化地图
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api/map") -@RequiredArgsConstructor -public class MapController { - - private final MapGridRepository mapGridRepository; - - /** - * 获取完整地图网格数据 - * - * @return 包含所有地图网格数据的响应实体 - */ - @GetMapping("/grid") - public ResponseEntity> getFullMap() { - List mapGrids = mapGridRepository.findAllByOrderByYAscXAsc(); - return ResponseEntity.ok(mapGrids); - } - - /** - * 创建或更新网格单元 - * - * @param gridCellRequest 包含网格单元数据的请求体 - * @return 包含保存后的网格单元的响应实体 - * @throws IllegalArgumentException 如果坐标值为负数 - */ - @PostMapping("/grid") - @Transactional - public ResponseEntity createOrUpdateGridCell(@RequestBody MapGrid gridCellRequest) { - // Simple validation - if (gridCellRequest.getX() < 0 || gridCellRequest.getY() < 0) { - return ResponseEntity.badRequest().build(); - } - - MapGrid gridCell = mapGridRepository.findByXAndY(gridCellRequest.getX(), gridCellRequest.getY()) - .orElse(new MapGrid()); - - gridCell.setX(gridCellRequest.getX()); - gridCell.setY(gridCellRequest.getY()); - gridCell.setObstacle(gridCellRequest.isObstacle()); - gridCell.setTerrainType(gridCellRequest.getTerrainType()); - - MapGrid savedCell = mapGridRepository.save(gridCell); - return ResponseEntity.ok(savedCell); - } - - /** - * 初始化地图 - * - * @param width 地图宽度(默认20) - * @param height 地图高度(默认20) - * @return 包含初始化结果的响应实体 - */ - @PostMapping("/initialize") - @Transactional - public ResponseEntity initializeMap( - @RequestParam(defaultValue = "20") int width, - @RequestParam(defaultValue = "20") int height) { - - mapGridRepository.deleteAll(); // Clear existing map - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - MapGrid cell = MapGrid.builder() - .x(x) - .y(y) - .isObstacle(false) - .terrainType("ground") - .build(); - mapGridRepository.save(cell); - } - } - return ResponseEntity.ok("Initialized a " + width + "x" + height + " map."); - } +package com.dne.ems.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.model.MapGrid; +import com.dne.ems.repository.MapGridRepository; + +import lombok.RequiredArgsConstructor; + +/** + * 地图控制器,处理地图网格数据的操作 + * + *

主要功能包括: + *

    + *
  • 获取完整地图网格数据
  • + *
  • 创建或更新网格单元
  • + *
  • 初始化地图
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api/map") +@RequiredArgsConstructor +public class MapController { + + private final MapGridRepository mapGridRepository; + + /** + * 获取完整地图网格数据 + * + * @return 包含所有地图网格数据的响应实体 + */ + @GetMapping("/grid") + public ResponseEntity> getFullMap() { + List mapGrids = mapGridRepository.findAllByOrderByYAscXAsc(); + return ResponseEntity.ok(mapGrids); + } + + /** + * 创建或更新网格单元 + * + * @param gridCellRequest 包含网格单元数据的请求体 + * @return 包含保存后的网格单元的响应实体 + * @throws IllegalArgumentException 如果坐标值为负数 + */ + @PostMapping("/grid") + @Transactional + public ResponseEntity createOrUpdateGridCell(@RequestBody MapGrid gridCellRequest) { + // Simple validation + if (gridCellRequest.getX() < 0 || gridCellRequest.getY() < 0) { + return ResponseEntity.badRequest().build(); + } + + MapGrid gridCell = mapGridRepository.findByXAndY(gridCellRequest.getX(), gridCellRequest.getY()) + .orElse(new MapGrid()); + + gridCell.setX(gridCellRequest.getX()); + gridCell.setY(gridCellRequest.getY()); + gridCell.setObstacle(gridCellRequest.isObstacle()); + gridCell.setTerrainType(gridCellRequest.getTerrainType()); + + MapGrid savedCell = mapGridRepository.save(gridCell); + return ResponseEntity.ok(savedCell); + } + + /** + * 初始化地图 + * + * @param width 地图宽度(默认20) + * @param height 地图高度(默认20) + * @return 包含初始化结果的响应实体 + */ + @PostMapping("/initialize") + @Transactional + public ResponseEntity initializeMap( + @RequestParam(defaultValue = "20") int width, + @RequestParam(defaultValue = "20") int height) { + + mapGridRepository.deleteAll(); // Clear existing map + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + MapGrid cell = MapGrid.builder() + .x(x) + .y(y) + .isObstacle(false) + .terrainType("ground") + .build(); + mapGridRepository.save(cell); + } + } + return ResponseEntity.ok("Initialized a " + width + "x" + height + " map."); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/OperationLogController.java b/ems-backend/src/main/java/com/dne/ems/controller/OperationLogController.java index 3fe90b6..a2a0fc0 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/OperationLogController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/OperationLogController.java @@ -1,89 +1,89 @@ -package com.dne.ems.controller; - -import java.time.LocalDateTime; -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.dto.OperationLogDTO; -import com.dne.ems.model.enums.OperationType; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.OperationLogService; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; - -/** - * 操作日志控制器,提供查询用户操作日志的API。 - * - * @version 1.0 - * @since 2025-06-20 - */ -@RestController -@RequestMapping("/api/logs") -@Tag(name = "操作日志管理", description = "提供操作日志查询功能") -public class OperationLogController { - - @Autowired - private OperationLogService operationLogService; - - /** - * 获取当前用户的操作日志 - * - * @param userDetails 当前用户详情 - * @return 操作日志列表 - */ - @GetMapping("/my-logs") - @Operation(summary = "获取当前用户的操作日志", description = "返回当前登录用户的所有操作记录") - public ResponseEntity> getMyOperationLogs( - @AuthenticationPrincipal CustomUserDetails userDetails) { - - Long userId = userDetails.getUserAccount().getId(); - List logs = operationLogService.getUserOperationLogs(userId); - return ResponseEntity.ok(logs); - } - - /** - * 获取所有用户的操作日志(仅限管理员) - * - * @param operationType 操作类型(可选) - * @param userId 用户ID(可选) - * @param startTime 开始时间(可选) - * @param endTime 结束时间(可选) - * @return 操作日志列表 - */ - @GetMapping - @PreAuthorize("hasRole('ADMIN')") - @Operation(summary = "获取所有操作日志", description = "管理员可以查询所有用户的操作日志,支持多种过滤条件") - public ResponseEntity> getAllOperationLogs( - @RequestParam(required = false) - @Parameter(description = "操作类型") - OperationType operationType, - - @RequestParam(required = false) - @Parameter(description = "用户ID") - Long userId, - - @RequestParam(required = false) - @Parameter(description = "开始时间,格式:yyyy-MM-dd'T'HH:mm:ss") - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - LocalDateTime startTime, - - @RequestParam(required = false) - @Parameter(description = "结束时间,格式:yyyy-MM-dd'T'HH:mm:ss") - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - LocalDateTime endTime) { - - List logs = operationLogService.queryOperationLogs(userId, operationType, startTime, endTime); - return ResponseEntity.ok(logs); - } +package com.dne.ems.controller; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.dto.OperationLogDTO; +import com.dne.ems.model.enums.OperationType; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.OperationLogService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * 操作日志控制器,提供查询用户操作日志的API。 + * + * @version 1.0 + * @since 2025-06-20 + */ +@RestController +@RequestMapping("/api/logs") +@Tag(name = "操作日志管理", description = "提供操作日志查询功能") +public class OperationLogController { + + @Autowired + private OperationLogService operationLogService; + + /** + * 获取当前用户的操作日志 + * + * @param userDetails 当前用户详情 + * @return 操作日志列表 + */ + @GetMapping("/my-logs") + @Operation(summary = "获取当前用户的操作日志", description = "返回当前登录用户的所有操作记录") + public ResponseEntity> getMyOperationLogs( + @AuthenticationPrincipal CustomUserDetails userDetails) { + + Long userId = userDetails.getUserAccount().getId(); + List logs = operationLogService.getUserOperationLogs(userId); + return ResponseEntity.ok(logs); + } + + /** + * 获取所有用户的操作日志(仅限管理员) + * + * @param operationType 操作类型(可选) + * @param userId 用户ID(可选) + * @param startTime 开始时间(可选) + * @param endTime 结束时间(可选) + * @return 操作日志列表 + */ + @GetMapping + @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "获取所有操作日志", description = "管理员可以查询所有用户的操作日志,支持多种过滤条件") + public ResponseEntity> getAllOperationLogs( + @RequestParam(required = false) + @Parameter(description = "操作类型") + OperationType operationType, + + @RequestParam(required = false) + @Parameter(description = "用户ID") + Long userId, + + @RequestParam(required = false) + @Parameter(description = "开始时间,格式:yyyy-MM-dd'T'HH:mm:ss") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + LocalDateTime startTime, + + @RequestParam(required = false) + @Parameter(description = "结束时间,格式:yyyy-MM-dd'T'HH:mm:ss") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + LocalDateTime endTime) { + + List logs = operationLogService.queryOperationLogs(userId, operationType, startTime, endTime); + return ResponseEntity.ok(logs); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/PathfindingController.java b/ems-backend/src/main/java/com/dne/ems/controller/PathfindingController.java index 12c0c0a..83ce63e 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/PathfindingController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/PathfindingController.java @@ -1,62 +1,62 @@ -package com.dne.ems.controller; - -import java.util.List; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.dto.PathfindingRequest; -import com.dne.ems.dto.Point; -import com.dne.ems.service.pathfinding.AStarService; - -import lombok.RequiredArgsConstructor; - -/** - * 路径规划控制器,提供A*寻路算法功能 - * - *

主要功能: - *

    - *
  • 计算两点之间的最优路径
  • - *
  • 考虑地图障碍物
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api/pathfinding") -@RequiredArgsConstructor -public class PathfindingController { - - private final AStarService aStarService; - - /** - * 使用A*算法查找两点之间的路径 - * - *

实现细节: - *

    - *
  1. 验证请求参数
  2. - *
  3. 调用A*算法服务计算路径
  4. - *
  5. 返回路径点列表
  6. - *
- * - * @param request 包含起点和终点坐标的请求体 - * @return 包含路径点列表的响应实体,若无路径则返回空列表 - * @throws IllegalArgumentException 如果请求参数无效 - */ - @PostMapping("/find") - public ResponseEntity> findPath(@RequestBody PathfindingRequest request) { - if (request == null) { - return ResponseEntity.badRequest().build(); - } - Point start = new Point(request.getStartX(), request.getStartY()); - Point end = new Point(request.getEndX(), request.getEndY()); - - List path = aStarService.findPath(start, end); - return ResponseEntity.ok(path); - } +package com.dne.ems.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.dto.PathfindingRequest; +import com.dne.ems.dto.Point; +import com.dne.ems.service.pathfinding.AStarService; + +import lombok.RequiredArgsConstructor; + +/** + * 路径规划控制器,提供A*寻路算法功能 + * + *

主要功能: + *

    + *
  • 计算两点之间的最优路径
  • + *
  • 考虑地图障碍物
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api/pathfinding") +@RequiredArgsConstructor +public class PathfindingController { + + private final AStarService aStarService; + + /** + * 使用A*算法查找两点之间的路径 + * + *

实现细节: + *

    + *
  1. 验证请求参数
  2. + *
  3. 调用A*算法服务计算路径
  4. + *
  5. 返回路径点列表
  6. + *
+ * + * @param request 包含起点和终点坐标的请求体 + * @return 包含路径点列表的响应实体,若无路径则返回空列表 + * @throws IllegalArgumentException 如果请求参数无效 + */ + @PostMapping("/find") + public ResponseEntity> findPath(@RequestBody PathfindingRequest request) { + if (request == null) { + return ResponseEntity.badRequest().build(); + } + Point start = new Point(request.getStartX(), request.getStartY()); + Point end = new Point(request.getEndX(), request.getEndY()); + + List path = aStarService.findPath(start, end); + return ResponseEntity.ok(path); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/PersonnelController.java b/ems-backend/src/main/java/com/dne/ems/controller/PersonnelController.java index 3c8ffd6..fd93646 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/PersonnelController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/PersonnelController.java @@ -1,157 +1,157 @@ -package com.dne.ems.controller; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.dto.PageDTO; -import com.dne.ems.dto.UserCreationRequest; -import com.dne.ems.dto.UserRoleUpdateRequest; -import com.dne.ems.dto.UserUpdateRequest; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.Role; -import com.dne.ems.service.PersonnelService; -import com.dne.ems.service.UserAccountService; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -/** - * 人员管理控制器,处理用户账号的CRUD操作 - * - *

主要功能包括: - *

    - *
  • 用户账号创建
  • - *
  • 用户信息查询与更新
  • - *
  • 用户角色管理
  • - *
  • 用户删除
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api/personnel") -@RequiredArgsConstructor -@PreAuthorize("hasRole('ADMIN')") -public class PersonnelController { - - private final PersonnelService personnelService; - private final UserAccountService userAccountService; - - /** - * 创建新用户 - * - * @param request 包含用户信息的请求体 - * @return 创建成功的用户实体 - * @throws UserAlreadyExistsException 如果用户名或邮箱已存在 - */ - @PostMapping("/users") - @ResponseStatus(HttpStatus.CREATED) - public UserAccount createUser(@RequestBody @Valid UserCreationRequest request) { - return personnelService.createUser(request); - } - - /** - * 获取用户分页列表 - * - * @param role 角色过滤条件(可选) - * @param name 姓名过滤条件(可选,不区分大小写) - * @param pageable 分页参数 - * @return 用户分页列表 - */ - @Operation(summary = "Get a paginated list of users", description = "Retrieves a list of users, with optional filtering by role and name.") - @GetMapping("/users") - @PreAuthorize("hasRole('ADMIN') or hasRole('GRID_WORKER')") - public ResponseEntity> getUsers( - @Parameter(description = "Filter by user role") @RequestParam(required = false) Role role, - @Parameter(description = "Filter by user name (case-insensitive search)") @RequestParam(required = false) String name, - Pageable pageable) { - Page usersPage = personnelService.getUsers(role, name, pageable); - PageDTO userPageDTO = new PageDTO<>( - usersPage.getContent(), - usersPage.getTotalElements(), - usersPage.getTotalPages(), - usersPage.getNumber(), - usersPage.getSize() - ); - return ResponseEntity.ok(userPageDTO); - } - - /** - * 根据ID获取用户详情 - * - * @param userId 用户ID - * @return 用户详情 - * @throws ResourceNotFoundException 如果用户不存在 - */ - @Operation(summary = "Get user by ID", description = "Fetches the details of a specific user by their ID.") - @GetMapping("/users/{userId}") - public ResponseEntity getUserById(@PathVariable Long userId) { - UserAccount user = userAccountService.getUserById(userId); - user.setPassword(null); // Do not expose password - return ResponseEntity.ok(user); - } - - /** - * 更新用户信息 - * - * @param userId 用户ID - * @param request 包含更新信息的请求体 - * @return 更新后的用户实体 - * @throws ResourceNotFoundException 如果用户不存在 - */ - @PatchMapping("/users/{userId}") - public ResponseEntity updateUser( - @PathVariable Long userId, - @RequestBody UserUpdateRequest request) { - UserAccount updatedUser = personnelService.updateUser(userId, request); - return ResponseEntity.ok(updatedUser); - } - - /** - * 更新用户角色 - * - * @param userId 用户ID - * @param request 包含角色信息的请求体 - * @return 更新后的用户实体 - * @throws ResourceNotFoundException 如果用户不存在 - * @throws InvalidOperationException 如果角色更新无效 - */ - @PutMapping("/users/{userId}/role") - public ResponseEntity updateUserRole( - @PathVariable Long userId, - @RequestBody @Valid UserRoleUpdateRequest request) { - UserAccount updatedUser = userAccountService.updateUserRole(userId, request); - updatedUser.setPassword(null); // Do not expose password - return ResponseEntity.ok(updatedUser); - } - - /** - * 删除用户 - * - * @param userId 用户ID - * @throws ResourceNotFoundException 如果用户不存在 - */ - @DeleteMapping("/users/{userId}") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteUser(@PathVariable Long userId) { - personnelService.deleteUser(userId); - } +package com.dne.ems.controller; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.dto.PageDTO; +import com.dne.ems.dto.UserCreationRequest; +import com.dne.ems.dto.UserRoleUpdateRequest; +import com.dne.ems.dto.UserUpdateRequest; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.Role; +import com.dne.ems.service.PersonnelService; +import com.dne.ems.service.UserAccountService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +/** + * 人员管理控制器,处理用户账号的CRUD操作 + * + *

主要功能包括: + *

    + *
  • 用户账号创建
  • + *
  • 用户信息查询与更新
  • + *
  • 用户角色管理
  • + *
  • 用户删除
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api/personnel") +@RequiredArgsConstructor +@PreAuthorize("hasRole('ADMIN')") +public class PersonnelController { + + private final PersonnelService personnelService; + private final UserAccountService userAccountService; + + /** + * 创建新用户 + * + * @param request 包含用户信息的请求体 + * @return 创建成功的用户实体 + * @throws UserAlreadyExistsException 如果用户名或邮箱已存在 + */ + @PostMapping("/users") + @ResponseStatus(HttpStatus.CREATED) + public UserAccount createUser(@RequestBody @Valid UserCreationRequest request) { + return personnelService.createUser(request); + } + + /** + * 获取用户分页列表 + * + * @param role 角色过滤条件(可选) + * @param name 姓名过滤条件(可选,不区分大小写) + * @param pageable 分页参数 + * @return 用户分页列表 + */ + @Operation(summary = "Get a paginated list of users", description = "Retrieves a list of users, with optional filtering by role and name.") + @GetMapping("/users") + @PreAuthorize("hasRole('ADMIN') or hasRole('GRID_WORKER')") + public ResponseEntity> getUsers( + @Parameter(description = "Filter by user role") @RequestParam(required = false) Role role, + @Parameter(description = "Filter by user name (case-insensitive search)") @RequestParam(required = false) String name, + Pageable pageable) { + Page usersPage = personnelService.getUsers(role, name, pageable); + PageDTO userPageDTO = new PageDTO<>( + usersPage.getContent(), + usersPage.getTotalElements(), + usersPage.getTotalPages(), + usersPage.getNumber(), + usersPage.getSize() + ); + return ResponseEntity.ok(userPageDTO); + } + + /** + * 根据ID获取用户详情 + * + * @param userId 用户ID + * @return 用户详情 + * @throws ResourceNotFoundException 如果用户不存在 + */ + @Operation(summary = "Get user by ID", description = "Fetches the details of a specific user by their ID.") + @GetMapping("/users/{userId}") + public ResponseEntity getUserById(@PathVariable Long userId) { + UserAccount user = userAccountService.getUserById(userId); + user.setPassword(null); // Do not expose password + return ResponseEntity.ok(user); + } + + /** + * 更新用户信息 + * + * @param userId 用户ID + * @param request 包含更新信息的请求体 + * @return 更新后的用户实体 + * @throws ResourceNotFoundException 如果用户不存在 + */ + @PatchMapping("/users/{userId}") + public ResponseEntity updateUser( + @PathVariable Long userId, + @RequestBody UserUpdateRequest request) { + UserAccount updatedUser = personnelService.updateUser(userId, request); + return ResponseEntity.ok(updatedUser); + } + + /** + * 更新用户角色 + * + * @param userId 用户ID + * @param request 包含角色信息的请求体 + * @return 更新后的用户实体 + * @throws ResourceNotFoundException 如果用户不存在 + * @throws InvalidOperationException 如果角色更新无效 + */ + @PutMapping("/users/{userId}/role") + public ResponseEntity updateUserRole( + @PathVariable Long userId, + @RequestBody @Valid UserRoleUpdateRequest request) { + UserAccount updatedUser = userAccountService.updateUserRole(userId, request); + updatedUser.setPassword(null); // Do not expose password + return ResponseEntity.ok(updatedUser); + } + + /** + * 删除用户 + * + * @param userId 用户ID + * @throws ResourceNotFoundException 如果用户不存在 + */ + @DeleteMapping("/users/{userId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteUser(@PathVariable Long userId) { + personnelService.deleteUser(userId); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/ProfileController.java b/ems-backend/src/main/java/com/dne/ems/controller/ProfileController.java index 5461a80..5f9a8c4 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/ProfileController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/ProfileController.java @@ -1,69 +1,69 @@ -package com.dne.ems.controller; - -import java.util.List; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.dto.UserFeedbackSummaryDTO; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.UserFeedbackService; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; - -/** - * 用户个人资料控制器,处理认证用户相关数据 - * - *

主要功能: - *

    - *
  • 获取用户提交的反馈历史
  • - *
  • 管理个人资料相关数据
  • - *
- * - * @author DNE开发团队 - * @version 1.2 - * @since 2025-06-16 - */ -@RestController -@RequestMapping("/api/me") -@RequiredArgsConstructor -@Tag(name = "User Profile", description = "Endpoints related to the authenticated user's own data") -@SecurityRequirement(name = "bearerAuth") -public class ProfileController { - - private final UserFeedbackService userFeedbackService; - - /** - * 获取当前用户的反馈历史记录 - * - *

实现细节: - *

    - *
  1. 从认证信息获取用户ID
  2. - *
  3. 查询该用户提交的所有反馈
  4. - *
  5. 返回分页结果
  6. - *
- * - * @param userDetails 认证用户详情 - * @param pageable 分页参数 - * @return 用户反馈历史的分页列表 - * @throws AuthenticationException 如果用户未认证 - */ - @Operation(summary = "Get current user's feedback history", description = "Retrieves a paginated list of feedback submitted by the currently authenticated user.") - @GetMapping("/feedback") - @PreAuthorize("isAuthenticated()") - public ResponseEntity> getMyFeedbackHistory( - @AuthenticationPrincipal CustomUserDetails userDetails, - Pageable pageable) { - Page historyPage = userFeedbackService.getFeedbackHistoryByUserId(userDetails.getId(), pageable); - return ResponseEntity.ok(historyPage.getContent()); - } +package com.dne.ems.controller; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.dto.UserFeedbackSummaryDTO; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.UserFeedbackService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +/** + * 用户个人资料控制器,处理认证用户相关数据 + * + *

主要功能: + *

    + *
  • 获取用户提交的反馈历史
  • + *
  • 管理个人资料相关数据
  • + *
+ * + * @author DNE开发团队 + * @version 1.2 + * @since 2025-06-16 + */ +@RestController +@RequestMapping("/api/me") +@RequiredArgsConstructor +@Tag(name = "User Profile", description = "Endpoints related to the authenticated user's own data") +@SecurityRequirement(name = "bearerAuth") +public class ProfileController { + + private final UserFeedbackService userFeedbackService; + + /** + * 获取当前用户的反馈历史记录 + * + *

实现细节: + *

    + *
  1. 从认证信息获取用户ID
  2. + *
  3. 查询该用户提交的所有反馈
  4. + *
  5. 返回分页结果
  6. + *
+ * + * @param userDetails 认证用户详情 + * @param pageable 分页参数 + * @return 用户反馈历史的分页列表 + * @throws AuthenticationException 如果用户未认证 + */ + @Operation(summary = "Get current user's feedback history", description = "Retrieves a paginated list of feedback submitted by the currently authenticated user.") + @GetMapping("/feedback") + @PreAuthorize("isAuthenticated()") + public ResponseEntity> getMyFeedbackHistory( + @AuthenticationPrincipal CustomUserDetails userDetails, + Pageable pageable) { + Page historyPage = userFeedbackService.getFeedbackHistoryByUserId(userDetails.getId(), pageable); + return ResponseEntity.ok(historyPage.getContent()); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/PublicController.java b/ems-backend/src/main/java/com/dne/ems/controller/PublicController.java index 23a6530..84abe05 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/PublicController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/PublicController.java @@ -1,63 +1,63 @@ -package com.dne.ems.controller; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import com.dne.ems.dto.PublicFeedbackRequest; -import com.dne.ems.model.Feedback; -import com.dne.ems.service.FeedbackService; - -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -/** - * 公共API控制器,处理无需认证的公共接口 - * - *

主要功能: - *

    - *
  • 接收公众反馈提交
  • - *
  • 处理带图片上传的表单
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api/public") -@RequiredArgsConstructor -public class PublicController { - - private final FeedbackService feedbackService; - - /** - * 提交公共反馈(支持图片上传) - * - *

实现细节: - *

    - *
  1. 验证反馈请求参数
  2. - *
  3. 处理上传的图片文件(如果有)
  4. - *
  5. 保存反馈到数据库
  6. - *
- * - * @param request 包含反馈信息的请求体(JSON格式) - * @param files 上传的图片文件数组(可选) - * @return 创建成功的反馈实体 - * @throws InvalidRequestException 如果请求参数无效 - * @throws FileUploadException 如果文件上传失败 - */ - @PostMapping(value = "/feedback", consumes = "multipart/form-data") - public ResponseEntity submitPublicFeedback( - @Valid @RequestPart("feedback") PublicFeedbackRequest request, - @RequestPart(value = "files", required = false) MultipartFile[] files) { - // We will need to update the service method to handle files. - // For now, let's pass null and focus on the controller signature. - Feedback createdFeedback = feedbackService.createPublicFeedback(request, files); - return new ResponseEntity<>(createdFeedback, HttpStatus.CREATED); - } +package com.dne.ems.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.dne.ems.dto.PublicFeedbackRequest; +import com.dne.ems.model.Feedback; +import com.dne.ems.service.FeedbackService; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +/** + * 公共API控制器,处理无需认证的公共接口 + * + *

主要功能: + *

    + *
  • 接收公众反馈提交
  • + *
  • 处理带图片上传的表单
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api/public") +@RequiredArgsConstructor +public class PublicController { + + private final FeedbackService feedbackService; + + /** + * 提交公共反馈(支持图片上传) + * + *

实现细节: + *

    + *
  1. 验证反馈请求参数
  2. + *
  3. 处理上传的图片文件(如果有)
  4. + *
  5. 保存反馈到数据库
  6. + *
+ * + * @param request 包含反馈信息的请求体(JSON格式) + * @param files 上传的图片文件数组(可选) + * @return 创建成功的反馈实体 + * @throws InvalidRequestException 如果请求参数无效 + * @throws FileUploadException 如果文件上传失败 + */ + @PostMapping(value = "/feedback", consumes = "multipart/form-data") + public ResponseEntity submitPublicFeedback( + @Valid @RequestPart("feedback") PublicFeedbackRequest request, + @RequestPart(value = "files", required = false) MultipartFile[] files) { + // We will need to update the service method to handle files. + // For now, let's pass null and focus on the controller signature. + Feedback createdFeedback = feedbackService.createPublicFeedback(request, files); + return new ResponseEntity<>(createdFeedback, HttpStatus.CREATED); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/SupervisorController.java b/ems-backend/src/main/java/com/dne/ems/controller/SupervisorController.java index d920090..92a907d 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/SupervisorController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/SupervisorController.java @@ -1,90 +1,90 @@ -package com.dne.ems.controller; - -import java.util.List; - -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.dto.RejectFeedbackRequest; -import com.dne.ems.model.Feedback; -import com.dne.ems.service.SupervisorService; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; - -/** - * 主管控制器,处理反馈审核相关功能 - * - *

主要职责: - *

    - *
  • 获取待审核反馈列表
  • - *
  • 审核通过/拒绝反馈
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api/supervisor") -@RequiredArgsConstructor -@Tag(name = "Supervisor", description = "Endpoints for supervisor to review and manage feedback") -public class SupervisorController { - - private final SupervisorService supervisorService; - - /** - * 获取待审核反馈列表 - * - * @return 待审核反馈列表 - * @throws AccessDeniedException 如果用户无权限 - */ - @GetMapping("/reviews") - @PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") - @Operation(summary = "Get feedback list pending for supervisor review") - public ResponseEntity> getFeedbackForReview() { - List feedbackList = supervisorService.getFeedbackForReview(); - return ResponseEntity.ok(feedbackList); - } - - /** - * 审核通过反馈 - * - * @param feedbackId 反馈ID - * @return 空响应 - * @throws ResourceNotFoundException 如果反馈不存在 - * @throws InvalidOperationException 如果反馈已审核 - */ - @PostMapping("/reviews/{feedbackId}/approve") - @PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") - @Operation(summary = "Approve a feedback") - public ResponseEntity approveFeedback(@PathVariable Long feedbackId) { - supervisorService.approveFeedback(feedbackId); - return ResponseEntity.ok().build(); - } - - /** - * 审核拒绝反馈 - * - * @param feedbackId 反馈ID - * @return 空响应 - * @throws ResourceNotFoundException 如果反馈不存在 - * @throws InvalidOperationException 如果反馈已审核 - */ - @PostMapping("/reviews/{feedbackId}/reject") - @PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") - @Operation(summary = "Reject a feedback") - public ResponseEntity rejectFeedback( - @PathVariable Long feedbackId, - @RequestBody RejectFeedbackRequest request) { - supervisorService.rejectFeedback(feedbackId, request); - return ResponseEntity.ok().build(); - } +package com.dne.ems.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.dto.RejectFeedbackRequest; +import com.dne.ems.model.Feedback; +import com.dne.ems.service.SupervisorService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +/** + * 主管控制器,处理反馈审核相关功能 + * + *

主要职责: + *

    + *
  • 获取待审核反馈列表
  • + *
  • 审核通过/拒绝反馈
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api/supervisor") +@RequiredArgsConstructor +@Tag(name = "Supervisor", description = "Endpoints for supervisor to review and manage feedback") +public class SupervisorController { + + private final SupervisorService supervisorService; + + /** + * 获取待审核反馈列表 + * + * @return 待审核反馈列表 + * @throws AccessDeniedException 如果用户无权限 + */ + @GetMapping("/reviews") + @PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") + @Operation(summary = "Get feedback list pending for supervisor review") + public ResponseEntity> getFeedbackForReview() { + List feedbackList = supervisorService.getFeedbackForReview(); + return ResponseEntity.ok(feedbackList); + } + + /** + * 审核通过反馈 + * + * @param feedbackId 反馈ID + * @return 空响应 + * @throws ResourceNotFoundException 如果反馈不存在 + * @throws InvalidOperationException 如果反馈已审核 + */ + @PostMapping("/reviews/{feedbackId}/approve") + @PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") + @Operation(summary = "Approve a feedback") + public ResponseEntity approveFeedback(@PathVariable Long feedbackId) { + supervisorService.approveFeedback(feedbackId); + return ResponseEntity.ok().build(); + } + + /** + * 审核拒绝反馈 + * + * @param feedbackId 反馈ID + * @return 空响应 + * @throws ResourceNotFoundException 如果反馈不存在 + * @throws InvalidOperationException 如果反馈已审核 + */ + @PostMapping("/reviews/{feedbackId}/reject") + @PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") + @Operation(summary = "Reject a feedback") + public ResponseEntity rejectFeedback( + @PathVariable Long feedbackId, + @RequestBody RejectFeedbackRequest request) { + supervisorService.rejectFeedback(feedbackId, request); + return ResponseEntity.ok().build(); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/TaskAssignmentController.java b/ems-backend/src/main/java/com/dne/ems/controller/TaskAssignmentController.java index e531794..6a4c244 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/TaskAssignmentController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/TaskAssignmentController.java @@ -1,105 +1,105 @@ -package com.dne.ems.controller; - -import java.util.List; - -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.model.Assignment; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.UserAccount; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.TaskAssignmentService; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * 任务分配控制器,处理网格任务分配相关功能 - * - *

主要职责: - *

    - *
  • 获取未分配任务列表
  • - *
  • 查询可用网格工作人员
  • - *
  • 分配任务给工作人员
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api/tasks") -@RequiredArgsConstructor -@Slf4j -public class TaskAssignmentController { - - private final TaskAssignmentService taskAssignmentService; - - /** - * 获取未分配的任务列表 - * - * @return 未分配的反馈任务列表 - * @throws AccessDeniedException 如果用户无权限 - */ - @GetMapping("/unassigned") - @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") - public ResponseEntity> getUnassignedFeedback() { - List feedbackList = taskAssignmentService.getUnassignedFeedback(); - return ResponseEntity.ok(feedbackList); - } - - /** - * 获取可用的网格工作人员列表 - * - * @return 可用工作人员列表 - * @throws AccessDeniedException 如果用户无权限 - */ - @GetMapping("/grid-workers") - @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") - public ResponseEntity> getAvailableGridWorkers() { - log.info("API接收到获取可用网格员列表的请求"); - try { - List workers = taskAssignmentService.getAvailableGridWorkers(); - log.info("成功获取到 {} 名可用的网格员", workers.size()); - return ResponseEntity.ok(workers); - } catch (Exception e) { - log.error("获取可用网格员列表时发生未知异常", e); - return ResponseEntity.internalServerError().build(); - } - } - - /** - * 分配任务给工作人员 - * - * @param request 包含任务ID和分配人ID的请求体 - * @param adminDetails 当前管理员认证信息 - * @return 创建的任务分配记录 - * @throws ResourceNotFoundException 如果任务或工作人员不存在 - * @throws InvalidOperationException 如果任务已分配 - * @throws AccessDeniedException 如果用户无权限 - */ - @PostMapping("/assign") - @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") - public ResponseEntity assignTask( - @RequestBody AssignmentRequest request, - @AuthenticationPrincipal CustomUserDetails adminDetails) { - - Long assignerId = adminDetails.getId(); - Assignment newAssignment = taskAssignmentService.assignTask( - request.feedbackId(), - request.assigneeId(), - assignerId - ); - return ResponseEntity.ok(newAssignment); - } - - // A simple record to represent the assignment request payload - public record AssignmentRequest(Long feedbackId, Long assigneeId) {} +package com.dne.ems.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.model.Assignment; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.UserAccount; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.TaskAssignmentService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 任务分配控制器,处理网格任务分配相关功能 + * + *

主要职责: + *

    + *
  • 获取未分配任务列表
  • + *
  • 查询可用网格工作人员
  • + *
  • 分配任务给工作人员
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api/tasks") +@RequiredArgsConstructor +@Slf4j +public class TaskAssignmentController { + + private final TaskAssignmentService taskAssignmentService; + + /** + * 获取未分配的任务列表 + * + * @return 未分配的反馈任务列表 + * @throws AccessDeniedException 如果用户无权限 + */ + @GetMapping("/unassigned") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") + public ResponseEntity> getUnassignedFeedback() { + List feedbackList = taskAssignmentService.getUnassignedFeedback(); + return ResponseEntity.ok(feedbackList); + } + + /** + * 获取可用的网格工作人员列表 + * + * @return 可用工作人员列表 + * @throws AccessDeniedException 如果用户无权限 + */ + @GetMapping("/grid-workers") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") + public ResponseEntity> getAvailableGridWorkers() { + log.info("API接收到获取可用网格员列表的请求"); + try { + List workers = taskAssignmentService.getAvailableGridWorkers(); + log.info("成功获取到 {} 名可用的网格员", workers.size()); + return ResponseEntity.ok(workers); + } catch (Exception e) { + log.error("获取可用网格员列表时发生未知异常", e); + return ResponseEntity.internalServerError().build(); + } + } + + /** + * 分配任务给工作人员 + * + * @param request 包含任务ID和分配人ID的请求体 + * @param adminDetails 当前管理员认证信息 + * @return 创建的任务分配记录 + * @throws ResourceNotFoundException 如果任务或工作人员不存在 + * @throws InvalidOperationException 如果任务已分配 + * @throws AccessDeniedException 如果用户无权限 + */ + @PostMapping("/assign") + @PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')") + public ResponseEntity assignTask( + @RequestBody AssignmentRequest request, + @AuthenticationPrincipal CustomUserDetails adminDetails) { + + Long assignerId = adminDetails.getId(); + Assignment newAssignment = taskAssignmentService.assignTask( + request.feedbackId(), + request.assigneeId(), + assignerId + ); + return ResponseEntity.ok(newAssignment); + } + + // A simple record to represent the assignment request payload + public record AssignmentRequest(Long feedbackId, Long assigneeId) {} } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/controller/TaskManagementController.java b/ems-backend/src/main/java/com/dne/ems/controller/TaskManagementController.java index c692be8..adf15b0 100644 --- a/ems-backend/src/main/java/com/dne/ems/controller/TaskManagementController.java +++ b/ems-backend/src/main/java/com/dne/ems/controller/TaskManagementController.java @@ -1,231 +1,231 @@ -package com.dne.ems.controller; - -import java.time.LocalDate; -import java.util.List; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import com.dne.ems.dto.TaskApprovalRequest; -import com.dne.ems.dto.TaskAssignmentRequest; -import com.dne.ems.dto.TaskCreationRequest; -import com.dne.ems.dto.TaskDetailDTO; -import com.dne.ems.dto.TaskFromFeedbackRequest; -import com.dne.ems.dto.TaskRejectionRequest; -import com.dne.ems.dto.TaskSummaryDTO; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.model.enums.TaskStatus; -import com.dne.ems.service.TaskManagementService; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - -/** - * 任务管理控制器,处理任务全生命周期管理 - * - *

主要功能: - *

    - *
  • 任务创建与分配
  • - *
  • 任务状态管理
  • - *
  • 任务审核与审批
  • - *
  • 从反馈创建任务
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@RestController -@RequestMapping("/api/management/tasks") -@RequiredArgsConstructor -@PreAuthorize("hasRole('SUPERVISOR')") -public class TaskManagementController { - - private final TaskManagementService taskManagementService; - - /** - * 获取任务分页列表(支持多条件过滤) - * - * @param status 任务状态过滤条件 - * @param assigneeId 分配人ID过滤条件 - * @param severity 严重程度过滤条件 - * @param pollutionType 污染类型过滤条件 - * @param startDate 开始日期过滤条件(yyyy-MM-dd) - * @param endDate 结束日期过滤条件(yyyy-MM-dd) - * @param pageable 分页参数 - * @return 任务分页列表 - * @throws AccessDeniedException 如果用户无权限 - */ - @Operation(summary = "Get a paginated list of tasks", description = "Retrieves tasks with advanced filtering options.") - @GetMapping - @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPERVISOR', 'DECISION_MAKER')") - public ResponseEntity> getTasks( - @Parameter(description = "Filter by task status") @RequestParam(required = false) TaskStatus status, - @Parameter(description = "Filter by assignee ID") @RequestParam(required = false) Long assigneeId, - @Parameter(description = "Filter by severity level") @RequestParam(required = false) SeverityLevel severity, - @Parameter(description = "Filter by pollution type") @RequestParam(required = false) PollutionType pollutionType, - @Parameter(description = "Start date for filtering tasks (format: yyyy-MM-dd)") @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @Parameter(description = "End date for filtering tasks (format: yyyy-MM-dd)") @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, - Pageable pageable - ) { - Page tasks = taskManagementService.getTasks(status, assigneeId, severity, pollutionType, startDate, endDate, pageable); - return ResponseEntity.ok(tasks.getContent()); - } - - /** - * 分配任务给工作人员 - * - * @param taskId 任务ID - * @param request 包含分配信息的请求体 - * @return 更新后的任务摘要 - * @throws ResourceNotFoundException 如果任务不存在 - * @throws InvalidOperationException 如果任务已分配 - */ - @PostMapping("/{taskId}/assign") - public ResponseEntity assignTask( - @PathVariable Long taskId, - @RequestBody @Valid TaskAssignmentRequest request) { - TaskSummaryDTO updatedTask = taskManagementService.assignTask(taskId, request); - return ResponseEntity.ok(updatedTask); - } - - /** - * 获取任务详情 - * - * @param taskId 任务ID - * @return 任务详情DTO - * @throws ResourceNotFoundException 如果任务不存在 - */ - @GetMapping("/{taskId}") - public ResponseEntity getTaskDetails(@PathVariable Long taskId) { - TaskDetailDTO taskDetails = taskManagementService.getTaskDetails(taskId); - return ResponseEntity.ok(taskDetails); - } - - /** - * 审核任务 - * - * @param taskId 任务ID - * @param request 包含审核信息的请求体 - * @return 更新后的任务详情 - * @throws ResourceNotFoundException 如果任务不存在 - * @throws InvalidOperationException 如果任务状态不允许审核 - */ - @PostMapping("/{taskId}/review") - public ResponseEntity reviewTask( - @PathVariable Long taskId, - @RequestBody @Valid TaskApprovalRequest request) { - TaskDetailDTO updatedTask = taskManagementService.reviewTask(taskId, request); - return ResponseEntity.ok(updatedTask); - } - - /** - * 取消任务 - * - * @param taskId 任务ID - * @return 更新后的任务详情 - * @throws ResourceNotFoundException 如果任务不存在 - * @throws InvalidOperationException 如果任务状态不允许取消 - */ - @PostMapping("/{taskId}/cancel") - public ResponseEntity cancelTask(@PathVariable Long taskId) { - TaskDetailDTO updatedTask = taskManagementService.cancelTask(taskId); - return ResponseEntity.ok(updatedTask); - } - - /** - * 创建新任务 - * - * @param request 包含任务信息的请求体 - * @return 创建的任务详情 - * @throws InvalidRequestException 如果请求参数无效 - */ - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity createTask(@RequestBody @Valid TaskCreationRequest request) { - TaskDetailDTO createdTask = taskManagementService.createTask(request); - return ResponseEntity.status(HttpStatus.CREATED).body(createdTask); - } - - /** - * 获取待处理反馈列表 - * - * @return 待处理反馈列表 - * @throws AccessDeniedException 如果用户无权限 - */ - @GetMapping("/feedback") - @PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") - public ResponseEntity> getPendingFeedback() { - List feedbackList = taskManagementService.getPendingFeedback(); - return ResponseEntity.ok(feedbackList); - } - - /** - * 从反馈创建任务 - * - * @param feedbackId 反馈ID - * @param request 包含任务信息的请求体 - * @return 创建的任务详情 - * @throws ResourceNotFoundException 如果反馈不存在 - * @throws InvalidOperationException 如果反馈已处理 - */ - @PostMapping("/feedback/{feedbackId}/create-task") - @PreAuthorize("hasRole('SUPERVISOR')") - public ResponseEntity createTaskFromFeedback( - @PathVariable Long feedbackId, - @RequestBody @Valid TaskFromFeedbackRequest request) { - TaskDetailDTO createdTask = taskManagementService.createTaskFromFeedback(feedbackId, request); - return ResponseEntity.status(HttpStatus.CREATED).body(createdTask); - } - - /** - * 批准任务,将状态更新为"已完成" - * - * @param taskId 任务ID - * @return 更新后的任务详情 - * @throws ResourceNotFoundException 如果任务不存在 - * @throws InvalidOperationException 如果任务状态不是"已提交" - */ - @PostMapping("/{taskId}/approve") - @PreAuthorize("hasRole('ADMIN')") - @Operation(summary = "Approve a task", description = "Approves a submitted task, changing its status to COMPLETED.") - public ResponseEntity approveTask(@PathVariable Long taskId) { - TaskDetailDTO updatedTask = taskManagementService.approveTask(taskId); - return ResponseEntity.ok(updatedTask); - } - - /** - * 拒绝任务,将状态更新为"进行中" - * - * @param taskId 任务ID - * @param request 包含拒绝理由的请求体 - * @return 更新后的任务详情 - * @throws ResourceNotFoundException 如果任务不存在 - * @throws InvalidOperationException 如果任务状态不是"已提交" - */ - @PostMapping("/{taskId}/reject") - @PreAuthorize("hasRole('ADMIN')") - @Operation(summary = "Reject a task", description = "Rejects a submitted task, changing its status back to IN_PROGRESS.") - public ResponseEntity rejectTask( - @PathVariable Long taskId, - @RequestBody @Valid TaskRejectionRequest request) { - TaskDetailDTO updatedTask = taskManagementService.rejectTask(taskId, request.getReason()); - return ResponseEntity.ok(updatedTask); - } +package com.dne.ems.controller; + +import java.time.LocalDate; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import com.dne.ems.dto.TaskApprovalRequest; +import com.dne.ems.dto.TaskAssignmentRequest; +import com.dne.ems.dto.TaskCreationRequest; +import com.dne.ems.dto.TaskDetailDTO; +import com.dne.ems.dto.TaskFromFeedbackRequest; +import com.dne.ems.dto.TaskRejectionRequest; +import com.dne.ems.dto.TaskSummaryDTO; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.model.enums.TaskStatus; +import com.dne.ems.service.TaskManagementService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +/** + * 任务管理控制器,处理任务全生命周期管理 + * + *

主要功能: + *

    + *
  • 任务创建与分配
  • + *
  • 任务状态管理
  • + *
  • 任务审核与审批
  • + *
  • 从反馈创建任务
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@RestController +@RequestMapping("/api/management/tasks") +@RequiredArgsConstructor +@PreAuthorize("hasRole('SUPERVISOR')") +public class TaskManagementController { + + private final TaskManagementService taskManagementService; + + /** + * 获取任务分页列表(支持多条件过滤) + * + * @param status 任务状态过滤条件 + * @param assigneeId 分配人ID过滤条件 + * @param severity 严重程度过滤条件 + * @param pollutionType 污染类型过滤条件 + * @param startDate 开始日期过滤条件(yyyy-MM-dd) + * @param endDate 结束日期过滤条件(yyyy-MM-dd) + * @param pageable 分页参数 + * @return 任务分页列表 + * @throws AccessDeniedException 如果用户无权限 + */ + @Operation(summary = "Get a paginated list of tasks", description = "Retrieves tasks with advanced filtering options.") + @GetMapping + @PreAuthorize("hasAnyAuthority('ADMIN', 'SUPERVISOR', 'DECISION_MAKER')") + public ResponseEntity> getTasks( + @Parameter(description = "Filter by task status") @RequestParam(required = false) TaskStatus status, + @Parameter(description = "Filter by assignee ID") @RequestParam(required = false) Long assigneeId, + @Parameter(description = "Filter by severity level") @RequestParam(required = false) SeverityLevel severity, + @Parameter(description = "Filter by pollution type") @RequestParam(required = false) PollutionType pollutionType, + @Parameter(description = "Start date for filtering tasks (format: yyyy-MM-dd)") @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @Parameter(description = "End date for filtering tasks (format: yyyy-MM-dd)") @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + Pageable pageable + ) { + Page tasks = taskManagementService.getTasks(status, assigneeId, severity, pollutionType, startDate, endDate, pageable); + return ResponseEntity.ok(tasks.getContent()); + } + + /** + * 分配任务给工作人员 + * + * @param taskId 任务ID + * @param request 包含分配信息的请求体 + * @return 更新后的任务摘要 + * @throws ResourceNotFoundException 如果任务不存在 + * @throws InvalidOperationException 如果任务已分配 + */ + @PostMapping("/{taskId}/assign") + public ResponseEntity assignTask( + @PathVariable Long taskId, + @RequestBody @Valid TaskAssignmentRequest request) { + TaskSummaryDTO updatedTask = taskManagementService.assignTask(taskId, request); + return ResponseEntity.ok(updatedTask); + } + + /** + * 获取任务详情 + * + * @param taskId 任务ID + * @return 任务详情DTO + * @throws ResourceNotFoundException 如果任务不存在 + */ + @GetMapping("/{taskId}") + public ResponseEntity getTaskDetails(@PathVariable Long taskId) { + TaskDetailDTO taskDetails = taskManagementService.getTaskDetails(taskId); + return ResponseEntity.ok(taskDetails); + } + + /** + * 审核任务 + * + * @param taskId 任务ID + * @param request 包含审核信息的请求体 + * @return 更新后的任务详情 + * @throws ResourceNotFoundException 如果任务不存在 + * @throws InvalidOperationException 如果任务状态不允许审核 + */ + @PostMapping("/{taskId}/review") + public ResponseEntity reviewTask( + @PathVariable Long taskId, + @RequestBody @Valid TaskApprovalRequest request) { + TaskDetailDTO updatedTask = taskManagementService.reviewTask(taskId, request); + return ResponseEntity.ok(updatedTask); + } + + /** + * 取消任务 + * + * @param taskId 任务ID + * @return 更新后的任务详情 + * @throws ResourceNotFoundException 如果任务不存在 + * @throws InvalidOperationException 如果任务状态不允许取消 + */ + @PostMapping("/{taskId}/cancel") + public ResponseEntity cancelTask(@PathVariable Long taskId) { + TaskDetailDTO updatedTask = taskManagementService.cancelTask(taskId); + return ResponseEntity.ok(updatedTask); + } + + /** + * 创建新任务 + * + * @param request 包含任务信息的请求体 + * @return 创建的任务详情 + * @throws InvalidRequestException 如果请求参数无效 + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createTask(@RequestBody @Valid TaskCreationRequest request) { + TaskDetailDTO createdTask = taskManagementService.createTask(request); + return ResponseEntity.status(HttpStatus.CREATED).body(createdTask); + } + + /** + * 获取待处理反馈列表 + * + * @return 待处理反馈列表 + * @throws AccessDeniedException 如果用户无权限 + */ + @GetMapping("/feedback") + @PreAuthorize("hasAnyRole('SUPERVISOR', 'ADMIN')") + public ResponseEntity> getPendingFeedback() { + List feedbackList = taskManagementService.getPendingFeedback(); + return ResponseEntity.ok(feedbackList); + } + + /** + * 从反馈创建任务 + * + * @param feedbackId 反馈ID + * @param request 包含任务信息的请求体 + * @return 创建的任务详情 + * @throws ResourceNotFoundException 如果反馈不存在 + * @throws InvalidOperationException 如果反馈已处理 + */ + @PostMapping("/feedback/{feedbackId}/create-task") + @PreAuthorize("hasRole('SUPERVISOR')") + public ResponseEntity createTaskFromFeedback( + @PathVariable Long feedbackId, + @RequestBody @Valid TaskFromFeedbackRequest request) { + TaskDetailDTO createdTask = taskManagementService.createTaskFromFeedback(feedbackId, request); + return ResponseEntity.status(HttpStatus.CREATED).body(createdTask); + } + + /** + * 批准任务,将状态更新为"已完成" + * + * @param taskId 任务ID + * @return 更新后的任务详情 + * @throws ResourceNotFoundException 如果任务不存在 + * @throws InvalidOperationException 如果任务状态不是"已提交" + */ + @PostMapping("/{taskId}/approve") + @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Approve a task", description = "Approves a submitted task, changing its status to COMPLETED.") + public ResponseEntity approveTask(@PathVariable Long taskId) { + TaskDetailDTO updatedTask = taskManagementService.approveTask(taskId); + return ResponseEntity.ok(updatedTask); + } + + /** + * 拒绝任务,将状态更新为"进行中" + * + * @param taskId 任务ID + * @param request 包含拒绝理由的请求体 + * @return 更新后的任务详情 + * @throws ResourceNotFoundException 如果任务不存在 + * @throws InvalidOperationException 如果任务状态不是"已提交" + */ + @PostMapping("/{taskId}/reject") + @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Reject a task", description = "Rejects a submitted task, changing its status back to IN_PROGRESS.") + public ResponseEntity rejectTask( + @PathVariable Long taskId, + @RequestBody @Valid TaskRejectionRequest request) { + TaskDetailDTO updatedTask = taskManagementService.rejectTask(taskId, request.getReason()); + return ResponseEntity.ok(updatedTask); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/AqiDataSubmissionRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/AqiDataSubmissionRequest.java index 3ccfe49..31d1f41 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/AqiDataSubmissionRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/AqiDataSubmissionRequest.java @@ -1,29 +1,29 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.NotNull; - -/** - * DTO for submitting AQI data from a grid worker. - * - * @param gridId The ID of the grid cell where data was recorded. - * @param aqiValue The overall AQI value. - * @param pm25 PM2.5 concentration. - * @param pm10 PM10 concentration. - * @param so2 SO2 concentration. - * @param no2 NO2 concentration. - * @param co CO concentration. - * @param o3 O3 concentration. - * @param primaryPollutant The main pollutant identified. - */ -public record AqiDataSubmissionRequest( - @NotNull Long gridId, - Integer aqiValue, - Double pm25, - Double pm10, - Double so2, - Double no2, - Double co, - Double o3, - String primaryPollutant -) { +package com.dne.ems.dto; + +import jakarta.validation.constraints.NotNull; + +/** + * DTO for submitting AQI data from a grid worker. + * + * @param gridId The ID of the grid cell where data was recorded. + * @param aqiValue The overall AQI value. + * @param pm25 PM2.5 concentration. + * @param pm10 PM10 concentration. + * @param so2 SO2 concentration. + * @param no2 NO2 concentration. + * @param co CO concentration. + * @param o3 O3 concentration. + * @param primaryPollutant The main pollutant identified. + */ +public record AqiDataSubmissionRequest( + @NotNull Long gridId, + Integer aqiValue, + Double pm25, + Double pm10, + Double so2, + Double no2, + Double co, + Double o3, + String primaryPollutant +) { } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/AqiDistributionDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/AqiDistributionDTO.java index aa9830c..85b5e95 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/AqiDistributionDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/AqiDistributionDTO.java @@ -1,53 +1,53 @@ -package com.dne.ems.dto; - -import lombok.Data; - -/** - * 空气质量指数分布数据传输对象 - * - *

该类用于封装空气质量指数(AQI)的分布统计数据,包括不同等级的AQI及其对应的数量。 - * 主要用于仪表盘展示和数据分析报告。

- * - * @version 1.0 - * @since 2024 - */ -@Data -public class AqiDistributionDTO { - /** - * AQI等级(如优、良、轻度污染等) - */ - private String level; - - /** - * 该等级的记录数量 - */ - private long count; - - /** - * 默认构造函数,用于框架反序列化 - */ - public AqiDistributionDTO() { - } - - /** - * 适用于Long类型计数的构造函数,通常由JPQL的COUNT查询返回 - * - * @param level AQI等级 - * @param count 该等级的记录数量 - */ - public AqiDistributionDTO(String level, Long count) { - this.level = level; - this.count = (count != null) ? count : 0L; - } - - /** - * 适用于JPQL查询的构造函数,提供对不同数字类型的灵活支持 - * - * @param level AQI等级 - * @param count 该等级的记录数量(任意Number类型) - */ - public AqiDistributionDTO(String level, Number count) { - this.level = level; - this.count = (count != null) ? count.longValue() : 0L; - } +package com.dne.ems.dto; + +import lombok.Data; + +/** + * 空气质量指数分布数据传输对象 + * + *

该类用于封装空气质量指数(AQI)的分布统计数据,包括不同等级的AQI及其对应的数量。 + * 主要用于仪表盘展示和数据分析报告。

+ * + * @version 1.0 + * @since 2024 + */ +@Data +public class AqiDistributionDTO { + /** + * AQI等级(如优、良、轻度污染等) + */ + private String level; + + /** + * 该等级的记录数量 + */ + private long count; + + /** + * 默认构造函数,用于框架反序列化 + */ + public AqiDistributionDTO() { + } + + /** + * 适用于Long类型计数的构造函数,通常由JPQL的COUNT查询返回 + * + * @param level AQI等级 + * @param count 该等级的记录数量 + */ + public AqiDistributionDTO(String level, Long count) { + this.level = level; + this.count = (count != null) ? count : 0L; + } + + /** + * 适用于JPQL查询的构造函数,提供对不同数字类型的灵活支持 + * + * @param level AQI等级 + * @param count 该等级的记录数量(任意Number类型) + */ + public AqiDistributionDTO(String level, Number count) { + this.level = level; + this.count = (count != null) ? count.longValue() : 0L; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/AqiHeatmapPointDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/AqiHeatmapPointDTO.java index ded3cda..457a258 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/AqiHeatmapPointDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/AqiHeatmapPointDTO.java @@ -1,34 +1,34 @@ -package com.dne.ems.dto; - -/** - * 空气质量指数热力图数据点传输对象 - * - *

该类用于表示AQI热力图上的单个数据点,包含网格坐标和该网格的平均AQI值。 - * 主要用于生成热力图可视化展示。

- * - * @param gridX 网格X坐标 - * @param gridY 网格Y坐标 - * @param averageAqi 该网格的平均AQI值 - * @version 1.0 - * @since 2024 - */ -public record AqiHeatmapPointDTO( - Integer gridX, - Integer gridY, - Double averageAqi -) { - /** - * 辅助构造函数,用于处理从数据库查询返回的Number类型数据 - * - * @param gridX 网格X坐标(Number类型) - * @param gridY 网格Y坐标(Number类型) - * @param averageAqi 平均AQI值(Number类型) - */ - public AqiHeatmapPointDTO(Number gridX, Number gridY, Number averageAqi) { - this( - gridX != null ? gridX.intValue() : null, - gridY != null ? gridY.intValue() : null, - averageAqi != null ? averageAqi.doubleValue() : null - ); - } -} +package com.dne.ems.dto; + +/** + * 空气质量指数热力图数据点传输对象 + * + *

该类用于表示AQI热力图上的单个数据点,包含网格坐标和该网格的平均AQI值。 + * 主要用于生成热力图可视化展示。

+ * + * @param gridX 网格X坐标 + * @param gridY 网格Y坐标 + * @param averageAqi 该网格的平均AQI值 + * @version 1.0 + * @since 2024 + */ +public record AqiHeatmapPointDTO( + Integer gridX, + Integer gridY, + Double averageAqi +) { + /** + * 辅助构造函数,用于处理从数据库查询返回的Number类型数据 + * + * @param gridX 网格X坐标(Number类型) + * @param gridY 网格Y坐标(Number类型) + * @param averageAqi 平均AQI值(Number类型) + */ + public AqiHeatmapPointDTO(Number gridX, Number gridY, Number averageAqi) { + this( + gridX != null ? gridX.intValue() : null, + gridY != null ? gridY.intValue() : null, + averageAqi != null ? averageAqi.doubleValue() : null + ); + } +} diff --git a/ems-backend/src/main/java/com/dne/ems/dto/AssigneeInfoDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/AssigneeInfoDTO.java index b161694..3e2b511 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/AssigneeInfoDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/AssigneeInfoDTO.java @@ -1,34 +1,34 @@ -package com.dne.ems.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 任务受理人信息数据传输对象 - * - *

该类用于封装任务受理人的基本信息,包括ID、姓名和联系电话。 - * 主要用于任务分配和展示受理人信息的场景。

- * - * @version 1.0 - * @since 2024 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class AssigneeInfoDTO { - /** - * 受理人唯一标识符 - */ - private Long id; - - /** - * 受理人姓名 - */ - private String name; - - /** - * 受理人联系电话 - */ - private String phone; +package com.dne.ems.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 任务受理人信息数据传输对象 + * + *

该类用于封装任务受理人的基本信息,包括ID、姓名和联系电话。 + * 主要用于任务分配和展示受理人信息的场景。

+ * + * @version 1.0 + * @since 2024 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AssigneeInfoDTO { + /** + * 受理人唯一标识符 + */ + private Long id; + + /** + * 受理人姓名 + */ + private String name; + + /** + * 受理人联系电话 + */ + private String phone; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/AttachmentDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/AttachmentDTO.java index 26283c2..5e9f464 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/AttachmentDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/AttachmentDTO.java @@ -1,34 +1,34 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.Attachment; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class AttachmentDTO { - private Long id; - private String fileName; - private String fileType; - private String url; - private LocalDateTime uploadedAt; - - public static AttachmentDTO fromEntity(Attachment attachment) { - if (attachment == null) { - return null; - } - String url = "/api/files/" + attachment.getStoredFileName(); - - return new AttachmentDTO( - attachment.getId(), - attachment.getFileName(), - attachment.getFileType(), - url, - attachment.getUploadDate() - ); - } +package com.dne.ems.dto; + +import com.dne.ems.model.Attachment; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AttachmentDTO { + private Long id; + private String fileName; + private String fileType; + private String url; + private LocalDateTime uploadedAt; + + public static AttachmentDTO fromEntity(Attachment attachment) { + if (attachment == null) { + return null; + } + String url = "/api/files/" + attachment.getStoredFileName(); + + return new AttachmentDTO( + attachment.getId(), + attachment.getFileName(), + attachment.getFileType(), + url, + attachment.getUploadDate() + ); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/CityBlueprint.java b/ems-backend/src/main/java/com/dne/ems/dto/CityBlueprint.java index 8f81cc0..6e3c7ec 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/CityBlueprint.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/CityBlueprint.java @@ -1,26 +1,26 @@ -package com.dne.ems.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * Represents the blueprint for generating a city within the grid. - * Used during data initialization. - */ -@AllArgsConstructor -@Getter -public class CityBlueprint { - - @SuppressWarnings("FieldMayBeFinal") - private String cityName; - @SuppressWarnings("FieldMayBeFinal") - private String districtName; - @SuppressWarnings("FieldMayBeFinal") - private int startX; - @SuppressWarnings("FieldMayBeFinal") - private int startY; - @SuppressWarnings("FieldMayBeFinal") - private int width; - @SuppressWarnings("FieldMayBeFinal") - private int height; +package com.dne.ems.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Represents the blueprint for generating a city within the grid. + * Used during data initialization. + */ +@AllArgsConstructor +@Getter +public class CityBlueprint { + + @SuppressWarnings("FieldMayBeFinal") + private String cityName; + @SuppressWarnings("FieldMayBeFinal") + private String districtName; + @SuppressWarnings("FieldMayBeFinal") + private int startX; + @SuppressWarnings("FieldMayBeFinal") + private int startY; + @SuppressWarnings("FieldMayBeFinal") + private int width; + @SuppressWarnings("FieldMayBeFinal") + private int height; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/DashboardStatsDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/DashboardStatsDTO.java index 0fd68f9..4bd887e 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/DashboardStatsDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/DashboardStatsDTO.java @@ -1,17 +1,17 @@ -package com.dne.ems.dto; - -/** - * DTO for carrying key statistics for the main dashboard. - * - * @param totalFeedbacks Total number of feedback submissions. - * @param confirmedFeedbacks Total number of feedback entries that have been confirmed. - * @param totalAqiRecords Total number of AQI data records submitted. - * @param activeGridWorkers Number of grid workers with an 'ACTIVE' status. - */ -public record DashboardStatsDTO( - Long totalFeedbacks, - Long confirmedFeedbacks, - Long totalAqiRecords, - Long activeGridWorkers -) { +package com.dne.ems.dto; + +/** + * DTO for carrying key statistics for the main dashboard. + * + * @param totalFeedbacks Total number of feedback submissions. + * @param confirmedFeedbacks Total number of feedback entries that have been confirmed. + * @param totalAqiRecords Total number of AQI data records submitted. + * @param activeGridWorkers Number of grid workers with an 'ACTIVE' status. + */ +public record DashboardStatsDTO( + Long totalFeedbacks, + Long confirmedFeedbacks, + Long totalAqiRecords, + Long activeGridWorkers +) { } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/ErrorResponseDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/ErrorResponseDTO.java index 09e7022..126c49a 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/ErrorResponseDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/ErrorResponseDTO.java @@ -1,51 +1,51 @@ -package com.dne.ems.dto; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 错误响应数据传输对象 - * - *

该类用于封装API错误响应的标准格式,包含错误发生的时间戳、HTTP状态码、 - * 错误类型、错误消息以及请求路径等信息。主要用于统一的异常处理和客户端错误提示。

- * - * @version 1.0 - * @since 2025-06 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class ErrorResponseDTO { - - /** - * 错误发生的时间戳 - */ - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss") - private LocalDateTime timestamp; - - /** - * HTTP状态码 - */ - private int status; - - /** - * 错误类型 - */ - private String error; - - /** - * 错误详细信息 - */ - private String message; - - /** - * 发生错误的请求路径 - */ - private String path; - +package com.dne.ems.dto; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 错误响应数据传输对象 + * + *

该类用于封装API错误响应的标准格式,包含错误发生的时间戳、HTTP状态码、 + * 错误类型、错误消息以及请求路径等信息。主要用于统一的异常处理和客户端错误提示。

+ * + * @version 1.0 + * @since 2025-06 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ErrorResponseDTO { + + /** + * 错误发生的时间戳 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss") + private LocalDateTime timestamp; + + /** + * HTTP状态码 + */ + private int status; + + /** + * 错误类型 + */ + private String error; + + /** + * 错误详细信息 + */ + private String message; + + /** + * 发生错误的请求路径 + */ + private String path; + } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/FeedbackDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/FeedbackDTO.java index a5664c1..7d43193 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/FeedbackDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/FeedbackDTO.java @@ -1,51 +1,51 @@ -/** - * 反馈数据传输对象 - * - *

用于在服务层和表示层之间传递反馈信息, - * 包含反馈的基本信息和关联数据。 - * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -package com.dne.ems.dto; - -import java.time.LocalDateTime; -import java.util.List; - -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; - -import lombok.Data; - -/** - * 反馈数据传输对象 - * - *

包含反馈的完整信息,用于展示和传输 - */ -@Data -public class FeedbackDTO { - /** - * 反馈ID,唯一标识 - */ - private Long id; - /** - * 反馈标题 - */ - private String title; - private String description; - private PollutionType pollutionType; - private SeverityLevel severityLevel; - private Double latitude; - private Double longitude; - private String textAddress; - private Integer gridX; - private Integer gridY; - private String eventId; - private FeedbackStatus status; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - private Long submitterId; - private List attachmentUrls; +/** + * 反馈数据传输对象 + * + *

用于在服务层和表示层之间传递反馈信息, + * 包含反馈的基本信息和关联数据。 + * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +package com.dne.ems.dto; + +import java.time.LocalDateTime; +import java.util.List; + +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; + +import lombok.Data; + +/** + * 反馈数据传输对象 + * + *

包含反馈的完整信息,用于展示和传输 + */ +@Data +public class FeedbackDTO { + /** + * 反馈ID,唯一标识 + */ + private Long id; + /** + * 反馈标题 + */ + private String title; + private String description; + private PollutionType pollutionType; + private SeverityLevel severityLevel; + private Double latitude; + private Double longitude; + private String textAddress; + private Integer gridX; + private Integer gridY; + private String eventId; + private FeedbackStatus status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private Long submitterId; + private List attachmentUrls; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/FeedbackResponseDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/FeedbackResponseDTO.java index 1b55c72..a07da32 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/FeedbackResponseDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/FeedbackResponseDTO.java @@ -1,151 +1,151 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.Feedback; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; - -/** - * 反馈响应数据传输对象 - * - *

该类用于封装反馈信息的响应数据,包含反馈的详细信息、状态、位置信息、 - * 提交者信息、关联任务以及附件信息等。主要用于前端展示和API响应。

- * - * @version 1.0 - * @since 2025-06 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class FeedbackResponseDTO { - - /** - * 反馈记录的唯一标识符 - */ - private Long id; - - /** - * 事件唯一标识符 - */ - private String eventId; - - /** - * 反馈标题 - */ - private String title; - - /** - * 反馈详细描述 - */ - private String description; - - /** - * 污染类型 - */ - private PollutionType pollutionType; - - /** - * 严重程度级别 - */ - private SeverityLevel severityLevel; - - /** - * 反馈当前状态 - */ - private FeedbackStatus status; - - /** - * 文本地址描述 - */ - private String textAddress; - - /** - * 网格X坐标 - */ - private Integer gridX; - - /** - * 网格Y坐标 - */ - private Integer gridY; - - /** - * 地理纬度 - */ - private Double latitude; - - /** - * 地理经度 - */ - private Double longitude; - - /** - * 创建时间 - */ - private LocalDateTime createdAt; - - /** - * 最后更新时间 - */ - private LocalDateTime updatedAt; - - /** - * 提交者ID - */ - private Long submitterId; - - /** - * 提交用户信息 - */ - private UserInfoDTO user; - - /** - * 关联任务详情 - */ - private TaskDetailDTO task; - - /** - * 附件列表 - */ - private List attachments; - - public static FeedbackResponseDTO fromEntity(Feedback feedback, TaskDetailDTO taskDetail) { - FeedbackResponseDTO dto = new FeedbackResponseDTO(); - dto.setId(feedback.getId()); - dto.setEventId(feedback.getEventId()); - dto.setTitle(feedback.getTitle()); - dto.setDescription(feedback.getDescription()); - dto.setPollutionType(feedback.getPollutionType()); - dto.setSeverityLevel(feedback.getSeverityLevel()); - dto.setStatus(feedback.getStatus()); - dto.setTextAddress(feedback.getTextAddress()); - dto.setGridX(feedback.getGridX()); - dto.setGridY(feedback.getGridY()); - dto.setLatitude(feedback.getLatitude()); - dto.setLongitude(feedback.getLongitude()); - dto.setCreatedAt(feedback.getCreatedAt()); - dto.setUpdatedAt(feedback.getUpdatedAt()); - dto.setSubmitterId(feedback.getSubmitterId()); - - if (feedback.getUser() != null) { - dto.setUser(UserInfoDTO.fromEntity(feedback.getUser())); - } - - if (feedback.getAttachments() != null) { - dto.setAttachments(feedback.getAttachments().stream() - .map(AttachmentDTO::fromEntity) - .collect(Collectors.toList())); - } - - dto.setTask(taskDetail); - - return dto; - } +package com.dne.ems.dto; + +import com.dne.ems.model.Feedback; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 反馈响应数据传输对象 + * + *

该类用于封装反馈信息的响应数据,包含反馈的详细信息、状态、位置信息、 + * 提交者信息、关联任务以及附件信息等。主要用于前端展示和API响应。

+ * + * @version 1.0 + * @since 2025-06 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FeedbackResponseDTO { + + /** + * 反馈记录的唯一标识符 + */ + private Long id; + + /** + * 事件唯一标识符 + */ + private String eventId; + + /** + * 反馈标题 + */ + private String title; + + /** + * 反馈详细描述 + */ + private String description; + + /** + * 污染类型 + */ + private PollutionType pollutionType; + + /** + * 严重程度级别 + */ + private SeverityLevel severityLevel; + + /** + * 反馈当前状态 + */ + private FeedbackStatus status; + + /** + * 文本地址描述 + */ + private String textAddress; + + /** + * 网格X坐标 + */ + private Integer gridX; + + /** + * 网格Y坐标 + */ + private Integer gridY; + + /** + * 地理纬度 + */ + private Double latitude; + + /** + * 地理经度 + */ + private Double longitude; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; + + /** + * 最后更新时间 + */ + private LocalDateTime updatedAt; + + /** + * 提交者ID + */ + private Long submitterId; + + /** + * 提交用户信息 + */ + private UserInfoDTO user; + + /** + * 关联任务详情 + */ + private TaskDetailDTO task; + + /** + * 附件列表 + */ + private List attachments; + + public static FeedbackResponseDTO fromEntity(Feedback feedback, TaskDetailDTO taskDetail) { + FeedbackResponseDTO dto = new FeedbackResponseDTO(); + dto.setId(feedback.getId()); + dto.setEventId(feedback.getEventId()); + dto.setTitle(feedback.getTitle()); + dto.setDescription(feedback.getDescription()); + dto.setPollutionType(feedback.getPollutionType()); + dto.setSeverityLevel(feedback.getSeverityLevel()); + dto.setStatus(feedback.getStatus()); + dto.setTextAddress(feedback.getTextAddress()); + dto.setGridX(feedback.getGridX()); + dto.setGridY(feedback.getGridY()); + dto.setLatitude(feedback.getLatitude()); + dto.setLongitude(feedback.getLongitude()); + dto.setCreatedAt(feedback.getCreatedAt()); + dto.setUpdatedAt(feedback.getUpdatedAt()); + dto.setSubmitterId(feedback.getSubmitterId()); + + if (feedback.getUser() != null) { + dto.setUser(UserInfoDTO.fromEntity(feedback.getUser())); + } + + if (feedback.getAttachments() != null) { + dto.setAttachments(feedback.getAttachments().stream() + .map(AttachmentDTO::fromEntity) + .collect(Collectors.toList())); + } + + dto.setTask(taskDetail); + + return dto; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/FeedbackStatsResponse.java b/ems-backend/src/main/java/com/dne/ems/dto/FeedbackStatsResponse.java index 1acc800..16e51d9 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/FeedbackStatsResponse.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/FeedbackStatsResponse.java @@ -1,56 +1,56 @@ -package com.dne.ems.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 反馈统计响应DTO - * - *

包含系统反馈数据的统计信息,用于仪表盘展示 - * - *

统计指标说明: - *

    - *
  • total - 总反馈数
  • - *
  • pending - 待处理总数
  • - *
  • confirmed - 已确认数
  • - *
  • assigned - 已分配数
  • - *
  • inProgress - 处理中数
  • - *
  • resolved - 已解决数
  • - *
  • closed - 已关闭数
  • - *
  • rejected - 已拒绝数
  • - *
  • pendingReview - 待审核数
  • - *
  • pendingAssignment - 待分配数
  • - *
  • aiReviewing - AI审核中数
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class FeedbackStatsResponse { - /** - * 总反馈数量 - */ - private int total; - /** - * 待处理反馈总数(包含所有待处理状态) - */ - private int pending; - private int confirmed; - private int assigned; - private int inProgress; - private int resolved; - private int closed; - private int rejected; - - // 待分配和待审核的特殊计数 - private int pendingReview; - private int pendingAssignment; - private int aiReviewing; +package com.dne.ems.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 反馈统计响应DTO + * + *

包含系统反馈数据的统计信息,用于仪表盘展示 + * + *

统计指标说明: + *

    + *
  • total - 总反馈数
  • + *
  • pending - 待处理总数
  • + *
  • confirmed - 已确认数
  • + *
  • assigned - 已分配数
  • + *
  • inProgress - 处理中数
  • + *
  • resolved - 已解决数
  • + *
  • closed - 已关闭数
  • + *
  • rejected - 已拒绝数
  • + *
  • pendingReview - 待审核数
  • + *
  • pendingAssignment - 待分配数
  • + *
  • aiReviewing - AI审核中数
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FeedbackStatsResponse { + /** + * 总反馈数量 + */ + private int total; + /** + * 待处理反馈总数(包含所有待处理状态) + */ + private int pending; + private int confirmed; + private int assigned; + private int inProgress; + private int resolved; + private int closed; + private int rejected; + + // 待分配和待审核的特殊计数 + private int pendingReview; + private int pendingAssignment; + private int aiReviewing; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/FeedbackSubmissionRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/FeedbackSubmissionRequest.java index af622e1..57c4507 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/FeedbackSubmissionRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/FeedbackSubmissionRequest.java @@ -1,78 +1,78 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; - -/** - * 公众反馈提交请求数据传输对象 - * - *

用于接收公众用户提交的环境问题反馈,包含问题描述、分类和位置信息 - * - *

业务规则: - *

    - *
  • 提交后状态自动设为PENDING_REVIEW(待审核)
  • - *
  • 需经过主管审核后才能创建任务
  • - *
  • 位置信息必须包含有效的网格坐标
  • - *
- * - * @param title 反馈标题(必填,最长255字符) - * @param description 问题详细描述(可选) - * @param pollutionType 污染类型(必填) - * @param severityLevel 严重等级(必填) - * @param location 地理位置信息(必填) - * @see com.dne.ems.model.enums.PollutionType 污染类型枚举 - * @see com.dne.ems.model.enums.SeverityLevel 严重级别枚举 - */ -public record FeedbackSubmissionRequest( - @NotEmpty(message = "标题不能为空") - @Size(max = 255, message = "标题长度不能超过255个字符") - String title, - - String description, - - @NotNull(message = "污染类型不能为空") - PollutionType pollutionType, - - @NotNull(message = "严重等级不能为空") - SeverityLevel severityLevel, - - @NotNull(message = "地理位置信息不能为空") - @Valid - LocationData location -) { - /** - * 地理位置数据(嵌套记录) - * - *

用于精确定位反馈问题的发生位置 - * - *

验证规则: - *

    - *
  • 文字地址必须符合"省-市-区"格式
  • - *
  • 网格坐标必须在有效范围内(1-1000)
  • - *
  • 经纬度坐标可选,但提供时可提高定位精度
  • - *
- * - * @param latitude 纬度坐标(可选) - * @param longitude 经度坐标(可选) - * @param textAddress 文字地址(必填,格式:"省-市-区") - * @param gridX 网格X坐标(必填,范围1-1000) - * @param gridY 网格Y坐标(必填,范围1-1000) - */ - public record LocationData( - Double latitude, - Double longitude, - @NotEmpty(message = "文字地址不能为空") - String textAddress, - - @NotNull(message = "网格X坐标不能为空") - Integer gridX, - - @NotNull(message = "网格Y坐标不能为空") - Integer gridY - ) {} +package com.dne.ems.dto; + +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +/** + * 公众反馈提交请求数据传输对象 + * + *

用于接收公众用户提交的环境问题反馈,包含问题描述、分类和位置信息 + * + *

业务规则: + *

    + *
  • 提交后状态自动设为PENDING_REVIEW(待审核)
  • + *
  • 需经过主管审核后才能创建任务
  • + *
  • 位置信息必须包含有效的网格坐标
  • + *
+ * + * @param title 反馈标题(必填,最长255字符) + * @param description 问题详细描述(可选) + * @param pollutionType 污染类型(必填) + * @param severityLevel 严重等级(必填) + * @param location 地理位置信息(必填) + * @see com.dne.ems.model.enums.PollutionType 污染类型枚举 + * @see com.dne.ems.model.enums.SeverityLevel 严重级别枚举 + */ +public record FeedbackSubmissionRequest( + @NotEmpty(message = "标题不能为空") + @Size(max = 255, message = "标题长度不能超过255个字符") + String title, + + String description, + + @NotNull(message = "污染类型不能为空") + PollutionType pollutionType, + + @NotNull(message = "严重等级不能为空") + SeverityLevel severityLevel, + + @NotNull(message = "地理位置信息不能为空") + @Valid + LocationData location +) { + /** + * 地理位置数据(嵌套记录) + * + *

用于精确定位反馈问题的发生位置 + * + *

验证规则: + *

    + *
  • 文字地址必须符合"省-市-区"格式
  • + *
  • 网格坐标必须在有效范围内(1-1000)
  • + *
  • 经纬度坐标可选,但提供时可提高定位精度
  • + *
+ * + * @param latitude 纬度坐标(可选) + * @param longitude 经度坐标(可选) + * @param textAddress 文字地址(必填,格式:"省-市-区") + * @param gridX 网格X坐标(必填,范围1-1000) + * @param gridY 网格Y坐标(必填,范围1-1000) + */ + public record LocationData( + Double latitude, + Double longitude, + @NotEmpty(message = "文字地址不能为空") + String textAddress, + + @NotNull(message = "网格X坐标不能为空") + Integer gridX, + + @NotNull(message = "网格Y坐标不能为空") + Integer gridY + ) {} } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/GridAssignmentRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/GridAssignmentRequest.java index c53e5e2..fb1fc46 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/GridAssignmentRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/GridAssignmentRequest.java @@ -1,19 +1,19 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * 网格员分配请求的数据传输对象 (DTO)。 - *

- * 用于封装将用户分配到特定网格时所需的 `userId`。 - */ -@Data -public class GridAssignmentRequest { - - /** - * 要分配的用户的ID。 - */ - @NotNull(message = "用户ID不能为空") - private Long userId; +package com.dne.ems.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 网格员分配请求的数据传输对象 (DTO)。 + *

+ * 用于封装将用户分配到特定网格时所需的 `userId`。 + */ +@Data +public class GridAssignmentRequest { + + /** + * 要分配的用户的ID。 + */ + @NotNull(message = "用户ID不能为空") + private Long userId; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/GridCoverageDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/GridCoverageDTO.java index 5230733..e80d563 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/GridCoverageDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/GridCoverageDTO.java @@ -1,20 +1,20 @@ -package com.dne.ems.dto; - -/** - * DTO for representing grid coverage statistics for a specific area. - */ -public record GridCoverageDTO( - String city, // 城市名称 - Long coveredGrids, // 已覆盖的网格数量 - Long totalGrids, // 总网格数量 - Double coverageRate // 覆盖率 -) { - /** - * 原始构造函数,用于兼容现有查询 - * @param areaName 区域名称 - * @param gridCount 网格数量 - */ - public GridCoverageDTO(String areaName, Long gridCount) { - this(areaName, gridCount, gridCount * 2, gridCount > 0 ? (double) gridCount / (gridCount * 2) * 100 : 0.0); - } +package com.dne.ems.dto; + +/** + * DTO for representing grid coverage statistics for a specific area. + */ +public record GridCoverageDTO( + String city, // 城市名称 + Long coveredGrids, // 已覆盖的网格数量 + Long totalGrids, // 总网格数量 + Double coverageRate // 覆盖率 +) { + /** + * 原始构造函数,用于兼容现有查询 + * @param areaName 区域名称 + * @param gridCount 网格数量 + */ + public GridCoverageDTO(String areaName, Long gridCount) { + this(areaName, gridCount, gridCount * 2, gridCount > 0 ? (double) gridCount / (gridCount * 2) * 100 : 0.0); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/GridUpdateRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/GridUpdateRequest.java index 515fb01..a9f1845 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/GridUpdateRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/GridUpdateRequest.java @@ -1,22 +1,22 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * DTO for updating a grid's properties. - * - * @param isObstacle The new obstacle status for the grid. - * @param description The new description for the grid. - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class GridUpdateRequest { - - @NotNull(message = "Obstacle status cannot be null") - private Boolean isObstacle; - private String description; +package com.dne.ems.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * DTO for updating a grid's properties. + * + * @param isObstacle The new obstacle status for the grid. + * @param description The new description for the grid. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GridUpdateRequest { + + @NotNull(message = "Obstacle status cannot be null") + private Boolean isObstacle; + private String description; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/HeatmapPointDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/HeatmapPointDTO.java index b8c6433..14ee37b 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/HeatmapPointDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/HeatmapPointDTO.java @@ -1,20 +1,20 @@ -package com.dne.ems.dto; - -/** - * 热力图数据点传输对象 - * - *

该类用于表示热力图上的单个数据点,包含网格坐标和该点的强度值。 - * 主要用于生成各类热力图可视化展示,如反馈密度、任务分布等。

- * - * @param gridX 网格X坐标 - * @param gridY 网格Y坐标 - * @param intensity 该点的强度值(如反馈事件数量、任务数量等) - * @version 1.0 - * @since 2024 - */ -public record HeatmapPointDTO( - Integer gridX, - Integer gridY, - long intensity -) { +package com.dne.ems.dto; + +/** + * 热力图数据点传输对象 + * + *

该类用于表示热力图上的单个数据点,包含网格坐标和该点的强度值。 + * 主要用于生成各类热力图可视化展示,如反馈密度、任务分布等。

+ * + * @param gridX 网格X坐标 + * @param gridY 网格Y坐标 + * @param intensity 该点的强度值(如反馈事件数量、任务数量等) + * @version 1.0 + * @since 2024 + */ +public record HeatmapPointDTO( + Integer gridX, + Integer gridY, + long intensity +) { } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/JwtAuthenticationResponse.java b/ems-backend/src/main/java/com/dne/ems/dto/JwtAuthenticationResponse.java index 74b8311..d44f8ef 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/JwtAuthenticationResponse.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/JwtAuthenticationResponse.java @@ -1,15 +1,15 @@ -package com.dne.ems.dto; - -/** - * JWT认证响应数据传输对象 - * - *

该类用于封装认证成功后返回给客户端的JWT令牌信息。 - * 主要用于用户登录或令牌刷新操作的响应数据。

- * - * @param accessToken JWT访问令牌,用于后续请求的身份验证 - * @version 1.0 - * @since 2024 - */ -public record JwtAuthenticationResponse( - String accessToken +package com.dne.ems.dto; + +/** + * JWT认证响应数据传输对象 + * + *

该类用于封装认证成功后返回给客户端的JWT令牌信息。 + * 主要用于用户登录或令牌刷新操作的响应数据。

+ * + * @param accessToken JWT访问令牌,用于后续请求的身份验证 + * @version 1.0 + * @since 2024 + */ +public record JwtAuthenticationResponse( + String accessToken ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/LocationUpdateRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/LocationUpdateRequest.java index d11e54f..26b81f5 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/LocationUpdateRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/LocationUpdateRequest.java @@ -1,34 +1,34 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; - -/** - * 位置更新请求数据传输对象 - * - *

用于接收用户位置更新请求,包含经纬度坐标信息。 - * 主要用于网格员上报当前位置或移动设备用户更新位置信息。

- * - *

验证规则: - *

    - *
  • 纬度范围必须在-90到90度之间
  • - *
  • 经度范围必须在-180到180度之间
  • - *
- * - * @param latitude 纬度坐标,有效范围-90到90度 - * @param longitude 经度坐标,有效范围-180到180度 - * @version 1.0 - * @since 2024 - */ -public record LocationUpdateRequest( - @NotNull(message = "Latitude cannot be null") - @Min(value = -90, message = "Latitude must be between -90 and 90") - @Max(value = 90, message = "Latitude must be between -90 and 90") - Double latitude, - - @NotNull(message = "Longitude cannot be null") - @Min(value = -180, message = "Longitude must be between -180 and 180") - @Max(value = 180, message = "Longitude must be between -180 and 180") - Double longitude +package com.dne.ems.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +/** + * 位置更新请求数据传输对象 + * + *

用于接收用户位置更新请求,包含经纬度坐标信息。 + * 主要用于网格员上报当前位置或移动设备用户更新位置信息。

+ * + *

验证规则: + *

    + *
  • 纬度范围必须在-90到90度之间
  • + *
  • 经度范围必须在-180到180度之间
  • + *
+ * + * @param latitude 纬度坐标,有效范围-90到90度 + * @param longitude 经度坐标,有效范围-180到180度 + * @version 1.0 + * @since 2024 + */ +public record LocationUpdateRequest( + @NotNull(message = "Latitude cannot be null") + @Min(value = -90, message = "Latitude must be between -90 and 90") + @Max(value = 90, message = "Latitude must be between -90 and 90") + Double latitude, + + @NotNull(message = "Longitude cannot be null") + @Min(value = -180, message = "Longitude must be between -180 and 180") + @Max(value = 180, message = "Longitude must be between -180 and 180") + Double longitude ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/LoginRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/LoginRequest.java index 98311ca..33bfe5b 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/LoginRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/LoginRequest.java @@ -1,30 +1,30 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; - -/** - * 登录请求数据传输对象 - * - *

该类用于封装用户登录请求的数据,包含邮箱和密码信息。 - * 主要用于用户认证流程中的登录请求处理。

- * - *

验证规则: - *

    - *
  • 邮箱不能为空且必须符合邮箱格式
  • - *
  • 密码不能为空
  • - *
- * - * @param email 用户邮箱,作为登录标识 - * @param password 用户密码,用于验证身份 - * @version 1.0 - * @since 2024 - */ -public record LoginRequest( - @NotEmpty(message = "Email cannot be empty.") - @Email(message = "Invalid email format.") - String email, - - @NotEmpty(message = "Password cannot be empty.") - String password +package com.dne.ems.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; + +/** + * 登录请求数据传输对象 + * + *

该类用于封装用户登录请求的数据,包含邮箱和密码信息。 + * 主要用于用户认证流程中的登录请求处理。

+ * + *

验证规则: + *

    + *
  • 邮箱不能为空且必须符合邮箱格式
  • + *
  • 密码不能为空
  • + *
+ * + * @param email 用户邮箱,作为登录标识 + * @param password 用户密码,用于验证身份 + * @version 1.0 + * @since 2024 + */ +public record LoginRequest( + @NotEmpty(message = "Email cannot be empty.") + @Email(message = "Invalid email format.") + String email, + + @NotEmpty(message = "Password cannot be empty.") + String password ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/OperationLogDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/OperationLogDTO.java index 9de1a2e..b2c6d2e 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/OperationLogDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/OperationLogDTO.java @@ -1,101 +1,101 @@ -package com.dne.ems.dto; - -import java.time.LocalDateTime; - -import com.dne.ems.model.enums.OperationType; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 操作日志数据传输对象,用于前端展示用户操作记录。 - * - * @version 1.0 - * @since 2025-06-20 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class OperationLogDTO { - /** - * 操作日志ID - */ - private Long id; - - /** - * 用户ID - */ - private Long userId; - - /** - * 用户名称 - */ - private String userName; - - /** - * 操作类型 - */ - private OperationType operationType; - - /** - * 操作类型的中文描述 - */ - private String operationTypeDesc; - - /** - * 操作描述 - */ - private String description; - - /** - * 操作对象ID - */ - private String targetId; - - /** - * 操作对象类型 - */ - private String targetType; - - /** - * 客户端IP地址 - */ - private String ipAddress; - - /** - * 操作时间 - */ - private LocalDateTime createdAt; - - /** - * 根据操作类型获取中文描述 - * - * @param operationType 操作类型 - * @return 中文描述 - */ - public static String getOperationTypeDesc(OperationType operationType) { - if (operationType == null) { - return "未知操作"; - } - - return switch (operationType) { - case LOGIN -> "登录"; - case LOGOUT -> "登出"; - case CREATE -> "创建"; - case UPDATE -> "更新"; - case DELETE -> "删除"; - case ASSIGN_TASK -> "分配任务"; - case SUBMIT_TASK -> "提交任务"; - case APPROVE_TASK -> "审批任务"; - case REJECT_TASK -> "拒绝任务"; - case SUBMIT_FEEDBACK -> "提交反馈"; - case APPROVE_FEEDBACK -> "审批反馈"; - case REJECT_FEEDBACK -> "拒绝反馈"; - case OTHER -> "其他操作"; - default -> "未知操作"; - }; - } +package com.dne.ems.dto; + +import java.time.LocalDateTime; + +import com.dne.ems.model.enums.OperationType; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 操作日志数据传输对象,用于前端展示用户操作记录。 + * + * @version 1.0 + * @since 2025-06-20 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OperationLogDTO { + /** + * 操作日志ID + */ + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名称 + */ + private String userName; + + /** + * 操作类型 + */ + private OperationType operationType; + + /** + * 操作类型的中文描述 + */ + private String operationTypeDesc; + + /** + * 操作描述 + */ + private String description; + + /** + * 操作对象ID + */ + private String targetId; + + /** + * 操作对象类型 + */ + private String targetType; + + /** + * 客户端IP地址 + */ + private String ipAddress; + + /** + * 操作时间 + */ + private LocalDateTime createdAt; + + /** + * 根据操作类型获取中文描述 + * + * @param operationType 操作类型 + * @return 中文描述 + */ + public static String getOperationTypeDesc(OperationType operationType) { + if (operationType == null) { + return "未知操作"; + } + + return switch (operationType) { + case LOGIN -> "登录"; + case LOGOUT -> "登出"; + case CREATE -> "创建"; + case UPDATE -> "更新"; + case DELETE -> "删除"; + case ASSIGN_TASK -> "分配任务"; + case SUBMIT_TASK -> "提交任务"; + case APPROVE_TASK -> "审批任务"; + case REJECT_TASK -> "拒绝任务"; + case SUBMIT_FEEDBACK -> "提交反馈"; + case APPROVE_FEEDBACK -> "审批反馈"; + case REJECT_FEEDBACK -> "拒绝反馈"; + case OTHER -> "其他操作"; + default -> "未知操作"; + }; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/PageDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/PageDTO.java index f07bce5..ab25365 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/PageDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/PageDTO.java @@ -1,48 +1,48 @@ -package com.dne.ems.dto; - -import java.util.List; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 分页数据传输对象 - * - *

该类用于封装分页查询的结果数据,安全地向前端暴露分页信息, - * 避免了Spring Data的Page接口序列化问题。

- * - * @param 分页内容的数据类型 - * @version 1.0 - * @since 2024 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class PageDTO { - - /** - * 当前页的内容列表 - */ - private List content; - - /** - * 所有页面的元素总数 - */ - private long totalElements; - - /** - * 总页数 - */ - private int totalPages; - - /** - * 当前页码(从0开始) - */ - private int number; - - /** - * 当前页面的元素数量 - */ - private int size; +package com.dne.ems.dto; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 分页数据传输对象 + * + *

该类用于封装分页查询的结果数据,安全地向前端暴露分页信息, + * 避免了Spring Data的Page接口序列化问题。

+ * + * @param 分页内容的数据类型 + * @version 1.0 + * @since 2024 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PageDTO { + + /** + * 当前页的内容列表 + */ + private List content; + + /** + * 所有页面的元素总数 + */ + private long totalElements; + + /** + * 总页数 + */ + private int totalPages; + + /** + * 当前页码(从0开始) + */ + private int number; + + /** + * 当前页面的元素数量 + */ + private int size; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetDto.java b/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetDto.java index 7971905..f2ce61a 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetDto.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetDto.java @@ -1,13 +1,13 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.Size; - -public record PasswordResetDto( - @NotEmpty(message = "Token cannot be empty.") - String token, - - @NotEmpty(message = "New password cannot be empty.") - @Size(min = 8, message = "Password must be at least 8 characters long.") - String newPassword +package com.dne.ems.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; + +public record PasswordResetDto( + @NotEmpty(message = "Token cannot be empty.") + String token, + + @NotEmpty(message = "New password cannot be empty.") + @Size(min = 8, message = "Password must be at least 8 characters long.") + String newPassword ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetRequest.java index 7a76c27..7c98755 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetRequest.java @@ -1,10 +1,10 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; - -public record PasswordResetRequest( - @NotEmpty(message = "Email cannot be empty.") - @Email(message = "Invalid email format.") - String email +package com.dne.ems.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; + +public record PasswordResetRequest( + @NotEmpty(message = "Email cannot be empty.") + @Email(message = "Invalid email format.") + String email ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetWithCodeDto.java b/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetWithCodeDto.java index e608fe8..396e542 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetWithCodeDto.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/PasswordResetWithCodeDto.java @@ -1,21 +1,21 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.Size; - -/** - * 基于验证码的密码重置DTO - */ -public record PasswordResetWithCodeDto( - @NotEmpty(message = "邮箱不能为空") - @Email(message = "邮箱格式不正确") - String email, - - @NotEmpty(message = "验证码不能为空") - String code, - - @NotEmpty(message = "新密码不能为空") - @Size(min = 8, message = "密码长度至少为8个字符") - String newPassword +package com.dne.ems.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; + +/** + * 基于验证码的密码重置DTO + */ +public record PasswordResetWithCodeDto( + @NotEmpty(message = "邮箱不能为空") + @Email(message = "邮箱格式不正确") + String email, + + @NotEmpty(message = "验证码不能为空") + String code, + + @NotEmpty(message = "新密码不能为空") + @Size(min = 8, message = "密码长度至少为8个字符") + String newPassword ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/PathfindingRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/PathfindingRequest.java index da290c9..e95d1bf 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/PathfindingRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/PathfindingRequest.java @@ -1,30 +1,30 @@ -package com.dne.ems.dto; - - -import lombok.Data; - -/** - * A DTO for requesting a path from the A* service. - * - *

This data transfer object encapsulates the start and end coordinates - * needed for the A* pathfinding algorithm to calculate an optimal path. - * Used primarily by the {@link com.dne.ems.controller.PathfindingController}.

- * - * @see com.dne.ems.service.pathfinding.AStarService - * @see com.dne.ems.dto.Point - */ -@Data -public class PathfindingRequest { - - /** - * The starting point for the pathfinding. - */ - private int startX; - private int startY; - - /** - * The target (end) point for the pathfinding. - */ - private int endX; - private int endY; +package com.dne.ems.dto; + + +import lombok.Data; + +/** + * A DTO for requesting a path from the A* service. + * + *

This data transfer object encapsulates the start and end coordinates + * needed for the A* pathfinding algorithm to calculate an optimal path. + * Used primarily by the {@link com.dne.ems.controller.PathfindingController}.

+ * + * @see com.dne.ems.service.pathfinding.AStarService + * @see com.dne.ems.dto.Point + */ +@Data +public class PathfindingRequest { + + /** + * The starting point for the pathfinding. + */ + private int startX; + private int startY; + + /** + * The target (end) point for the pathfinding. + */ + private int endX; + private int endY; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/Point.java b/ems-backend/src/main/java/com/dne/ems/dto/Point.java index b651fea..017cdda 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/Point.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/Point.java @@ -1,15 +1,15 @@ -package com.dne.ems.dto; - -/** - * 二维坐标点数据传输对象 - * - *

该类用于表示具有整数坐标的二维点,主要用于路径查找请求和在网格上表示位置。 - * 作为简单的数据结构,用于各种空间计算和位置表示场景。

- * - * @param x X坐标值 - * @param y Y坐标值 - * @version 1.0 - * @since 2024 - */ -public record Point(int x, int y) { +package com.dne.ems.dto; + +/** + * 二维坐标点数据传输对象 + * + *

该类用于表示具有整数坐标的二维点,主要用于路径查找请求和在网格上表示位置。 + * 作为简单的数据结构,用于各种空间计算和位置表示场景。

+ * + * @param x X坐标值 + * @param y Y坐标值 + * @version 1.0 + * @since 2024 + */ +public record Point(int x, int y) { } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/PollutantThresholdDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/PollutantThresholdDTO.java index 663199e..fcfe2df 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/PollutantThresholdDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/PollutantThresholdDTO.java @@ -1,135 +1,135 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.PollutionType; - -/** - * 污染物阈值数据传输对象 - * - *

该类用于封装污染物的阈值相关信息,包括污染物类型、名称、阈值值、单位和描述等。 - * 主要用于环境监测系统中污染物标准管理和阈值展示。

- * - *

提供了多种构造方法,支持通过污染物类型枚举或污染物名称字符串创建对象。 - * 当使用名称字符串创建时,会尝试将其转换为对应的PollutionType枚举值。

- * - * @version 1.0 - * @since 2024 - * @see com.dne.ems.model.enums.PollutionType 污染物类型枚举 - */ -public class PollutantThresholdDTO { - /** - * 污染物类型枚举 - */ - private PollutionType pollutionType; - - /** - * 污染物名称 - */ - private String pollutantName; - - /** - * 污染物阈值 - */ - private Double threshold; - - /** - * 阈值单位(如mg/m³, μg/m³等) - */ - private String unit; - - /** - * 污染物及阈值的描述说明 - */ - private String description; - - /** - * 默认构造函数 - */ - public PollutantThresholdDTO() { - } - - /** - * 全参数构造函数 - * - * @param pollutionType 污染物类型 - * @param pollutantName 污染物名称 - * @param threshold 阈值 - * @param unit 单位 - * @param description 描述 - */ - public PollutantThresholdDTO(PollutionType pollutionType, String pollutantName, Double threshold, String unit, String description) { - this.pollutionType = pollutionType; - this.pollutantName = pollutantName; - this.threshold = threshold; - this.unit = unit; - this.description = description; - } - - /** - * 使用污染物名称的构造函数 - * - * @param pollutantName 污染物名称 - * @param threshold 阈值 - * @param unit 单位 - * @param description 描述 - */ - public PollutantThresholdDTO(String pollutantName, Double threshold, String unit, String description) { - this.pollutantName = pollutantName; - try { - this.pollutionType = PollutionType.valueOf(pollutantName); - } catch (IllegalArgumentException e) { - // 如果不是有效的枚举值,默认为OTHER - this.pollutionType = PollutionType.OTHER; - } - this.threshold = threshold; - this.unit = unit; - this.description = description; - } - - // Getters and Setters - public PollutionType getPollutionType() { - return pollutionType; - } - - public void setPollutionType(PollutionType pollutionType) { - this.pollutionType = pollutionType; - this.pollutantName = pollutionType.name(); - } - - public String getPollutantName() { - return pollutantName; - } - - public void setPollutantName(String pollutantName) { - this.pollutantName = pollutantName; - try { - this.pollutionType = PollutionType.valueOf(pollutantName); - } catch (IllegalArgumentException e) { - // 如果不是有效的枚举值,默认为OTHER - this.pollutionType = PollutionType.OTHER; - } - } - - public Double getThreshold() { - return threshold; - } - - public void setThreshold(Double threshold) { - this.threshold = threshold; - } - - public String getUnit() { - return unit; - } - - public void setUnit(String unit) { - this.unit = unit; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } +package com.dne.ems.dto; + +import com.dne.ems.model.enums.PollutionType; + +/** + * 污染物阈值数据传输对象 + * + *

该类用于封装污染物的阈值相关信息,包括污染物类型、名称、阈值值、单位和描述等。 + * 主要用于环境监测系统中污染物标准管理和阈值展示。

+ * + *

提供了多种构造方法,支持通过污染物类型枚举或污染物名称字符串创建对象。 + * 当使用名称字符串创建时,会尝试将其转换为对应的PollutionType枚举值。

+ * + * @version 1.0 + * @since 2024 + * @see com.dne.ems.model.enums.PollutionType 污染物类型枚举 + */ +public class PollutantThresholdDTO { + /** + * 污染物类型枚举 + */ + private PollutionType pollutionType; + + /** + * 污染物名称 + */ + private String pollutantName; + + /** + * 污染物阈值 + */ + private Double threshold; + + /** + * 阈值单位(如mg/m³, μg/m³等) + */ + private String unit; + + /** + * 污染物及阈值的描述说明 + */ + private String description; + + /** + * 默认构造函数 + */ + public PollutantThresholdDTO() { + } + + /** + * 全参数构造函数 + * + * @param pollutionType 污染物类型 + * @param pollutantName 污染物名称 + * @param threshold 阈值 + * @param unit 单位 + * @param description 描述 + */ + public PollutantThresholdDTO(PollutionType pollutionType, String pollutantName, Double threshold, String unit, String description) { + this.pollutionType = pollutionType; + this.pollutantName = pollutantName; + this.threshold = threshold; + this.unit = unit; + this.description = description; + } + + /** + * 使用污染物名称的构造函数 + * + * @param pollutantName 污染物名称 + * @param threshold 阈值 + * @param unit 单位 + * @param description 描述 + */ + public PollutantThresholdDTO(String pollutantName, Double threshold, String unit, String description) { + this.pollutantName = pollutantName; + try { + this.pollutionType = PollutionType.valueOf(pollutantName); + } catch (IllegalArgumentException e) { + // 如果不是有效的枚举值,默认为OTHER + this.pollutionType = PollutionType.OTHER; + } + this.threshold = threshold; + this.unit = unit; + this.description = description; + } + + // Getters and Setters + public PollutionType getPollutionType() { + return pollutionType; + } + + public void setPollutionType(PollutionType pollutionType) { + this.pollutionType = pollutionType; + this.pollutantName = pollutionType.name(); + } + + public String getPollutantName() { + return pollutantName; + } + + public void setPollutantName(String pollutantName) { + this.pollutantName = pollutantName; + try { + this.pollutionType = PollutionType.valueOf(pollutantName); + } catch (IllegalArgumentException e) { + // 如果不是有效的枚举值,默认为OTHER + this.pollutionType = PollutionType.OTHER; + } + } + + public Double getThreshold() { + return threshold; + } + + public void setThreshold(Double threshold) { + this.threshold = threshold; + } + + public String getUnit() { + return unit; + } + + public void setUnit(String unit) { + this.unit = unit; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/PollutionStatsDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/PollutionStatsDTO.java index c910010..8a2e452 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/PollutionStatsDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/PollutionStatsDTO.java @@ -1,14 +1,14 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.PollutionType; - -/** - * DTO for representing pollution statistics. - * - * @param pollutionType The type of pollution. - * @param count The number of feedback events for this pollution type. - */ -public record PollutionStatsDTO( - PollutionType pollutionType, - Long count +package com.dne.ems.dto; + +import com.dne.ems.model.enums.PollutionType; + +/** + * DTO for representing pollution statistics. + * + * @param pollutionType The type of pollution. + * @param count The number of feedback events for this pollution type. + */ +public record PollutionStatsDTO( + PollutionType pollutionType, + Long count ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/ProcessFeedbackRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/ProcessFeedbackRequest.java index 488eee9..8fe317f 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/ProcessFeedbackRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/ProcessFeedbackRequest.java @@ -1,25 +1,25 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.FeedbackStatus; - -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * DTO for processing a feedback. - * This is used when an admin or supervisor approves or takes action on a feedback entry. - */ -@Data -public class ProcessFeedbackRequest { - - /** - * The new status to set for the feedback. - */ - @NotNull(message = "New status cannot be null") - private FeedbackStatus status; - - /** - * Optional notes related to the processing action. - */ - private String notes; +package com.dne.ems.dto; + +import com.dne.ems.model.enums.FeedbackStatus; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * DTO for processing a feedback. + * This is used when an admin or supervisor approves or takes action on a feedback entry. + */ +@Data +public class ProcessFeedbackRequest { + + /** + * The new status to set for the feedback. + */ + @NotNull(message = "New status cannot be null") + private FeedbackStatus status; + + /** + * Optional notes related to the processing action. + */ + private String notes; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/PublicFeedbackRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/PublicFeedbackRequest.java index d627910..8ff2483 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/PublicFeedbackRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/PublicFeedbackRequest.java @@ -1,56 +1,56 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.SeverityLevel; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * DTO for receiving feedback from the public. - */ -@Data -public class PublicFeedbackRequest { - - /** - * The title of the feedback. - */ - @NotBlank(message = "Title cannot be blank") - private String title; - - /** - * The detailed description of the feedback. - */ - @NotBlank(message = "Description cannot be blank") - private String description; - - /** - * The type of pollution. - */ - @NotNull(message = "Pollution type cannot be null") - private String pollutionType; - - /** - * The severity level of the issue. - */ - @NotNull(message = "Severity level cannot be null") - private SeverityLevel severityLevel; - - /** - * The text description of the address of the event. - */ - @NotBlank(message = "Text address cannot be blank") - private String textAddress; - - /** - * The grid coordinate X. - */ - @NotNull(message = "Grid X coordinate cannot be null") - private Double gridX; - - /** - * The grid coordinate Y. - */ - @NotNull(message = "Grid Y coordinate cannot be null") - private Double gridY; +package com.dne.ems.dto; + +import com.dne.ems.model.enums.SeverityLevel; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * DTO for receiving feedback from the public. + */ +@Data +public class PublicFeedbackRequest { + + /** + * The title of the feedback. + */ + @NotBlank(message = "Title cannot be blank") + private String title; + + /** + * The detailed description of the feedback. + */ + @NotBlank(message = "Description cannot be blank") + private String description; + + /** + * The type of pollution. + */ + @NotNull(message = "Pollution type cannot be null") + private String pollutionType; + + /** + * The severity level of the issue. + */ + @NotNull(message = "Severity level cannot be null") + private SeverityLevel severityLevel; + + /** + * The text description of the address of the event. + */ + @NotBlank(message = "Text address cannot be blank") + private String textAddress; + + /** + * The grid coordinate X. + */ + @NotNull(message = "Grid X coordinate cannot be null") + private Double gridX; + + /** + * The grid coordinate Y. + */ + @NotNull(message = "Grid Y coordinate cannot be null") + private Double gridY; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/RejectFeedbackRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/RejectFeedbackRequest.java index 5f833ea..bc74ca5 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/RejectFeedbackRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/RejectFeedbackRequest.java @@ -1,17 +1,17 @@ -package com.dne.ems.dto; - -import lombok.Data; - -/** - * DTO for rejecting a feedback. - * This is used when a supervisor rejects a feedback submission. - */ -@Data -public class RejectFeedbackRequest { - - /** - * The reason for rejecting the feedback. - */ - private String notes; - +package com.dne.ems.dto; + +import lombok.Data; + +/** + * DTO for rejecting a feedback. + * This is used when a supervisor rejects a feedback submission. + */ +@Data +public class RejectFeedbackRequest { + + /** + * The reason for rejecting the feedback. + */ + private String notes; + } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/SignUpRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/SignUpRequest.java index cd459a8..81867be 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/SignUpRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/SignUpRequest.java @@ -1,30 +1,30 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.Role; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; - -public record SignUpRequest( - @NotBlank(message = "姓名不能为空") - String name, - - @NotBlank(message = "邮箱不能为空") - @Email(message = "邮箱格式不正确") - String email, - - @NotBlank(message = "手机号不能为空") - String phone, - - @NotBlank(message = "密码不能为空") - @Size(min = 8, max = 20, message = "密码长度必须在8到20个字符之间") - String password, - - @NotNull(message = "角色不能为空") - Role role, - - @NotBlank(message = "验证码不能为空") - String verificationCode +package com.dne.ems.dto; + +import com.dne.ems.model.enums.Role; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record SignUpRequest( + @NotBlank(message = "姓名不能为空") + String name, + + @NotBlank(message = "邮箱不能为空") + @Email(message = "邮箱格式不正确") + String email, + + @NotBlank(message = "手机号不能为空") + String phone, + + @NotBlank(message = "密码不能为空") + @Size(min = 8, max = 20, message = "密码长度必须在8到20个字符之间") + String password, + + @NotNull(message = "角色不能为空") + Role role, + + @NotBlank(message = "验证码不能为空") + String verificationCode ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskApprovalRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskApprovalRequest.java index 63cbcd1..13f208b 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskApprovalRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskApprovalRequest.java @@ -1,24 +1,24 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.NotNull; - -/** - * 任务审核请求DTO - * - *

用于管理员对提交的任务进行审核决策 - * - * @param approved 是否通过审核(必填) - * @param comments 审核意见(可选) - * - *

业务规则: - *

    - *
  • 只有状态为SUBMITTED的任务才能审核
  • - *
  • 审核通过后任务状态变为COMPLETED
  • - *
  • 审核不通过则返回ASSIGNED状态
  • - *
- */ -public record TaskApprovalRequest( - @NotNull - Boolean approved, - String comments +package com.dne.ems.dto; + +import jakarta.validation.constraints.NotNull; + +/** + * 任务审核请求DTO + * + *

用于管理员对提交的任务进行审核决策 + * + * @param approved 是否通过审核(必填) + * @param comments 审核意见(可选) + * + *

业务规则: + *

    + *
  • 只有状态为SUBMITTED的任务才能审核
  • + *
  • 审核通过后任务状态变为COMPLETED
  • + *
  • 审核不通过则返回ASSIGNED状态
  • + *
+ */ +public record TaskApprovalRequest( + @NotNull + Boolean approved, + String comments ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskAssignmentRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskAssignmentRequest.java index 64238d0..12475cf 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskAssignmentRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskAssignmentRequest.java @@ -1,25 +1,25 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.NotNull; - -/** - * 任务分配请求数据传输对象 - * - *

用于主管将待分配任务指派给特定网格工作人员 - * - *

业务规则: - *

    - *
  • 只能分配给角色为GRID_WORKER的用户
  • - *
  • 任务状态必须为PENDING_ASSIGNMENT(待分配)或ASSIGNED(已分配)
  • - *
  • 分配后任务状态变为ASSIGNED
  • - *
  • 会生成任务分配记录
  • - *
- * - * @param assigneeId 网格工作人员ID(必填) - * @see com.dne.ems.model.enums.TaskStatus 任务状态枚举 - * @see com.dne.ems.model.Assignment 任务分配记录实体 - */ -public record TaskAssignmentRequest( - @NotNull - Long assigneeId +package com.dne.ems.dto; + +import jakarta.validation.constraints.NotNull; + +/** + * 任务分配请求数据传输对象 + * + *

用于主管将待分配任务指派给特定网格工作人员 + * + *

业务规则: + *

    + *
  • 只能分配给角色为GRID_WORKER的用户
  • + *
  • 任务状态必须为PENDING_ASSIGNMENT(待分配)或ASSIGNED(已分配)
  • + *
  • 分配后任务状态变为ASSIGNED
  • + *
  • 会生成任务分配记录
  • + *
+ * + * @param assigneeId 网格工作人员ID(必填) + * @see com.dne.ems.model.enums.TaskStatus 任务状态枚举 + * @see com.dne.ems.model.Assignment 任务分配记录实体 + */ +public record TaskAssignmentRequest( + @NotNull + Long assigneeId ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskCreationRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskCreationRequest.java index 0a38a4e..5421158 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskCreationRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskCreationRequest.java @@ -1,74 +1,74 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -/** - * 任务创建请求数据传输对象 - * - *

用于主管直接创建任务,包含任务基本属性和位置信息 - * - *

业务规则: - *

    - *
  • 创建后状态为PENDING_ASSIGNMENT(待分配)
  • - *
  • 需指定有效的污染类型和严重级别
  • - *
  • 位置信息必须包含有效的网格坐标
  • - *
- * - * @param title 任务标题(必填,最长255字符) - * @param description 任务详细描述(可选) - * @param pollutionType 污染类型(必填) - * @param severityLevel 严重等级(必填) - * @param location 位置信息(必填) - * @see com.dne.ems.model.enums.PollutionType 污染类型枚举 - * @see com.dne.ems.model.enums.SeverityLevel 严重级别枚举 - */ -public record TaskCreationRequest( - @NotBlank(message = "Title is mandatory") - String title, - - String description, - - @NotNull(message = "Pollution type is mandatory") - PollutionType pollutionType, - - @NotNull(message = "Severity level is mandatory") - SeverityLevel severityLevel, - - @NotNull(message = "Location information is mandatory") - LocationDTO location -) { - /** - * 位置数据(嵌套记录) - * - *

用于精确定位任务发生位置 - * - *

验证规则: - *

    - *
  • 文字地址必须符合"省-市-区"格式
  • - *
  • 网格坐标必须在有效范围内(1-1000)
  • - *
  • 经纬度坐标可选,但提供时可提高定位精度
  • - *
- * - * @param latitude 纬度坐标(可选) - * @param longitude 经度坐标(可选) - * @param textAddress 文字地址(必填,格式:"省-市-区") - * @param gridX 网格X坐标(必填,范围1-1000) - * @param gridY 网格Y坐标(必填,范围1-1000) - */ - public record LocationDTO( - Double latitude, - Double longitude, - @NotBlank(message = "Text address is mandatory") - String textAddress, - - @NotNull(message = "Grid X coordinate is mandatory") - Integer gridX, - - @NotNull(message = "Grid Y coordinate is mandatory") - Integer gridY - ) {} +package com.dne.ems.dto; + +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * 任务创建请求数据传输对象 + * + *

用于主管直接创建任务,包含任务基本属性和位置信息 + * + *

业务规则: + *

    + *
  • 创建后状态为PENDING_ASSIGNMENT(待分配)
  • + *
  • 需指定有效的污染类型和严重级别
  • + *
  • 位置信息必须包含有效的网格坐标
  • + *
+ * + * @param title 任务标题(必填,最长255字符) + * @param description 任务详细描述(可选) + * @param pollutionType 污染类型(必填) + * @param severityLevel 严重等级(必填) + * @param location 位置信息(必填) + * @see com.dne.ems.model.enums.PollutionType 污染类型枚举 + * @see com.dne.ems.model.enums.SeverityLevel 严重级别枚举 + */ +public record TaskCreationRequest( + @NotBlank(message = "Title is mandatory") + String title, + + String description, + + @NotNull(message = "Pollution type is mandatory") + PollutionType pollutionType, + + @NotNull(message = "Severity level is mandatory") + SeverityLevel severityLevel, + + @NotNull(message = "Location information is mandatory") + LocationDTO location +) { + /** + * 位置数据(嵌套记录) + * + *

用于精确定位任务发生位置 + * + *

验证规则: + *

    + *
  • 文字地址必须符合"省-市-区"格式
  • + *
  • 网格坐标必须在有效范围内(1-1000)
  • + *
  • 经纬度坐标可选,但提供时可提高定位精度
  • + *
+ * + * @param latitude 纬度坐标(可选) + * @param longitude 经度坐标(可选) + * @param textAddress 文字地址(必填,格式:"省-市-区") + * @param gridX 网格X坐标(必填,范围1-1000) + * @param gridY 网格Y坐标(必填,范围1-1000) + */ + public record LocationDTO( + Double latitude, + Double longitude, + @NotBlank(message = "Text address is mandatory") + String textAddress, + + @NotNull(message = "Grid X coordinate is mandatory") + Integer gridX, + + @NotNull(message = "Grid Y coordinate is mandatory") + Integer gridY + ) {} } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskDTO.java index c300d3e..7d5b1f7 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskDTO.java @@ -1,41 +1,41 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.TaskStatus; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - -/** - * 任务数据传输对象 - * - *

该类用于封装任务的基本信息,包含任务ID、标题、描述和状态等核心数据。 - * 主要用于任务列表展示和简单数据传输场景。

- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class TaskDTO { - /** - * 任务ID,唯一标识 - */ - private Long id; - - /** - * 任务标题 - */ - private String title; - - /** - * 任务详细描述 - */ - private String description; - - /** - * 任务当前状态 - */ - private TaskStatus status; +package com.dne.ems.dto; + +import com.dne.ems.model.enums.TaskStatus; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * 任务数据传输对象 + * + *

该类用于封装任务的基本信息,包含任务ID、标题、描述和状态等核心数据。 + * 主要用于任务列表展示和简单数据传输场景。

+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TaskDTO { + /** + * 任务ID,唯一标识 + */ + private Long id; + + /** + * 任务标题 + */ + private String title; + + /** + * 任务详细描述 + */ + private String description; + + /** + * 任务当前状态 + */ + private TaskStatus status; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskDetailDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskDetailDTO.java index 9e186bc..ef71ca2 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskDetailDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskDetailDTO.java @@ -1,98 +1,98 @@ -package com.dne.ems.dto; - -import java.time.LocalDateTime; -import java.util.List; - -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.model.enums.TaskStatus; - -/** - * 任务详情DTO - * - *

包含任务的完整详细信息,用于详情展示 - * - * @param id 任务ID - * @param feedback 关联的反馈详情 - * @param title 任务标题 - * @param description 任务描述 - * @param severityLevel 严重等级 - * @param pollutionType 污染类型 - * @param textAddress 文字地址 - * @param gridX 网格X坐标 - * @param gridY 网格Y坐标 - * @param status 任务状态 - * @param assignee 分配的工作人员(可能为null) - * @param assignedAt 分配时间(可能为null) - * @param completedAt 完成时间(可能为null) - * @param history 任务历史记录 - * @param submissionInfo 任务提交信息(如果已提交) - */ -public record TaskDetailDTO( - Long id, - FeedbackDTO feedback, - String title, - String description, - SeverityLevel severityLevel, - PollutionType pollutionType, - String textAddress, - Integer gridX, - Integer gridY, - TaskStatus status, - AssigneeDTO assignee, - LocalDateTime assignedAt, - LocalDateTime completedAt, - List history, - SubmissionInfoDTO submissionInfo -) { - /** - * 任务关联的反馈详情DTO - * - * @param feedbackId 反馈ID - * @param eventId 事件ID - * @param title 反馈标题 - * @param description 反馈描述 - * @param severityLevel 严重等级 - * @param textAddress 文字地址 - * @param submitterId 提交人ID - * @param submitterName 提交人姓名 - * @param createdAt 创建时间 - * @param imageUrls 图片URL列表 - */ - public record FeedbackDTO( - Long feedbackId, - String eventId, - String title, - String description, - SeverityLevel severityLevel, - String textAddress, - Long submitterId, - String submitterName, - LocalDateTime createdAt, - List imageUrls - ) {} - - /** - * 任务分配人详情DTO - * - * @param id 工作人员ID - * @param name 工作人员姓名 - * @param phone 联系电话 - */ - public record AssigneeDTO( - Long id, - String name, - String phone - ) {} - - /** - * DTO for task submission details. - * - * @param comments The comments or notes from the submission. - * @param attachments A list of attachments included in the submission. - */ - public record SubmissionInfoDTO( - String comments, - List attachments - ) {} +package com.dne.ems.dto; + +import java.time.LocalDateTime; +import java.util.List; + +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.model.enums.TaskStatus; + +/** + * 任务详情DTO + * + *

包含任务的完整详细信息,用于详情展示 + * + * @param id 任务ID + * @param feedback 关联的反馈详情 + * @param title 任务标题 + * @param description 任务描述 + * @param severityLevel 严重等级 + * @param pollutionType 污染类型 + * @param textAddress 文字地址 + * @param gridX 网格X坐标 + * @param gridY 网格Y坐标 + * @param status 任务状态 + * @param assignee 分配的工作人员(可能为null) + * @param assignedAt 分配时间(可能为null) + * @param completedAt 完成时间(可能为null) + * @param history 任务历史记录 + * @param submissionInfo 任务提交信息(如果已提交) + */ +public record TaskDetailDTO( + Long id, + FeedbackDTO feedback, + String title, + String description, + SeverityLevel severityLevel, + PollutionType pollutionType, + String textAddress, + Integer gridX, + Integer gridY, + TaskStatus status, + AssigneeDTO assignee, + LocalDateTime assignedAt, + LocalDateTime completedAt, + List history, + SubmissionInfoDTO submissionInfo +) { + /** + * 任务关联的反馈详情DTO + * + * @param feedbackId 反馈ID + * @param eventId 事件ID + * @param title 反馈标题 + * @param description 反馈描述 + * @param severityLevel 严重等级 + * @param textAddress 文字地址 + * @param submitterId 提交人ID + * @param submitterName 提交人姓名 + * @param createdAt 创建时间 + * @param imageUrls 图片URL列表 + */ + public record FeedbackDTO( + Long feedbackId, + String eventId, + String title, + String description, + SeverityLevel severityLevel, + String textAddress, + Long submitterId, + String submitterName, + LocalDateTime createdAt, + List imageUrls + ) {} + + /** + * 任务分配人详情DTO + * + * @param id 工作人员ID + * @param name 工作人员姓名 + * @param phone 联系电话 + */ + public record AssigneeDTO( + Long id, + String name, + String phone + ) {} + + /** + * DTO for task submission details. + * + * @param comments The comments or notes from the submission. + * @param attachments A list of attachments included in the submission. + */ + public record SubmissionInfoDTO( + String comments, + List attachments + ) {} } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskFromFeedbackRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskFromFeedbackRequest.java index 24d8126..f540864 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskFromFeedbackRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskFromFeedbackRequest.java @@ -1,61 +1,61 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.SeverityLevel; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -/** - * 从反馈创建任务请求数据传输对象 - * - *

用于主管将公众反馈转化为可分配的任务,包含任务基本属性和优先级设置 - * - *

业务规则: - *

    - *
  • 反馈状态必须为PENDING_REVIEW(待审核)
  • - *
  • 创建后原反馈状态自动更新为PROCESSED(已处理)
  • - *
  • 新任务初始状态为PENDING_ASSIGNMENT(待分配)
  • - *
  • 任务标题需简明描述问题本质
  • - *
  • 严重级别影响任务分配优先级和响应时限
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024-03 - * @see com.dne.ems.model.enums.SeverityLevel 严重级别枚举定义 - * @see com.dne.ems.model.Feedback 关联的反馈实体 - */ -@Data -public class TaskFromFeedbackRequest { - - /** - * 任务标题 - * - *

必填字段,用于简要描述任务内容 - *

验证规则: - *

    - *
  • 不能为空(@NotBlank)
  • - *
  • 最大长度255字符(JPA约束)
  • - *
  • 建议包含问题类型和位置关键词
  • - *
- */ - @NotBlank(message = "任务标题不能为空") - private String title; - - /** - * 严重级别 - * - *

必填字段,用于确定任务处理优先级和响应时限 - *

可选值: - *

    - *
  • LOW - 低优先级(72小时内处理)
  • - *
  • MEDIUM - 中优先级(48小时内处理)
  • - *
  • HIGH - 高优先级(24小时内处理)
  • - *
  • CRITICAL - 紧急(立即处理)
  • - *
- * @see com.dne.ems.model.enums.SeverityLevel 完整枚举定义 - */ - @NotNull(message = "严重级别不能为空") - private SeverityLevel severityLevel; +package com.dne.ems.dto; + +import com.dne.ems.model.enums.SeverityLevel; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 从反馈创建任务请求数据传输对象 + * + *

用于主管将公众反馈转化为可分配的任务,包含任务基本属性和优先级设置 + * + *

业务规则: + *

    + *
  • 反馈状态必须为PENDING_REVIEW(待审核)
  • + *
  • 创建后原反馈状态自动更新为PROCESSED(已处理)
  • + *
  • 新任务初始状态为PENDING_ASSIGNMENT(待分配)
  • + *
  • 任务标题需简明描述问题本质
  • + *
  • 严重级别影响任务分配优先级和响应时限
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024-03 + * @see com.dne.ems.model.enums.SeverityLevel 严重级别枚举定义 + * @see com.dne.ems.model.Feedback 关联的反馈实体 + */ +@Data +public class TaskFromFeedbackRequest { + + /** + * 任务标题 + * + *

必填字段,用于简要描述任务内容 + *

验证规则: + *

    + *
  • 不能为空(@NotBlank)
  • + *
  • 最大长度255字符(JPA约束)
  • + *
  • 建议包含问题类型和位置关键词
  • + *
+ */ + @NotBlank(message = "任务标题不能为空") + private String title; + + /** + * 严重级别 + * + *

必填字段,用于确定任务处理优先级和响应时限 + *

可选值: + *

    + *
  • LOW - 低优先级(72小时内处理)
  • + *
  • MEDIUM - 中优先级(48小时内处理)
  • + *
  • HIGH - 高优先级(24小时内处理)
  • + *
  • CRITICAL - 紧急(立即处理)
  • + *
+ * @see com.dne.ems.model.enums.SeverityLevel 完整枚举定义 + */ + @NotNull(message = "严重级别不能为空") + private SeverityLevel severityLevel; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskHistoryDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskHistoryDTO.java index 7635d47..4d4c8b3 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskHistoryDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskHistoryDTO.java @@ -1,16 +1,16 @@ -package com.dne.ems.dto; - -import java.time.LocalDateTime; - -import com.dne.ems.model.enums.TaskStatus; - -public record TaskHistoryDTO( - Long id, - TaskStatus oldStatus, - TaskStatus newStatus, - String comments, - LocalDateTime changedAt, - UserDTO changedBy -) { - public record UserDTO(Long id, String name) {} +package com.dne.ems.dto; + +import java.time.LocalDateTime; + +import com.dne.ems.model.enums.TaskStatus; + +public record TaskHistoryDTO( + Long id, + TaskStatus oldStatus, + TaskStatus newStatus, + String comments, + LocalDateTime changedAt, + UserDTO changedBy +) { + public record UserDTO(Long id, String name) {} } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskInfoDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskInfoDTO.java index 347849e..074b647 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskInfoDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskInfoDTO.java @@ -1,15 +1,15 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.TaskStatus; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class TaskInfoDTO { - private Long id; - private TaskStatus status; - private AssigneeInfoDTO assignee; +package com.dne.ems.dto; + +import com.dne.ems.model.enums.TaskStatus; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TaskInfoDTO { + private Long id; + private TaskStatus status; + private AssigneeInfoDTO assignee; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskRejectionRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskRejectionRequest.java index 84d6c97..1f325cd 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskRejectionRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskRejectionRequest.java @@ -1,11 +1,11 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -public class TaskRejectionRequest { - - @NotBlank(message = "Rejection reason cannot be blank.") - private String reason; +package com.dne.ems.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class TaskRejectionRequest { + + @NotBlank(message = "Rejection reason cannot be blank.") + private String reason; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskStatsDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskStatsDTO.java index c7c5feb..a3697f6 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskStatsDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskStatsDTO.java @@ -1,14 +1,14 @@ -package com.dne.ems.dto; - -/** - * DTO for representing task completion statistics. - * - * @param totalTasks The total number of tasks. - * @param completedTasks The number of completed tasks. - * @param completionRate The calculated completion rate (completedTasks / totalTasks). - */ -public record TaskStatsDTO( - Long totalTasks, - Long completedTasks, - Double completionRate +package com.dne.ems.dto; + +/** + * DTO for representing task completion statistics. + * + * @param totalTasks The total number of tasks. + * @param completedTasks The number of completed tasks. + * @param completionRate The calculated completion rate (completedTasks / totalTasks). + */ +public record TaskStatsDTO( + Long totalTasks, + Long completedTasks, + Double completionRate ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskSubmissionRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskSubmissionRequest.java index 48c22a3..5e113d3 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskSubmissionRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskSubmissionRequest.java @@ -1,13 +1,13 @@ -package com.dne.ems.dto; - -import jakarta.validation.constraints.NotBlank; - -/** - * DTO for submitting task updates. - * - * @param comments The comments or notes for the task update. - */ -public record TaskSubmissionRequest( - @NotBlank(message = "Comments cannot be blank") - String comments +package com.dne.ems.dto; + +import jakarta.validation.constraints.NotBlank; + +/** + * DTO for submitting task updates. + * + * @param comments The comments or notes for the task update. + */ +public record TaskSubmissionRequest( + @NotBlank(message = "Comments cannot be blank") + String comments ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TaskSummaryDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/TaskSummaryDTO.java index 243aed2..e1b9086 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TaskSummaryDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TaskSummaryDTO.java @@ -1,36 +1,36 @@ -package com.dne.ems.dto; - -import java.time.LocalDateTime; - -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.model.enums.TaskStatus; - -/** - * 任务摘要数据传输对象 - * - *

该类用于封装任务的摘要信息,主要用于任务列表展示和简单数据传输场景。 - * 包含任务的基本标识、状态、分配信息和位置等核心数据。

- * - * @param id 任务ID,唯一标识 - * @param title 任务标题 - * @param description 任务描述 - * @param status 任务当前状态 - * @param assigneeName 被分配的网格员姓名(可能为空) - * @param createdAt 任务创建时间 - * @param textAddress 任务地点文字描述 - * @param imageUrl 任务相关图片URL - * @param severity 任务严重程度 - * @version 1.0 - * @since 2024 - */ -public record TaskSummaryDTO( - Long id, - String title, - String description, - TaskStatus status, - String assigneeName, - LocalDateTime createdAt, - String textAddress, - String imageUrl, - SeverityLevel severity +package com.dne.ems.dto; + +import java.time.LocalDateTime; + +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.model.enums.TaskStatus; + +/** + * 任务摘要数据传输对象 + * + *

该类用于封装任务的摘要信息,主要用于任务列表展示和简单数据传输场景。 + * 包含任务的基本标识、状态、分配信息和位置等核心数据。

+ * + * @param id 任务ID,唯一标识 + * @param title 任务标题 + * @param description 任务描述 + * @param status 任务当前状态 + * @param assigneeName 被分配的网格员姓名(可能为空) + * @param createdAt 任务创建时间 + * @param textAddress 任务地点文字描述 + * @param imageUrl 任务相关图片URL + * @param severity 任务严重程度 + * @version 1.0 + * @since 2024 + */ +public record TaskSummaryDTO( + Long id, + String title, + String description, + TaskStatus status, + String assigneeName, + LocalDateTime createdAt, + String textAddress, + String imageUrl, + SeverityLevel severity ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/TrendDataPointDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/TrendDataPointDTO.java index 586c780..1985cb1 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/TrendDataPointDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/TrendDataPointDTO.java @@ -1,55 +1,55 @@ -package com.dne.ems.dto; - -/** - * 趋势数据点传输对象 - * - *

该类用于表示时间序列趋势图中的单个数据点,主要用于环境数据趋势分析和可视化展示。 - * 包含时间标识(通常为年月格式)和对应的数值(如超标次数)。

- * - * @version 1.0 - * @since 2024 - */ -public class TrendDataPointDTO { - /** - * 月份标识,通常使用"YYYY-MM"格式 - */ - private String yearMonth; - - /** - * 该月的数值(如超标次数) - */ - private Long count; - - /** - * JPA所需的默认构造函数 - */ - public TrendDataPointDTO() { - } - - /** - * 用于JPQL查询的构造函数 - * - * @param yearMonth 月份标识,格式为"YYYY-MM" - * @param count 该月的计数值 - */ - public TrendDataPointDTO(String yearMonth, Long count) { - this.yearMonth = yearMonth; - this.count = count; - } - - public String getYearMonth() { - return yearMonth; - } - - public void setYearMonth(String yearMonth) { - this.yearMonth = yearMonth; - } - - public Long getCount() { - return count; - } - - public void setCount(Long count) { - this.count = count; - } +package com.dne.ems.dto; + +/** + * 趋势数据点传输对象 + * + *

该类用于表示时间序列趋势图中的单个数据点,主要用于环境数据趋势分析和可视化展示。 + * 包含时间标识(通常为年月格式)和对应的数值(如超标次数)。

+ * + * @version 1.0 + * @since 2024 + */ +public class TrendDataPointDTO { + /** + * 月份标识,通常使用"YYYY-MM"格式 + */ + private String yearMonth; + + /** + * 该月的数值(如超标次数) + */ + private Long count; + + /** + * JPA所需的默认构造函数 + */ + public TrendDataPointDTO() { + } + + /** + * 用于JPQL查询的构造函数 + * + * @param yearMonth 月份标识,格式为"YYYY-MM" + * @param count 该月的计数值 + */ + public TrendDataPointDTO(String yearMonth, Long count) { + this.yearMonth = yearMonth; + this.count = count; + } + + public String getYearMonth() { + return yearMonth; + } + + public void setYearMonth(String yearMonth) { + this.yearMonth = yearMonth; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/UserCreationRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/UserCreationRequest.java index 33b49f7..04d6db8 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/UserCreationRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/UserCreationRequest.java @@ -1,30 +1,30 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.Role; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; - -public record UserCreationRequest( - @NotBlank(message = "Name is mandatory") - String name, - - @NotBlank(message = "Email is mandatory") - @Email(message = "Email should be valid") - String email, - - @NotBlank(message = "Phone number is mandatory") - String phone, - - @NotBlank(message = "Password is mandatory") - @Size(min = 8, message = "Password must be at least 8 characters long") - String password, - - @NotNull(message = "Role is mandatory") - Role role, - - String region -) { +package com.dne.ems.dto; + +import com.dne.ems.model.enums.Role; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record UserCreationRequest( + @NotBlank(message = "Name is mandatory") + String name, + + @NotBlank(message = "Email is mandatory") + @Email(message = "Email should be valid") + String email, + + @NotBlank(message = "Phone number is mandatory") + String phone, + + @NotBlank(message = "Password is mandatory") + @Size(min = 8, message = "Password must be at least 8 characters long") + String password, + + @NotNull(message = "Role is mandatory") + Role role, + + String region +) { } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/UserFeedbackSummaryDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/UserFeedbackSummaryDTO.java index 7d7f6ff..bfae650 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/UserFeedbackSummaryDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/UserFeedbackSummaryDTO.java @@ -1,21 +1,21 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.FeedbackStatus; -import java.time.LocalDateTime; - -/** - * A DTO representing a summary of a feedback submission for the user's history view. - * - * @param eventId The unique business ID of the feedback event. - * @param title The title of the feedback. - * @param status The current status of the feedback. - * @param createdAt The timestamp when the feedback was submitted. - * @param imageUrl The URL of the first attachment image, if any. - */ -public record UserFeedbackSummaryDTO( - String eventId, - String title, - FeedbackStatus status, - LocalDateTime createdAt, - String imageUrl +package com.dne.ems.dto; + +import com.dne.ems.model.enums.FeedbackStatus; +import java.time.LocalDateTime; + +/** + * A DTO representing a summary of a feedback submission for the user's history view. + * + * @param eventId The unique business ID of the feedback event. + * @param title The title of the feedback. + * @param status The current status of the feedback. + * @param createdAt The timestamp when the feedback was submitted. + * @param imageUrl The URL of the first attachment image, if any. + */ +public record UserFeedbackSummaryDTO( + String eventId, + String title, + FeedbackStatus status, + LocalDateTime createdAt, + String imageUrl ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/UserInfoDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/UserInfoDTO.java index ccc3fa4..05ad7b3 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/UserInfoDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/UserInfoDTO.java @@ -1,55 +1,55 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.UserAccount; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 用户信息数据传输对象 - * - *

该类用于封装用户基本信息,包含用户ID、姓名、电话和邮箱等核心数据。 - * 主要用于在不暴露敏感信息的情况下,向前端传递用户信息。

- * - *

提供了从用户实体(UserAccount)转换为DTO的静态方法,便于服务层使用。

- * - * @version 1.0 - * @since 2024 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class UserInfoDTO { - /** - * 用户ID,唯一标识 - */ - private Long id; - - /** - * 用户姓名 - */ - private String name; - - /** - * 用户电话号码 - */ - private String phone; - - /** - * 用户邮箱地址 - */ - private String email; - - /** - * 从用户实体创建DTO对象的工厂方法 - * - * @param user 用户实体对象 - * @return 对应的UserInfoDTO对象,如果输入为null则返回null - */ - public static UserInfoDTO fromEntity(UserAccount user) { - if (user == null) { - return null; - } - return new UserInfoDTO(user.getId(), user.getName(), user.getPhone(), user.getEmail()); - } +package com.dne.ems.dto; + +import com.dne.ems.model.UserAccount; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 用户信息数据传输对象 + * + *

该类用于封装用户基本信息,包含用户ID、姓名、电话和邮箱等核心数据。 + * 主要用于在不暴露敏感信息的情况下,向前端传递用户信息。

+ * + *

提供了从用户实体(UserAccount)转换为DTO的静态方法,便于服务层使用。

+ * + * @version 1.0 + * @since 2024 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserInfoDTO { + /** + * 用户ID,唯一标识 + */ + private Long id; + + /** + * 用户姓名 + */ + private String name; + + /** + * 用户电话号码 + */ + private String phone; + + /** + * 用户邮箱地址 + */ + private String email; + + /** + * 从用户实体创建DTO对象的工厂方法 + * + * @param user 用户实体对象 + * @return 对应的UserInfoDTO对象,如果输入为null则返回null + */ + public static UserInfoDTO fromEntity(UserAccount user) { + if (user == null) { + return null; + } + return new UserInfoDTO(user.getId(), user.getName(), user.getPhone(), user.getEmail()); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/UserRegistrationRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/UserRegistrationRequest.java index 7a72aeb..c865181 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/UserRegistrationRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/UserRegistrationRequest.java @@ -1,33 +1,33 @@ -package com.dne.ems.dto; - -import com.dne.ems.validation.ValidPassword; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; -// import jakarta.validation.constraints.Pattern; -// import jakarta.validation.constraints.Size; - -/** - * Data Transfer Object for user registration requests. - * Carries data from the controller to the service layer. - * Includes validation annotations to ensure data integrity. - * - * @param name The user's real name. - * @param phone The user's unique phone number. - * @param email The user's unique email address. - * @param password The user's password, which must meet complexity requirements. - */ -public record UserRegistrationRequest( - @NotEmpty(message = "姓名不能为空") - String name, - - @NotEmpty(message = "手机号不能为空") - String phone, - - @NotEmpty(message = "邮箱不能为空") - @Email(message = "无效的邮箱格式") - String email, - - @NotEmpty(message = "密码不能为空") - @ValidPassword - String password +package com.dne.ems.dto; + +import com.dne.ems.validation.ValidPassword; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +// import jakarta.validation.constraints.Pattern; +// import jakarta.validation.constraints.Size; + +/** + * Data Transfer Object for user registration requests. + * Carries data from the controller to the service layer. + * Includes validation annotations to ensure data integrity. + * + * @param name The user's real name. + * @param phone The user's unique phone number. + * @param email The user's unique email address. + * @param password The user's password, which must meet complexity requirements. + */ +public record UserRegistrationRequest( + @NotEmpty(message = "姓名不能为空") + String name, + + @NotEmpty(message = "手机号不能为空") + String phone, + + @NotEmpty(message = "邮箱不能为空") + @Email(message = "无效的邮箱格式") + String email, + + @NotEmpty(message = "密码不能为空") + @ValidPassword + String password ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/UserRoleUpdateRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/UserRoleUpdateRequest.java index a758eb7..53d93b9 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/UserRoleUpdateRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/UserRoleUpdateRequest.java @@ -1,19 +1,19 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.Role; -import com.dne.ems.validation.GridWorkerLocationRequired; - -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Data -@GridWorkerLocationRequired -public class UserRoleUpdateRequest { - - @NotNull(message = "角色不能为空") - private Role role; - - private Integer gridX; - - private Integer gridY; +package com.dne.ems.dto; + +import com.dne.ems.model.enums.Role; +import com.dne.ems.validation.GridWorkerLocationRequired; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@GridWorkerLocationRequired +public class UserRoleUpdateRequest { + + @NotNull(message = "角色不能为空") + private Role role; + + private Integer gridX; + + private Integer gridY; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/UserSummaryDTO.java b/ems-backend/src/main/java/com/dne/ems/dto/UserSummaryDTO.java index f0dc78a..8d81784 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/UserSummaryDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/UserSummaryDTO.java @@ -1,28 +1,28 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.UserStatus; - -/** - * 用户摘要数据传输对象 - * - *

该类用于封装用户的摘要信息,主要用于用户列表展示和简单数据传输场景。 - * 包含用户的基本标识、个人信息、角色和状态等核心数据。

- * - * @param id 用户ID,唯一标识 - * @param name 用户姓名 - * @param email 用户邮箱地址 - * @param phone 用户电话号码 - * @param role 用户角色 - * @param status 用户账号状态 - * @version 1.0 - * @since 2024 - */ -public record UserSummaryDTO( - Long id, - String name, - String email, - String phone, - Role role, - UserStatus status +package com.dne.ems.dto; + +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.UserStatus; + +/** + * 用户摘要数据传输对象 + * + *

该类用于封装用户的摘要信息,主要用于用户列表展示和简单数据传输场景。 + * 包含用户的基本标识、个人信息、角色和状态等核心数据。

+ * + * @param id 用户ID,唯一标识 + * @param name 用户姓名 + * @param email 用户邮箱地址 + * @param phone 用户电话号码 + * @param role 用户角色 + * @param status 用户账号状态 + * @version 1.0 + * @since 2024 + */ +public record UserSummaryDTO( + Long id, + String name, + String email, + String phone, + Role role, + UserStatus status ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/UserUpdateRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/UserUpdateRequest.java index cbb9234..0e15c83 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/UserUpdateRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/UserUpdateRequest.java @@ -1,39 +1,39 @@ -package com.dne.ems.dto; - -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.UserStatus; -import com.dne.ems.model.enums.Gender; -import com.dne.ems.model.enums.Level; - -import java.util.List; - -/** - * A DTO for updating user account information. - * - * @param name The new name for the user. Can be null if not updating. - * @param phone The new phone for the user. Can be null if not updating. - * @param region The new region for the user. Can be null if not updating. - * @param role The new role for the user. Can be null if not updating. - * @param status The new status for the user. Can be null if not updating. - * @param gender The new gender for the user. Can be null if not updating. - * @param gridX The new grid X coordinate. - * @param gridY The new grid Y coordinate. - * @param level The new level for the user. - * @param skills The new skills for the user. - * @param currentLatitude The new latitude. - * @param currentLongitude The new longitude. - */ -public record UserUpdateRequest( - String name, - String phone, - String region, - Role role, - UserStatus status, - Gender gender, - Integer gridX, - Integer gridY, - Level level, - List skills, - Double currentLatitude, - Double currentLongitude +package com.dne.ems.dto; + +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.UserStatus; +import com.dne.ems.model.enums.Gender; +import com.dne.ems.model.enums.Level; + +import java.util.List; + +/** + * A DTO for updating user account information. + * + * @param name The new name for the user. Can be null if not updating. + * @param phone The new phone for the user. Can be null if not updating. + * @param region The new region for the user. Can be null if not updating. + * @param role The new role for the user. Can be null if not updating. + * @param status The new status for the user. Can be null if not updating. + * @param gender The new gender for the user. Can be null if not updating. + * @param gridX The new grid X coordinate. + * @param gridY The new grid Y coordinate. + * @param level The new level for the user. + * @param skills The new skills for the user. + * @param currentLatitude The new latitude. + * @param currentLongitude The new longitude. + */ +public record UserUpdateRequest( + String name, + String phone, + String region, + Role role, + UserStatus status, + Gender gender, + Integer gridX, + Integer gridY, + Level level, + List skills, + Double currentLatitude, + Double currentLongitude ) {} \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/ai/VolcanoChatRequest.java b/ems-backend/src/main/java/com/dne/ems/dto/ai/VolcanoChatRequest.java index 9ea583b..dba5b59 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/ai/VolcanoChatRequest.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/ai/VolcanoChatRequest.java @@ -1,145 +1,145 @@ -package com.dne.ems.dto.ai; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.Builder; -import lombok.Data; -import lombok.experimental.SuperBuilder; - -/** - * 火山引擎聊天API请求DTO - * - *

该类封装了向火山引擎AI聊天服务发送请求所需的所有数据结构。 - * 包括模型名称、消息列表和工具列表等核心组件。

- * - * @version 1.0 - * @since 2025-06 - */ -@Data -@Builder -@JsonInclude(JsonInclude.Include.NON_NULL) -public class VolcanoChatRequest { - /** - * 要使用的AI模型名称 - */ - private String model; - - /** - * 对话消息列表,包含用户和系统的对话历史 - */ - private List messages; - - /** - * 可用工具列表,定义AI可以调用的函数 - */ - private List tools; - - /** - * 表示对话中的一条消息 - *

包含角色信息和内容部分列表

- */ - @Data - @SuperBuilder - public static class Message { - private String role; - private List content; - } - - /** - * 内容部分接口,用于定义消息内容的不同类型 - *

作为TextContentPart和ImageUrlContentPart的共同父接口

- */ - public interface ContentPart { - } - - @Data - @SuperBuilder - public static class TextContentPart implements ContentPart { - /** - * 内容类型,固定为"text" - */ - @Builder.Default - private String type = "text"; - - /** - * 文本内容 - */ - private String text; - } - - @Data - @SuperBuilder - public static class ImageUrlContentPart implements ContentPart { - /** - * 内容类型,固定为"image_url" - */ - @Builder.Default - private String type = "image_url"; - - /** - * 图片URL信息 - */ - @JsonProperty("image_url") - private ImageUrl imageUrl; - } - - @Data - @Builder - public static class ImageUrl { - /** - * 图片的URL地址 - */ - private String url; - } - - @Data - @Builder - public static class Tool { - /** - * 工具类型 - */ - private String type; - /** - * 函数定义 - */ - private FunctionDef function; - } - - @Data - @Builder - public static class FunctionDef { - /** - * 函数名称 - */ - private String name; - /** - * 函数描述 - */ - private String description; - /** - * 函数参数定义 - */ - private Parameters parameters; - } - - @Data - @Builder - public static class Parameters { - /** - * 参数类型 - */ - private String type; - /** - * 参数属性定义 - */ - private Object properties; - /** - * 必需的参数列表 - */ - @JsonProperty("required") - private List required; - } +package com.dne.ems.dto.ai; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Builder; +import lombok.Data; +import lombok.experimental.SuperBuilder; + +/** + * 火山引擎聊天API请求DTO + * + *

该类封装了向火山引擎AI聊天服务发送请求所需的所有数据结构。 + * 包括模型名称、消息列表和工具列表等核心组件。

+ * + * @version 1.0 + * @since 2025-06 + */ +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class VolcanoChatRequest { + /** + * 要使用的AI模型名称 + */ + private String model; + + /** + * 对话消息列表,包含用户和系统的对话历史 + */ + private List messages; + + /** + * 可用工具列表,定义AI可以调用的函数 + */ + private List tools; + + /** + * 表示对话中的一条消息 + *

包含角色信息和内容部分列表

+ */ + @Data + @SuperBuilder + public static class Message { + private String role; + private List content; + } + + /** + * 内容部分接口,用于定义消息内容的不同类型 + *

作为TextContentPart和ImageUrlContentPart的共同父接口

+ */ + public interface ContentPart { + } + + @Data + @SuperBuilder + public static class TextContentPart implements ContentPart { + /** + * 内容类型,固定为"text" + */ + @Builder.Default + private String type = "text"; + + /** + * 文本内容 + */ + private String text; + } + + @Data + @SuperBuilder + public static class ImageUrlContentPart implements ContentPart { + /** + * 内容类型,固定为"image_url" + */ + @Builder.Default + private String type = "image_url"; + + /** + * 图片URL信息 + */ + @JsonProperty("image_url") + private ImageUrl imageUrl; + } + + @Data + @Builder + public static class ImageUrl { + /** + * 图片的URL地址 + */ + private String url; + } + + @Data + @Builder + public static class Tool { + /** + * 工具类型 + */ + private String type; + /** + * 函数定义 + */ + private FunctionDef function; + } + + @Data + @Builder + public static class FunctionDef { + /** + * 函数名称 + */ + private String name; + /** + * 函数描述 + */ + private String description; + /** + * 函数参数定义 + */ + private Parameters parameters; + } + + @Data + @Builder + public static class Parameters { + /** + * 参数类型 + */ + private String type; + /** + * 参数属性定义 + */ + private Object properties; + /** + * 必需的参数列表 + */ + @JsonProperty("required") + private List required; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/dto/ai/VolcanoChatResponse.java b/ems-backend/src/main/java/com/dne/ems/dto/ai/VolcanoChatResponse.java index 600a1b6..25b196a 100644 --- a/ems-backend/src/main/java/com/dne/ems/dto/ai/VolcanoChatResponse.java +++ b/ems-backend/src/main/java/com/dne/ems/dto/ai/VolcanoChatResponse.java @@ -1,85 +1,85 @@ -package com.dne.ems.dto.ai; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 火山引擎聊天API响应DTO - * - *

该类封装了从火山引擎AI聊天服务接收到的响应数据结构。 - * 包含AI生成的回复内容和可能的工具调用信息。

- * - * @version 1.0 - * @since 2025-06 - */ -@Data -@NoArgsConstructor -public class VolcanoChatResponse { - private List choices; - - @Data - @NoArgsConstructor - public static class Choice { - /** - * 包含AI回复的消息对象 - */ - private Message message; - } - - @Data - @NoArgsConstructor - public static class Message { - /** - * 消息的角色(如system、user、assistant) - */ - private String role; - - /** - * 消息的文本内容 - */ - private String content; - - /** - * 工具调用列表,包含AI请求调用的工具信息 - */ - @JsonProperty("tool_calls") - private List toolCalls; - } - - @Data - @NoArgsConstructor - public static class ToolCall { - /** - * 工具调用的唯一标识符 - */ - private String id; - - /** - * 工具调用的类型 - */ - private String type; - - /** - * 函数调用信息 - */ - private FunctionCall function; - } - - @Data - @NoArgsConstructor - public static class FunctionCall { - /** - * 被调用的函数名称 - */ - private String name; - - /** - * 函数调用的参数,JSON格式字符串 - */ - private String arguments; - } +package com.dne.ems.dto.ai; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 火山引擎聊天API响应DTO + * + *

该类封装了从火山引擎AI聊天服务接收到的响应数据结构。 + * 包含AI生成的回复内容和可能的工具调用信息。

+ * + * @version 1.0 + * @since 2025-06 + */ +@Data +@NoArgsConstructor +public class VolcanoChatResponse { + private List choices; + + @Data + @NoArgsConstructor + public static class Choice { + /** + * 包含AI回复的消息对象 + */ + private Message message; + } + + @Data + @NoArgsConstructor + public static class Message { + /** + * 消息的角色(如system、user、assistant) + */ + private String role; + + /** + * 消息的文本内容 + */ + private String content; + + /** + * 工具调用列表,包含AI请求调用的工具信息 + */ + @JsonProperty("tool_calls") + private List toolCalls; + } + + @Data + @NoArgsConstructor + public static class ToolCall { + /** + * 工具调用的唯一标识符 + */ + private String id; + + /** + * 工具调用的类型 + */ + private String type; + + /** + * 函数调用信息 + */ + private FunctionCall function; + } + + @Data + @NoArgsConstructor + public static class FunctionCall { + /** + * 被调用的函数名称 + */ + private String name; + + /** + * 函数调用的参数,JSON格式字符串 + */ + private String arguments; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/event/FeedbackSubmittedForAiReviewEvent.java b/ems-backend/src/main/java/com/dne/ems/event/FeedbackSubmittedForAiReviewEvent.java index 121cd05..defd3b0 100644 --- a/ems-backend/src/main/java/com/dne/ems/event/FeedbackSubmittedForAiReviewEvent.java +++ b/ems-backend/src/main/java/com/dne/ems/event/FeedbackSubmittedForAiReviewEvent.java @@ -1,19 +1,19 @@ -package com.dne.ems.event; - -import org.springframework.context.ApplicationEvent; - -import com.dne.ems.model.Feedback; - -public class FeedbackSubmittedForAiReviewEvent extends ApplicationEvent { - - private final Feedback feedback; - - public FeedbackSubmittedForAiReviewEvent(Object source, Feedback feedback) { - super(source); - this.feedback = feedback; - } - - public Feedback getFeedback() { - return feedback; - } +package com.dne.ems.event; + +import org.springframework.context.ApplicationEvent; + +import com.dne.ems.model.Feedback; + +public class FeedbackSubmittedForAiReviewEvent extends ApplicationEvent { + + private final Feedback feedback; + + public FeedbackSubmittedForAiReviewEvent(Object source, Feedback feedback) { + super(source); + this.feedback = feedback; + } + + public Feedback getFeedback() { + return feedback; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/event/TaskReadyForAssignmentEvent.java b/ems-backend/src/main/java/com/dne/ems/event/TaskReadyForAssignmentEvent.java index 8ec8b25..b0ed71b 100644 --- a/ems-backend/src/main/java/com/dne/ems/event/TaskReadyForAssignmentEvent.java +++ b/ems-backend/src/main/java/com/dne/ems/event/TaskReadyForAssignmentEvent.java @@ -1,18 +1,18 @@ -package com.dne.ems.event; - -import org.springframework.context.ApplicationEvent; - -import com.dne.ems.model.Task; - -import lombok.Getter; - -@Getter -public class TaskReadyForAssignmentEvent extends ApplicationEvent { - - private final Task task; - - public TaskReadyForAssignmentEvent(Object source, Task task) { - super(source); - this.task = task; - } +package com.dne.ems.event; + +import org.springframework.context.ApplicationEvent; + +import com.dne.ems.model.Task; + +import lombok.Getter; + +@Getter +public class TaskReadyForAssignmentEvent extends ApplicationEvent { + + private final Task task; + + public TaskReadyForAssignmentEvent(Object source, Task task) { + super(source); + this.task = task; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/exception/ErrorResponseDTO.java b/ems-backend/src/main/java/com/dne/ems/exception/ErrorResponseDTO.java index 53b81b0..ef5d866 100644 --- a/ems-backend/src/main/java/com/dne/ems/exception/ErrorResponseDTO.java +++ b/ems-backend/src/main/java/com/dne/ems/exception/ErrorResponseDTO.java @@ -1,24 +1,24 @@ -package com.dne.ems.exception; - -import java.time.LocalDateTime; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * A standard DTO for returning API error responses. - * This class provides a consistent error structure for all API endpoints. - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class ErrorResponseDTO { - - private LocalDateTime timestamp; - private int status; - private String error; - private String message; - private String path; - +package com.dne.ems.exception; + +import java.time.LocalDateTime; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A standard DTO for returning API error responses. + * This class provides a consistent error structure for all API endpoints. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ErrorResponseDTO { + + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; + } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/exception/FileNotFoundException.java b/ems-backend/src/main/java/com/dne/ems/exception/FileNotFoundException.java index 5d5c66f..62f0fc4 100644 --- a/ems-backend/src/main/java/com/dne/ems/exception/FileNotFoundException.java +++ b/ems-backend/src/main/java/com/dne/ems/exception/FileNotFoundException.java @@ -1,20 +1,20 @@ -package com.dne.ems.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -/** - * Custom exception thrown when a requested file is not found. - * Annotated with @ResponseStatus to automatically return a 404 Not Found HTTP status. - */ -@ResponseStatus(HttpStatus.NOT_FOUND) -public class FileNotFoundException extends RuntimeException { - - public FileNotFoundException(String message) { - super(message); - } - - public FileNotFoundException(String message, Throwable cause) { - super(message, cause); - } +package com.dne.ems.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Custom exception thrown when a requested file is not found. + * Annotated with @ResponseStatus to automatically return a 404 Not Found HTTP status. + */ +@ResponseStatus(HttpStatus.NOT_FOUND) +public class FileNotFoundException extends RuntimeException { + + public FileNotFoundException(String message) { + super(message); + } + + public FileNotFoundException(String message, Throwable cause) { + super(message, cause); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/exception/FileStorageException.java b/ems-backend/src/main/java/com/dne/ems/exception/FileStorageException.java index a239d61..4a22669 100644 --- a/ems-backend/src/main/java/com/dne/ems/exception/FileStorageException.java +++ b/ems-backend/src/main/java/com/dne/ems/exception/FileStorageException.java @@ -1,15 +1,15 @@ -package com.dne.ems.exception; - -/** - * Custom exception for file storage related errors. - */ -public class FileStorageException extends RuntimeException { - - public FileStorageException(String message) { - super(message); - } - - public FileStorageException(String message, Throwable cause) { - super(message, cause); - } +package com.dne.ems.exception; + +/** + * Custom exception for file storage related errors. + */ +public class FileStorageException extends RuntimeException { + + public FileStorageException(String message) { + super(message); + } + + public FileStorageException(String message, Throwable cause) { + super(message, cause); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/exception/GlobalExceptionHandler.java b/ems-backend/src/main/java/com/dne/ems/exception/GlobalExceptionHandler.java index 1837099..45c645b 100644 --- a/ems-backend/src/main/java/com/dne/ems/exception/GlobalExceptionHandler.java +++ b/ems-backend/src/main/java/com/dne/ems/exception/GlobalExceptionHandler.java @@ -1,99 +1,99 @@ -package com.dne.ems.exception; - -import java.time.LocalDateTime; -import java.util.Map; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; - -import com.dne.ems.dto.ErrorResponseDTO; -import lombok.extern.slf4j.Slf4j; - -/** - * 全局异常处理器,统一处理控制器层抛出的异常 - * - *

处理流程: - *

    - *
  1. 捕获控制器抛出的异常
  2. - *
  3. 根据异常类型构建错误响应
  4. - *
  5. 记录错误日志
  6. - *
  7. 返回标准化的错误响应
  8. - *
- * - *

已处理的异常类型: - *

    - *
  • ResourceNotFoundException - 资源不存在(404)
  • - *
  • InvalidOperationException - 无效操作(400)
  • - *
  • Exception - 其他未处理异常(500)
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024-03 - * @see com.dne.ems.exception.ResourceNotFoundException - * @see com.dne.ems.exception.InvalidOperationException - * @see com.dne.ems.dto.ErrorResponseDTO - */ -@ControllerAdvice -@Slf4j -public class GlobalExceptionHandler { - - /** - * 处理资源不存在异常 - * - *

当查询的资源不存在时抛出,返回404状态码 - * - * @param ex 捕获的异常实例 - * @param request 当前Web请求 - * @return 包含错误详情的响应实体(404) - * @see com.dne.ems.exception.ResourceNotFoundException - */ - @ExceptionHandler(ResourceNotFoundException.class) - public ResponseEntity handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) { - ErrorResponseDTO errorResponse = new ErrorResponseDTO( - LocalDateTime.now(), - HttpStatus.NOT_FOUND.value(), - "Not Found", - ex.getMessage(), - request.getDescription(false).replace("uri=", "")); - return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); - } - - /** - * 处理无效操作异常 - * - *

当业务规则不允许的操作时抛出,返回400状态码 - * - * @param ex 捕获的异常实例 - * @param request 当前Web请求 - * @return 包含错误详情的响应实体(400) - * @see com.dne.ems.exception.InvalidOperationException - */ - @ExceptionHandler(InvalidOperationException.class) - public ResponseEntity handleInvalidOperationException(InvalidOperationException ex, WebRequest request) { - ErrorResponseDTO errorResponse = new ErrorResponseDTO( - LocalDateTime.now(), - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - ex.getMessage(), - request.getDescription(false).replace("uri=", "")); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { - log.error("捕获到全局未处理异常! 请求: {}", request.getDescription(false), ex); - - // 创建一个更详细的错误响应体 - Map errorDetails = Map.of( - "message", "服务器内部发生未知错误,请联系技术支持", - "error", ex.getClass().getName(), - "details", ex.getMessage() - ); - - return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR); - } +package com.dne.ems.exception; + +import java.time.LocalDateTime; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +import com.dne.ems.dto.ErrorResponseDTO; +import lombok.extern.slf4j.Slf4j; + +/** + * 全局异常处理器,统一处理控制器层抛出的异常 + * + *

处理流程: + *

    + *
  1. 捕获控制器抛出的异常
  2. + *
  3. 根据异常类型构建错误响应
  4. + *
  5. 记录错误日志
  6. + *
  7. 返回标准化的错误响应
  8. + *
+ * + *

已处理的异常类型: + *

    + *
  • ResourceNotFoundException - 资源不存在(404)
  • + *
  • InvalidOperationException - 无效操作(400)
  • + *
  • Exception - 其他未处理异常(500)
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024-03 + * @see com.dne.ems.exception.ResourceNotFoundException + * @see com.dne.ems.exception.InvalidOperationException + * @see com.dne.ems.dto.ErrorResponseDTO + */ +@ControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * 处理资源不存在异常 + * + *

当查询的资源不存在时抛出,返回404状态码 + * + * @param ex 捕获的异常实例 + * @param request 当前Web请求 + * @return 包含错误详情的响应实体(404) + * @see com.dne.ems.exception.ResourceNotFoundException + */ + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) { + ErrorResponseDTO errorResponse = new ErrorResponseDTO( + LocalDateTime.now(), + HttpStatus.NOT_FOUND.value(), + "Not Found", + ex.getMessage(), + request.getDescription(false).replace("uri=", "")); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + /** + * 处理无效操作异常 + * + *

当业务规则不允许的操作时抛出,返回400状态码 + * + * @param ex 捕获的异常实例 + * @param request 当前Web请求 + * @return 包含错误详情的响应实体(400) + * @see com.dne.ems.exception.InvalidOperationException + */ + @ExceptionHandler(InvalidOperationException.class) + public ResponseEntity handleInvalidOperationException(InvalidOperationException ex, WebRequest request) { + ErrorResponseDTO errorResponse = new ErrorResponseDTO( + LocalDateTime.now(), + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + ex.getMessage(), + request.getDescription(false).replace("uri=", "")); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + log.error("捕获到全局未处理异常! 请求: {}", request.getDescription(false), ex); + + // 创建一个更详细的错误响应体 + Map errorDetails = Map.of( + "message", "服务器内部发生未知错误,请联系技术支持", + "error", ex.getClass().getName(), + "details", ex.getMessage() + ); + + return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/exception/InvalidOperationException.java b/ems-backend/src/main/java/com/dne/ems/exception/InvalidOperationException.java index 66b57a1..0f133c3 100644 --- a/ems-backend/src/main/java/com/dne/ems/exception/InvalidOperationException.java +++ b/ems-backend/src/main/java/com/dne/ems/exception/InvalidOperationException.java @@ -1,16 +1,16 @@ -package com.dne.ems.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -/** - * Custom exception for cases where an operation is invalid given the current state of the application or resource. - * When thrown, this will result in an HTTP 400 Bad Request response. - */ -@ResponseStatus(HttpStatus.BAD_REQUEST) -public class InvalidOperationException extends RuntimeException { - - public InvalidOperationException(String message) { - super(message); - } +package com.dne.ems.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Custom exception for cases where an operation is invalid given the current state of the application or resource. + * When thrown, this will result in an HTTP 400 Bad Request response. + */ +@ResponseStatus(HttpStatus.BAD_REQUEST) +public class InvalidOperationException extends RuntimeException { + + public InvalidOperationException(String message) { + super(message); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/exception/ResourceNotFoundException.java b/ems-backend/src/main/java/com/dne/ems/exception/ResourceNotFoundException.java index 73f83d3..d401d72 100644 --- a/ems-backend/src/main/java/com/dne/ems/exception/ResourceNotFoundException.java +++ b/ems-backend/src/main/java/com/dne/ems/exception/ResourceNotFoundException.java @@ -1,16 +1,16 @@ -package com.dne.ems.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -/** - * Custom exception for cases where a requested resource cannot be found. - * When thrown, this will result in an HTTP 404 Not Found response. - */ -@ResponseStatus(HttpStatus.NOT_FOUND) -public class ResourceNotFoundException extends RuntimeException { - - public ResourceNotFoundException(String message) { - super(message); - } +package com.dne.ems.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Custom exception for cases where a requested resource cannot be found. + * When thrown, this will result in an HTTP 404 Not Found response. + */ +@ResponseStatus(HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/exception/UserAlreadyExistsException.java b/ems-backend/src/main/java/com/dne/ems/exception/UserAlreadyExistsException.java index a62e5f3..811e8aa 100644 --- a/ems-backend/src/main/java/com/dne/ems/exception/UserAlreadyExistsException.java +++ b/ems-backend/src/main/java/com/dne/ems/exception/UserAlreadyExistsException.java @@ -1,12 +1,12 @@ -package com.dne.ems.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.CONFLICT) // Returns HTTP 409 Conflict status -public class UserAlreadyExistsException extends RuntimeException { - - public UserAlreadyExistsException(String message) { - super(message); - } +package com.dne.ems.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.CONFLICT) // Returns HTTP 409 Conflict status +public class UserAlreadyExistsException extends RuntimeException { + + public UserAlreadyExistsException(String message) { + super(message); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/listener/AuthenticationFailureEventListener.java b/ems-backend/src/main/java/com/dne/ems/listener/AuthenticationFailureEventListener.java index ee29210..061e4e1 100644 --- a/ems-backend/src/main/java/com/dne/ems/listener/AuthenticationFailureEventListener.java +++ b/ems-backend/src/main/java/com/dne/ems/listener/AuthenticationFailureEventListener.java @@ -1,34 +1,34 @@ -package com.dne.ems.listener; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationListener; -import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; -import org.springframework.stereotype.Component; - -import com.dne.ems.service.LoginAttemptService; - -import jakarta.servlet.http.HttpServletRequest; - -@Component -public class AuthenticationFailureEventListener implements ApplicationListener { - - @Autowired - private LoginAttemptService loginAttemptService; - - @Autowired - private HttpServletRequest request; - - @Override - public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) { - final String xfHeader = request.getHeader("X-Forwarded-For"); - String ip; - if (xfHeader == null || xfHeader.isEmpty() || !xfHeader.contains(request.getRemoteAddr())) { - ip = request.getRemoteAddr(); - } else { - ip = xfHeader.split(",")[0]; - } - - // 使用IP地址作为锁定键 - loginAttemptService.loginFailed(ip); - } +package com.dne.ems.listener; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; +import org.springframework.stereotype.Component; + +import com.dne.ems.service.LoginAttemptService; + +import jakarta.servlet.http.HttpServletRequest; + +@Component +public class AuthenticationFailureEventListener implements ApplicationListener { + + @Autowired + private LoginAttemptService loginAttemptService; + + @Autowired + private HttpServletRequest request; + + @Override + public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) { + final String xfHeader = request.getHeader("X-Forwarded-For"); + String ip; + if (xfHeader == null || xfHeader.isEmpty() || !xfHeader.contains(request.getRemoteAddr())) { + ip = request.getRemoteAddr(); + } else { + ip = xfHeader.split(",")[0]; + } + + // 使用IP地址作为锁定键 + loginAttemptService.loginFailed(ip); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/listener/AuthenticationSuccessEventListener.java b/ems-backend/src/main/java/com/dne/ems/listener/AuthenticationSuccessEventListener.java index 63ffec2..56a420d 100644 --- a/ems-backend/src/main/java/com/dne/ems/listener/AuthenticationSuccessEventListener.java +++ b/ems-backend/src/main/java/com/dne/ems/listener/AuthenticationSuccessEventListener.java @@ -1,33 +1,33 @@ -package com.dne.ems.listener; - -import com.dne.ems.model.UserAccount; -import com.dne.ems.repository.UserAccountRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationListener; -import org.springframework.lang.NonNull; -import org.springframework.security.authentication.event.AuthenticationSuccessEvent; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; - -@Component -public class AuthenticationSuccessEventListener implements ApplicationListener { - - @Autowired - private UserAccountRepository userAccountRepository; - - @Override - public void onApplicationEvent(@NonNull AuthenticationSuccessEvent event) { - Object principal = event.getAuthentication().getPrincipal(); - - if (principal instanceof UserDetails userDetails) { - String username = userDetails.getUsername(); - UserAccount user = userAccountRepository.findByEmail(username).orElse(null); - - if (user != null && user.getFailedLoginAttempts() > 0) { - user.setFailedLoginAttempts(0); - user.setLockoutEndTime(null); - userAccountRepository.save(user); - } - } - } +package com.dne.ems.listener; + +import com.dne.ems.model.UserAccount; +import com.dne.ems.repository.UserAccountRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +@Component +public class AuthenticationSuccessEventListener implements ApplicationListener { + + @Autowired + private UserAccountRepository userAccountRepository; + + @Override + public void onApplicationEvent(@NonNull AuthenticationSuccessEvent event) { + Object principal = event.getAuthentication().getPrincipal(); + + if (principal instanceof UserDetails userDetails) { + String username = userDetails.getUsername(); + UserAccount user = userAccountRepository.findByEmail(username).orElse(null); + + if (user != null && user.getFailedLoginAttempts() > 0) { + user.setFailedLoginAttempts(0); + user.setLockoutEndTime(null); + userAccountRepository.save(user); + } + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/listener/FeedbackEventListener.java b/ems-backend/src/main/java/com/dne/ems/listener/FeedbackEventListener.java index ee58c30..248a590 100644 --- a/ems-backend/src/main/java/com/dne/ems/listener/FeedbackEventListener.java +++ b/ems-backend/src/main/java/com/dne/ems/listener/FeedbackEventListener.java @@ -1,26 +1,26 @@ -package com.dne.ems.listener; - -import org.springframework.context.event.EventListener; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; - -import com.dne.ems.event.FeedbackSubmittedForAiReviewEvent; -import com.dne.ems.service.AiReviewService; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Component -@Slf4j -@RequiredArgsConstructor -public class FeedbackEventListener { - - private final AiReviewService aiReviewService; - - @Async - @EventListener - public void handleFeedbackSubmittedForAiReview(FeedbackSubmittedForAiReviewEvent event) { - log.info("Received feedback submission event for AI review. Feedback ID: {}", event.getFeedback().getId()); - aiReviewService.reviewFeedback(event.getFeedback()); - } +package com.dne.ems.listener; + +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import com.dne.ems.event.FeedbackSubmittedForAiReviewEvent; +import com.dne.ems.service.AiReviewService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +@RequiredArgsConstructor +public class FeedbackEventListener { + + private final AiReviewService aiReviewService; + + @Async + @EventListener + public void handleFeedbackSubmittedForAiReview(FeedbackSubmittedForAiReviewEvent event) { + log.info("Received feedback submission event for AI review. Feedback ID: {}", event.getFeedback().getId()); + aiReviewService.reviewFeedback(event.getFeedback()); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/AqiData.java b/ems-backend/src/main/java/com/dne/ems/model/AqiData.java index 66d1746..1282940 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/AqiData.java +++ b/ems-backend/src/main/java/com/dne/ems/model/AqiData.java @@ -1,112 +1,112 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.Data; - -/** - * 空气质量指数(AQI)数据实体 - * - *

表示网格工作人员提交的空气质量数据,映射到'aqi_data'表, - * 存储特定网格位置收集的详细污染物信息。 - * - *

主要字段: - *

    - *
  • aqiValue - 空气质量指数值
  • - *
  • pm25/pm10 - 颗粒物浓度
  • - *
  • so2/no2/co/o3 - 气体污染物浓度
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2025-06-16 - */ -@Entity -@Table(name = "aqi_data") -@Data -public class AqiData { - - /** - * 主键ID,AQI数据记录的唯一标识 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * 关联的反馈ID - * - *

可为null,表示工作人员主动提交的数据而非响应反馈任务 - */ - private Long feedbackId; - - /** - * The grid cell (location) where this data was recorded. - * This field is mandatory. - */ - @ManyToOne - @JoinColumn(name = "grid_id", nullable = false) - private Grid grid; - - /** - * The ID of the grid worker who reported this data. - * This field is mandatory. - */ - @Column(nullable = false) - private Long reporterId; - - /** - * The overall Air Quality Index (AQI) value. - */ - private Integer aqiValue; - - /** - * The concentration of PM2.5 (fine particulate matter) in µg/m³. - */ - private Double pm25; - - /** - * The concentration of PM10 (inhalable particulate matter) in µg/m³. - */ - private Double pm10; - - /** - * The concentration of SO2 (Sulfur Dioxide) in µg/m³. - */ - private Double so2; - - /** - * The concentration of NO2 (Nitrogen Dioxide) in µg/m³. - */ - private Double no2; - - /** - * The concentration of CO (Carbon Monoxide) in mg/m³. - */ - private Double co; - - /** - * The concentration of O3 (Ozone) in µg/m³. - */ - private Double o3; - - /** - * The name of the primary pollutant contributing to the AQI value. - */ - private String primaryPollutant; - - /** - * The timestamp when this data was recorded. - * Defaults to the current time upon creation. - */ - @Column(nullable = false) - private LocalDateTime recordTime = LocalDateTime.now(); +package com.dne.ems.model; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * 空气质量指数(AQI)数据实体 + * + *

表示网格工作人员提交的空气质量数据,映射到'aqi_data'表, + * 存储特定网格位置收集的详细污染物信息。 + * + *

主要字段: + *

    + *
  • aqiValue - 空气质量指数值
  • + *
  • pm25/pm10 - 颗粒物浓度
  • + *
  • so2/no2/co/o3 - 气体污染物浓度
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2025-06-16 + */ +@Entity +@Table(name = "aqi_data") +@Data +public class AqiData { + + /** + * 主键ID,AQI数据记录的唯一标识 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 关联的反馈ID + * + *

可为null,表示工作人员主动提交的数据而非响应反馈任务 + */ + private Long feedbackId; + + /** + * The grid cell (location) where this data was recorded. + * This field is mandatory. + */ + @ManyToOne + @JoinColumn(name = "grid_id", nullable = false) + private Grid grid; + + /** + * The ID of the grid worker who reported this data. + * This field is mandatory. + */ + @Column(nullable = false) + private Long reporterId; + + /** + * The overall Air Quality Index (AQI) value. + */ + private Integer aqiValue; + + /** + * The concentration of PM2.5 (fine particulate matter) in µg/m³. + */ + private Double pm25; + + /** + * The concentration of PM10 (inhalable particulate matter) in µg/m³. + */ + private Double pm10; + + /** + * The concentration of SO2 (Sulfur Dioxide) in µg/m³. + */ + private Double so2; + + /** + * The concentration of NO2 (Nitrogen Dioxide) in µg/m³. + */ + private Double no2; + + /** + * The concentration of CO (Carbon Monoxide) in mg/m³. + */ + private Double co; + + /** + * The concentration of O3 (Ozone) in µg/m³. + */ + private Double o3; + + /** + * The name of the primary pollutant contributing to the AQI value. + */ + private String primaryPollutant; + + /** + * The timestamp when this data was recorded. + * Defaults to the current time upon creation. + */ + @Column(nullable = false) + private LocalDateTime recordTime = LocalDateTime.now(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/AqiRecord.java b/ems-backend/src/main/java/com/dne/ems/model/AqiRecord.java index 5ad35c2..feba776 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/AqiRecord.java +++ b/ems-backend/src/main/java/com/dne/ems/model/AqiRecord.java @@ -1,166 +1,166 @@ -package com.dne.ems.model; - - -import java.time.LocalDateTime; - -import org.hibernate.annotations.CreationTimestamp; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Represents a historical record of Air Quality Index (AQI) for a specific location. - * This entity is typically populated from external data sources or aggregated from {@link AqiData}. - * It is used for trend analysis and heatmap generation. - * Maps to the 'aqi_records' table. - * - * @version 1.0 - * @since 2025-06-16 - */ -@Data -@Entity -@Table(name = "aqi_records") -@NoArgsConstructor -public class AqiRecord { - - /** - * The unique identifier for the AQI record. - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * The name of the city where the AQI was recorded. - */ - @Column(nullable = false) - private String cityName; - - /** - * The overall Air Quality Index (AQI) value. - */ - @Column(nullable = false) - private Integer aqiValue; - - /** - * The timestamp when the record was created in the database. - * This is automatically set upon creation and cannot be updated. - */ - @CreationTimestamp - @Column(nullable = false, updatable = false) - private LocalDateTime recordTime; - - /** - * The concentration of PM2.5 in µg/m³. - */ - private Double pm25; - /** - * The concentration of PM10 in µg/m³. - */ - private Double pm10; - /** - * The concentration of SO2 in µg/m³. - */ - private Double so2; - /** - * The concentration of NO2 in µg/m³. - */ - private Double no2; - /** - * The concentration of CO in mg/m³. - */ - private Double co; - /** - * The concentration of O3 in µg/m³. - */ - private Double o3; - - /** - * The geographical latitude of the record. - */ - private Double latitude; - /** - * The geographical longitude of the record. - */ - private Double longitude; - - /** - * The x-coordinate of the grid cell corresponding to the record's location. - */ - @Column(name = "grid_x") - private Integer gridX; - - /** - * The y-coordinate of the grid cell corresponding to the record's location. - */ - @Column(name = "grid_y") - private Integer gridY; - - /** - * The associated grid cell for this AQI record. - */ - @ManyToOne - @JoinColumn(name = "grid_id") - private Grid grid; - - /** - * Basic constructor for creating an AQI record with pollutant data. - * Used primarily by the {@link com.dne.ems.config.DataInitializer}. - * - * @param cityName The city name. - * @param aqiValue The AQI value. - * @param recordTime The time of the record. - * @param pm25 PM2.5 concentration. - * @param pm10 PM10 concentration. - * @param so2 SO2 concentration. - * @param no2 NO2 concentration. - * @param co CO concentration. - * @param o3 O3 concentration. - */ - public AqiRecord(String cityName, Integer aqiValue, LocalDateTime recordTime, Double pm25, Double pm10, Double so2, Double no2, Double co, Double o3) { - this.cityName = cityName; - this.aqiValue = aqiValue; - this.recordTime = recordTime; - this.pm25 = pm25; - this.pm10 = pm10; - this.so2 = so2; - this.no2 = no2; - this.co = co; - this.o3 = o3; - } - - /** - * Extended constructor that includes geographical and grid information. - * - * @param cityName The city name. - * @param aqiValue The AQI value. - * @param recordTime The time of the record. - * @param pm25 PM2.5 concentration. - * @param pm10 PM10 concentration. - * @param so2 SO2 concentration. - * @param no2 NO2 concentration. - * @param co CO concentration. - * @param o3 O3 concentration. - * @param latitude Geographical latitude. - * @param longitude Geographical longitude. - * @param gridX Grid x-coordinate. - * @param gridY Grid y-coordinate. - * @param grid The associated Grid entity. - */ - public AqiRecord(String cityName, Integer aqiValue, LocalDateTime recordTime, Double pm25, Double pm10, Double so2, Double no2, Double co, Double o3, Double latitude, Double longitude, Integer gridX, Integer gridY, Grid grid) { - this(cityName, aqiValue, recordTime, pm25, pm10, so2, no2, co, o3); - this.latitude = latitude; - this.longitude = longitude; - this.gridX = gridX; - this.gridY = gridY; - this.grid = grid; - } +package com.dne.ems.model; + + +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a historical record of Air Quality Index (AQI) for a specific location. + * This entity is typically populated from external data sources or aggregated from {@link AqiData}. + * It is used for trend analysis and heatmap generation. + * Maps to the 'aqi_records' table. + * + * @version 1.0 + * @since 2025-06-16 + */ +@Data +@Entity +@Table(name = "aqi_records") +@NoArgsConstructor +public class AqiRecord { + + /** + * The unique identifier for the AQI record. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The name of the city where the AQI was recorded. + */ + @Column(nullable = false) + private String cityName; + + /** + * The overall Air Quality Index (AQI) value. + */ + @Column(nullable = false) + private Integer aqiValue; + + /** + * The timestamp when the record was created in the database. + * This is automatically set upon creation and cannot be updated. + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime recordTime; + + /** + * The concentration of PM2.5 in µg/m³. + */ + private Double pm25; + /** + * The concentration of PM10 in µg/m³. + */ + private Double pm10; + /** + * The concentration of SO2 in µg/m³. + */ + private Double so2; + /** + * The concentration of NO2 in µg/m³. + */ + private Double no2; + /** + * The concentration of CO in mg/m³. + */ + private Double co; + /** + * The concentration of O3 in µg/m³. + */ + private Double o3; + + /** + * The geographical latitude of the record. + */ + private Double latitude; + /** + * The geographical longitude of the record. + */ + private Double longitude; + + /** + * The x-coordinate of the grid cell corresponding to the record's location. + */ + @Column(name = "grid_x") + private Integer gridX; + + /** + * The y-coordinate of the grid cell corresponding to the record's location. + */ + @Column(name = "grid_y") + private Integer gridY; + + /** + * The associated grid cell for this AQI record. + */ + @ManyToOne + @JoinColumn(name = "grid_id") + private Grid grid; + + /** + * Basic constructor for creating an AQI record with pollutant data. + * Used primarily by the {@link com.dne.ems.config.DataInitializer}. + * + * @param cityName The city name. + * @param aqiValue The AQI value. + * @param recordTime The time of the record. + * @param pm25 PM2.5 concentration. + * @param pm10 PM10 concentration. + * @param so2 SO2 concentration. + * @param no2 NO2 concentration. + * @param co CO concentration. + * @param o3 O3 concentration. + */ + public AqiRecord(String cityName, Integer aqiValue, LocalDateTime recordTime, Double pm25, Double pm10, Double so2, Double no2, Double co, Double o3) { + this.cityName = cityName; + this.aqiValue = aqiValue; + this.recordTime = recordTime; + this.pm25 = pm25; + this.pm10 = pm10; + this.so2 = so2; + this.no2 = no2; + this.co = co; + this.o3 = o3; + } + + /** + * Extended constructor that includes geographical and grid information. + * + * @param cityName The city name. + * @param aqiValue The AQI value. + * @param recordTime The time of the record. + * @param pm25 PM2.5 concentration. + * @param pm10 PM10 concentration. + * @param so2 SO2 concentration. + * @param no2 NO2 concentration. + * @param co CO concentration. + * @param o3 O3 concentration. + * @param latitude Geographical latitude. + * @param longitude Geographical longitude. + * @param gridX Grid x-coordinate. + * @param gridY Grid y-coordinate. + * @param grid The associated Grid entity. + */ + public AqiRecord(String cityName, Integer aqiValue, LocalDateTime recordTime, Double pm25, Double pm10, Double so2, Double no2, Double co, Double o3, Double latitude, Double longitude, Integer gridX, Integer gridY, Grid grid) { + this(cityName, aqiValue, recordTime, pm25, pm10, so2, no2, co, o3); + this.latitude = latitude; + this.longitude = longitude; + this.gridX = gridX; + this.gridY = gridY; + this.grid = grid; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/Assignment.java b/ems-backend/src/main/java/com/dne/ems/model/Assignment.java index 30dc9c5..6d79cb1 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/Assignment.java +++ b/ems-backend/src/main/java/com/dne/ems/model/Assignment.java @@ -1,82 +1,82 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; - -import com.dne.ems.model.enums.AssignmentStatus; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; -import lombok.Data; - -/** - * Represents the assignment of a feedback-derived task to a grid worker. - * This entity links a piece of feedback to the worker responsible for handling it. - * It tracks the lifecycle of the assignment from creation to completion. - * Maps to the 'assignments' table. - * - * @version 1.0 - * @since 2025-06-16 - */ -@Entity -@Table(name = "assignments") -@Data -public class Assignment { - - /** - * The unique identifier for the assignment. - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * The task associated with this assignment. - * An assignment is always linked to one specific task. - */ - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "task_id", referencedColumnName = "id", nullable = false) - private Task task; - - /** - * The user (admin or supervisor) who created the assignment. - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "assigner_id", referencedColumnName = "id", nullable = false) - private UserAccount assigner; - - /** - * The timestamp when the assignment was created. - * This is automatically set to the current time and is not updatable. - */ - @Column(nullable = false, updatable = false) - private LocalDateTime assignmentTime = LocalDateTime.now(); - - /** - * The deadline by which the task should be completed. - * Can be null if no deadline is set. - */ - private LocalDateTime deadline; - - /** - * The current status of the assignment (e.g., PENDING, IN_PROGRESS, COMPLETED). - * @see AssignmentStatus - */ - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private AssignmentStatus status; - - /** - * Any notes or remarks from the assigner regarding the task. - */ - private String remarks; +package com.dne.ems.model; + +import java.time.LocalDateTime; + +import com.dne.ems.model.enums.AssignmentStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * Represents the assignment of a feedback-derived task to a grid worker. + * This entity links a piece of feedback to the worker responsible for handling it. + * It tracks the lifecycle of the assignment from creation to completion. + * Maps to the 'assignments' table. + * + * @version 1.0 + * @since 2025-06-16 + */ +@Entity +@Table(name = "assignments") +@Data +public class Assignment { + + /** + * The unique identifier for the assignment. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The task associated with this assignment. + * An assignment is always linked to one specific task. + */ + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "task_id", referencedColumnName = "id", nullable = false) + private Task task; + + /** + * The user (admin or supervisor) who created the assignment. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "assigner_id", referencedColumnName = "id", nullable = false) + private UserAccount assigner; + + /** + * The timestamp when the assignment was created. + * This is automatically set to the current time and is not updatable. + */ + @Column(nullable = false, updatable = false) + private LocalDateTime assignmentTime = LocalDateTime.now(); + + /** + * The deadline by which the task should be completed. + * Can be null if no deadline is set. + */ + private LocalDateTime deadline; + + /** + * The current status of the assignment (e.g., PENDING, IN_PROGRESS, COMPLETED). + * @see AssignmentStatus + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private AssignmentStatus status; + + /** + * Any notes or remarks from the assigner regarding the task. + */ + private String remarks; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/AssignmentRecord.java b/ems-backend/src/main/java/com/dne/ems/model/AssignmentRecord.java index 81f7de3..7573840 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/AssignmentRecord.java +++ b/ems-backend/src/main/java/com/dne/ems/model/AssignmentRecord.java @@ -1,100 +1,100 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; - -import org.hibernate.annotations.CreationTimestamp; - -import com.dne.ems.model.enums.AssignmentMethod; -import com.dne.ems.model.enums.AssignmentStatus; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.Lob; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.Data; - -/** - * Represents a detailed record of a feedback assignment to a grid worker. - * This entity provides a rich, relational view of an assignment, linking directly - * to the Feedback, Grid Worker, and Admin entities. It also captures metadata - * about how the assignment was made. - * Maps to the 'assignment_record' table. - * - * @version 1.0 - * @since 2025-06-16 - */ -@Data -@Entity -@Table(name = "assignment_record") -public class AssignmentRecord { - - /** - * The unique identifier for the assignment record. - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * The {@link Feedback} entity that is being assigned. - * This uses a lazy fetch type for performance optimization. - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "feedbackId", nullable = false) - private Feedback feedback; - - /** - * The {@link UserAccount} (grid worker) to whom the task is assigned. - * This uses a lazy fetch type for performance optimization. - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "gridWorkerId", nullable = false) - private UserAccount gridWorker; - - /** - * The {@link UserAccount} (administrator) who made the assignment. - * This uses a lazy fetch type for performance optimization. - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "adminId", nullable = false) - private UserAccount admin; - - /** - * The method used for the assignment (e.g., MANUAL, INTELLIGENT). - * @see AssignmentMethod - */ - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 50) - private AssignmentMethod assignmentMethod; - - /** - * A JSON snapshot of the details from the intelligent assignment algorithm, if applicable. - * Stored as a large object (TEXT/CLOB) in the database. - */ - @Lob - private String algorithmDetails; - - /** - * The current status of the assignment (e.g., PENDING, COMPLETED). - * @see AssignmentStatus - */ - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 50) - private AssignmentStatus status; - - /** - * The timestamp when the assignment record was created. - * This is automatically set by Hibernate upon creation and is not updatable. - */ - @CreationTimestamp - @Column(nullable = false, updatable = false) - private LocalDateTime createdAt; +package com.dne.ems.model; + +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +import com.dne.ems.model.enums.AssignmentMethod; +import com.dne.ems.model.enums.AssignmentStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * Represents a detailed record of a feedback assignment to a grid worker. + * This entity provides a rich, relational view of an assignment, linking directly + * to the Feedback, Grid Worker, and Admin entities. It also captures metadata + * about how the assignment was made. + * Maps to the 'assignment_record' table. + * + * @version 1.0 + * @since 2025-06-16 + */ +@Data +@Entity +@Table(name = "assignment_record") +public class AssignmentRecord { + + /** + * The unique identifier for the assignment record. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The {@link Feedback} entity that is being assigned. + * This uses a lazy fetch type for performance optimization. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "feedbackId", nullable = false) + private Feedback feedback; + + /** + * The {@link UserAccount} (grid worker) to whom the task is assigned. + * This uses a lazy fetch type for performance optimization. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "gridWorkerId", nullable = false) + private UserAccount gridWorker; + + /** + * The {@link UserAccount} (administrator) who made the assignment. + * This uses a lazy fetch type for performance optimization. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "adminId", nullable = false) + private UserAccount admin; + + /** + * The method used for the assignment (e.g., MANUAL, INTELLIGENT). + * @see AssignmentMethod + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 50) + private AssignmentMethod assignmentMethod; + + /** + * A JSON snapshot of the details from the intelligent assignment algorithm, if applicable. + * Stored as a large object (TEXT/CLOB) in the database. + */ + @Lob + private String algorithmDetails; + + /** + * The current status of the assignment (e.g., PENDING, COMPLETED). + * @see AssignmentStatus + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 50) + private AssignmentStatus status; + + /** + * The timestamp when the assignment record was created. + * This is automatically set by Hibernate upon creation and is not updatable. + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/Attachment.java b/ems-backend/src/main/java/com/dne/ems/model/Attachment.java index 111a814..16910a0 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/Attachment.java +++ b/ems-backend/src/main/java/com/dne/ems/model/Attachment.java @@ -1,92 +1,92 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; - -import org.hibernate.annotations.CreationTimestamp; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Represents a file attachment, typically associated with a {@link Feedback} submission. - * This entity stores metadata about an uploaded file, such as its name, type, size, - * and how it is stored on the server. - * Maps to the 'attachment' table. - * - * @version 1.0 - * @since 2025-06-16 - */ -@Entity -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Attachment { - - /** - * The unique identifier for the attachment. - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * The original name of the file as provided by the user (e.g., "photo1.jpg"). - */ - @Column(nullable = false) - private String fileName; - - /** - * The MIME type of the file (e.g., "image/jpeg", "application/pdf"). - */ - @Column(nullable = false) - private String fileType; - - /** - * A unique name generated for storing the file on the server to prevent name conflicts. - * This is the name used to retrieve the file. - */ - @Column(nullable = false, unique = true) - private String storedFileName; - - /** - * The size of the file in bytes. - */ - @Column(nullable = false) - private Long fileSize; - - /** - * The timestamp when the file was uploaded. - * This is automatically set upon creation and is not updatable. - */ - @CreationTimestamp - @Column(nullable = false, updatable = false) - private LocalDateTime uploadDate; - - /** - * The {@link Feedback} submission this attachment is associated with. - * This creates a many-to-one relationship, allowing a feedback to have multiple attachments. - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "feedback_id") - private Feedback feedback; - - /** - * The {@link TaskSubmission} this attachment is associated with. - * This creates a many-to-one relationship, allowing a task submission to have multiple attachments. - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "task_submission_id") - private TaskSubmission taskSubmission; - +package com.dne.ems.model; + +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a file attachment, typically associated with a {@link Feedback} submission. + * This entity stores metadata about an uploaded file, such as its name, type, size, + * and how it is stored on the server. + * Maps to the 'attachment' table. + * + * @version 1.0 + * @since 2025-06-16 + */ +@Entity +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Attachment { + + /** + * The unique identifier for the attachment. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The original name of the file as provided by the user (e.g., "photo1.jpg"). + */ + @Column(nullable = false) + private String fileName; + + /** + * The MIME type of the file (e.g., "image/jpeg", "application/pdf"). + */ + @Column(nullable = false) + private String fileType; + + /** + * A unique name generated for storing the file on the server to prevent name conflicts. + * This is the name used to retrieve the file. + */ + @Column(nullable = false, unique = true) + private String storedFileName; + + /** + * The size of the file in bytes. + */ + @Column(nullable = false) + private Long fileSize; + + /** + * The timestamp when the file was uploaded. + * This is automatically set upon creation and is not updatable. + */ + @CreationTimestamp + @Column(nullable = false, updatable = false) + private LocalDateTime uploadDate; + + /** + * The {@link Feedback} submission this attachment is associated with. + * This creates a many-to-one relationship, allowing a feedback to have multiple attachments. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "feedback_id") + private Feedback feedback; + + /** + * The {@link TaskSubmission} this attachment is associated with. + * This creates a many-to-one relationship, allowing a task submission to have multiple attachments. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "task_submission_id") + private TaskSubmission taskSubmission; + } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/Feedback.java b/ems-backend/src/main/java/com/dne/ems/model/Feedback.java index c062c2f..3b6475b 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/Feedback.java +++ b/ems-backend/src/main/java/com/dne/ems/model/Feedback.java @@ -1,186 +1,186 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.Lob; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 环境问题反馈实体 - * - *

系统核心实体,记录用户提交的环境问题报告的所有细节, - * 包括位置、严重程度和处理状态等信息,映射到'feedback'表。 - * - *

主要功能: - *

    - *
  • 记录环境问题详情
  • - *
  • 跟踪处理状态
  • - *
  • 关联附件和任务
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2025-06-16 - */ -@Entity -@Table(name = "feedback") -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Feedback { - - /** - * 主键ID,反馈记录的唯一标识 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * 事件ID,人类可读的唯一标识符 - * - *

用于外部引用和用户沟通 - */ - @Column(nullable = false, unique = true) - private String eventId; - - /** - * A brief, descriptive title for the feedback. - */ - @Column(nullable = false) - private String title; - - /** - * A detailed description of the environmental issue. - * Stored as a large object (TEXT/CLOB) to accommodate long text. - */ - @Lob // For longer text - private String description; - - /** - * The type of pollution being reported. - * @see PollutionType - */ - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private PollutionType pollutionType; - - /** - * The perceived severity level of the issue. - * @see SeverityLevel - */ - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private SeverityLevel severityLevel; - - /** - * The current status of the feedback in the processing workflow. - * @see FeedbackStatus - */ - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private FeedbackStatus status; - - /** - * The text-based address or location description of the event. - */ - private String textAddress; - - /** - * The x-coordinate of the grid cell where the event occurred. - */ - private Integer gridX; - - /** - * The y-coordinate of the grid cell where the event occurred. - */ - private Integer gridY; - - /** - * The ID of the user who submitted the feedback. - */ - private Long submitterId; - - /** - * The timestamp when the feedback was created. - * Automatically set and not updatable. - */ - @Builder.Default - @Column(nullable = false, updatable = false) - private LocalDateTime createdAt = LocalDateTime.now(); - - /** - * The timestamp of the last update to the feedback. - * Automatically set on creation and updated via {@link #onPreUpdate()}. - */ - @Builder.Default - @Column(nullable = false) - private LocalDateTime updatedAt = LocalDateTime.now(); - - /** - * The {@link UserAccount} who submitted the feedback. - * This provides a direct link to the user entity. - */ - @ManyToOne - @JoinColumn(name = "user_id") - private UserAccount user; - - /** - * The geographical latitude where the feedback was reported. Essential for location-based services. - */ - private Double latitude; - - /** - * The geographical longitude where the feedback was reported. Essential for location-based services. - */ - private Double longitude; - - /** - * A list of {@link Attachment} files associated with this feedback. - * Configured to cascade all operations and remove orphan attachments. - */ - @Builder.Default - @OneToMany(mappedBy = "feedback", cascade = CascadeType.ALL, orphanRemoval = true) - private List attachments = new ArrayList<>(); - - /** - * The task associated with this feedback. - * Establishes a bidirectional one-to-one relationship. - * The 'feedback' field in the Task entity owns this relationship. - */ - @OneToOne(mappedBy = "feedback", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private Task task; - - /** - * JPA callback method to automatically update the {@code updatedAt} timestamp before an entity update. - */ - @PreUpdate - public void onPreUpdate() { - this.updatedAt = LocalDateTime.now(); - } +package com.dne.ems.model; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 环境问题反馈实体 + * + *

系统核心实体,记录用户提交的环境问题报告的所有细节, + * 包括位置、严重程度和处理状态等信息,映射到'feedback'表。 + * + *

主要功能: + *

    + *
  • 记录环境问题详情
  • + *
  • 跟踪处理状态
  • + *
  • 关联附件和任务
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2025-06-16 + */ +@Entity +@Table(name = "feedback") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Feedback { + + /** + * 主键ID,反馈记录的唯一标识 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 事件ID,人类可读的唯一标识符 + * + *

用于外部引用和用户沟通 + */ + @Column(nullable = false, unique = true) + private String eventId; + + /** + * A brief, descriptive title for the feedback. + */ + @Column(nullable = false) + private String title; + + /** + * A detailed description of the environmental issue. + * Stored as a large object (TEXT/CLOB) to accommodate long text. + */ + @Lob // For longer text + private String description; + + /** + * The type of pollution being reported. + * @see PollutionType + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PollutionType pollutionType; + + /** + * The perceived severity level of the issue. + * @see SeverityLevel + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private SeverityLevel severityLevel; + + /** + * The current status of the feedback in the processing workflow. + * @see FeedbackStatus + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private FeedbackStatus status; + + /** + * The text-based address or location description of the event. + */ + private String textAddress; + + /** + * The x-coordinate of the grid cell where the event occurred. + */ + private Integer gridX; + + /** + * The y-coordinate of the grid cell where the event occurred. + */ + private Integer gridY; + + /** + * The ID of the user who submitted the feedback. + */ + private Long submitterId; + + /** + * The timestamp when the feedback was created. + * Automatically set and not updatable. + */ + @Builder.Default + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + /** + * The timestamp of the last update to the feedback. + * Automatically set on creation and updated via {@link #onPreUpdate()}. + */ + @Builder.Default + @Column(nullable = false) + private LocalDateTime updatedAt = LocalDateTime.now(); + + /** + * The {@link UserAccount} who submitted the feedback. + * This provides a direct link to the user entity. + */ + @ManyToOne + @JoinColumn(name = "user_id") + private UserAccount user; + + /** + * The geographical latitude where the feedback was reported. Essential for location-based services. + */ + private Double latitude; + + /** + * The geographical longitude where the feedback was reported. Essential for location-based services. + */ + private Double longitude; + + /** + * A list of {@link Attachment} files associated with this feedback. + * Configured to cascade all operations and remove orphan attachments. + */ + @Builder.Default + @OneToMany(mappedBy = "feedback", cascade = CascadeType.ALL, orphanRemoval = true) + private List attachments = new ArrayList<>(); + + /** + * The task associated with this feedback. + * Establishes a bidirectional one-to-one relationship. + * The 'feedback' field in the Task entity owns this relationship. + */ + @OneToOne(mappedBy = "feedback", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Task task; + + /** + * JPA callback method to automatically update the {@code updatedAt} timestamp before an entity update. + */ + @PreUpdate + public void onPreUpdate() { + this.updatedAt = LocalDateTime.now(); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/Grid.java b/ems-backend/src/main/java/com/dne/ems/model/Grid.java index 3167c51..cb834bf 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/Grid.java +++ b/ems-backend/src/main/java/com/dne/ems/model/Grid.java @@ -1,68 +1,68 @@ -package com.dne.ems.model; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.Data; - -/** - * Represents a geographical grid cell used for regional management and data aggregation. - * Each grid cell can be associated with a city and district and can be marked as an obstacle. - * Maps to the 'grid' table. - * - * @version 1.0 - * @since 2025-06-16 - */ -@Entity -@Table(name = "grid") -@Data -public class Grid { - - /** - * The unique identifier for the grid cell. - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * The X-coordinate of the grid. - */ - @Column(name = "gridx") - private Integer gridX; - - /** - * The Y-coordinate of the grid. - */ - @Column(name = "gridy") - private Integer gridY; - - /** - * The name of the city this grid cell belongs to. - */ - @Column(name = "city_name") - private String cityName; - - /** - * The name of the district this grid cell belongs to. - */ - @Column(name = "district_name") - private String districtName; - - /** - * A textual description of the grid cell, which can include notes or details. - * Stored as a large object (TEXT/CLOB). - */ - @Column(columnDefinition = "TEXT") - private String description; - - /** - * A flag indicating if this grid cell is an obstacle (e.g., a restricted area). - * Defaults to false. - */ - @Column(name = "is_obstacle") - private Boolean isObstacle = false; +package com.dne.ems.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * Represents a geographical grid cell used for regional management and data aggregation. + * Each grid cell can be associated with a city and district and can be marked as an obstacle. + * Maps to the 'grid' table. + * + * @version 1.0 + * @since 2025-06-16 + */ +@Entity +@Table(name = "grid") +@Data +public class Grid { + + /** + * The unique identifier for the grid cell. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The X-coordinate of the grid. + */ + @Column(name = "gridx") + private Integer gridX; + + /** + * The Y-coordinate of the grid. + */ + @Column(name = "gridy") + private Integer gridY; + + /** + * The name of the city this grid cell belongs to. + */ + @Column(name = "city_name") + private String cityName; + + /** + * The name of the district this grid cell belongs to. + */ + @Column(name = "district_name") + private String districtName; + + /** + * A textual description of the grid cell, which can include notes or details. + * Stored as a large object (TEXT/CLOB). + */ + @Column(columnDefinition = "TEXT") + private String description; + + /** + * A flag indicating if this grid cell is an obstacle (e.g., a restricted area). + * Defaults to false. + */ + @Column(name = "is_obstacle") + private Boolean isObstacle = false; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/MapGrid.java b/ems-backend/src/main/java/com/dne/ems/model/MapGrid.java index 6120469..aaa13ae 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/MapGrid.java +++ b/ems-backend/src/main/java/com/dne/ems/model/MapGrid.java @@ -1,66 +1,66 @@ -package com.dne.ems.model; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Represents a single cell in the grid-based map, specifically for A* pathfinding. - * Each cell has coordinates and can be marked as an obstacle. A unique constraint - * on (x, y) coordinates ensures that each position is represented only once. - * Maps to the 'map_grid' table. - * - * @version 1.0 - * @since 2025-06-16 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Entity -@Table(name = "map_grid", uniqueConstraints = { - @UniqueConstraint(columnNames = {"x", "y"}) -}) -public class MapGrid { - - /** - * The unique identifier for the map grid cell. - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * The x-coordinate of the grid cell. - */ - private int x; - - /** - * The y-coordinate of the grid cell. - */ - private int y; - - /** - * The name of the city this grid cell belongs to. Used to constrain pathfinding. - */ - private String cityName; - - /** - * A flag indicating if this grid cell is an obstacle (e.g., a wall, a river). - * The A* pathfinding algorithm cannot traverse through cells marked as obstacles. - */ - private boolean isObstacle; - - /** - * Optional: Describes the type of terrain (e.g., "road", "grass", "water"). - * This could be used in more advanced pathfinding algorithms to influence movement cost. - */ - private String terrainType; +package com.dne.ems.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a single cell in the grid-based map, specifically for A* pathfinding. + * Each cell has coordinates and can be marked as an obstacle. A unique constraint + * on (x, y) coordinates ensures that each position is represented only once. + * Maps to the 'map_grid' table. + * + * @version 1.0 + * @since 2025-06-16 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "map_grid", uniqueConstraints = { + @UniqueConstraint(columnNames = {"x", "y"}) +}) +public class MapGrid { + + /** + * The unique identifier for the map grid cell. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The x-coordinate of the grid cell. + */ + private int x; + + /** + * The y-coordinate of the grid cell. + */ + private int y; + + /** + * The name of the city this grid cell belongs to. Used to constrain pathfinding. + */ + private String cityName; + + /** + * A flag indicating if this grid cell is an obstacle (e.g., a wall, a river). + * The A* pathfinding algorithm cannot traverse through cells marked as obstacles. + */ + private boolean isObstacle; + + /** + * Optional: Describes the type of terrain (e.g., "road", "grass", "water"). + * This could be used in more advanced pathfinding algorithms to influence movement cost. + */ + private String terrainType; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/OperationLog.java b/ems-backend/src/main/java/com/dne/ems/model/OperationLog.java index 66729bf..735e28d 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/OperationLog.java +++ b/ems-backend/src/main/java/com/dne/ems/model/OperationLog.java @@ -1,113 +1,113 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; - -import org.hibernate.annotations.CreationTimestamp; - -import com.dne.ems.model.enums.OperationType; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 操作日志实体类,用于记录用户的所有操作(不包括查看行为,但包括登录行为)。 - * 每当用户执行重要操作时,会创建一条操作日志记录。 - * 映射到'operation_logs'表。 - * - * @version 1.0 - * @since 2025-06-20 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Entity -@Table(name = "operation_logs") -public class OperationLog { - - /** - * 操作日志记录的唯一标识符 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * 执行操作的用户 - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private UserAccount user; - - /** - * 操作类型 - */ - @Enumerated(EnumType.STRING) - @Column(name = "operation_type", nullable = false) - private OperationType operationType; - - /** - * 操作描述 - */ - @Column(name = "description", length = 1000) - private String description; - - /** - * 操作对象ID(可选) - */ - @Column(name = "target_id") - private String targetId; - - /** - * 操作对象类型(可选) - */ - @Column(name = "target_type") - private String targetType; - - /** - * 客户端IP地址 - */ - @Column(name = "ip_address") - private String ipAddress; - - /** - * 操作发生的时间戳,由Hibernate自动管理 - */ - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - /** - * 构造一个新的操作日志记录 - * - * @param user 执行操作的用户 - * @param operationType 操作类型 - * @param description 操作描述 - * @param targetId 操作对象ID - * @param targetType 操作对象类型 - * @param ipAddress 客户端IP地址 - */ - public OperationLog(UserAccount user, OperationType operationType, String description, - String targetId, String targetType, String ipAddress) { - this.user = user; - this.operationType = operationType; - this.description = description; - this.targetId = targetId; - this.targetType = targetType; - this.ipAddress = ipAddress; - this.createdAt = LocalDateTime.now(); - } +package com.dne.ems.model; + +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +import com.dne.ems.model.enums.OperationType; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 操作日志实体类,用于记录用户的所有操作(不包括查看行为,但包括登录行为)。 + * 每当用户执行重要操作时,会创建一条操作日志记录。 + * 映射到'operation_logs'表。 + * + * @version 1.0 + * @since 2025-06-20 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Entity +@Table(name = "operation_logs") +public class OperationLog { + + /** + * 操作日志记录的唯一标识符 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 执行操作的用户 + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private UserAccount user; + + /** + * 操作类型 + */ + @Enumerated(EnumType.STRING) + @Column(name = "operation_type", nullable = false) + private OperationType operationType; + + /** + * 操作描述 + */ + @Column(name = "description", length = 1000) + private String description; + + /** + * 操作对象ID(可选) + */ + @Column(name = "target_id") + private String targetId; + + /** + * 操作对象类型(可选) + */ + @Column(name = "target_type") + private String targetType; + + /** + * 客户端IP地址 + */ + @Column(name = "ip_address") + private String ipAddress; + + /** + * 操作发生的时间戳,由Hibernate自动管理 + */ + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * 构造一个新的操作日志记录 + * + * @param user 执行操作的用户 + * @param operationType 操作类型 + * @param description 操作描述 + * @param targetId 操作对象ID + * @param targetType 操作对象类型 + * @param ipAddress 客户端IP地址 + */ + public OperationLog(UserAccount user, OperationType operationType, String description, + String targetId, String targetType, String ipAddress) { + this.user = user; + this.operationType = operationType; + this.description = description; + this.targetId = targetId; + this.targetType = targetType; + this.ipAddress = ipAddress; + this.createdAt = LocalDateTime.now(); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/PasswordResetToken.java b/ems-backend/src/main/java/com/dne/ems/model/PasswordResetToken.java index 8b1f686..9cfac91 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/PasswordResetToken.java +++ b/ems-backend/src/main/java/com/dne/ems/model/PasswordResetToken.java @@ -1,92 +1,92 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Represents a token for resetting user passwords. This token is generated and sent to the user, - * who can then use it to set a new password. The token has a limited lifespan and is associated - * with a specific user account. - * Maps to the 'password_reset_token' table. - * - * @version 1.0 - * @since 2025-06-16 - */ -@Entity -@Data -@NoArgsConstructor -public class PasswordResetToken { - - /** - * The duration in minutes for which the token is valid. - */ - private static final int EXPIRATION_MINUTES = 60; - - /** - * The unique identifier for the password reset token. - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * The unique token string. - */ - @Column(nullable = false, unique = true) - private String token; - - /** - * The user account associated with this token. A token can only be used to reset the - * password for the associated user. - */ - @OneToOne(targetEntity = UserAccount.class, fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = "user_account_id") - private UserAccount user; - - /** - * The date and time when this token will expire. - */ - @Column(nullable = false) - private LocalDateTime expiryDate; - - /** - * Constructs a new PasswordResetToken. - * - * @param token The token string. - * @param user The user account associated with the token. - */ - public PasswordResetToken(String token, UserAccount user) { - this.token = token; - this.user = user; - this.expiryDate = calculateExpiryDate(EXPIRATION_MINUTES); - } - - /** - * Calculates the expiry date for the token. - * - * @param expiryTimeInMinutes The number of minutes until the token expires. - * @return The calculated expiry date and time. - */ - private LocalDateTime calculateExpiryDate(int expiryTimeInMinutes) { - return LocalDateTime.now().plusMinutes(expiryTimeInMinutes); - } - - /** - * Checks if the token has expired. - * - * @return true if the token has expired, false otherwise. - */ - public boolean isExpired() { - return LocalDateTime.now().isAfter(this.expiryDate); - } +package com.dne.ems.model; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a token for resetting user passwords. This token is generated and sent to the user, + * who can then use it to set a new password. The token has a limited lifespan and is associated + * with a specific user account. + * Maps to the 'password_reset_token' table. + * + * @version 1.0 + * @since 2025-06-16 + */ +@Entity +@Data +@NoArgsConstructor +public class PasswordResetToken { + + /** + * The duration in minutes for which the token is valid. + */ + private static final int EXPIRATION_MINUTES = 60; + + /** + * The unique identifier for the password reset token. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The unique token string. + */ + @Column(nullable = false, unique = true) + private String token; + + /** + * The user account associated with this token. A token can only be used to reset the + * password for the associated user. + */ + @OneToOne(targetEntity = UserAccount.class, fetch = FetchType.EAGER) + @JoinColumn(nullable = false, name = "user_account_id") + private UserAccount user; + + /** + * The date and time when this token will expire. + */ + @Column(nullable = false) + private LocalDateTime expiryDate; + + /** + * Constructs a new PasswordResetToken. + * + * @param token The token string. + * @param user The user account associated with the token. + */ + public PasswordResetToken(String token, UserAccount user) { + this.token = token; + this.user = user; + this.expiryDate = calculateExpiryDate(EXPIRATION_MINUTES); + } + + /** + * Calculates the expiry date for the token. + * + * @param expiryTimeInMinutes The number of minutes until the token expires. + * @return The calculated expiry date and time. + */ + private LocalDateTime calculateExpiryDate(int expiryTimeInMinutes) { + return LocalDateTime.now().plusMinutes(expiryTimeInMinutes); + } + + /** + * Checks if the token has expired. + * + * @return true if the token has expired, false otherwise. + */ + public boolean isExpired() { + return LocalDateTime.now().isAfter(this.expiryDate); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/PollutantThreshold.java b/ems-backend/src/main/java/com/dne/ems/model/PollutantThreshold.java index c74a6d1..c6061fa 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/PollutantThreshold.java +++ b/ems-backend/src/main/java/com/dne/ems/model/PollutantThreshold.java @@ -1,99 +1,99 @@ -package com.dne.ems.model; - -import com.dne.ems.model.enums.PollutionType; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 污染物超标阈值设置实体类 - * 用于存储不同污染物的超标阈值设置 - */ -@Data -@Entity -@Table(name = "pollutant_thresholds") -@NoArgsConstructor -public class PollutantThreshold { - - /** - * 主键ID - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * 污染物类型 - */ - @Enumerated(EnumType.STRING) - @Column(nullable = false, unique = true) - private PollutionType pollutionType; - - /** - * 污染物名称 - */ - @Column(nullable = false, unique = true) - private String pollutantName; - - /** - * 超标阈值 - */ - @Column(nullable = false) - private Double threshold; - - /** - * 单位 - */ - @Column(nullable = false) - private String unit; - - /** - * 描述 - */ - private String description; - - /** - * 构造函数 - * - * @param pollutantName 污染物名称 - * @param threshold 超标阈值 - * @param unit 单位 - * @param description 描述 - */ - public PollutantThreshold(String pollutantName, Double threshold, String unit, String description) { - this.pollutantName = pollutantName; - try { - this.pollutionType = PollutionType.valueOf(pollutantName); - } catch (IllegalArgumentException e) { - // 如果不是有效的枚举值,默认为OTHER - this.pollutionType = PollutionType.OTHER; - } - this.threshold = threshold; - this.unit = unit; - this.description = description; - } - - /** - * 使用枚举类型的构造函数 - * - * @param pollutionType 污染物类型 - * @param threshold 超标阈值 - * @param unit 单位 - * @param description 描述 - */ - public PollutantThreshold(PollutionType pollutionType, Double threshold, String unit, String description) { - this.pollutionType = pollutionType; - this.pollutantName = pollutionType.name(); - this.threshold = threshold; - this.unit = unit; - this.description = description; - } +package com.dne.ems.model; + +import com.dne.ems.model.enums.PollutionType; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 污染物超标阈值设置实体类 + * 用于存储不同污染物的超标阈值设置 + */ +@Data +@Entity +@Table(name = "pollutant_thresholds") +@NoArgsConstructor +public class PollutantThreshold { + + /** + * 主键ID + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 污染物类型 + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false, unique = true) + private PollutionType pollutionType; + + /** + * 污染物名称 + */ + @Column(nullable = false, unique = true) + private String pollutantName; + + /** + * 超标阈值 + */ + @Column(nullable = false) + private Double threshold; + + /** + * 单位 + */ + @Column(nullable = false) + private String unit; + + /** + * 描述 + */ + private String description; + + /** + * 构造函数 + * + * @param pollutantName 污染物名称 + * @param threshold 超标阈值 + * @param unit 单位 + * @param description 描述 + */ + public PollutantThreshold(String pollutantName, Double threshold, String unit, String description) { + this.pollutantName = pollutantName; + try { + this.pollutionType = PollutionType.valueOf(pollutantName); + } catch (IllegalArgumentException e) { + // 如果不是有效的枚举值,默认为OTHER + this.pollutionType = PollutionType.OTHER; + } + this.threshold = threshold; + this.unit = unit; + this.description = description; + } + + /** + * 使用枚举类型的构造函数 + * + * @param pollutionType 污染物类型 + * @param threshold 超标阈值 + * @param unit 单位 + * @param description 描述 + */ + public PollutantThreshold(PollutionType pollutionType, Double threshold, String unit, String description) { + this.pollutionType = pollutionType; + this.pollutantName = pollutionType.name(); + this.threshold = threshold; + this.unit = unit; + this.description = description; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/Task.java b/ems-backend/src/main/java/com/dne/ems/model/Task.java index b37e646..3c81bb2 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/Task.java +++ b/ems-backend/src/main/java/com/dne/ems/model/Task.java @@ -1,184 +1,184 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.model.enums.TaskStatus; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; -import lombok.Data; - -/** - * 网格任务实体 - * - *

表示由网格工作人员执行的任务,通常根据用户反馈创建, - * 包含解决报告问题所需的所有详细信息,映射到'tasks'表。 - * - *

主要功能: - *

    - *
  • 记录任务详情和要求
  • - *
  • 跟踪任务状态和进度
  • - *
  • 关联工作人员和反馈
  • - *
- * - * @author DNE开发团队 - * @version 1.2 - * @since 2025-06-16 - */ -@Data -@Entity -@Table(name = "tasks") -public class Task { - - /** - * 主键ID,任务的唯一标识 - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * 关联的原始反馈 - * - *

可为null,表示手动创建的任务 - */ - @OneToOne - @JoinColumn(name = "feedback_id") - private Feedback feedback; - - /** - * The grid worker to whom this task is assigned. - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "assignee_id") - private UserAccount assignee; - - /** - * The user (typically a supervisor) who created this task. - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "created_by") - private UserAccount createdBy; - - /** - * The current status of the task (e.g., PENDING, IN_PROGRESS, COMPLETED). - */ - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private TaskStatus status; - - /** - * The timestamp when the task was assigned to the worker. - */ - @Column(name = "assigned_at") - private LocalDateTime assignedAt; - - /** - * The timestamp when the task was marked as completed. - */ - @Column(name = "completed_at") - private LocalDateTime completedAt; - - /** - * The timestamp when the task was created. Automatically managed by Hibernate. - */ - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - /** - * The timestamp when the task was last updated. Automatically managed by Hibernate. - */ - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; - - /** - * A brief title for the task. - */ - private String title; - - /** - * A detailed description of the task requirements. - */ - @Column(columnDefinition = "TEXT") - private String description; - - /** - * The type of pollution this task addresses. - */ - @Enumerated(EnumType.STRING) - private PollutionType pollutionType; - - /** - * The severity level of the issue. - */ - @Enumerated(EnumType.STRING) - private SeverityLevel severityLevel; - - /** - * A human-readable text address for the task location. - */ - private String textAddress; - - /** - * The x-coordinate of the grid cell where the task is located. - */ - private Integer gridX; - - /** - * The y-coordinate of the grid cell where the task is located. - */ - private Integer gridY; - - /** - * The geographical latitude of the task location. - */ - private Double latitude; - - /** - * The geographical longitude of the task location. - */ - private Double longitude; - - /** - * A list of history records for this task, tracking all status changes and updates. - * Managed with a one-to-many relationship, and orphan removal ensures that - * history records are deleted when the task is deleted. - */ - @OneToMany(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true) - private List history = new ArrayList<>(); - - /** - * The assignment details for this task. - * This establishes a bidirectional link between a task and its assignment. - */ - @OneToOne(mappedBy = "task", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - private Assignment assignment; - - /** - * A list of all submissions made for this task. - * Managed with a one-to-many relationship. - */ - @OneToMany(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true) - private List submissions = new ArrayList<>(); +package com.dne.ems.model; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.model.enums.TaskStatus; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * 网格任务实体 + * + *

表示由网格工作人员执行的任务,通常根据用户反馈创建, + * 包含解决报告问题所需的所有详细信息,映射到'tasks'表。 + * + *

主要功能: + *

    + *
  • 记录任务详情和要求
  • + *
  • 跟踪任务状态和进度
  • + *
  • 关联工作人员和反馈
  • + *
+ * + * @author DNE开发团队 + * @version 1.2 + * @since 2025-06-16 + */ +@Data +@Entity +@Table(name = "tasks") +public class Task { + + /** + * 主键ID,任务的唯一标识 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 关联的原始反馈 + * + *

可为null,表示手动创建的任务 + */ + @OneToOne + @JoinColumn(name = "feedback_id") + private Feedback feedback; + + /** + * The grid worker to whom this task is assigned. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "assignee_id") + private UserAccount assignee; + + /** + * The user (typically a supervisor) who created this task. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "created_by") + private UserAccount createdBy; + + /** + * The current status of the task (e.g., PENDING, IN_PROGRESS, COMPLETED). + */ + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private TaskStatus status; + + /** + * The timestamp when the task was assigned to the worker. + */ + @Column(name = "assigned_at") + private LocalDateTime assignedAt; + + /** + * The timestamp when the task was marked as completed. + */ + @Column(name = "completed_at") + private LocalDateTime completedAt; + + /** + * The timestamp when the task was created. Automatically managed by Hibernate. + */ + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * The timestamp when the task was last updated. Automatically managed by Hibernate. + */ + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + /** + * A brief title for the task. + */ + private String title; + + /** + * A detailed description of the task requirements. + */ + @Column(columnDefinition = "TEXT") + private String description; + + /** + * The type of pollution this task addresses. + */ + @Enumerated(EnumType.STRING) + private PollutionType pollutionType; + + /** + * The severity level of the issue. + */ + @Enumerated(EnumType.STRING) + private SeverityLevel severityLevel; + + /** + * A human-readable text address for the task location. + */ + private String textAddress; + + /** + * The x-coordinate of the grid cell where the task is located. + */ + private Integer gridX; + + /** + * The y-coordinate of the grid cell where the task is located. + */ + private Integer gridY; + + /** + * The geographical latitude of the task location. + */ + private Double latitude; + + /** + * The geographical longitude of the task location. + */ + private Double longitude; + + /** + * A list of history records for this task, tracking all status changes and updates. + * Managed with a one-to-many relationship, and orphan removal ensures that + * history records are deleted when the task is deleted. + */ + @OneToMany(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true) + private List history = new ArrayList<>(); + + /** + * The assignment details for this task. + * This establishes a bidirectional link between a task and its assignment. + */ + @OneToOne(mappedBy = "task", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Assignment assignment; + + /** + * A list of all submissions made for this task. + * Managed with a one-to-many relationship. + */ + @OneToMany(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true) + private List submissions = new ArrayList<>(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/TaskHistory.java b/ems-backend/src/main/java/com/dne/ems/model/TaskHistory.java index 7589700..fb2cdab 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/TaskHistory.java +++ b/ems-backend/src/main/java/com/dne/ems/model/TaskHistory.java @@ -1,105 +1,105 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; - -import org.hibernate.annotations.CreationTimestamp; - -import com.dne.ems.model.enums.TaskStatus; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Represents a historical record of a change in a task's status. Each time a task's - * state is modified (e.g., from PENDING to IN_PROGRESS), a new TaskHistory entry - * is created to log the change. This provides an audit trail for tasks. - * Maps to the 'task_history' table. - * - * @version 1.1 - * @since 2025-06-16 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -@Entity -@Table(name = "task_history") -public class TaskHistory { - - /** - * The unique identifier for the task history record. - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * The task to which this history record belongs. - */ - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "task_id") - private Task task; - - /** - * The status of the task before the change. Can be null for the initial creation event. - */ - @Enumerated(EnumType.STRING) - @Column(name = "old_status") - private TaskStatus oldStatus; - - /** - * The new status of the task after the change. - */ - @Enumerated(EnumType.STRING) - @Column(name = "new_status", nullable = false) - private TaskStatus newStatus; - - /** - * The user who initiated the status change. - */ - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "changed_by_id") - private UserAccount changedBy; - - /** - * Optional comments or notes regarding the status change. - */ - private String comments; - - /** - * The timestamp when the status change occurred. Automatically managed by Hibernate. - */ - @CreationTimestamp - @Column(name = "changed_at", nullable = false, updatable = false) - private LocalDateTime changedAt; - - /** - * Constructs a new TaskHistory record. - * - * @param task The associated task. - * @param oldStatus The previous status of the task. - * @param newStatus The new status of the task. - * @param changedBy The user who made the change. - * @param comments Any comments related to the change. - */ - public TaskHistory(Task task, TaskStatus oldStatus, TaskStatus newStatus, UserAccount changedBy, String comments) { - this.task = task; - this.oldStatus = oldStatus; - this.newStatus = newStatus; - this.changedBy = changedBy; - this.comments = comments; - } +package com.dne.ems.model; + +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +import com.dne.ems.model.enums.TaskStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a historical record of a change in a task's status. Each time a task's + * state is modified (e.g., from PENDING to IN_PROGRESS), a new TaskHistory entry + * is created to log the change. This provides an audit trail for tasks. + * Maps to the 'task_history' table. + * + * @version 1.1 + * @since 2025-06-16 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Entity +@Table(name = "task_history") +public class TaskHistory { + + /** + * The unique identifier for the task history record. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The task to which this history record belongs. + */ + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "task_id") + private Task task; + + /** + * The status of the task before the change. Can be null for the initial creation event. + */ + @Enumerated(EnumType.STRING) + @Column(name = "old_status") + private TaskStatus oldStatus; + + /** + * The new status of the task after the change. + */ + @Enumerated(EnumType.STRING) + @Column(name = "new_status", nullable = false) + private TaskStatus newStatus; + + /** + * The user who initiated the status change. + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "changed_by_id") + private UserAccount changedBy; + + /** + * Optional comments or notes regarding the status change. + */ + private String comments; + + /** + * The timestamp when the status change occurred. Automatically managed by Hibernate. + */ + @CreationTimestamp + @Column(name = "changed_at", nullable = false, updatable = false) + private LocalDateTime changedAt; + + /** + * Constructs a new TaskHistory record. + * + * @param task The associated task. + * @param oldStatus The previous status of the task. + * @param newStatus The new status of the task. + * @param changedBy The user who made the change. + * @param comments Any comments related to the change. + */ + public TaskHistory(Task task, TaskStatus oldStatus, TaskStatus newStatus, UserAccount changedBy, String comments) { + this.task = task; + this.oldStatus = oldStatus; + this.newStatus = newStatus; + this.changedBy = changedBy; + this.comments = comments; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/TaskSubmission.java b/ems-backend/src/main/java/com/dne/ems/model/TaskSubmission.java index 1849e1e..3806123 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/TaskSubmission.java +++ b/ems-backend/src/main/java/com/dne/ems/model/TaskSubmission.java @@ -1,46 +1,46 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; - -import org.hibernate.annotations.CreationTimestamp; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.Lob; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@Entity -@Table(name = "task_submissions") -public class TaskSubmission { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "task_id", referencedColumnName = "id") - private Task task; - - @Lob - @Column(nullable = false, columnDefinition = "TEXT") - private String notes; - - @CreationTimestamp - @Column(name = "submitted_at", nullable = false, updatable = false) - private LocalDateTime submittedAt; - - public TaskSubmission(Task task, String notes) { - this.task = task; - this.notes = notes; - } +package com.dne.ems.model; + +import java.time.LocalDateTime; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@Entity +@Table(name = "task_submissions") +public class TaskSubmission { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "task_id", referencedColumnName = "id") + private Task task; + + @Lob + @Column(nullable = false, columnDefinition = "TEXT") + private String notes; + + @CreationTimestamp + @Column(name = "submitted_at", nullable = false, updatable = false) + private LocalDateTime submittedAt; + + public TaskSubmission(Task task, String notes) { + this.task = task; + this.notes = notes; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/UserAccount.java b/ems-backend/src/main/java/com/dne/ems/model/UserAccount.java index 08c3fc0..a209742 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/UserAccount.java +++ b/ems-backend/src/main/java/com/dne/ems/model/UserAccount.java @@ -1,188 +1,188 @@ -package com.dne.ems.model; - -import java.time.LocalDateTime; -import java.util.List; - -import com.dne.ems.config.converter.StringListConverter; -import com.dne.ems.model.enums.Gender; -import com.dne.ems.model.enums.Level; -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.UserStatus; - -import jakarta.persistence.Column; -import jakarta.persistence.Convert; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.Data; - -/** - * Represents a user account in the system. This entity stores all information - * related to a user, including credentials, personal details, role, and status. - * It is a central part of the application's security and data model. - * Maps to the 'user_account' table, with unique constraints on phone and email. - * - * @version 1.2 - * @since 2025-06-16 - */ -@Entity -@Table(name = "user_account", uniqueConstraints = { - @UniqueConstraint(columnNames = "phone"), - @UniqueConstraint(columnNames = "email") -}) -@Data -public class UserAccount { - - /** - * The unique identifier for the user account. - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * The full name of the user. - */ - @NotEmpty - private String name; - - /** - * The user's unique phone number. Used for communication and as a login identifier. - */ - @NotEmpty - @Column(nullable = false, unique = true) - private String phone; - - /** - * The user's unique email address. Used for communication and as a login identifier. - */ - @NotEmpty - @Email - @Column(nullable = false, unique = true) - private String email; - - /** - * The user's hashed password. Must be at least 8 characters long. - */ - @NotEmpty - @Size(min = 8) - private String password; - - /** - * The gender of the user. - */ - @Enumerated(EnumType.STRING) - private Gender gender; - - /** - * The role of the user within the system (e.g., ADMIN, SUPERVISOR, GRID_WORKER). - * Determines the user's permissions. - */ - @Enumerated(EnumType.STRING) - @Column(length = 50) - private Role role; - - /** - * The current status of the user account (e.g., ACTIVE, INACTIVE, SUSPENDED). - * Defaults to ACTIVE. - */ - @NotNull - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 50) - private UserStatus status = UserStatus.ACTIVE; - - /** - * The x-coordinate of the grid cell this user is primarily associated with, - * if applicable (e.g., for a grid worker). - */ - @Column(name = "grid_x") - private Integer gridX; - - /** - * The y-coordinate of the grid cell this user is primarily associated with, - * if applicable (e.g., for a grid worker). - */ - @Column(name = "grid_y") - private Integer gridY; - - /** - * The geographical region or district the user belongs to. - */ - @Column(length = 255) - private String region; - - /** - * The proficiency level of the user, if applicable (e.g., for a grid worker). - */ - @Enumerated(EnumType.STRING) - private Level level; - - /** - * A JSON-formatted string to store a list of the user's skills. - */ - @Column(columnDefinition = "JSON") - @Convert(converter = StringListConverter.class) - private List skills; - - /** - * The timestamp when the user account was created. - */ - @Column(nullable = false, updatable = false) - private LocalDateTime createdAt = LocalDateTime.now(); - - /** - * The timestamp when the user account was last updated. - */ - @Column(nullable = false) - private LocalDateTime updatedAt = LocalDateTime.now(); - - /** - * A JPA callback method that automatically updates the `updatedAt` timestamp - * before an update operation is persisted to the database. - */ - @PreUpdate - protected void onUpdate() { - updatedAt = LocalDateTime.now(); - } - - /** - * A flag indicating if the user's account is enabled. A disabled account cannot be logged into. - * Defaults to true. - */ - @Column(nullable = false) - private boolean enabled = true; - - /** - * The current geographical latitude of the user, typically for grid workers. - * This can be updated periodically by the user's mobile device. - */ - private Double currentLatitude; - - /** - * The current geographical longitude of the user, typically for grid workers. - * This can be updated periodically by the user's mobile device. - */ - private Double currentLongitude; - - /** - * A counter for the number of consecutive failed login attempts. - * Used for implementing account lockout policies. - */ - private int failedLoginAttempts = 0; - - /** - * The timestamp until which the user's account is locked out due to - * excessive failed login attempts. A null value means the account is not locked. - */ - private LocalDateTime lockoutEndTime; +package com.dne.ems.model; + +import java.time.LocalDateTime; +import java.util.List; + +import com.dne.ems.config.converter.StringListConverter; +import com.dne.ems.model.enums.Gender; +import com.dne.ems.model.enums.Level; +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.UserStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * Represents a user account in the system. This entity stores all information + * related to a user, including credentials, personal details, role, and status. + * It is a central part of the application's security and data model. + * Maps to the 'user_account' table, with unique constraints on phone and email. + * + * @version 1.2 + * @since 2025-06-16 + */ +@Entity +@Table(name = "user_account", uniqueConstraints = { + @UniqueConstraint(columnNames = "phone"), + @UniqueConstraint(columnNames = "email") +}) +@Data +public class UserAccount { + + /** + * The unique identifier for the user account. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * The full name of the user. + */ + @NotEmpty + private String name; + + /** + * The user's unique phone number. Used for communication and as a login identifier. + */ + @NotEmpty + @Column(nullable = false, unique = true) + private String phone; + + /** + * The user's unique email address. Used for communication and as a login identifier. + */ + @NotEmpty + @Email + @Column(nullable = false, unique = true) + private String email; + + /** + * The user's hashed password. Must be at least 8 characters long. + */ + @NotEmpty + @Size(min = 8) + private String password; + + /** + * The gender of the user. + */ + @Enumerated(EnumType.STRING) + private Gender gender; + + /** + * The role of the user within the system (e.g., ADMIN, SUPERVISOR, GRID_WORKER). + * Determines the user's permissions. + */ + @Enumerated(EnumType.STRING) + @Column(length = 50) + private Role role; + + /** + * The current status of the user account (e.g., ACTIVE, INACTIVE, SUSPENDED). + * Defaults to ACTIVE. + */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 50) + private UserStatus status = UserStatus.ACTIVE; + + /** + * The x-coordinate of the grid cell this user is primarily associated with, + * if applicable (e.g., for a grid worker). + */ + @Column(name = "grid_x") + private Integer gridX; + + /** + * The y-coordinate of the grid cell this user is primarily associated with, + * if applicable (e.g., for a grid worker). + */ + @Column(name = "grid_y") + private Integer gridY; + + /** + * The geographical region or district the user belongs to. + */ + @Column(length = 255) + private String region; + + /** + * The proficiency level of the user, if applicable (e.g., for a grid worker). + */ + @Enumerated(EnumType.STRING) + private Level level; + + /** + * A JSON-formatted string to store a list of the user's skills. + */ + @Column(columnDefinition = "JSON") + @Convert(converter = StringListConverter.class) + private List skills; + + /** + * The timestamp when the user account was created. + */ + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + /** + * The timestamp when the user account was last updated. + */ + @Column(nullable = false) + private LocalDateTime updatedAt = LocalDateTime.now(); + + /** + * A JPA callback method that automatically updates the `updatedAt` timestamp + * before an update operation is persisted to the database. + */ + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + /** + * A flag indicating if the user's account is enabled. A disabled account cannot be logged into. + * Defaults to true. + */ + @Column(nullable = false) + private boolean enabled = true; + + /** + * The current geographical latitude of the user, typically for grid workers. + * This can be updated periodically by the user's mobile device. + */ + private Double currentLatitude; + + /** + * The current geographical longitude of the user, typically for grid workers. + * This can be updated periodically by the user's mobile device. + */ + private Double currentLongitude; + + /** + * A counter for the number of consecutive failed login attempts. + * Used for implementing account lockout policies. + */ + private int failedLoginAttempts = 0; + + /** + * The timestamp until which the user's account is locked out due to + * excessive failed login attempts. A null value means the account is not locked. + */ + private LocalDateTime lockoutEndTime; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/AssignmentMethod.java b/ems-backend/src/main/java/com/dne/ems/model/enums/AssignmentMethod.java index c038f39..ae41485 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/AssignmentMethod.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/AssignmentMethod.java @@ -1,23 +1,23 @@ -package com.dne.ems.model.enums; - -/** - * Defines the method used to assign a task to a grid worker. This helps - * in auditing and analyzing the effectiveness of different assignment strategies. - * - * @version 1.1 - * @since 2025-06-16 - */ -public enum AssignmentMethod { - /** - * Indicates that the task was assigned manually by a user, typically a supervisor - * or administrator, who made a direct choice of the assignee. - */ - MANUAL, - - /** - * Indicates that the task was assigned automatically by the system. This usually - * involves an algorithm that considers factors like worker availability, location, - * skills, and current workload. - */ - INTELLIGENT +package com.dne.ems.model.enums; + +/** + * Defines the method used to assign a task to a grid worker. This helps + * in auditing and analyzing the effectiveness of different assignment strategies. + * + * @version 1.1 + * @since 2025-06-16 + */ +public enum AssignmentMethod { + /** + * Indicates that the task was assigned manually by a user, typically a supervisor + * or administrator, who made a direct choice of the assignee. + */ + MANUAL, + + /** + * Indicates that the task was assigned automatically by the system. This usually + * involves an algorithm that considers factors like worker availability, location, + * skills, and current workload. + */ + INTELLIGENT } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/AssignmentStatus.java b/ems-backend/src/main/java/com/dne/ems/model/enums/AssignmentStatus.java index 397f7b7..85eb9c9 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/AssignmentStatus.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/AssignmentStatus.java @@ -1,35 +1,35 @@ -package com.dne.ems.model.enums; - -/** - * Represents the status of a specific task assignment record. This tracks the - * state of a task as it pertains to the assigned worker. - * - * @version 1.1 - * @since 2025-06-16 - */ -public enum AssignmentStatus { - /** - * The task has been assigned to a grid worker, but they have not yet - * started working on it. - * (任务已分配,但网格员尚未开始处理。) - */ - PENDING, - - /** - * The grid worker is actively engaged in handling the assigned task. - * (网格员正在处理任务。) - */ - IN_PROGRESS, - - /** - * The grid worker has successfully completed all work for the task. - * (任务已由网格员完成。) - */ - COMPLETED, - - /** - * The task was not completed by its designated deadline and is now considered overdue. - * (任务未在截止日期前完成。) - */ - OVERDUE +package com.dne.ems.model.enums; + +/** + * Represents the status of a specific task assignment record. This tracks the + * state of a task as it pertains to the assigned worker. + * + * @version 1.1 + * @since 2025-06-16 + */ +public enum AssignmentStatus { + /** + * The task has been assigned to a grid worker, but they have not yet + * started working on it. + * (任务已分配,但网格员尚未开始处理。) + */ + PENDING, + + /** + * The grid worker is actively engaged in handling the assigned task. + * (网格员正在处理任务。) + */ + IN_PROGRESS, + + /** + * The grid worker has successfully completed all work for the task. + * (任务已由网格员完成。) + */ + COMPLETED, + + /** + * The task was not completed by its designated deadline and is now considered overdue. + * (任务未在截止日期前完成。) + */ + OVERDUE } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/FeedbackStatus.java b/ems-backend/src/main/java/com/dne/ems/model/enums/FeedbackStatus.java index 44eb372..5cb0536 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/FeedbackStatus.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/FeedbackStatus.java @@ -1,83 +1,83 @@ -package com.dne.ems.model.enums; - -/** - * Defines the lifecycle status of a feedback submission. This enum tracks the journey - * of a feedback report from initial submission through review, processing, and resolution. - * - * @version 1.1 - * @since 2025-06-16 - */ -public enum FeedbackStatus { - /** - * Initial state after a public user submits feedback. It is waiting for a supervisor - * to begin the review process. - */ - PENDING_REVIEW, - - /** - * The feedback is currently being analyzed by an automated AI system to - * extract relevant details and suggest actions. - */ - AI_REVIEWING, - - /** - * The automated AI review could not be completed due to a technical error. - * This status indicates that manual intervention is required. - */ - AI_REVIEW_FAILED, - - /** - * The AI has successfully reviewed the feedback and is now processing it, - * which may include generating summaries or categorizing the issue. - */ - AI_PROCESSING, - - /** - * The feedback has been validated and is now in a queue, waiting to be - * assigned as a task to a grid worker. - */ - PENDING_ASSIGNMENT, - - /** - * A task has been created from the feedback and assigned to a specific grid worker. - */ - ASSIGNED, - - /** - * The assigned grid worker has investigated the issue on-site and confirmed the - * details reported in the feedback. - */ - CONFIRMED, - - /** - * The feedback has been closed after being deemed invalid, a duplicate, or irrelevant - * by a supervisor or administrator. No further action will be taken. - */ - CLOSED_INVALID, - - /** - * The feedback has been fully processed, and a corresponding task has been created - * and is being managed in the task lifecycle. This marks the end of the feedback's own active lifecycle. - */ - PROCESSED, - - // ---- Virtual statuses for filtering purposes ---- - - /** - * Virtual status indicating that the associated task is currently being worked on. - * This status does not exist in the feedback table itself but is used for API filtering. - */ - IN_PROGRESS, - - /** - * Virtual status indicating that the feedback was resolved, possibly automatically. - * Used for filtering to include various "completed" states. - */ - RESOLVED, - - /** - * Virtual status indicating that the associated task has been completed. - * Used for filtering to include various "completed" states. - */ - COMPLETED +package com.dne.ems.model.enums; + +/** + * Defines the lifecycle status of a feedback submission. This enum tracks the journey + * of a feedback report from initial submission through review, processing, and resolution. + * + * @version 1.1 + * @since 2025-06-16 + */ +public enum FeedbackStatus { + /** + * Initial state after a public user submits feedback. It is waiting for a supervisor + * to begin the review process. + */ + PENDING_REVIEW, + + /** + * The feedback is currently being analyzed by an automated AI system to + * extract relevant details and suggest actions. + */ + AI_REVIEWING, + + /** + * The automated AI review could not be completed due to a technical error. + * This status indicates that manual intervention is required. + */ + AI_REVIEW_FAILED, + + /** + * The AI has successfully reviewed the feedback and is now processing it, + * which may include generating summaries or categorizing the issue. + */ + AI_PROCESSING, + + /** + * The feedback has been validated and is now in a queue, waiting to be + * assigned as a task to a grid worker. + */ + PENDING_ASSIGNMENT, + + /** + * A task has been created from the feedback and assigned to a specific grid worker. + */ + ASSIGNED, + + /** + * The assigned grid worker has investigated the issue on-site and confirmed the + * details reported in the feedback. + */ + CONFIRMED, + + /** + * The feedback has been closed after being deemed invalid, a duplicate, or irrelevant + * by a supervisor or administrator. No further action will be taken. + */ + CLOSED_INVALID, + + /** + * The feedback has been fully processed, and a corresponding task has been created + * and is being managed in the task lifecycle. This marks the end of the feedback's own active lifecycle. + */ + PROCESSED, + + // ---- Virtual statuses for filtering purposes ---- + + /** + * Virtual status indicating that the associated task is currently being worked on. + * This status does not exist in the feedback table itself but is used for API filtering. + */ + IN_PROGRESS, + + /** + * Virtual status indicating that the feedback was resolved, possibly automatically. + * Used for filtering to include various "completed" states. + */ + RESOLVED, + + /** + * Virtual status indicating that the associated task has been completed. + * Used for filtering to include various "completed" states. + */ + COMPLETED } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/Gender.java b/ems-backend/src/main/java/com/dne/ems/model/enums/Gender.java index 621faee..c6fc0a6 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/Gender.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/Gender.java @@ -1,24 +1,24 @@ -package com.dne.ems.model.enums; - -/** - * Defines the gender of a user. - * - * @version 1.1 - * @since 2025-06-16 - */ -public enum Gender { - /** - * Represents the male gender. - */ - MALE, - - /** - * Represents the female gender. - */ - FEMALE, - - /** - * Represents cases where the gender is not specified or is unknown. - */ - UNKNOWN +package com.dne.ems.model.enums; + +/** + * Defines the gender of a user. + * + * @version 1.1 + * @since 2025-06-16 + */ +public enum Gender { + /** + * Represents the male gender. + */ + MALE, + + /** + * Represents the female gender. + */ + FEMALE, + + /** + * Represents cases where the gender is not specified or is unknown. + */ + UNKNOWN } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/Level.java b/ems-backend/src/main/java/com/dne/ems/model/enums/Level.java index ad05d2a..7e139a7 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/Level.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/Level.java @@ -1,22 +1,22 @@ -package com.dne.ems.model.enums; - -/** - * Defines the administrative or hierarchical level, typically for a user or a geographical region. - * This is used to scope data visibility and permissions. - * - * @version 1.1 - * @since 2025-06-16 - */ -public enum Level { - /** - * Represents the provincial level, the highest administrative division in this context. - * Users or data at this level may have broad oversight. - */ - PROVINCE, - - /** - * Represents the city level, a subdivision of a province. Users or data at this - * level have a more localized scope. - */ - CITY +package com.dne.ems.model.enums; + +/** + * Defines the administrative or hierarchical level, typically for a user or a geographical region. + * This is used to scope data visibility and permissions. + * + * @version 1.1 + * @since 2025-06-16 + */ +public enum Level { + /** + * Represents the provincial level, the highest administrative division in this context. + * Users or data at this level may have broad oversight. + */ + PROVINCE, + + /** + * Represents the city level, a subdivision of a province. Users or data at this + * level have a more localized scope. + */ + CITY } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/OperationType.java b/ems-backend/src/main/java/com/dne/ems/model/enums/OperationType.java index d61995d..397bcd0 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/OperationType.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/OperationType.java @@ -1,75 +1,75 @@ -package com.dne.ems.model.enums; - -/** - * 操作类型枚举,用于标识用户执行的各种操作类型。 - * 包括登录、登出、创建、更新、删除等操作。 - * - * @version 1.0 - * @since 2025-06-20 - */ -public enum OperationType { - /** - * 用户登录操作 - */ - LOGIN, - - /** - * 用户登出操作 - */ - LOGOUT, - - /** - * 创建资源操作 - */ - CREATE, - - /** - * 更新资源操作 - */ - UPDATE, - - /** - * 删除资源操作 - */ - DELETE, - - /** - * 分配任务操作 - */ - ASSIGN_TASK, - - /** - * 提交任务操作 - */ - SUBMIT_TASK, - - /** - * 审批任务操作 - */ - APPROVE_TASK, - - /** - * 拒绝任务操作 - */ - REJECT_TASK, - - /** - * 提交反馈操作 - */ - SUBMIT_FEEDBACK, - - /** - * 审批反馈操作 - */ - APPROVE_FEEDBACK, - - /** - * 拒绝反馈操作 - */ - REJECT_FEEDBACK, - - /** - * 其他操作 - */ - OTHER +package com.dne.ems.model.enums; + +/** + * 操作类型枚举,用于标识用户执行的各种操作类型。 + * 包括登录、登出、创建、更新、删除等操作。 + * + * @version 1.0 + * @since 2025-06-20 + */ +public enum OperationType { + /** + * 用户登录操作 + */ + LOGIN, + + /** + * 用户登出操作 + */ + LOGOUT, + + /** + * 创建资源操作 + */ + CREATE, + + /** + * 更新资源操作 + */ + UPDATE, + + /** + * 删除资源操作 + */ + DELETE, + + /** + * 分配任务操作 + */ + ASSIGN_TASK, + + /** + * 提交任务操作 + */ + SUBMIT_TASK, + + /** + * 审批任务操作 + */ + APPROVE_TASK, + + /** + * 拒绝任务操作 + */ + REJECT_TASK, + + /** + * 提交反馈操作 + */ + SUBMIT_FEEDBACK, + + /** + * 审批反馈操作 + */ + APPROVE_FEEDBACK, + + /** + * 拒绝反馈操作 + */ + REJECT_FEEDBACK, + + /** + * 其他操作 + */ + OTHER } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/PollutionType.java b/ems-backend/src/main/java/com/dne/ems/model/enums/PollutionType.java index b7a9cc0..efaccc5 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/PollutionType.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/PollutionType.java @@ -1,39 +1,39 @@ -package com.dne.ems.model.enums; - -/** - * Defines the specific types of pollution that can be reported in a feedback submission. - * This categorization helps in directing the issue to the right teams and resources. - * - * @version 1.1 - * @since 2025-06-16 - */ -public enum PollutionType { - /** - * Represents fine particulate matter (PM2.5), which are tiny particles in the air - * that can cause serious health problems. - */ - PM25, - - /** - * Represents ground-level Ozone (O3), a major component of smog. - */ - O3, - - /** - * Represents Nitrogen Dioxide (NO2), a gas commonly emitted from vehicles - * and industrial sources. - */ - NO2, - - /** - * Represents Sulfur Dioxide (SO2), a gas produced by burning fossil fuels - * containing sulfur. - */ - SO2, - - /** - * A catch-all category for other types of pollution not explicitly listed. - * Submissions with this type may require more detailed manual review. - */ - OTHER +package com.dne.ems.model.enums; + +/** + * Defines the specific types of pollution that can be reported in a feedback submission. + * This categorization helps in directing the issue to the right teams and resources. + * + * @version 1.1 + * @since 2025-06-16 + */ +public enum PollutionType { + /** + * Represents fine particulate matter (PM2.5), which are tiny particles in the air + * that can cause serious health problems. + */ + PM25, + + /** + * Represents ground-level Ozone (O3), a major component of smog. + */ + O3, + + /** + * Represents Nitrogen Dioxide (NO2), a gas commonly emitted from vehicles + * and industrial sources. + */ + NO2, + + /** + * Represents Sulfur Dioxide (SO2), a gas produced by burning fossil fuels + * containing sulfur. + */ + SO2, + + /** + * A catch-all category for other types of pollution not explicitly listed. + * Submissions with this type may require more detailed manual review. + */ + OTHER } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/Role.java b/ems-backend/src/main/java/com/dne/ems/model/enums/Role.java index bab1914..477a24a 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/Role.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/Role.java @@ -1,45 +1,45 @@ -package com.dne.ems.model.enums; - -/** - * Defines the roles a user can have within the system. Each role grants a different - * set of permissions and capabilities, governing access to various system functionalities. - * This enum is used in the `UserAccount` entity and is crucial for authorization. - * - * @version 1.1 - * @since 2025-06-16 - */ -public enum Role { - /** - * Represents a public supervisor, a user from the public who is tasked with - * overseeing and validating feedback submissions. They act as a first line of quality control. - */ - PUBLIC_SUPERVISOR, - - /** - * Represents a supervisor, an internal role with responsibilities to manage, - * assign, and review tasks based on feedback. They bridge the gap between public - * submissions and actionable tasks for grid workers. - */ - SUPERVISOR, - - /** - * Represents a grid worker, a field agent who performs on-the-ground tasks. - * They are assigned specific tasks by supervisors and are responsible for executing - * them and reporting back on their status. - */ - GRID_WORKER, - - /** - * Represents a system administrator, who has the highest level of permissions. - * Administrators can manage user accounts, system settings, and have full - * oversight of all data and operations. - */ - ADMIN, - - /** - * Represents a decision-maker, a high-level role focused on analytics and reporting. - * They have access to dashboards, system-wide statistics, and reports to inform - * strategic decisions, but typically do not perform day-to-day operations. - */ - DECISION_MAKER +package com.dne.ems.model.enums; + +/** + * Defines the roles a user can have within the system. Each role grants a different + * set of permissions and capabilities, governing access to various system functionalities. + * This enum is used in the `UserAccount` entity and is crucial for authorization. + * + * @version 1.1 + * @since 2025-06-16 + */ +public enum Role { + /** + * Represents a public supervisor, a user from the public who is tasked with + * overseeing and validating feedback submissions. They act as a first line of quality control. + */ + PUBLIC_SUPERVISOR, + + /** + * Represents a supervisor, an internal role with responsibilities to manage, + * assign, and review tasks based on feedback. They bridge the gap between public + * submissions and actionable tasks for grid workers. + */ + SUPERVISOR, + + /** + * Represents a grid worker, a field agent who performs on-the-ground tasks. + * They are assigned specific tasks by supervisors and are responsible for executing + * them and reporting back on their status. + */ + GRID_WORKER, + + /** + * Represents a system administrator, who has the highest level of permissions. + * Administrators can manage user accounts, system settings, and have full + * oversight of all data and operations. + */ + ADMIN, + + /** + * Represents a decision-maker, a high-level role focused on analytics and reporting. + * They have access to dashboards, system-wide statistics, and reports to inform + * strategic decisions, but typically do not perform day-to-day operations. + */ + DECISION_MAKER } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/SeverityLevel.java b/ems-backend/src/main/java/com/dne/ems/model/enums/SeverityLevel.java index 672e158..2b338bf 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/SeverityLevel.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/SeverityLevel.java @@ -1,28 +1,28 @@ -package com.dne.ems.model.enums; - -/** - * Defines the severity levels for a reported issue, such as a pollution event. - * This helps in prioritizing tasks and allocating resources effectively. - * - * @version 1.1 - * @since 2025-06-16 - */ -public enum SeverityLevel { - /** - * Indicates a critical issue that requires immediate attention and response. - * High severity tasks should be prioritized above all others. - */ - HIGH, - - /** - * Indicates a significant issue that requires attention in a timely manner, - * but is not as critical as a high-severity issue. - */ - MEDIUM, - - /** - * Indicates a minor issue that can be addressed with lower priority - * or during routine maintenance. - */ - LOW +package com.dne.ems.model.enums; + +/** + * Defines the severity levels for a reported issue, such as a pollution event. + * This helps in prioritizing tasks and allocating resources effectively. + * + * @version 1.1 + * @since 2025-06-16 + */ +public enum SeverityLevel { + /** + * Indicates a critical issue that requires immediate attention and response. + * High severity tasks should be prioritized above all others. + */ + HIGH, + + /** + * Indicates a significant issue that requires attention in a timely manner, + * but is not as critical as a high-severity issue. + */ + MEDIUM, + + /** + * Indicates a minor issue that can be addressed with lower priority + * or during routine maintenance. + */ + LOW } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/TaskStatus.java b/ems-backend/src/main/java/com/dne/ems/model/enums/TaskStatus.java index 6c73c55..15c24d7 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/TaskStatus.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/TaskStatus.java @@ -1,43 +1,43 @@ -package com.dne.ems.model.enums; - -/** - * Represents the lifecycle status of a task, tracking its progress from creation to completion. - * - * @version 1.1 - * @since 2025-06-16 - */ -public enum TaskStatus { - /** - * The task has been created, typically from a processed feedback report, - * but has not yet been assigned to a specific grid worker. - */ - PENDING_ASSIGNMENT, - - /** - * The task has been officially assigned to a grid worker but work has not yet begun. - */ - ASSIGNED, - - /** - * The assigned grid worker has acknowledged the task and is actively working to resolve it. - */ - IN_PROGRESS, - - /** - * The grid worker has finished their work and submitted their findings or results - * for review by a supervisor or administrator. - */ - SUBMITTED, - - /** - * A supervisor or administrator has reviewed the submitted work and confirmed that - * the task has been successfully resolved. This is a final state. - */ - COMPLETED, - - /** - * The task has been cancelled, either before or during its execution. - * No further action will be taken on this task. This is a final state. - */ - CANCELLED +package com.dne.ems.model.enums; + +/** + * Represents the lifecycle status of a task, tracking its progress from creation to completion. + * + * @version 1.1 + * @since 2025-06-16 + */ +public enum TaskStatus { + /** + * The task has been created, typically from a processed feedback report, + * but has not yet been assigned to a specific grid worker. + */ + PENDING_ASSIGNMENT, + + /** + * The task has been officially assigned to a grid worker but work has not yet begun. + */ + ASSIGNED, + + /** + * The assigned grid worker has acknowledged the task and is actively working to resolve it. + */ + IN_PROGRESS, + + /** + * The grid worker has finished their work and submitted their findings or results + * for review by a supervisor or administrator. + */ + SUBMITTED, + + /** + * A supervisor or administrator has reviewed the submitted work and confirmed that + * the task has been successfully resolved. This is a final state. + */ + COMPLETED, + + /** + * The task has been cancelled, either before or during its execution. + * No further action will be taken on this task. This is a final state. + */ + CANCELLED } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/enums/UserStatus.java b/ems-backend/src/main/java/com/dne/ems/model/enums/UserStatus.java index c7f3951..3613cd2 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/enums/UserStatus.java +++ b/ems-backend/src/main/java/com/dne/ems/model/enums/UserStatus.java @@ -1,28 +1,28 @@ -package com.dne.ems.model.enums; - -/** - * Defines the operational status of a user account, which controls their - * ability to access the system. - * - * @version 1.1 - * @since 2025-06-16 - */ -public enum UserStatus { - /** - * The user account is active and fully operational. The user can log in - * and perform all actions permitted by their role. - */ - ACTIVE, - - /** - * The user account has been deactivated, either manually by an admin or - * automatically. The user cannot log in or access the system. - */ - INACTIVE, - - /** - * The user is temporarily on leave. This status can be used to disable - * task assignments or notifications without fully deactivating the account. - */ - ON_LEAVE +package com.dne.ems.model.enums; + +/** + * Defines the operational status of a user account, which controls their + * ability to access the system. + * + * @version 1.1 + * @since 2025-06-16 + */ +public enum UserStatus { + /** + * The user account is active and fully operational. The user can log in + * and perform all actions permitted by their role. + */ + ACTIVE, + + /** + * The user account has been deactivated, either manually by an admin or + * automatically. The user cannot log in or access the system. + */ + INACTIVE, + + /** + * The user is temporarily on leave. This status can be used to disable + * task assignments or notifications without fully deactivating the account. + */ + ON_LEAVE } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/model/package-info.java b/ems-backend/src/main/java/com/dne/ems/model/package-info.java index 502dd23..061dc00 100644 --- a/ems-backend/src/main/java/com/dne/ems/model/package-info.java +++ b/ems-backend/src/main/java/com/dne/ems/model/package-info.java @@ -1,24 +1,24 @@ -/** - * 这个包包含所有的实体模型类。 - * - * 注意:我们使用自定义的空JPA注解来保持代码兼容性,而不是实际的JPA注解。 - * 这些注解不会有任何实际效果,仅用于编译通过。 - * - * @see com.dne.ems.config.EmptyJpaAnnotations - */ -package com.dne.ems.model; -// 导入我们的空JPA注解,替代原来的JPA注解 -// import com.dne.ems.config.EmptyJpaAnnotations.Entity; -// import com.dne.ems.config.EmptyJpaAnnotations.Table; -// import com.dne.ems.config.EmptyJpaAnnotations.Column; -// import com.dne.ems.config.EmptyJpaAnnotations.Id; -// import com.dne.ems.config.EmptyJpaAnnotations.GeneratedValue; -// import com.dne.ems.config.EmptyJpaAnnotations.ManyToOne; -// import com.dne.ems.config.EmptyJpaAnnotations.OneToMany; -// import com.dne.ems.config.EmptyJpaAnnotations.OneToOne; -// import com.dne.ems.config.EmptyJpaAnnotations.ManyToMany; -// import com.dne.ems.config.EmptyJpaAnnotations.JoinColumn; -// import com.dne.ems.config.EmptyJpaAnnotations.GenerationType; -// import com.dne.ems.config.EmptyJpaAnnotations.FetchType; -// import com.dne.ems.config.EmptyJpaAnnotations.CascadeType; -// import com.dne.ems.config.EmptyJpaAnnotations.UniqueConstraint; +/** + * 这个包包含所有的实体模型类。 + * + * 注意:我们使用自定义的空JPA注解来保持代码兼容性,而不是实际的JPA注解。 + * 这些注解不会有任何实际效果,仅用于编译通过。 + * + * @see com.dne.ems.config.EmptyJpaAnnotations + */ +package com.dne.ems.model; +// 导入我们的空JPA注解,替代原来的JPA注解 +// import com.dne.ems.config.EmptyJpaAnnotations.Entity; +// import com.dne.ems.config.EmptyJpaAnnotations.Table; +// import com.dne.ems.config.EmptyJpaAnnotations.Column; +// import com.dne.ems.config.EmptyJpaAnnotations.Id; +// import com.dne.ems.config.EmptyJpaAnnotations.GeneratedValue; +// import com.dne.ems.config.EmptyJpaAnnotations.ManyToOne; +// import com.dne.ems.config.EmptyJpaAnnotations.OneToMany; +// import com.dne.ems.config.EmptyJpaAnnotations.OneToOne; +// import com.dne.ems.config.EmptyJpaAnnotations.ManyToMany; +// import com.dne.ems.config.EmptyJpaAnnotations.JoinColumn; +// import com.dne.ems.config.EmptyJpaAnnotations.GenerationType; +// import com.dne.ems.config.EmptyJpaAnnotations.FetchType; +// import com.dne.ems.config.EmptyJpaAnnotations.CascadeType; +// import com.dne.ems.config.EmptyJpaAnnotations.UniqueConstraint; diff --git a/ems-backend/src/main/java/com/dne/ems/repository/AqiDataRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/AqiDataRepository.java index f66ee4b..1f1a96f 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/AqiDataRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/AqiDataRepository.java @@ -1,28 +1,28 @@ -package com.dne.ems.repository; - -import com.dne.ems.dto.AqiDistributionDTO; -import com.dne.ems.dto.AqiHeatmapPointDTO; -import com.dne.ems.dto.TrendDataPointDTO; -import com.dne.ems.model.AqiData; - -import java.time.LocalDateTime; -import java.util.List; - -public interface AqiDataRepository { - - AqiData save(AqiData aqiData); - - List saveAll(Iterable aqiData); - - void deleteAll(); - - List findAll(); - - long count(); - - List getAqiDistribution(); - - List getMonthlyExceedanceTrend(LocalDateTime startDate); - - List getAqiHeatmapData(); +package com.dne.ems.repository; + +import com.dne.ems.dto.AqiDistributionDTO; +import com.dne.ems.dto.AqiHeatmapPointDTO; +import com.dne.ems.dto.TrendDataPointDTO; +import com.dne.ems.model.AqiData; + +import java.time.LocalDateTime; +import java.util.List; + +public interface AqiDataRepository { + + AqiData save(AqiData aqiData); + + List saveAll(Iterable aqiData); + + void deleteAll(); + + List findAll(); + + long count(); + + List getAqiDistribution(); + + List getMonthlyExceedanceTrend(LocalDateTime startDate); + + List getAqiHeatmapData(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/AqiRecordRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/AqiRecordRepository.java index 2e05d57..15b2a91 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/AqiRecordRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/AqiRecordRepository.java @@ -1,39 +1,39 @@ -package com.dne.ems.repository; - -import java.time.LocalDateTime; -import java.util.List; - -import com.dne.ems.dto.AqiDistributionDTO; -import com.dne.ems.dto.AqiHeatmapPointDTO; -import com.dne.ems.model.AqiRecord; - -public interface AqiRecordRepository { - - AqiRecord save(AqiRecord record); - - List saveAll(Iterable records); - - void deleteAll(); - - long count(); - - List getAqiDistribution(); - - List findExceedanceRecordsSince(LocalDateTime startDate); - - List findExceedanceRecordsWithThresholds( - LocalDateTime startDate, - Double aqiThreshold, - Double pm25Threshold, - Double o3Threshold, - Double no2Threshold, - Double so2Threshold); - - List findByRecordTimeAfter(LocalDateTime startDate); - - List getAqiHeatmapData(); - - List getAqiHeatmapDataRaw(); - - List findAll(); +package com.dne.ems.repository; + +import java.time.LocalDateTime; +import java.util.List; + +import com.dne.ems.dto.AqiDistributionDTO; +import com.dne.ems.dto.AqiHeatmapPointDTO; +import com.dne.ems.model.AqiRecord; + +public interface AqiRecordRepository { + + AqiRecord save(AqiRecord record); + + List saveAll(Iterable records); + + void deleteAll(); + + long count(); + + List getAqiDistribution(); + + List findExceedanceRecordsSince(LocalDateTime startDate); + + List findExceedanceRecordsWithThresholds( + LocalDateTime startDate, + Double aqiThreshold, + Double pm25Threshold, + Double o3Threshold, + Double no2Threshold, + Double so2Threshold); + + List findByRecordTimeAfter(LocalDateTime startDate); + + List getAqiHeatmapData(); + + List getAqiHeatmapDataRaw(); + + List findAll(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/AssignmentRecordRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/AssignmentRecordRepository.java index aaffed4..fe63f1b 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/AssignmentRecordRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/AssignmentRecordRepository.java @@ -1,18 +1,18 @@ -package com.dne.ems.repository; - -import com.dne.ems.model.AssignmentRecord; -import java.util.List; -import java.util.Optional; - -public interface AssignmentRecordRepository { - - AssignmentRecord save(AssignmentRecord record); - - List saveAll(Iterable records); - - Optional findById(Long id); - - List findAll(); - - void deleteAll(); +package com.dne.ems.repository; + +import com.dne.ems.model.AssignmentRecord; +import java.util.List; +import java.util.Optional; + +public interface AssignmentRecordRepository { + + AssignmentRecord save(AssignmentRecord record); + + List saveAll(Iterable records); + + Optional findById(Long id); + + List findAll(); + + void deleteAll(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/AssignmentRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/AssignmentRepository.java index c9d5125..eb40e10 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/AssignmentRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/AssignmentRepository.java @@ -1,13 +1,13 @@ -package com.dne.ems.repository; - -import com.dne.ems.model.Assignment; -import java.util.List; - -public interface AssignmentRepository { - - Assignment save(Assignment assignment); - - void deleteAll(); - - List findByTaskAssigneeId(Long assigneeId); +package com.dne.ems.repository; + +import com.dne.ems.model.Assignment; +import java.util.List; + +public interface AssignmentRepository { + + Assignment save(Assignment assignment); + + void deleteAll(); + + List findByTaskAssigneeId(Long assigneeId); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/AttachmentRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/AttachmentRepository.java index 8180f1d..383b940 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/AttachmentRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/AttachmentRepository.java @@ -1,19 +1,19 @@ -package com.dne.ems.repository; - -import com.dne.ems.model.Attachment; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.TaskSubmission; - -import java.util.List; -import java.util.Optional; - -public interface AttachmentRepository { - - Attachment save(Attachment attachment); - - Optional findByStoredFileName(String storedFileName); - - List findByTaskSubmission(TaskSubmission taskSubmission); - - List findByFeedback(Feedback feedback); +package com.dne.ems.repository; + +import com.dne.ems.model.Attachment; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.TaskSubmission; + +import java.util.List; +import java.util.Optional; + +public interface AttachmentRepository { + + Attachment save(Attachment attachment); + + Optional findByStoredFileName(String storedFileName); + + List findByTaskSubmission(TaskSubmission taskSubmission); + + List findByFeedback(Feedback feedback); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/FeedbackRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/FeedbackRepository.java index 3a7b14f..4b1710f 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/FeedbackRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/FeedbackRepository.java @@ -1,44 +1,44 @@ -package com.dne.ems.repository; - -import com.dne.ems.dto.HeatmapPointDTO; -import com.dne.ems.dto.PollutionStatsDTO; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.enums.FeedbackStatus; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.util.List; -import java.util.Optional; - -public interface FeedbackRepository { - - Feedback save(Feedback feedback); - - List saveAll(Iterable feedbacks); - - Optional findById(Long id); - - List findAll(); - - void deleteAll(); - - long count(); - - Optional findByEventId(String eventId); - - List findByStatus(FeedbackStatus status); - - long countByStatus(FeedbackStatus status); - - List getHeatmapData(); - - List countByPollutionType(); - - Page findBySubmitterId(Long submitterId, Pageable pageable); - - List findBySubmitterId(Long submitterId); - - long countBySubmitterId(Long submitterId); - - long countByStatusAndSubmitterId(FeedbackStatus status, Long submitterId); +package com.dne.ems.repository; + +import com.dne.ems.dto.HeatmapPointDTO; +import com.dne.ems.dto.PollutionStatsDTO; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.enums.FeedbackStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +public interface FeedbackRepository { + + Feedback save(Feedback feedback); + + List saveAll(Iterable feedbacks); + + Optional findById(Long id); + + List findAll(); + + void deleteAll(); + + long count(); + + Optional findByEventId(String eventId); + + List findByStatus(FeedbackStatus status); + + long countByStatus(FeedbackStatus status); + + List getHeatmapData(); + + List countByPollutionType(); + + Page findBySubmitterId(Long submitterId, Pageable pageable); + + List findBySubmitterId(Long submitterId); + + long countBySubmitterId(Long submitterId); + + long countByStatusAndSubmitterId(FeedbackStatus status, Long submitterId); } // End of FeedbackRepository interface \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/GridRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/GridRepository.java index bd99fc2..24e0005 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/GridRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/GridRepository.java @@ -1,81 +1,81 @@ -package com.dne.ems.repository; - -import java.util.List; -import java.util.Optional; - -import com.dne.ems.model.Grid; - -/** - * Repository interface for accessing Grid data. - * This acts as an abstraction layer over the data source, - * which can be a database (JPA) or a JSON file. - */ -public interface GridRepository { - - /** - * Retrieves all grid cells. - * - * @return a list of all Grid cells. - */ - List findAll(); - - /** - * Retrieves grid coverage statistics by city. - * - * @return a list of {@link com.dne.ems.dto.GridCoverageDTO} objects. - */ - List getGridCoverageByCity(); - - /** - * Saves a given grid entity. - * Use the returned instance for further operations as the save operation might have changed the - * entity instance completely. - * - * @param grid must not be {@literal null}. - * @return the saved entity; will never be {@literal null}. - */ - Grid save(Grid grid); - - /** - * Saves all given entities. - * - * @param entities must not be {@literal null} nor contain {@literal null}. - * @return the saved entities; will never be {@literal null}. The returned {@link List} will have the same size as the - * input {@link Iterable}. - * @throws IllegalArgumentException in case the given {@link Iterable entities} or one of its entities is - * {@literal null}. - */ - List saveAll(Iterable entities); - - /** - * Finds a grid cell by its ID. - * - * @param id The ID of the grid. - * @return An {@link Optional} containing the found grid cell, or {@link Optional#empty()} if not found. - */ - Optional findById(Long id); - - /** - * Finds a grid cell by its X and Y coordinates. - * - * @param gridX The X coordinate of the grid. - * @param gridY The Y coordinate of the grid. - * @return An {@link Optional} containing the found grid cell, or {@link Optional#empty()} if not found. - */ - Optional findByGridXAndGridY(Integer gridX, Integer gridY); - - /** - * Finds all grid cells by their obstacle status. - * - * @param isObstacle The obstacle status to search for. - * @return A list of grids matching the obstacle status. - */ - List findByIsObstacle(boolean isObstacle); - - /** - * Returns the number of entities available. - * - * @return the number of entities. - */ - long count(); +package com.dne.ems.repository; + +import java.util.List; +import java.util.Optional; + +import com.dne.ems.model.Grid; + +/** + * Repository interface for accessing Grid data. + * This acts as an abstraction layer over the data source, + * which can be a database (JPA) or a JSON file. + */ +public interface GridRepository { + + /** + * Retrieves all grid cells. + * + * @return a list of all Grid cells. + */ + List findAll(); + + /** + * Retrieves grid coverage statistics by city. + * + * @return a list of {@link com.dne.ems.dto.GridCoverageDTO} objects. + */ + List getGridCoverageByCity(); + + /** + * Saves a given grid entity. + * Use the returned instance for further operations as the save operation might have changed the + * entity instance completely. + * + * @param grid must not be {@literal null}. + * @return the saved entity; will never be {@literal null}. + */ + Grid save(Grid grid); + + /** + * Saves all given entities. + * + * @param entities must not be {@literal null} nor contain {@literal null}. + * @return the saved entities; will never be {@literal null}. The returned {@link List} will have the same size as the + * input {@link Iterable}. + * @throws IllegalArgumentException in case the given {@link Iterable entities} or one of its entities is + * {@literal null}. + */ + List saveAll(Iterable entities); + + /** + * Finds a grid cell by its ID. + * + * @param id The ID of the grid. + * @return An {@link Optional} containing the found grid cell, or {@link Optional#empty()} if not found. + */ + Optional findById(Long id); + + /** + * Finds a grid cell by its X and Y coordinates. + * + * @param gridX The X coordinate of the grid. + * @param gridY The Y coordinate of the grid. + * @return An {@link Optional} containing the found grid cell, or {@link Optional#empty()} if not found. + */ + Optional findByGridXAndGridY(Integer gridX, Integer gridY); + + /** + * Finds all grid cells by their obstacle status. + * + * @param isObstacle The obstacle status to search for. + * @return A list of grids matching the obstacle status. + */ + List findByIsObstacle(boolean isObstacle); + + /** + * Returns the number of entities available. + * + * @return the number of entities. + */ + long count(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/MapGridRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/MapGridRepository.java index ed03e84..8d83f6f 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/MapGridRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/MapGridRepository.java @@ -1,26 +1,26 @@ -package com.dne.ems.repository; - -import com.dne.ems.model.MapGrid; -import java.util.List; -import java.util.Optional; - -public interface MapGridRepository { - - MapGrid save(MapGrid mapGrid); - - List saveAll(Iterable mapGrids); - - Optional findById(Long id); - - List findAll(); - - long count(); - - void deleteById(Long id); - - void deleteAll(); - - Optional findByXAndY(int x, int y); - - List findAllByOrderByYAscXAsc(); +package com.dne.ems.repository; + +import com.dne.ems.model.MapGrid; +import java.util.List; +import java.util.Optional; + +public interface MapGridRepository { + + MapGrid save(MapGrid mapGrid); + + List saveAll(Iterable mapGrids); + + Optional findById(Long id); + + List findAll(); + + long count(); + + void deleteById(Long id); + + void deleteAll(); + + Optional findByXAndY(int x, int y); + + List findAllByOrderByYAscXAsc(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/OperationLogRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/OperationLogRepository.java index a038ccd..ba961a4 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/OperationLogRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/OperationLogRepository.java @@ -1,88 +1,88 @@ -package com.dne.ems.repository; - -import java.time.LocalDateTime; -import java.util.List; - -import com.dne.ems.model.OperationLog; -import com.dne.ems.model.enums.OperationType; - -/** - * 操作日志仓库接口,提供操作日志的CRUD操作。 - * - * @version 1.0 - * @since 2025-06-20 - */ -public interface OperationLogRepository { - - /** - * 保存操作日志 - * - * @param operationLog 操作日志实体 - * @return 保存后的操作日志实体 - */ - OperationLog save(OperationLog operationLog); - - /** - * 根据ID查找操作日志 - * - * @param id 操作日志ID - * @return 操作日志实体,如果不存在则返回null - */ - OperationLog findById(Long id); - - /** - * 查找所有操作日志 - * - * @return 操作日志列表 - */ - List findAll(); - - /** - * 根据用户ID查找操作日志 - * - * @param userId 用户ID - * @return 该用户的操作日志列表 - */ - List findByUserId(Long userId); - - /** - * 根据操作类型查找操作日志 - * - * @param operationType 操作类型 - * @return 该类型的操作日志列表 - */ - List findByOperationType(OperationType operationType); - - /** - * 根据时间范围查找操作日志 - * - * @param startTime 开始时间 - * @param endTime 结束时间 - * @return 该时间范围内的操作日志列表 - */ - List findByTimeRange(LocalDateTime startTime, LocalDateTime endTime); - - /** - * 根据条件查询操作日志 - * - * @param userId 用户ID,可为null - * @param operationType 操作类型,可为null - * @param startTime 开始时间,可为null - * @param endTime 结束时间,可为null - * @return 符合条件的操作日志列表 - */ - List findByConditions(Long userId, OperationType operationType, - LocalDateTime startTime, LocalDateTime endTime); - - /** - * 删除操作日志 - * - * @param id 操作日志ID - */ - void deleteById(Long id); - - /** - * 删除所有操作日志 - */ - void deleteAll(); +package com.dne.ems.repository; + +import java.time.LocalDateTime; +import java.util.List; + +import com.dne.ems.model.OperationLog; +import com.dne.ems.model.enums.OperationType; + +/** + * 操作日志仓库接口,提供操作日志的CRUD操作。 + * + * @version 1.0 + * @since 2025-06-20 + */ +public interface OperationLogRepository { + + /** + * 保存操作日志 + * + * @param operationLog 操作日志实体 + * @return 保存后的操作日志实体 + */ + OperationLog save(OperationLog operationLog); + + /** + * 根据ID查找操作日志 + * + * @param id 操作日志ID + * @return 操作日志实体,如果不存在则返回null + */ + OperationLog findById(Long id); + + /** + * 查找所有操作日志 + * + * @return 操作日志列表 + */ + List findAll(); + + /** + * 根据用户ID查找操作日志 + * + * @param userId 用户ID + * @return 该用户的操作日志列表 + */ + List findByUserId(Long userId); + + /** + * 根据操作类型查找操作日志 + * + * @param operationType 操作类型 + * @return 该类型的操作日志列表 + */ + List findByOperationType(OperationType operationType); + + /** + * 根据时间范围查找操作日志 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 该时间范围内的操作日志列表 + */ + List findByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + /** + * 根据条件查询操作日志 + * + * @param userId 用户ID,可为null + * @param operationType 操作类型,可为null + * @param startTime 开始时间,可为null + * @param endTime 结束时间,可为null + * @return 符合条件的操作日志列表 + */ + List findByConditions(Long userId, OperationType operationType, + LocalDateTime startTime, LocalDateTime endTime); + + /** + * 删除操作日志 + * + * @param id 操作日志ID + */ + void deleteById(Long id); + + /** + * 删除所有操作日志 + */ + void deleteAll(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/PasswordResetTokenRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/PasswordResetTokenRepository.java index 16ba033..ea58c11 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/PasswordResetTokenRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/PasswordResetTokenRepository.java @@ -1,17 +1,17 @@ -package com.dne.ems.repository; - -import com.dne.ems.model.PasswordResetToken; -import com.dne.ems.model.UserAccount; - -import java.util.Optional; - -public interface PasswordResetTokenRepository { - - PasswordResetToken save(PasswordResetToken token); - - Optional findByToken(String token); - - Optional findByUser(UserAccount user); - - void delete(PasswordResetToken token); +package com.dne.ems.repository; + +import com.dne.ems.model.PasswordResetToken; +import com.dne.ems.model.UserAccount; + +import java.util.Optional; + +public interface PasswordResetTokenRepository { + + PasswordResetToken save(PasswordResetToken token); + + Optional findByToken(String token); + + Optional findByUser(UserAccount user); + + void delete(PasswordResetToken token); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/PollutantThresholdRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/PollutantThresholdRepository.java index 93895c5..886addd 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/PollutantThresholdRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/PollutantThresholdRepository.java @@ -1,16 +1,16 @@ -package com.dne.ems.repository; - -import com.dne.ems.model.PollutantThreshold; -import java.util.List; -import java.util.Optional; - -public interface PollutantThresholdRepository { - - PollutantThreshold save(PollutantThreshold threshold); - - List saveAll(Iterable thresholds); - - List findAll(); - - Optional findByPollutantName(String pollutantName); +package com.dne.ems.repository; + +import com.dne.ems.model.PollutantThreshold; +import java.util.List; +import java.util.Optional; + +public interface PollutantThresholdRepository { + + PollutantThreshold save(PollutantThreshold threshold); + + List saveAll(Iterable thresholds); + + List findAll(); + + Optional findByPollutantName(String pollutantName); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/TaskHistoryRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/TaskHistoryRepository.java index d6c79a6..e4bbc9e 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/TaskHistoryRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/TaskHistoryRepository.java @@ -1,14 +1,14 @@ -package com.dne.ems.repository; - -import java.util.List; - -import com.dne.ems.model.TaskHistory; - -public interface TaskHistoryRepository { - - TaskHistory save(TaskHistory taskHistory); - - List findByTaskIdOrderByChangedAtDesc(Long taskId); - - void deleteAll(); +package com.dne.ems.repository; + +import java.util.List; + +import com.dne.ems.model.TaskHistory; + +public interface TaskHistoryRepository { + + TaskHistory save(TaskHistory taskHistory); + + List findByTaskIdOrderByChangedAtDesc(Long taskId); + + void deleteAll(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/TaskRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/TaskRepository.java index 2edba9f..9e16408 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/TaskRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/TaskRepository.java @@ -1,36 +1,36 @@ -package com.dne.ems.repository; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -import com.dne.ems.model.Feedback; -import com.dne.ems.model.Task; -import com.dne.ems.model.enums.TaskStatus; - -public interface TaskRepository { - - Optional findById(Long id); - - List findAll(); - - Task save(Task task); - - List saveAll(Iterable tasks); - - void deleteById(Long id); - - long count(); - - boolean existsById(Long id); - - void deleteAll(); - - Optional findByFeedback(Feedback feedback); - - long countByStatus(TaskStatus status); - - long countByAssigneeIdAndStatusIn(Long assigneeId, Collection statuses); - - List findByAssigneeIdAndStatus(Long assigneeId, TaskStatus status); +package com.dne.ems.repository; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import com.dne.ems.model.Feedback; +import com.dne.ems.model.Task; +import com.dne.ems.model.enums.TaskStatus; + +public interface TaskRepository { + + Optional findById(Long id); + + List findAll(); + + Task save(Task task); + + List saveAll(Iterable tasks); + + void deleteById(Long id); + + long count(); + + boolean existsById(Long id); + + void deleteAll(); + + Optional findByFeedback(Feedback feedback); + + long countByStatus(TaskStatus status); + + long countByAssigneeIdAndStatusIn(Long assigneeId, Collection statuses); + + List findByAssigneeIdAndStatus(Long assigneeId, TaskStatus status); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/TaskSubmissionRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/TaskSubmissionRepository.java index 19ea82b..5ae2a2d 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/TaskSubmissionRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/TaskSubmissionRepository.java @@ -1,25 +1,25 @@ -package com.dne.ems.repository; - -import com.dne.ems.model.Task; -import com.dne.ems.model.TaskSubmission; - -import java.util.List; - -public interface TaskSubmissionRepository { - - TaskSubmission save(TaskSubmission submission); - - List findByTaskId(Long taskId); - - /** - * Finds the most recent task submission for a given task. - * - * @param task The task entity to find the submission for. - * @return The latest TaskSubmission, or null if none exists. - */ - TaskSubmission findFirstByTaskOrderBySubmittedAtDesc(Task task); - - List findAll(); - - void deleteAll(); +package com.dne.ems.repository; + +import com.dne.ems.model.Task; +import com.dne.ems.model.TaskSubmission; + +import java.util.List; + +public interface TaskSubmissionRepository { + + TaskSubmission save(TaskSubmission submission); + + List findByTaskId(Long taskId); + + /** + * Finds the most recent task submission for a given task. + * + * @param task The task entity to find the submission for. + * @return The latest TaskSubmission, or null if none exists. + */ + TaskSubmission findFirstByTaskOrderBySubmittedAtDesc(Task task); + + List findAll(); + + void deleteAll(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/UserAccountRepository.java b/ems-backend/src/main/java/com/dne/ems/repository/UserAccountRepository.java index 5ad1a0f..7d7c3b0 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/UserAccountRepository.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/UserAccountRepository.java @@ -1,42 +1,42 @@ -package com.dne.ems.repository; - -import java.util.List; -import java.util.Optional; - -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.Role; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -/** - * Repository interface for accessing UserAccount data. - * This acts as an abstraction layer over the data source. - */ -public interface UserAccountRepository { - - Optional findByEmail(String email); - - Optional findByPhone(String phone); - - Optional findById(Long id); - - List findByRole(Role role); - - List findGridWorkersByCoordinates(Integer gridX, Integer gridY); - - Optional findByGridXAndGridY(Integer gridX, Integer gridY); - - List findAll(); - - UserAccount save(UserAccount userAccount); - - List saveAll(Iterable entities); - - long countByRole(Role role); - - long count(); - - void deleteById(Long id); - - Page findUsers(Role role, String name, Pageable pageable); +package com.dne.ems.repository; + +import java.util.List; +import java.util.Optional; + +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.Role; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +/** + * Repository interface for accessing UserAccount data. + * This acts as an abstraction layer over the data source. + */ +public interface UserAccountRepository { + + Optional findByEmail(String email); + + Optional findByPhone(String phone); + + Optional findById(Long id); + + List findByRole(Role role); + + List findGridWorkersByCoordinates(Integer gridX, Integer gridY); + + Optional findByGridXAndGridY(Integer gridX, Integer gridY); + + List findAll(); + + UserAccount save(UserAccount userAccount); + + List saveAll(Iterable entities); + + long countByRole(Role role); + + long count(); + + void deleteById(Long id); + + Page findUsers(Role role, String name, Pageable pageable); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAqiDataRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAqiDataRepositoryImpl.java index 4ba673a..78dca9b 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAqiDataRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAqiDataRepositoryImpl.java @@ -1,151 +1,151 @@ -package com.dne.ems.repository.impl; - -import com.dne.ems.dto.AqiDistributionDTO; -import com.dne.ems.dto.AqiHeatmapPointDTO; -import com.dne.ems.dto.TrendDataPointDTO; -import com.dne.ems.model.AqiData; -import com.dne.ems.repository.AqiDataRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -@Repository -@Primary -public class JsonAqiDataRepositoryImpl implements AqiDataRepository { - - private static final String FILE_NAME = "aqi_data.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonAqiDataRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(AqiData::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public AqiData save(AqiData aqiData) { - synchronized (cache) { - if (aqiData.getId() == null) { - aqiData.setId(idCounter.incrementAndGet()); - cache.add(aqiData); - } else { - cache.removeIf(a -> a.getId().equals(aqiData.getId())); - cache.add(aqiData); - } - jsonStorageService.writeData(FILE_NAME, cache); - return aqiData; - } - } - - @Override - public List saveAll(Iterable aqiData) { - synchronized (cache) { - List savedData = StreamSupport.stream(aqiData.spliterator(), false) - .map(this::save) - .collect(Collectors.toList()); - return savedData; - } - } - - @Override - public void deleteAll() { - synchronized(cache) { - cache.clear(); - jsonStorageService.writeData(FILE_NAME, cache); - } - } - - @Override - public List findAll() { - synchronized(cache) { - return new ArrayList<>(cache); - } - } - - @Override - public long count() { - synchronized(cache) { - return cache.size(); - } - } - - @Override - public List getAqiDistribution() { - synchronized (cache) { - return cache.stream() - .collect(Collectors.groupingBy(this::getAqiCategory, Collectors.counting())) - .entrySet().stream() - .map(entry -> new AqiDistributionDTO(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - } - } - - @Override - public List getMonthlyExceedanceTrend(LocalDateTime startDate) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); - synchronized (cache) { - List trendData = cache.stream() - .filter(a -> a.getAqiValue() > 100 && !a.getRecordTime().isBefore(startDate)) - .collect(Collectors.groupingBy( - a -> a.getRecordTime().format(formatter), - Collectors.counting() - )) - .entrySet().stream() - .map(entry -> new TrendDataPointDTO(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - - trendData.sort(Comparator.comparing(dto -> dto.getYearMonth())); - - return trendData; - } - } - - @Override - public List getAqiHeatmapData() { - synchronized (cache) { - return cache.stream() - .filter(a -> a.getGrid() != null && a.getGrid().getGridX() != null && a.getGrid().getGridY() != null) - .collect(Collectors.groupingBy( - a -> new AqiHeatmapPointDTO(a.getGrid().getGridX(), a.getGrid().getGridY(), 0.0), - Collectors.averagingDouble(AqiData::getAqiValue) - )) - .entrySet().stream() - .map(entry -> new AqiHeatmapPointDTO(entry.getKey().gridX(), entry.getKey().gridY(), entry.getValue())) - .collect(Collectors.toList()); - } - } - - private String getAqiCategory(AqiData data) { - int aqi = data.getAqiValue(); - if (aqi <= 50) return "Good"; - if (aqi <= 100) return "Moderate"; - if (aqi <= 150) return "Unhealthy for Sensitive Groups"; - if (aqi <= 200) return "Unhealthy"; - if (aqi <= 300) return "Very Unhealthy"; - return "Hazardous"; - } +package com.dne.ems.repository.impl; + +import com.dne.ems.dto.AqiDistributionDTO; +import com.dne.ems.dto.AqiHeatmapPointDTO; +import com.dne.ems.dto.TrendDataPointDTO; +import com.dne.ems.model.AqiData; +import com.dne.ems.repository.AqiDataRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@Repository +@Primary +public class JsonAqiDataRepositoryImpl implements AqiDataRepository { + + private static final String FILE_NAME = "aqi_data.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonAqiDataRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(AqiData::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public AqiData save(AqiData aqiData) { + synchronized (cache) { + if (aqiData.getId() == null) { + aqiData.setId(idCounter.incrementAndGet()); + cache.add(aqiData); + } else { + cache.removeIf(a -> a.getId().equals(aqiData.getId())); + cache.add(aqiData); + } + jsonStorageService.writeData(FILE_NAME, cache); + return aqiData; + } + } + + @Override + public List saveAll(Iterable aqiData) { + synchronized (cache) { + List savedData = StreamSupport.stream(aqiData.spliterator(), false) + .map(this::save) + .collect(Collectors.toList()); + return savedData; + } + } + + @Override + public void deleteAll() { + synchronized(cache) { + cache.clear(); + jsonStorageService.writeData(FILE_NAME, cache); + } + } + + @Override + public List findAll() { + synchronized(cache) { + return new ArrayList<>(cache); + } + } + + @Override + public long count() { + synchronized(cache) { + return cache.size(); + } + } + + @Override + public List getAqiDistribution() { + synchronized (cache) { + return cache.stream() + .collect(Collectors.groupingBy(this::getAqiCategory, Collectors.counting())) + .entrySet().stream() + .map(entry -> new AqiDistributionDTO(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + } + + @Override + public List getMonthlyExceedanceTrend(LocalDateTime startDate) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + synchronized (cache) { + List trendData = cache.stream() + .filter(a -> a.getAqiValue() > 100 && !a.getRecordTime().isBefore(startDate)) + .collect(Collectors.groupingBy( + a -> a.getRecordTime().format(formatter), + Collectors.counting() + )) + .entrySet().stream() + .map(entry -> new TrendDataPointDTO(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + + trendData.sort(Comparator.comparing(dto -> dto.getYearMonth())); + + return trendData; + } + } + + @Override + public List getAqiHeatmapData() { + synchronized (cache) { + return cache.stream() + .filter(a -> a.getGrid() != null && a.getGrid().getGridX() != null && a.getGrid().getGridY() != null) + .collect(Collectors.groupingBy( + a -> new AqiHeatmapPointDTO(a.getGrid().getGridX(), a.getGrid().getGridY(), 0.0), + Collectors.averagingDouble(AqiData::getAqiValue) + )) + .entrySet().stream() + .map(entry -> new AqiHeatmapPointDTO(entry.getKey().gridX(), entry.getKey().gridY(), entry.getValue())) + .collect(Collectors.toList()); + } + } + + private String getAqiCategory(AqiData data) { + int aqi = data.getAqiValue(); + if (aqi <= 50) return "Good"; + if (aqi <= 100) return "Moderate"; + if (aqi <= 150) return "Unhealthy for Sensitive Groups"; + if (aqi <= 200) return "Unhealthy"; + if (aqi <= 300) return "Very Unhealthy"; + return "Hazardous"; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAqiRecordRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAqiRecordRepositoryImpl.java index b7960e2..4cb1d78 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAqiRecordRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAqiRecordRepositoryImpl.java @@ -1,178 +1,178 @@ -package com.dne.ems.repository.impl; - -import com.dne.ems.dto.AqiDistributionDTO; -import com.dne.ems.dto.AqiHeatmapPointDTO; -import com.dne.ems.model.AqiRecord; -import com.dne.ems.repository.AqiRecordRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -@Repository -@Primary -public class JsonAqiRecordRepositoryImpl implements AqiRecordRepository { - - private static final String FILE_NAME = "aqi_records.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonAqiRecordRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(AqiRecord::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public List findAll() { - synchronized(cache) { - return new ArrayList<>(cache); - } - } - - @Override - public AqiRecord save(AqiRecord record) { - synchronized (cache) { - if (record.getId() == null) { - record.setId(idCounter.incrementAndGet()); - cache.add(record); - } else { - cache.removeIf(r -> r.getId().equals(record.getId())); - cache.add(record); - } - jsonStorageService.writeData(FILE_NAME, cache); - return record; - } - } - - @Override - public List saveAll(Iterable records) { - synchronized (cache) { - List savedRecords = StreamSupport.stream(records.spliterator(), false) - .map(this::save) - .collect(Collectors.toList()); - return savedRecords; - } - } - - @Override - public void deleteAll() { - synchronized (cache) { - cache.clear(); - jsonStorageService.writeData(FILE_NAME, cache); - } - } - - @Override - public long count() { - synchronized (cache) { - return cache.size(); - } - } - - @Override - public List getAqiDistribution() { - synchronized (cache) { - return cache.stream() - .collect(Collectors.groupingBy(this::getAqiCategory, Collectors.counting())) - .entrySet().stream() - .map(entry -> new AqiDistributionDTO(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - } - } - - private String getAqiCategory(AqiRecord record) { - int aqi = record.getAqiValue(); - if (aqi <= 50) return "Good"; - if (aqi <= 100) return "Moderate"; - if (aqi <= 150) return "Unhealthy for Sensitive Groups"; - if (aqi <= 200) return "Unhealthy"; - if (aqi <= 300) return "Very Unhealthy"; - return "Hazardous"; - } - - @Override - public List findExceedanceRecordsSince(LocalDateTime startDate) { - synchronized (cache) { - return cache.stream() - .filter(r -> r.getAqiValue() > 100 && !r.getRecordTime().isBefore(startDate)) - .collect(Collectors.toList()); - } - } - - @Override - public List findExceedanceRecordsWithThresholds(LocalDateTime startDate, Double aqiThreshold, Double pm25Threshold, Double o3Threshold, Double no2Threshold, Double so2Threshold) { - synchronized (cache) { - return cache.stream() - .filter(r -> !r.getRecordTime().isBefore(startDate)) - .filter(r -> r.getAqiValue() > aqiThreshold || - (r.getPm25() != null && r.getPm25() > pm25Threshold) || - (r.getO3() != null && r.getO3() > o3Threshold) || - (r.getNo2() != null && r.getNo2() > no2Threshold) || - (r.getSo2() != null && r.getSo2() > so2Threshold)) - .collect(Collectors.toList()); - } - } - - @Override - public List findByRecordTimeAfter(LocalDateTime startDate) { - synchronized (cache) { - return cache.stream() - .filter(r -> !r.getRecordTime().isBefore(startDate)) - .collect(Collectors.toList()); - } - } - - @Override - public List getAqiHeatmapData() { - synchronized (cache) { - return cache.stream() - .filter(r -> r.getGridX() != null && r.getGridY() != null) - .collect(Collectors.groupingBy( - r -> new AqiHeatmapPointDTO(r.getGridX(), r.getGridY(), 0.0), - Collectors.averagingDouble(AqiRecord::getAqiValue) - )) - .entrySet().stream() - .map(entry -> new AqiHeatmapPointDTO(entry.getKey().gridX(), entry.getKey().gridY(), entry.getValue())) - .collect(Collectors.toList()); - } - } - - @Override - public List getAqiHeatmapDataRaw() { - // This is a bit tricky to replicate exactly as it returns Object[]. - // We'll return a List of arrays, where each array is [gridX, gridY, avgAqi]. - synchronized (cache) { - return cache.stream() - .filter(r -> r.getGridX() != null && r.getGridY() != null) - .collect(Collectors.groupingBy( - r -> Map.entry(r.getGridX(), r.getGridY()), - Collectors.averagingDouble(AqiRecord::getAqiValue) - )) - .entrySet().stream() - .map(entry -> new Object[]{entry.getKey().getKey(), entry.getKey().getValue(), entry.getValue()}) - .collect(Collectors.toList()); - } - } +package com.dne.ems.repository.impl; + +import com.dne.ems.dto.AqiDistributionDTO; +import com.dne.ems.dto.AqiHeatmapPointDTO; +import com.dne.ems.model.AqiRecord; +import com.dne.ems.repository.AqiRecordRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@Repository +@Primary +public class JsonAqiRecordRepositoryImpl implements AqiRecordRepository { + + private static final String FILE_NAME = "aqi_records.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonAqiRecordRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(AqiRecord::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public List findAll() { + synchronized(cache) { + return new ArrayList<>(cache); + } + } + + @Override + public AqiRecord save(AqiRecord record) { + synchronized (cache) { + if (record.getId() == null) { + record.setId(idCounter.incrementAndGet()); + cache.add(record); + } else { + cache.removeIf(r -> r.getId().equals(record.getId())); + cache.add(record); + } + jsonStorageService.writeData(FILE_NAME, cache); + return record; + } + } + + @Override + public List saveAll(Iterable records) { + synchronized (cache) { + List savedRecords = StreamSupport.stream(records.spliterator(), false) + .map(this::save) + .collect(Collectors.toList()); + return savedRecords; + } + } + + @Override + public void deleteAll() { + synchronized (cache) { + cache.clear(); + jsonStorageService.writeData(FILE_NAME, cache); + } + } + + @Override + public long count() { + synchronized (cache) { + return cache.size(); + } + } + + @Override + public List getAqiDistribution() { + synchronized (cache) { + return cache.stream() + .collect(Collectors.groupingBy(this::getAqiCategory, Collectors.counting())) + .entrySet().stream() + .map(entry -> new AqiDistributionDTO(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + } + + private String getAqiCategory(AqiRecord record) { + int aqi = record.getAqiValue(); + if (aqi <= 50) return "Good"; + if (aqi <= 100) return "Moderate"; + if (aqi <= 150) return "Unhealthy for Sensitive Groups"; + if (aqi <= 200) return "Unhealthy"; + if (aqi <= 300) return "Very Unhealthy"; + return "Hazardous"; + } + + @Override + public List findExceedanceRecordsSince(LocalDateTime startDate) { + synchronized (cache) { + return cache.stream() + .filter(r -> r.getAqiValue() > 100 && !r.getRecordTime().isBefore(startDate)) + .collect(Collectors.toList()); + } + } + + @Override + public List findExceedanceRecordsWithThresholds(LocalDateTime startDate, Double aqiThreshold, Double pm25Threshold, Double o3Threshold, Double no2Threshold, Double so2Threshold) { + synchronized (cache) { + return cache.stream() + .filter(r -> !r.getRecordTime().isBefore(startDate)) + .filter(r -> r.getAqiValue() > aqiThreshold || + (r.getPm25() != null && r.getPm25() > pm25Threshold) || + (r.getO3() != null && r.getO3() > o3Threshold) || + (r.getNo2() != null && r.getNo2() > no2Threshold) || + (r.getSo2() != null && r.getSo2() > so2Threshold)) + .collect(Collectors.toList()); + } + } + + @Override + public List findByRecordTimeAfter(LocalDateTime startDate) { + synchronized (cache) { + return cache.stream() + .filter(r -> !r.getRecordTime().isBefore(startDate)) + .collect(Collectors.toList()); + } + } + + @Override + public List getAqiHeatmapData() { + synchronized (cache) { + return cache.stream() + .filter(r -> r.getGridX() != null && r.getGridY() != null) + .collect(Collectors.groupingBy( + r -> new AqiHeatmapPointDTO(r.getGridX(), r.getGridY(), 0.0), + Collectors.averagingDouble(AqiRecord::getAqiValue) + )) + .entrySet().stream() + .map(entry -> new AqiHeatmapPointDTO(entry.getKey().gridX(), entry.getKey().gridY(), entry.getValue())) + .collect(Collectors.toList()); + } + } + + @Override + public List getAqiHeatmapDataRaw() { + // This is a bit tricky to replicate exactly as it returns Object[]. + // We'll return a List of arrays, where each array is [gridX, gridY, avgAqi]. + synchronized (cache) { + return cache.stream() + .filter(r -> r.getGridX() != null && r.getGridY() != null) + .collect(Collectors.groupingBy( + r -> Map.entry(r.getGridX(), r.getGridY()), + Collectors.averagingDouble(AqiRecord::getAqiValue) + )) + .entrySet().stream() + .map(entry -> new Object[]{entry.getKey().getKey(), entry.getKey().getValue(), entry.getValue()}) + .collect(Collectors.toList()); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAssignmentRecordRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAssignmentRecordRepositoryImpl.java index b9cfc52..adc2080 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAssignmentRecordRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAssignmentRecordRepositoryImpl.java @@ -1,90 +1,90 @@ -package com.dne.ems.repository.impl; - -import com.dne.ems.model.AssignmentRecord; -import com.dne.ems.repository.AssignmentRecordRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -@Repository -@Primary -public class JsonAssignmentRecordRepositoryImpl implements AssignmentRecordRepository { - - private static final String FILE_NAME = "assignment_records.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonAssignmentRecordRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(AssignmentRecord::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public AssignmentRecord save(AssignmentRecord record) { - synchronized (cache) { - if (record.getId() == null) { - record.setId(idCounter.incrementAndGet()); - cache.add(record); - } else { - cache.removeIf(r -> r.getId().equals(record.getId())); - cache.add(record); - } - jsonStorageService.writeData(FILE_NAME, cache); - return record; - } - } - - @Override - public List saveAll(Iterable records) { - synchronized (cache) { - List savedRecords = StreamSupport.stream(records.spliterator(), false) - .map(this::save) - .collect(Collectors.toList()); - return savedRecords; - } - } - - @Override - public Optional findById(Long id) { - synchronized (cache) { - return cache.stream().filter(r -> r.getId().equals(id)).findFirst(); - } - } - - @Override - public List findAll() { - synchronized (cache) { - return new ArrayList<>(cache); - } - } - - @Override - public void deleteAll() { - synchronized (cache) { - cache.clear(); - jsonStorageService.writeData(FILE_NAME, cache); - } - } +package com.dne.ems.repository.impl; + +import com.dne.ems.model.AssignmentRecord; +import com.dne.ems.repository.AssignmentRecordRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@Repository +@Primary +public class JsonAssignmentRecordRepositoryImpl implements AssignmentRecordRepository { + + private static final String FILE_NAME = "assignment_records.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonAssignmentRecordRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(AssignmentRecord::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public AssignmentRecord save(AssignmentRecord record) { + synchronized (cache) { + if (record.getId() == null) { + record.setId(idCounter.incrementAndGet()); + cache.add(record); + } else { + cache.removeIf(r -> r.getId().equals(record.getId())); + cache.add(record); + } + jsonStorageService.writeData(FILE_NAME, cache); + return record; + } + } + + @Override + public List saveAll(Iterable records) { + synchronized (cache) { + List savedRecords = StreamSupport.stream(records.spliterator(), false) + .map(this::save) + .collect(Collectors.toList()); + return savedRecords; + } + } + + @Override + public Optional findById(Long id) { + synchronized (cache) { + return cache.stream().filter(r -> r.getId().equals(id)).findFirst(); + } + } + + @Override + public List findAll() { + synchronized (cache) { + return new ArrayList<>(cache); + } + } + + @Override + public void deleteAll() { + synchronized (cache) { + cache.clear(); + jsonStorageService.writeData(FILE_NAME, cache); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAssignmentRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAssignmentRepositoryImpl.java index 6e44454..6fd4a61 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAssignmentRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAssignmentRepositoryImpl.java @@ -1,73 +1,73 @@ -package com.dne.ems.repository.impl; - -import com.dne.ems.model.Assignment; -import com.dne.ems.repository.AssignmentRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; - -@Repository -@Primary -public class JsonAssignmentRepositoryImpl implements AssignmentRepository { - - private static final String FILE_NAME = "assignments.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonAssignmentRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(Assignment::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public Assignment save(Assignment assignment) { - synchronized (cache) { - if (assignment.getId() == null) { - assignment.setId(idCounter.incrementAndGet()); - cache.add(assignment); - } else { - cache.removeIf(a -> a.getId().equals(assignment.getId())); - cache.add(assignment); - } - jsonStorageService.writeData(FILE_NAME, cache); - return assignment; - } - } - - @Override - public void deleteAll() { - synchronized (cache) { - cache.clear(); - jsonStorageService.writeData(FILE_NAME, cache); - } - } - - @Override - public List findByTaskAssigneeId(Long assigneeId) { - synchronized (cache) { - return cache.stream() - .filter(a -> a.getTask() != null && a.getTask().getAssignee() != null && a.getTask().getAssignee().getId().equals(assigneeId)) - .collect(Collectors.toList()); - } - } +package com.dne.ems.repository.impl; + +import com.dne.ems.model.Assignment; +import com.dne.ems.repository.AssignmentRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@Repository +@Primary +public class JsonAssignmentRepositoryImpl implements AssignmentRepository { + + private static final String FILE_NAME = "assignments.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonAssignmentRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(Assignment::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public Assignment save(Assignment assignment) { + synchronized (cache) { + if (assignment.getId() == null) { + assignment.setId(idCounter.incrementAndGet()); + cache.add(assignment); + } else { + cache.removeIf(a -> a.getId().equals(assignment.getId())); + cache.add(assignment); + } + jsonStorageService.writeData(FILE_NAME, cache); + return assignment; + } + } + + @Override + public void deleteAll() { + synchronized (cache) { + cache.clear(); + jsonStorageService.writeData(FILE_NAME, cache); + } + } + + @Override + public List findByTaskAssigneeId(Long assigneeId) { + synchronized (cache) { + return cache.stream() + .filter(a -> a.getTask() != null && a.getTask().getAssignee() != null && a.getTask().getAssignee().getId().equals(assigneeId)) + .collect(Collectors.toList()); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAttachmentRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAttachmentRepositoryImpl.java index 1dee7aa..1467b47 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAttachmentRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonAttachmentRepositoryImpl.java @@ -1,84 +1,84 @@ -package com.dne.ems.repository.impl; - -import com.dne.ems.model.Attachment; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.TaskSubmission; -import com.dne.ems.repository.AttachmentRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; - -@Repository -@Primary -public class JsonAttachmentRepositoryImpl implements AttachmentRepository { - - private static final String FILE_NAME = "attachments.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonAttachmentRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(Attachment::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public Attachment save(Attachment attachment) { - synchronized (cache) { - if (attachment.getId() == null) { - attachment.setId(idCounter.incrementAndGet()); - cache.add(attachment); - } else { - cache.removeIf(a -> a.getId().equals(attachment.getId())); - cache.add(attachment); - } - jsonStorageService.writeData(FILE_NAME, cache); - return attachment; - } - } - - @Override - public Optional findByStoredFileName(String storedFileName) { - synchronized (cache) { - return cache.stream().filter(a -> a.getStoredFileName().equals(storedFileName)).findFirst(); - } - } - - @Override - public List findByTaskSubmission(TaskSubmission taskSubmission) { - synchronized (cache) { - return cache.stream() - .filter(a -> a.getTaskSubmission() != null && a.getTaskSubmission().equals(taskSubmission)) - .collect(Collectors.toList()); - } - } - - @Override - public List findByFeedback(Feedback feedback) { - synchronized (cache) { - return cache.stream() - .filter(a -> a.getFeedback() != null && a.getFeedback().equals(feedback)) - .collect(Collectors.toList()); - } - } +package com.dne.ems.repository.impl; + +import com.dne.ems.model.Attachment; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.TaskSubmission; +import com.dne.ems.repository.AttachmentRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@Repository +@Primary +public class JsonAttachmentRepositoryImpl implements AttachmentRepository { + + private static final String FILE_NAME = "attachments.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonAttachmentRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(Attachment::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public Attachment save(Attachment attachment) { + synchronized (cache) { + if (attachment.getId() == null) { + attachment.setId(idCounter.incrementAndGet()); + cache.add(attachment); + } else { + cache.removeIf(a -> a.getId().equals(attachment.getId())); + cache.add(attachment); + } + jsonStorageService.writeData(FILE_NAME, cache); + return attachment; + } + } + + @Override + public Optional findByStoredFileName(String storedFileName) { + synchronized (cache) { + return cache.stream().filter(a -> a.getStoredFileName().equals(storedFileName)).findFirst(); + } + } + + @Override + public List findByTaskSubmission(TaskSubmission taskSubmission) { + synchronized (cache) { + return cache.stream() + .filter(a -> a.getTaskSubmission() != null && a.getTaskSubmission().equals(taskSubmission)) + .collect(Collectors.toList()); + } + } + + @Override + public List findByFeedback(Feedback feedback) { + synchronized (cache) { + return cache.stream() + .filter(a -> a.getFeedback() != null && a.getFeedback().equals(feedback)) + .collect(Collectors.toList()); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonFeedbackRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonFeedbackRepositoryImpl.java index b499dd1..b92733c 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonFeedbackRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonFeedbackRepositoryImpl.java @@ -1,197 +1,197 @@ -package com.dne.ems.repository.impl; - -import com.dne.ems.dto.HeatmapPointDTO; -import com.dne.ems.dto.PollutionStatsDTO; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.repository.FeedbackRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Primary; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; - -@Repository -@Primary -public class JsonFeedbackRepositoryImpl implements FeedbackRepository { - - private static final String FILE_NAME = "feedbacks.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonFeedbackRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(Feedback::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public Feedback save(Feedback feedback) { - synchronized (cache) { - if (feedback.getId() == null) { - feedback.setId(idCounter.incrementAndGet()); - cache.add(feedback); - } else { - cache.removeIf(f -> f.getId().equals(feedback.getId())); - cache.add(feedback); - } - jsonStorageService.writeData(FILE_NAME, cache); - return feedback; - } - } - - @Override - public List saveAll(Iterable feedbacks) { - synchronized (cache) { - List result = new ArrayList<>(); - for (Feedback feedback : feedbacks) { - result.add(save(feedback)); - } - // Note: save() writes to file on every call. For bulk operations, - // it would be more efficient to save all at once after the loop. - // But for now, we stick to the existing pattern. - return result; - } - } - - @Override - public Optional findById(Long id) { - synchronized (cache) { - return cache.stream().filter(f -> f.getId().equals(id)).findFirst(); - } - } - - @Override - public List findAll() { - synchronized (cache) { - return new ArrayList<>(cache); - } - } - - @Override - public void deleteAll() { - synchronized (cache) { - cache.clear(); - jsonStorageService.writeData(FILE_NAME, cache); - } - } - - @Override - public long count() { - synchronized (cache) { - return cache.size(); - } - } - - @Override - public Optional findByEventId(String eventId) { - synchronized (cache) { - return cache.stream().filter(f -> f.getEventId().equals(eventId)).findFirst(); - } - } - - @Override - public List findByStatus(FeedbackStatus status) { - synchronized (cache) { - return cache.stream().filter(f -> f.getStatus() == status).collect(Collectors.toList()); - } - } - - @Override - public long countByStatus(FeedbackStatus status) { - synchronized (cache) { - return cache.stream().filter(f -> f.getStatus() == status).count(); - } - } - - @Override - public List getHeatmapData() { - synchronized (cache) { - return cache.stream() - .filter(f -> f.getStatus() == FeedbackStatus.CONFIRMED && f.getGridX() != null && f.getGridY() != null) - .collect(Collectors.groupingBy( - f -> new HeatmapPointDTO(f.getGridX(), f.getGridY(), 0L), - Collectors.counting() - )) - .entrySet().stream() - .map(entry -> new HeatmapPointDTO(entry.getKey().gridX(), entry.getKey().gridY(), entry.getValue())) - .collect(Collectors.toList()); - } - } - - @Override - public List countByPollutionType() { - synchronized (cache) { - return cache.stream() - .collect(Collectors.groupingBy(Feedback::getPollutionType, Collectors.counting())) - .entrySet().stream() - .map(entry -> new PollutionStatsDTO(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - } - } - - @Override - public long countBySubmitterId(Long submitterId) { - synchronized (cache) { - return cache.stream().filter(f -> Objects.equals(f.getSubmitterId(), submitterId)).count(); - } - } - - @Override - public long countByStatusAndSubmitterId(FeedbackStatus status, Long submitterId) { - synchronized (cache) { - return cache.stream() - .filter(f -> f.getStatus() == status && Objects.equals(f.getSubmitterId(), submitterId)) - .count(); - } - } - - @Override - public List findBySubmitterId(Long submitterId) { - synchronized (cache) { - return cache.stream() - .filter(f -> Objects.equals(f.getSubmitterId(), submitterId)) - .collect(Collectors.toList()); - } - } - - @Override - public Page findBySubmitterId(Long submitterId, Pageable pageable) { - synchronized (cache) { - List filteredFeedbacks = cache.stream() - .filter(f -> Objects.equals(f.getSubmitterId(), submitterId)) - .collect(Collectors.toList()); - - long total = filteredFeedbacks.size(); - - int start = (int) pageable.getOffset(); - int end = Math.min((start + pageable.getPageSize()), filteredFeedbacks.size()); - - List pageContent = (start > end) ? List.of() : filteredFeedbacks.subList(start, end); - - return new PageImpl<>(pageContent, pageable, total); - } - } +package com.dne.ems.repository.impl; + +import com.dne.ems.dto.HeatmapPointDTO; +import com.dne.ems.dto.PollutionStatsDTO; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.repository.FeedbackRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Primary; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +@Repository +@Primary +public class JsonFeedbackRepositoryImpl implements FeedbackRepository { + + private static final String FILE_NAME = "feedbacks.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonFeedbackRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(Feedback::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public Feedback save(Feedback feedback) { + synchronized (cache) { + if (feedback.getId() == null) { + feedback.setId(idCounter.incrementAndGet()); + cache.add(feedback); + } else { + cache.removeIf(f -> f.getId().equals(feedback.getId())); + cache.add(feedback); + } + jsonStorageService.writeData(FILE_NAME, cache); + return feedback; + } + } + + @Override + public List saveAll(Iterable feedbacks) { + synchronized (cache) { + List result = new ArrayList<>(); + for (Feedback feedback : feedbacks) { + result.add(save(feedback)); + } + // Note: save() writes to file on every call. For bulk operations, + // it would be more efficient to save all at once after the loop. + // But for now, we stick to the existing pattern. + return result; + } + } + + @Override + public Optional findById(Long id) { + synchronized (cache) { + return cache.stream().filter(f -> f.getId().equals(id)).findFirst(); + } + } + + @Override + public List findAll() { + synchronized (cache) { + return new ArrayList<>(cache); + } + } + + @Override + public void deleteAll() { + synchronized (cache) { + cache.clear(); + jsonStorageService.writeData(FILE_NAME, cache); + } + } + + @Override + public long count() { + synchronized (cache) { + return cache.size(); + } + } + + @Override + public Optional findByEventId(String eventId) { + synchronized (cache) { + return cache.stream().filter(f -> f.getEventId().equals(eventId)).findFirst(); + } + } + + @Override + public List findByStatus(FeedbackStatus status) { + synchronized (cache) { + return cache.stream().filter(f -> f.getStatus() == status).collect(Collectors.toList()); + } + } + + @Override + public long countByStatus(FeedbackStatus status) { + synchronized (cache) { + return cache.stream().filter(f -> f.getStatus() == status).count(); + } + } + + @Override + public List getHeatmapData() { + synchronized (cache) { + return cache.stream() + .filter(f -> f.getStatus() == FeedbackStatus.CONFIRMED && f.getGridX() != null && f.getGridY() != null) + .collect(Collectors.groupingBy( + f -> new HeatmapPointDTO(f.getGridX(), f.getGridY(), 0L), + Collectors.counting() + )) + .entrySet().stream() + .map(entry -> new HeatmapPointDTO(entry.getKey().gridX(), entry.getKey().gridY(), entry.getValue())) + .collect(Collectors.toList()); + } + } + + @Override + public List countByPollutionType() { + synchronized (cache) { + return cache.stream() + .collect(Collectors.groupingBy(Feedback::getPollutionType, Collectors.counting())) + .entrySet().stream() + .map(entry -> new PollutionStatsDTO(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + } + + @Override + public long countBySubmitterId(Long submitterId) { + synchronized (cache) { + return cache.stream().filter(f -> Objects.equals(f.getSubmitterId(), submitterId)).count(); + } + } + + @Override + public long countByStatusAndSubmitterId(FeedbackStatus status, Long submitterId) { + synchronized (cache) { + return cache.stream() + .filter(f -> f.getStatus() == status && Objects.equals(f.getSubmitterId(), submitterId)) + .count(); + } + } + + @Override + public List findBySubmitterId(Long submitterId) { + synchronized (cache) { + return cache.stream() + .filter(f -> Objects.equals(f.getSubmitterId(), submitterId)) + .collect(Collectors.toList()); + } + } + + @Override + public Page findBySubmitterId(Long submitterId, Pageable pageable) { + synchronized (cache) { + List filteredFeedbacks = cache.stream() + .filter(f -> Objects.equals(f.getSubmitterId(), submitterId)) + .collect(Collectors.toList()); + + long total = filteredFeedbacks.size(); + + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), filteredFeedbacks.size()); + + List pageContent = (start > end) ? List.of() : filteredFeedbacks.subList(start, end); + + return new PageImpl<>(pageContent, pageable, total); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonGridRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonGridRepositoryImpl.java index 2ea2733..300d6aa 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonGridRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonGridRepositoryImpl.java @@ -1,129 +1,129 @@ -package com.dne.ems.repository.impl; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import com.dne.ems.dto.GridCoverageDTO; -import com.dne.ems.model.Grid; -import com.dne.ems.repository.GridRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; - -import jakarta.annotation.PostConstruct; - -@Repository -@Primary // 优先使用此实现,覆盖JPA -public class JsonGridRepositoryImpl implements GridRepository { - - private static final String FILE_NAME = "grids.json"; - private final JsonStorageService jsonStorageService; - private final List gridCache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonGridRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List grids = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (gridCache) { - gridCache.addAll(grids); - long maxId = grids.stream().mapToLong(Grid::getId).max().orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public List findAll() { - synchronized (gridCache) { - return new ArrayList<>(gridCache); - } - } - - @Override - public Grid save(Grid grid) { - synchronized (gridCache) { - if (grid.getId() == null) { - grid.setId(idCounter.incrementAndGet()); - gridCache.add(grid); - } else { - Optional existingGrid = gridCache.stream() - .filter(g -> g.getId().equals(grid.getId())) - .findFirst(); - if (existingGrid.isPresent()) { - gridCache.remove(existingGrid.get()); - gridCache.add(grid); - } else { - gridCache.add(grid); - } - } - jsonStorageService.writeData(FILE_NAME, gridCache); - return grid; - } - } - - @Override - public List saveAll(Iterable entities) { - List savedEntities; - synchronized (gridCache) { - savedEntities = StreamSupport.stream(entities.spliterator(), false) - .map(this::save) // Re-use the save logic for each entity - .collect(Collectors.toList()); - } - return savedEntities; - } - - @Override - public Optional findById(Long id) { - synchronized (gridCache) { - return gridCache.stream().filter(g -> g.getId().equals(id)).findFirst(); - } - } - - @Override - public Optional findByGridXAndGridY(Integer gridX, Integer gridY) { - synchronized (gridCache) { - return gridCache.stream() - .filter(g -> g.getGridX().equals(gridX) && g.getGridY().equals(gridY)) - .findFirst(); - } - } - - @Override - public List findByIsObstacle(boolean isObstacle) { - synchronized (gridCache) { - return gridCache.stream() - .filter(g -> g.getIsObstacle() != null && g.getIsObstacle() == isObstacle) - .collect(Collectors.toList()); - } - } - - @Override - public long count() { - synchronized (gridCache) { - return gridCache.size(); - } - } - - @Override - public List getGridCoverageByCity() { - synchronized (gridCache) { - return gridCache.stream() - .filter(grid -> grid.getCityName() != null && !grid.getCityName().isEmpty()) - .collect(Collectors.groupingBy(Grid::getCityName, Collectors.counting())) - .entrySet().stream() - .map(entry -> new GridCoverageDTO(entry.getKey(), entry.getValue())) - .sorted(Comparator.comparing(GridCoverageDTO::coveredGrids).reversed()) - .collect(Collectors.toList()); - } - } +package com.dne.ems.repository.impl; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import com.dne.ems.dto.GridCoverageDTO; +import com.dne.ems.model.Grid; +import com.dne.ems.repository.GridRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; + +import jakarta.annotation.PostConstruct; + +@Repository +@Primary // 优先使用此实现,覆盖JPA +public class JsonGridRepositoryImpl implements GridRepository { + + private static final String FILE_NAME = "grids.json"; + private final JsonStorageService jsonStorageService; + private final List gridCache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonGridRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List grids = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (gridCache) { + gridCache.addAll(grids); + long maxId = grids.stream().mapToLong(Grid::getId).max().orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public List findAll() { + synchronized (gridCache) { + return new ArrayList<>(gridCache); + } + } + + @Override + public Grid save(Grid grid) { + synchronized (gridCache) { + if (grid.getId() == null) { + grid.setId(idCounter.incrementAndGet()); + gridCache.add(grid); + } else { + Optional existingGrid = gridCache.stream() + .filter(g -> g.getId().equals(grid.getId())) + .findFirst(); + if (existingGrid.isPresent()) { + gridCache.remove(existingGrid.get()); + gridCache.add(grid); + } else { + gridCache.add(grid); + } + } + jsonStorageService.writeData(FILE_NAME, gridCache); + return grid; + } + } + + @Override + public List saveAll(Iterable entities) { + List savedEntities; + synchronized (gridCache) { + savedEntities = StreamSupport.stream(entities.spliterator(), false) + .map(this::save) // Re-use the save logic for each entity + .collect(Collectors.toList()); + } + return savedEntities; + } + + @Override + public Optional findById(Long id) { + synchronized (gridCache) { + return gridCache.stream().filter(g -> g.getId().equals(id)).findFirst(); + } + } + + @Override + public Optional findByGridXAndGridY(Integer gridX, Integer gridY) { + synchronized (gridCache) { + return gridCache.stream() + .filter(g -> g.getGridX().equals(gridX) && g.getGridY().equals(gridY)) + .findFirst(); + } + } + + @Override + public List findByIsObstacle(boolean isObstacle) { + synchronized (gridCache) { + return gridCache.stream() + .filter(g -> g.getIsObstacle() != null && g.getIsObstacle() == isObstacle) + .collect(Collectors.toList()); + } + } + + @Override + public long count() { + synchronized (gridCache) { + return gridCache.size(); + } + } + + @Override + public List getGridCoverageByCity() { + synchronized (gridCache) { + return gridCache.stream() + .filter(grid -> grid.getCityName() != null && !grid.getCityName().isEmpty()) + .collect(Collectors.groupingBy(Grid::getCityName, Collectors.counting())) + .entrySet().stream() + .map(entry -> new GridCoverageDTO(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(GridCoverageDTO::coveredGrids).reversed()) + .collect(Collectors.toList()); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonMapGridRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonMapGridRepositoryImpl.java index e639cd8..44b7305 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonMapGridRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonMapGridRepositoryImpl.java @@ -1,122 +1,122 @@ -package com.dne.ems.repository.impl; - -import com.dne.ems.model.MapGrid; -import com.dne.ems.repository.MapGridRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -@Repository -@Primary -public class JsonMapGridRepositoryImpl implements MapGridRepository { - - private static final String FILE_NAME = "map_grids.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonMapGridRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(MapGrid::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public MapGrid save(MapGrid mapGrid) { - synchronized (cache) { - if (mapGrid.getId() == null) { - mapGrid.setId(idCounter.incrementAndGet()); - cache.add(mapGrid); - } else { - cache.removeIf(g -> g.getId().equals(mapGrid.getId())); - cache.add(mapGrid); - } - jsonStorageService.writeData(FILE_NAME, cache); - return mapGrid; - } - } - - @Override - public List saveAll(Iterable mapGrids) { - synchronized (cache) { - List savedGrids = StreamSupport.stream(mapGrids.spliterator(), false) - .map(this::save) - .collect(Collectors.toList()); - return savedGrids; - } - } - - @Override - public Optional findById(Long id) { - synchronized (cache) { - return cache.stream().filter(g -> g.getId().equals(id)).findFirst(); - } - } - - @Override - public List findAll() { - synchronized (cache) { - return new ArrayList<>(cache); - } - } - - @Override - public long count() { - synchronized (cache) { - return cache.size(); - } - } - - @Override - public void deleteById(Long id) { - synchronized (cache) { - cache.removeIf(g -> g.getId().equals(id)); - jsonStorageService.writeData(FILE_NAME, cache); - } - } - - @Override - public void deleteAll() { - synchronized (cache) { - cache.clear(); - jsonStorageService.writeData(FILE_NAME, cache); - } - } - - @Override - public Optional findByXAndY(int x, int y) { - synchronized (cache) { - return cache.stream().filter(g -> g.getX() == x && g.getY() == y).findFirst(); - } - } - - @Override - public List findAllByOrderByYAscXAsc() { - synchronized (cache) { - return cache.stream() - .sorted(Comparator.comparing(MapGrid::getY).thenComparing(MapGrid::getX)) - .collect(Collectors.toList()); - } - } +package com.dne.ems.repository.impl; + +import com.dne.ems.model.MapGrid; +import com.dne.ems.repository.MapGridRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@Repository +@Primary +public class JsonMapGridRepositoryImpl implements MapGridRepository { + + private static final String FILE_NAME = "map_grids.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonMapGridRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(MapGrid::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public MapGrid save(MapGrid mapGrid) { + synchronized (cache) { + if (mapGrid.getId() == null) { + mapGrid.setId(idCounter.incrementAndGet()); + cache.add(mapGrid); + } else { + cache.removeIf(g -> g.getId().equals(mapGrid.getId())); + cache.add(mapGrid); + } + jsonStorageService.writeData(FILE_NAME, cache); + return mapGrid; + } + } + + @Override + public List saveAll(Iterable mapGrids) { + synchronized (cache) { + List savedGrids = StreamSupport.stream(mapGrids.spliterator(), false) + .map(this::save) + .collect(Collectors.toList()); + return savedGrids; + } + } + + @Override + public Optional findById(Long id) { + synchronized (cache) { + return cache.stream().filter(g -> g.getId().equals(id)).findFirst(); + } + } + + @Override + public List findAll() { + synchronized (cache) { + return new ArrayList<>(cache); + } + } + + @Override + public long count() { + synchronized (cache) { + return cache.size(); + } + } + + @Override + public void deleteById(Long id) { + synchronized (cache) { + cache.removeIf(g -> g.getId().equals(id)); + jsonStorageService.writeData(FILE_NAME, cache); + } + } + + @Override + public void deleteAll() { + synchronized (cache) { + cache.clear(); + jsonStorageService.writeData(FILE_NAME, cache); + } + } + + @Override + public Optional findByXAndY(int x, int y) { + synchronized (cache) { + return cache.stream().filter(g -> g.getX() == x && g.getY() == y).findFirst(); + } + } + + @Override + public List findAllByOrderByYAscXAsc() { + synchronized (cache) { + return cache.stream() + .sorted(Comparator.comparing(MapGrid::getY).thenComparing(MapGrid::getX)) + .collect(Collectors.toList()); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonOperationLogRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonOperationLogRepositoryImpl.java index 2557c41..9f63d9d 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonOperationLogRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonOperationLogRepositoryImpl.java @@ -1,121 +1,121 @@ -package com.dne.ems.repository.impl; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; - -import com.dne.ems.model.OperationLog; -import com.dne.ems.model.enums.OperationType; -import com.dne.ems.repository.OperationLogRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; - -/** - * 操作日志仓库的JSON实现,使用JSON文件存储操作日志数据。 - * - * @version 1.0 - * @since 2025-06-20 - */ -@Repository -public class JsonOperationLogRepositoryImpl implements OperationLogRepository { - - private static final String OPERATION_LOGS_FILE = "operation_logs.json"; - - @Autowired - private JsonStorageService jsonStorageService; - - @Override - public OperationLog save(OperationLog operationLog) { - List logs = new ArrayList<>(findAll()); - - // 如果是新记录,生成ID - if (operationLog.getId() == null) { - long maxId = logs.stream() - .mapToLong(OperationLog::getId) - .max() - .orElse(0L); - operationLog.setId(maxId + 1); - } else { - // 如果是更新,先删除旧记录 - logs.removeIf(log -> log.getId().equals(operationLog.getId())); - } - - logs.add(operationLog); - jsonStorageService.writeData(OPERATION_LOGS_FILE, logs); - return operationLog; - } - - @Override - public OperationLog findById(Long id) { - return findAll().stream() - .filter(log -> log.getId().equals(id)) - .findFirst() - .orElse(null); - } - - @Override - public List findAll() { - return jsonStorageService.readData(OPERATION_LOGS_FILE, new TypeReference>() {}); - } - - @Override - public List findByUserId(Long userId) { - return findAll().stream() - .filter(log -> log.getUser() != null && Objects.equals(log.getUser().getId(), userId)) - .collect(Collectors.toList()); - } - - @Override - public List findByOperationType(OperationType operationType) { - return findAll().stream() - .filter(log -> log.getOperationType() == operationType) - .collect(Collectors.toList()); - } - - @Override - public List findByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { - return findAll().stream() - .filter(log -> { - LocalDateTime createdAt = log.getCreatedAt(); - return (startTime == null || !createdAt.isBefore(startTime)) && - (endTime == null || !createdAt.isAfter(endTime)); - }) - .collect(Collectors.toList()); - } - - @Override - public List findByConditions(Long userId, OperationType operationType, - LocalDateTime startTime, LocalDateTime endTime) { - return findAll().stream() - .filter(log -> userId == null || - (log.getUser() != null && Objects.equals(log.getUser().getId(), userId))) - .filter(log -> operationType == null || log.getOperationType() == operationType) - .filter(log -> { - LocalDateTime createdAt = log.getCreatedAt(); - if (createdAt == null) { - return false; // 如果创建时间为空,则不匹配时间范围 - } - return (startTime == null || !createdAt.isBefore(startTime)) && - (endTime == null || !createdAt.isAfter(endTime)); - }) - .collect(Collectors.toList()); - } - - @Override - public void deleteById(Long id) { - List logs = findAll().stream() - .filter(log -> !log.getId().equals(id)) - .collect(Collectors.toList()); - jsonStorageService.writeData(OPERATION_LOGS_FILE, logs); - } - - @Override - public void deleteAll() { - jsonStorageService.writeData(OPERATION_LOGS_FILE, new ArrayList<>()); - } +package com.dne.ems.repository.impl; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import com.dne.ems.model.OperationLog; +import com.dne.ems.model.enums.OperationType; +import com.dne.ems.repository.OperationLogRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; + +/** + * 操作日志仓库的JSON实现,使用JSON文件存储操作日志数据。 + * + * @version 1.0 + * @since 2025-06-20 + */ +@Repository +public class JsonOperationLogRepositoryImpl implements OperationLogRepository { + + private static final String OPERATION_LOGS_FILE = "operation_logs.json"; + + @Autowired + private JsonStorageService jsonStorageService; + + @Override + public OperationLog save(OperationLog operationLog) { + List logs = new ArrayList<>(findAll()); + + // 如果是新记录,生成ID + if (operationLog.getId() == null) { + long maxId = logs.stream() + .mapToLong(OperationLog::getId) + .max() + .orElse(0L); + operationLog.setId(maxId + 1); + } else { + // 如果是更新,先删除旧记录 + logs.removeIf(log -> log.getId().equals(operationLog.getId())); + } + + logs.add(operationLog); + jsonStorageService.writeData(OPERATION_LOGS_FILE, logs); + return operationLog; + } + + @Override + public OperationLog findById(Long id) { + return findAll().stream() + .filter(log -> log.getId().equals(id)) + .findFirst() + .orElse(null); + } + + @Override + public List findAll() { + return jsonStorageService.readData(OPERATION_LOGS_FILE, new TypeReference>() {}); + } + + @Override + public List findByUserId(Long userId) { + return findAll().stream() + .filter(log -> log.getUser() != null && Objects.equals(log.getUser().getId(), userId)) + .collect(Collectors.toList()); + } + + @Override + public List findByOperationType(OperationType operationType) { + return findAll().stream() + .filter(log -> log.getOperationType() == operationType) + .collect(Collectors.toList()); + } + + @Override + public List findByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + return findAll().stream() + .filter(log -> { + LocalDateTime createdAt = log.getCreatedAt(); + return (startTime == null || !createdAt.isBefore(startTime)) && + (endTime == null || !createdAt.isAfter(endTime)); + }) + .collect(Collectors.toList()); + } + + @Override + public List findByConditions(Long userId, OperationType operationType, + LocalDateTime startTime, LocalDateTime endTime) { + return findAll().stream() + .filter(log -> userId == null || + (log.getUser() != null && Objects.equals(log.getUser().getId(), userId))) + .filter(log -> operationType == null || log.getOperationType() == operationType) + .filter(log -> { + LocalDateTime createdAt = log.getCreatedAt(); + if (createdAt == null) { + return false; // 如果创建时间为空,则不匹配时间范围 + } + return (startTime == null || !createdAt.isBefore(startTime)) && + (endTime == null || !createdAt.isAfter(endTime)); + }) + .collect(Collectors.toList()); + } + + @Override + public void deleteById(Long id) { + List logs = findAll().stream() + .filter(log -> !log.getId().equals(id)) + .collect(Collectors.toList()); + jsonStorageService.writeData(OPERATION_LOGS_FILE, logs); + } + + @Override + public void deleteAll() { + jsonStorageService.writeData(OPERATION_LOGS_FILE, new ArrayList<>()); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonPasswordResetTokenRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonPasswordResetTokenRepositoryImpl.java index 8e32fe4..13e4971 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonPasswordResetTokenRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonPasswordResetTokenRepositoryImpl.java @@ -1,81 +1,81 @@ -package com.dne.ems.repository.impl; - -import com.dne.ems.model.PasswordResetToken; -import com.dne.ems.model.UserAccount; -import com.dne.ems.repository.PasswordResetTokenRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; - -@Repository -@Primary -public class JsonPasswordResetTokenRepositoryImpl implements PasswordResetTokenRepository { - - private static final String FILE_NAME = "password_reset_tokens.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonPasswordResetTokenRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(PasswordResetToken::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public PasswordResetToken save(PasswordResetToken token) { - synchronized (cache) { - if (token.getId() == null) { - token.setId(idCounter.incrementAndGet()); - cache.add(token); - } else { - cache.removeIf(t -> t.getId().equals(token.getId())); - cache.add(token); - } - jsonStorageService.writeData(FILE_NAME, cache); - return token; - } - } - - @Override - public Optional findByToken(String token) { - synchronized (cache) { - return cache.stream().filter(t -> t.getToken().equals(token)).findFirst(); - } - } - - @Override - public Optional findByUser(UserAccount user) { - synchronized (cache) { - return cache.stream() - .filter(t -> t.getUser() != null && t.getUser().equals(user)) - .findFirst(); - } - } - - @Override - public void delete(PasswordResetToken token) { - synchronized (cache) { - cache.removeIf(t -> t.getId().equals(token.getId())); - jsonStorageService.writeData(FILE_NAME, cache); - } - } +package com.dne.ems.repository.impl; + +import com.dne.ems.model.PasswordResetToken; +import com.dne.ems.model.UserAccount; +import com.dne.ems.repository.PasswordResetTokenRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; + +@Repository +@Primary +public class JsonPasswordResetTokenRepositoryImpl implements PasswordResetTokenRepository { + + private static final String FILE_NAME = "password_reset_tokens.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonPasswordResetTokenRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(PasswordResetToken::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public PasswordResetToken save(PasswordResetToken token) { + synchronized (cache) { + if (token.getId() == null) { + token.setId(idCounter.incrementAndGet()); + cache.add(token); + } else { + cache.removeIf(t -> t.getId().equals(token.getId())); + cache.add(token); + } + jsonStorageService.writeData(FILE_NAME, cache); + return token; + } + } + + @Override + public Optional findByToken(String token) { + synchronized (cache) { + return cache.stream().filter(t -> t.getToken().equals(token)).findFirst(); + } + } + + @Override + public Optional findByUser(UserAccount user) { + synchronized (cache) { + return cache.stream() + .filter(t -> t.getUser() != null && t.getUser().equals(user)) + .findFirst(); + } + } + + @Override + public void delete(PasswordResetToken token) { + synchronized (cache) { + cache.removeIf(t -> t.getId().equals(token.getId())); + jsonStorageService.writeData(FILE_NAME, cache); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonPollutantThresholdRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonPollutantThresholdRepositoryImpl.java index 15c2ecc..80fd45d 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonPollutantThresholdRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonPollutantThresholdRepositoryImpl.java @@ -1,89 +1,89 @@ -package com.dne.ems.repository.impl; - -import com.dne.ems.model.PollutantThreshold; -import com.dne.ems.repository.PollutantThresholdRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; -import jakarta.annotation.PostConstruct; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -@Repository -@Primary -public class JsonPollutantThresholdRepositoryImpl implements PollutantThresholdRepository { - - private static final String FILE_NAME = "pollutant_thresholds.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonPollutantThresholdRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(PollutantThreshold::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public PollutantThreshold save(PollutantThreshold threshold) { - synchronized (cache) { - // Since there's no unique constraint on pollutantName in the model, - // we'll just add/update based on ID. A more robust implementation - // might enforce the name uniqueness here. - if (threshold.getId() == null) { - threshold.setId(idCounter.incrementAndGet()); - cache.add(threshold); - } else { - cache.removeIf(t -> t.getId().equals(threshold.getId())); - cache.add(threshold); - } - jsonStorageService.writeData(FILE_NAME, cache); - return threshold; - } - } - - @Override - public List saveAll(Iterable thresholds) { - synchronized (cache) { - List savedThresholds = StreamSupport.stream(thresholds.spliterator(), false) - .map(this::save) - .collect(Collectors.toList()); - // 'save' already writes, but a single write after all operations would be more efficient. - // For simplicity, we reuse the single save logic. - return savedThresholds; - } - } - - @Override - public List findAll() { - synchronized (cache) { - return new ArrayList<>(cache); - } - } - - @Override - public Optional findByPollutantName(String pollutantName) { - synchronized (cache) { - return cache.stream() - .filter(t -> t.getPollutantName() != null && t.getPollutantName().equalsIgnoreCase(pollutantName)) - .findFirst(); - } - } +package com.dne.ems.repository.impl; + +import com.dne.ems.model.PollutantThreshold; +import com.dne.ems.repository.PollutantThresholdRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@Repository +@Primary +public class JsonPollutantThresholdRepositoryImpl implements PollutantThresholdRepository { + + private static final String FILE_NAME = "pollutant_thresholds.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonPollutantThresholdRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(PollutantThreshold::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public PollutantThreshold save(PollutantThreshold threshold) { + synchronized (cache) { + // Since there's no unique constraint on pollutantName in the model, + // we'll just add/update based on ID. A more robust implementation + // might enforce the name uniqueness here. + if (threshold.getId() == null) { + threshold.setId(idCounter.incrementAndGet()); + cache.add(threshold); + } else { + cache.removeIf(t -> t.getId().equals(threshold.getId())); + cache.add(threshold); + } + jsonStorageService.writeData(FILE_NAME, cache); + return threshold; + } + } + + @Override + public List saveAll(Iterable thresholds) { + synchronized (cache) { + List savedThresholds = StreamSupport.stream(thresholds.spliterator(), false) + .map(this::save) + .collect(Collectors.toList()); + // 'save' already writes, but a single write after all operations would be more efficient. + // For simplicity, we reuse the single save logic. + return savedThresholds; + } + } + + @Override + public List findAll() { + synchronized (cache) { + return new ArrayList<>(cache); + } + } + + @Override + public Optional findByPollutantName(String pollutantName) { + synchronized (cache) { + return cache.stream() + .filter(t -> t.getPollutantName() != null && t.getPollutantName().equalsIgnoreCase(pollutantName)) + .findFirst(); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskHistoryRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskHistoryRepositoryImpl.java index 355c815..03af19b 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskHistoryRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskHistoryRepositoryImpl.java @@ -1,77 +1,77 @@ -package com.dne.ems.repository.impl; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; - -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import com.dne.ems.model.TaskHistory; -import com.dne.ems.repository.TaskHistoryRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; - -import jakarta.annotation.PostConstruct; - -@Repository -@Primary -public class JsonTaskHistoryRepositoryImpl implements TaskHistoryRepository { - - private static final String FILE_NAME = "task_histories.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonTaskHistoryRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(TaskHistory::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public TaskHistory save(TaskHistory taskHistory) { - synchronized (cache) { - if (taskHistory.getId() == null) { - taskHistory.setId(idCounter.incrementAndGet()); - cache.add(taskHistory); - } else { - cache.removeIf(h -> h.getId().equals(taskHistory.getId())); - cache.add(taskHistory); - } - jsonStorageService.writeData(FILE_NAME, cache); - return taskHistory; - } - } - - @Override - public List findByTaskIdOrderByChangedAtDesc(Long taskId) { - synchronized (cache) { - return cache.stream() - .filter(h -> h.getTask() != null && h.getTask().getId().equals(taskId)) - .sorted(Comparator.comparing(TaskHistory::getChangedAt).reversed()) - .collect(Collectors.toList()); - } - } - - @Override - public void deleteAll() { - synchronized (cache) { - cache.clear(); - jsonStorageService.writeData(FILE_NAME, cache); - } - } +package com.dne.ems.repository.impl; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import com.dne.ems.model.TaskHistory; +import com.dne.ems.repository.TaskHistoryRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; + +import jakarta.annotation.PostConstruct; + +@Repository +@Primary +public class JsonTaskHistoryRepositoryImpl implements TaskHistoryRepository { + + private static final String FILE_NAME = "task_histories.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonTaskHistoryRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(TaskHistory::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public TaskHistory save(TaskHistory taskHistory) { + synchronized (cache) { + if (taskHistory.getId() == null) { + taskHistory.setId(idCounter.incrementAndGet()); + cache.add(taskHistory); + } else { + cache.removeIf(h -> h.getId().equals(taskHistory.getId())); + cache.add(taskHistory); + } + jsonStorageService.writeData(FILE_NAME, cache); + return taskHistory; + } + } + + @Override + public List findByTaskIdOrderByChangedAtDesc(Long taskId) { + synchronized (cache) { + return cache.stream() + .filter(h -> h.getTask() != null && h.getTask().getId().equals(taskId)) + .sorted(Comparator.comparing(TaskHistory::getChangedAt).reversed()) + .collect(Collectors.toList()); + } + } + + @Override + public void deleteAll() { + synchronized (cache) { + cache.clear(); + jsonStorageService.writeData(FILE_NAME, cache); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskRepositoryImpl.java index 372cca0..25a21ad 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskRepositoryImpl.java @@ -1,156 +1,156 @@ -package com.dne.ems.repository.impl; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import com.dne.ems.model.Feedback; -import com.dne.ems.model.Task; -import com.dne.ems.model.enums.TaskStatus; -import com.dne.ems.repository.TaskRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; - -import jakarta.annotation.PostConstruct; - -@Repository -@Primary -public class JsonTaskRepositoryImpl implements TaskRepository { - - private static final String FILE_NAME = "tasks.json"; - private final JsonStorageService jsonStorageService; - private final List taskCache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonTaskRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List tasks = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (taskCache) { - taskCache.addAll(tasks); - long maxId = tasks.stream() - .mapToLong(Task::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public Optional findById(Long id) { - synchronized (taskCache) { - return taskCache.stream().filter(t -> t.getId().equals(id)).findFirst(); - } - } - - @Override - public List findAll() { - synchronized (taskCache) { - return new ArrayList<>(taskCache); - } - } - - @Override - public Task save(Task task) { - synchronized (taskCache) { - if (task.getId() == null) { - task.setId(idCounter.incrementAndGet()); - taskCache.add(task); - } else { - Optional existing = findById(task.getId()); - if (existing.isPresent()) { - taskCache.remove(existing.get()); - } - taskCache.add(task); - } - jsonStorageService.writeData(FILE_NAME, taskCache); - return task; - } - } - - @Override - public List saveAll(Iterable tasks) { - synchronized (taskCache) { - List savedTasks = StreamSupport.stream(tasks.spliterator(), false) - .map(this::save) - .collect(Collectors.toList()); - return savedTasks; - } - } - - @Override - public void deleteById(Long id) { - synchronized (taskCache) { - taskCache.removeIf(task -> task.getId().equals(id)); - jsonStorageService.writeData(FILE_NAME, taskCache); - } - } - - @Override - public void deleteAll() { - synchronized (taskCache) { - taskCache.clear(); - jsonStorageService.writeData(FILE_NAME, taskCache); - } - } - - @Override - public boolean existsById(Long id) { - synchronized (taskCache) { - return taskCache.stream().anyMatch(t -> t.getId().equals(id)); - } - } - - @Override - public long count() { - synchronized (taskCache) { - return taskCache.size(); - } - } - - @Override - public Optional findByFeedback(Feedback feedback) { - synchronized (taskCache) { - return taskCache.stream() - .filter(task -> task.getFeedback() != null && task.getFeedback().equals(feedback)) - .findFirst(); - } - } - - @Override - public long countByStatus(TaskStatus status) { - synchronized (taskCache) { - return taskCache.stream().filter(task -> task.getStatus() == status).count(); - } - } - - @Override - public long countByAssigneeIdAndStatusIn(Long assigneeId, Collection statuses) { - synchronized (taskCache) { - return taskCache.stream() - .filter(task -> task.getAssignee() != null && task.getAssignee().getId().equals(assigneeId)) - .filter(task -> statuses.contains(task.getStatus())) - .count(); - } - } - - @Override - public List findByAssigneeIdAndStatus(Long assigneeId, TaskStatus status) { - synchronized (taskCache) { - return taskCache.stream() - .filter(task -> task.getAssignee() != null && task.getAssignee().getId().equals(assigneeId)) - .filter(task -> task.getStatus() == status) - .collect(Collectors.toList()); - } - } +package com.dne.ems.repository.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import com.dne.ems.model.Feedback; +import com.dne.ems.model.Task; +import com.dne.ems.model.enums.TaskStatus; +import com.dne.ems.repository.TaskRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; + +import jakarta.annotation.PostConstruct; + +@Repository +@Primary +public class JsonTaskRepositoryImpl implements TaskRepository { + + private static final String FILE_NAME = "tasks.json"; + private final JsonStorageService jsonStorageService; + private final List taskCache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonTaskRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List tasks = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (taskCache) { + taskCache.addAll(tasks); + long maxId = tasks.stream() + .mapToLong(Task::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public Optional findById(Long id) { + synchronized (taskCache) { + return taskCache.stream().filter(t -> t.getId().equals(id)).findFirst(); + } + } + + @Override + public List findAll() { + synchronized (taskCache) { + return new ArrayList<>(taskCache); + } + } + + @Override + public Task save(Task task) { + synchronized (taskCache) { + if (task.getId() == null) { + task.setId(idCounter.incrementAndGet()); + taskCache.add(task); + } else { + Optional existing = findById(task.getId()); + if (existing.isPresent()) { + taskCache.remove(existing.get()); + } + taskCache.add(task); + } + jsonStorageService.writeData(FILE_NAME, taskCache); + return task; + } + } + + @Override + public List saveAll(Iterable tasks) { + synchronized (taskCache) { + List savedTasks = StreamSupport.stream(tasks.spliterator(), false) + .map(this::save) + .collect(Collectors.toList()); + return savedTasks; + } + } + + @Override + public void deleteById(Long id) { + synchronized (taskCache) { + taskCache.removeIf(task -> task.getId().equals(id)); + jsonStorageService.writeData(FILE_NAME, taskCache); + } + } + + @Override + public void deleteAll() { + synchronized (taskCache) { + taskCache.clear(); + jsonStorageService.writeData(FILE_NAME, taskCache); + } + } + + @Override + public boolean existsById(Long id) { + synchronized (taskCache) { + return taskCache.stream().anyMatch(t -> t.getId().equals(id)); + } + } + + @Override + public long count() { + synchronized (taskCache) { + return taskCache.size(); + } + } + + @Override + public Optional findByFeedback(Feedback feedback) { + synchronized (taskCache) { + return taskCache.stream() + .filter(task -> task.getFeedback() != null && task.getFeedback().equals(feedback)) + .findFirst(); + } + } + + @Override + public long countByStatus(TaskStatus status) { + synchronized (taskCache) { + return taskCache.stream().filter(task -> task.getStatus() == status).count(); + } + } + + @Override + public long countByAssigneeIdAndStatusIn(Long assigneeId, Collection statuses) { + synchronized (taskCache) { + return taskCache.stream() + .filter(task -> task.getAssignee() != null && task.getAssignee().getId().equals(assigneeId)) + .filter(task -> statuses.contains(task.getStatus())) + .count(); + } + } + + @Override + public List findByAssigneeIdAndStatus(Long assigneeId, TaskStatus status) { + synchronized (taskCache) { + return taskCache.stream() + .filter(task -> task.getAssignee() != null && task.getAssignee().getId().equals(assigneeId)) + .filter(task -> task.getStatus() == status) + .collect(Collectors.toList()); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskSubmissionRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskSubmissionRepositoryImpl.java index 9cdccc3..9e701c7 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskSubmissionRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonTaskSubmissionRepositoryImpl.java @@ -1,95 +1,95 @@ -package com.dne.ems.repository.impl; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; - -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Repository; - -import com.dne.ems.model.Task; -import com.dne.ems.model.TaskSubmission; -import com.dne.ems.repository.TaskSubmissionRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; - -import jakarta.annotation.PostConstruct; - -@Repository -@Primary -public class JsonTaskSubmissionRepositoryImpl implements TaskSubmissionRepository { - - private static final String FILE_NAME = "task_submissions.json"; - private final JsonStorageService jsonStorageService; - private final List cache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonTaskSubmissionRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (cache) { - cache.addAll(data); - long maxId = data.stream() - .mapToLong(TaskSubmission::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public TaskSubmission save(TaskSubmission submission) { - synchronized (cache) { - if (submission.getId() == null) { - submission.setId(idCounter.incrementAndGet()); - cache.add(submission); - } else { - cache.removeIf(s -> s.getId().equals(submission.getId())); - cache.add(submission); - } - jsonStorageService.writeData(FILE_NAME, cache); - return submission; - } - } - - @Override - public TaskSubmission findFirstByTaskOrderBySubmittedAtDesc(Task task) { - synchronized (cache) { - return cache.stream() - .filter(s -> s.getTask() != null && s.getTask().equals(task)) - .max(Comparator.comparing(TaskSubmission::getSubmittedAt)) - .orElse(null); - } - } - - @Override - public List findByTaskId(Long taskId) { - synchronized (cache) { - return cache.stream() - .filter(s -> s.getTask() != null && Objects.equals(s.getTask().getId(), taskId)) - .collect(Collectors.toList()); - } - } - - @Override - public List findAll() { - synchronized (cache) { - return new ArrayList<>(cache); - } - } - - @Override - public void deleteAll() { - synchronized (cache) { - cache.clear(); - jsonStorageService.writeData(FILE_NAME, cache); - } - } +package com.dne.ems.repository.impl; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Repository; + +import com.dne.ems.model.Task; +import com.dne.ems.model.TaskSubmission; +import com.dne.ems.repository.TaskSubmissionRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; + +import jakarta.annotation.PostConstruct; + +@Repository +@Primary +public class JsonTaskSubmissionRepositoryImpl implements TaskSubmissionRepository { + + private static final String FILE_NAME = "task_submissions.json"; + private final JsonStorageService jsonStorageService; + private final List cache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonTaskSubmissionRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List data = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (cache) { + cache.addAll(data); + long maxId = data.stream() + .mapToLong(TaskSubmission::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public TaskSubmission save(TaskSubmission submission) { + synchronized (cache) { + if (submission.getId() == null) { + submission.setId(idCounter.incrementAndGet()); + cache.add(submission); + } else { + cache.removeIf(s -> s.getId().equals(submission.getId())); + cache.add(submission); + } + jsonStorageService.writeData(FILE_NAME, cache); + return submission; + } + } + + @Override + public TaskSubmission findFirstByTaskOrderBySubmittedAtDesc(Task task) { + synchronized (cache) { + return cache.stream() + .filter(s -> s.getTask() != null && s.getTask().equals(task)) + .max(Comparator.comparing(TaskSubmission::getSubmittedAt)) + .orElse(null); + } + } + + @Override + public List findByTaskId(Long taskId) { + synchronized (cache) { + return cache.stream() + .filter(s -> s.getTask() != null && Objects.equals(s.getTask().getId(), taskId)) + .collect(Collectors.toList()); + } + } + + @Override + public List findAll() { + synchronized (cache) { + return new ArrayList<>(cache); + } + } + + @Override + public void deleteAll() { + synchronized (cache) { + cache.clear(); + jsonStorageService.writeData(FILE_NAME, cache); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonUserAccountRepositoryImpl.java b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonUserAccountRepositoryImpl.java index c67e20d..2f78e1d 100644 --- a/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonUserAccountRepositoryImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/repository/impl/JsonUserAccountRepositoryImpl.java @@ -1,184 +1,184 @@ -package com.dne.ems.repository.impl; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import org.springframework.context.annotation.Primary; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; -import org.springframework.util.StringUtils; - -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.Role; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.service.JsonStorageService; -import com.fasterxml.jackson.core.type.TypeReference; - -import jakarta.annotation.PostConstruct; - -@Repository -@Primary -public class JsonUserAccountRepositoryImpl implements UserAccountRepository { - - private static final String FILE_NAME = "users.json"; - private final JsonStorageService jsonStorageService; - private final List userCache = new ArrayList<>(); - private final AtomicLong idCounter = new AtomicLong(0); - - public JsonUserAccountRepositoryImpl(JsonStorageService jsonStorageService) { - this.jsonStorageService = jsonStorageService; - } - - @PostConstruct - public void init() { - List users = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); - synchronized (userCache) { - userCache.addAll(users); - long maxId = users.stream() - .mapToLong(UserAccount::getId) - .max() - .orElse(0L); - idCounter.set(maxId); - } - } - - @Override - public Optional findByEmail(String email) { - synchronized (userCache) { - return userCache.stream() - .filter(user -> user.getEmail().equalsIgnoreCase(email)) - .findFirst(); - } - } - - @Override - public Optional findByPhone(String phone) { - synchronized (userCache) { - return userCache.stream() - .filter(user -> user.getPhone() != null && user.getPhone().equals(phone)) - .findFirst(); - } - } - - @Override - public Optional findById(Long id) { - synchronized (userCache) { - return userCache.stream() - .filter(user -> user.getId().equals(id)) - .findFirst(); - } - } - - @Override - public List findByRole(Role role) { - synchronized (userCache) { - return userCache.stream() - .filter(user -> user.getRole() == role) - .collect(Collectors.toList()); - } - } - - @Override - public List findGridWorkersByCoordinates(Integer gridX, Integer gridY) { - synchronized (userCache) { - return userCache.stream() - .filter(user -> user.getRole() == Role.GRID_WORKER && - Objects.equals(user.getGridX(), gridX) && - Objects.equals(user.getGridY(), gridY)) - .collect(Collectors.toList()); - } - } - - @Override - public Optional findByGridXAndGridY(Integer gridX, Integer gridY) { - synchronized (userCache) { - return userCache.stream() - .filter(user -> Objects.equals(user.getGridX(), gridX) && - Objects.equals(user.getGridY(), gridY)) - .findFirst(); - } - } - - @Override - public List findAll() { - synchronized (userCache) { - return new ArrayList<>(userCache); - } - } - - @Override - public UserAccount save(UserAccount userAccount) { - synchronized (userCache) { - if (userAccount.getId() == null) { - userAccount.setId(idCounter.incrementAndGet()); - userCache.add(userAccount); - } else { - Optional existing = findById(userAccount.getId()); - if (existing.isPresent()) { - userCache.remove(existing.get()); - } - userCache.add(userAccount); - } - jsonStorageService.writeData(FILE_NAME, userCache); - return userAccount; - } - } - - @Override - public List saveAll(Iterable entities) { - synchronized (userCache) { - List savedUsers = StreamSupport.stream(entities.spliterator(), false) - .map(this::save) - .collect(Collectors.toList()); - // The save method already writes to the file, but a final single write is more efficient. - // However, for simplicity and consistency with individual 'save', we leave it as is. - return savedUsers; - } - } - - @Override - public long countByRole(Role role) { - synchronized (userCache) { - return userCache.stream().filter(user -> user.getRole() == role).count(); - } - } - - @Override - public long count() { - synchronized (userCache) { - return userCache.size(); - } - } - - @Override - public void deleteById(Long id) { - synchronized (userCache) { - userCache.removeIf(user -> user.getId().equals(id)); - jsonStorageService.writeData(FILE_NAME, userCache); - } - } - - @Override - public Page findUsers(Role role, String name, Pageable pageable) { - synchronized (userCache) { - List filteredUsers = userCache.stream() - .filter(user -> role == null || user.getRole() == role) - .filter(user -> !StringUtils.hasText(name) || user.getName().toLowerCase().contains(name.toLowerCase())) - .collect(Collectors.toList()); - - int start = (int) pageable.getOffset(); - int end = Math.min((start + pageable.getPageSize()), filteredUsers.size()); - - List pageContent = (start > filteredUsers.size()) ? List.of() : filteredUsers.subList(start, end); - - return new PageImpl<>(pageContent, pageable, filteredUsers.size()); - } - } +package com.dne.ems.repository.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.springframework.context.annotation.Primary; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; + +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.Role; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.service.JsonStorageService; +import com.fasterxml.jackson.core.type.TypeReference; + +import jakarta.annotation.PostConstruct; + +@Repository +@Primary +public class JsonUserAccountRepositoryImpl implements UserAccountRepository { + + private static final String FILE_NAME = "users.json"; + private final JsonStorageService jsonStorageService; + private final List userCache = new ArrayList<>(); + private final AtomicLong idCounter = new AtomicLong(0); + + public JsonUserAccountRepositoryImpl(JsonStorageService jsonStorageService) { + this.jsonStorageService = jsonStorageService; + } + + @PostConstruct + public void init() { + List users = jsonStorageService.readData(FILE_NAME, new TypeReference<>() {}); + synchronized (userCache) { + userCache.addAll(users); + long maxId = users.stream() + .mapToLong(UserAccount::getId) + .max() + .orElse(0L); + idCounter.set(maxId); + } + } + + @Override + public Optional findByEmail(String email) { + synchronized (userCache) { + return userCache.stream() + .filter(user -> user.getEmail().equalsIgnoreCase(email)) + .findFirst(); + } + } + + @Override + public Optional findByPhone(String phone) { + synchronized (userCache) { + return userCache.stream() + .filter(user -> user.getPhone() != null && user.getPhone().equals(phone)) + .findFirst(); + } + } + + @Override + public Optional findById(Long id) { + synchronized (userCache) { + return userCache.stream() + .filter(user -> user.getId().equals(id)) + .findFirst(); + } + } + + @Override + public List findByRole(Role role) { + synchronized (userCache) { + return userCache.stream() + .filter(user -> user.getRole() == role) + .collect(Collectors.toList()); + } + } + + @Override + public List findGridWorkersByCoordinates(Integer gridX, Integer gridY) { + synchronized (userCache) { + return userCache.stream() + .filter(user -> user.getRole() == Role.GRID_WORKER && + Objects.equals(user.getGridX(), gridX) && + Objects.equals(user.getGridY(), gridY)) + .collect(Collectors.toList()); + } + } + + @Override + public Optional findByGridXAndGridY(Integer gridX, Integer gridY) { + synchronized (userCache) { + return userCache.stream() + .filter(user -> Objects.equals(user.getGridX(), gridX) && + Objects.equals(user.getGridY(), gridY)) + .findFirst(); + } + } + + @Override + public List findAll() { + synchronized (userCache) { + return new ArrayList<>(userCache); + } + } + + @Override + public UserAccount save(UserAccount userAccount) { + synchronized (userCache) { + if (userAccount.getId() == null) { + userAccount.setId(idCounter.incrementAndGet()); + userCache.add(userAccount); + } else { + Optional existing = findById(userAccount.getId()); + if (existing.isPresent()) { + userCache.remove(existing.get()); + } + userCache.add(userAccount); + } + jsonStorageService.writeData(FILE_NAME, userCache); + return userAccount; + } + } + + @Override + public List saveAll(Iterable entities) { + synchronized (userCache) { + List savedUsers = StreamSupport.stream(entities.spliterator(), false) + .map(this::save) + .collect(Collectors.toList()); + // The save method already writes to the file, but a final single write is more efficient. + // However, for simplicity and consistency with individual 'save', we leave it as is. + return savedUsers; + } + } + + @Override + public long countByRole(Role role) { + synchronized (userCache) { + return userCache.stream().filter(user -> user.getRole() == role).count(); + } + } + + @Override + public long count() { + synchronized (userCache) { + return userCache.size(); + } + } + + @Override + public void deleteById(Long id) { + synchronized (userCache) { + userCache.removeIf(user -> user.getId().equals(id)); + jsonStorageService.writeData(FILE_NAME, userCache); + } + } + + @Override + public Page findUsers(Role role, String name, Pageable pageable) { + synchronized (userCache) { + List filteredUsers = userCache.stream() + .filter(user -> role == null || user.getRole() == role) + .filter(user -> !StringUtils.hasText(name) || user.getName().toLowerCase().contains(name.toLowerCase())) + .collect(Collectors.toList()); + + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), filteredUsers.size()); + + List pageContent = (start > filteredUsers.size()) ? List.of() : filteredUsers.subList(start, end); + + return new PageImpl<>(pageContent, pageable, filteredUsers.size()); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/security/CustomUserDetails.java b/ems-backend/src/main/java/com/dne/ems/security/CustomUserDetails.java index 2cd22cf..1cf2b60 100644 --- a/ems-backend/src/main/java/com/dne/ems/security/CustomUserDetails.java +++ b/ems-backend/src/main/java/com/dne/ems/security/CustomUserDetails.java @@ -1,99 +1,99 @@ -package com.dne.ems.security; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.UserStatus; - -import lombok.Getter; - -@Getter -public class CustomUserDetails implements UserDetails { - - private final UserAccount userAccount; - - public CustomUserDetails(UserAccount userAccount) { - this.userAccount = userAccount; - } - - public Long getId() { - return userAccount.getId(); - } - - // ========== 新增的 Getters,暴露 UserAccount 的完整信息 ========== - - public String getName() { - return userAccount.getName(); - } - - public Role getRole() { - return userAccount.getRole(); - } - - public String getPhone() { - return userAccount.getPhone(); - } - - public UserStatus getStatus() { - return userAccount.getStatus(); - } - - public String getRegion() { - return userAccount.getRegion(); - } - - public List getSkills() { - return userAccount.getSkills(); - } - - // ========== Spring Security 标准接口实现 ========== - - @Override - public Collection getAuthorities() { - // Spring Security's "hasRole" implicitly adds the 'ROLE_' prefix. - // To ensure consistency, we explicitly add it here. - SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + getRole().name()); - return Collections.singletonList(authority); - } - - @Override - public String getPassword() { - return userAccount.getPassword(); - } - - @Override - public String getUsername() { - // 我们使用邮箱作为认证用户名 - return userAccount.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - // 可以根据 lockoutEndTime 字段实现更复杂的锁定逻辑 - return userAccount.getLockoutEndTime() == null; - } - - - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return userAccount.isEnabled() && userAccount.getStatus() == UserStatus.ACTIVE; - } +package com.dne.ems.security; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.UserStatus; + +import lombok.Getter; + +@Getter +public class CustomUserDetails implements UserDetails { + + private final UserAccount userAccount; + + public CustomUserDetails(UserAccount userAccount) { + this.userAccount = userAccount; + } + + public Long getId() { + return userAccount.getId(); + } + + // ========== 新增的 Getters,暴露 UserAccount 的完整信息 ========== + + public String getName() { + return userAccount.getName(); + } + + public Role getRole() { + return userAccount.getRole(); + } + + public String getPhone() { + return userAccount.getPhone(); + } + + public UserStatus getStatus() { + return userAccount.getStatus(); + } + + public String getRegion() { + return userAccount.getRegion(); + } + + public List getSkills() { + return userAccount.getSkills(); + } + + // ========== Spring Security 标准接口实现 ========== + + @Override + public Collection getAuthorities() { + // Spring Security's "hasRole" implicitly adds the 'ROLE_' prefix. + // To ensure consistency, we explicitly add it here. + SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + getRole().name()); + return Collections.singletonList(authority); + } + + @Override + public String getPassword() { + return userAccount.getPassword(); + } + + @Override + public String getUsername() { + // 我们使用邮箱作为认证用户名 + return userAccount.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + // 可以根据 lockoutEndTime 字段实现更复杂的锁定逻辑 + return userAccount.getLockoutEndTime() == null; + } + + + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return userAccount.isEnabled() && userAccount.getStatus() == UserStatus.ACTIVE; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/security/JwtAuthenticationFilter.java b/ems-backend/src/main/java/com/dne/ems/security/JwtAuthenticationFilter.java index d8b20f2..e855d88 100644 --- a/ems-backend/src/main/java/com/dne/ems/security/JwtAuthenticationFilter.java +++ b/ems-backend/src/main/java/com/dne/ems/security/JwtAuthenticationFilter.java @@ -1,103 +1,103 @@ -package com.dne.ems.security; - -import java.io.IOException; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.lang.NonNull; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import com.dne.ems.service.JwtService; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; - -/** - * JWT认证过滤器,处理每个请求的JWT认证 - * - *

工作流程: - *

    - *
  1. 从Authorization头提取JWT令牌
  2. - *
  3. 验证令牌有效性
  4. - *
  5. 加载用户详情
  6. - *
  7. 设置安全上下文
  8. - *
- * - *

安全特性: - *

    - *
  • 跳过认证相关路径(/api/auth)
  • - *
  • 验证令牌签名和有效期
  • - *
  • 不处理无效或过期的令牌
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024-03 - * @see com.dne.ems.service.JwtService JWT服务 - * @see org.springframework.security.core.userdetails.UserDetailsService 用户详情服务 - */ -@Component -@RequiredArgsConstructor -public class JwtAuthenticationFilter extends OncePerRequestFilter { - private final JwtService jwtService; - private final UserDetailsService userDetailsService; - - /** - * 执行JWT认证过滤逻辑 - * - *

处理步骤: - *

    - *
  1. 检查请求路径是否需要跳过认证
  2. - *
  3. 从Authorization头提取Bearer令牌
  4. - *
  5. 验证令牌并提取用户名
  6. - *
  7. 加载用户详情并设置认证上下文
  8. - *
- * - * @param request HTTP请求 - * @param response HTTP响应 - * @param filterChain 过滤器链 - * @throws ServletException 如果发生servlet异常 - * @throws IOException 如果发生I/O异常 - */ - @Override - protected void doFilterInternal(@NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) - throws ServletException, IOException { - // 如果请求路径是认证相关的,则直接跳过此过滤器 - if (request.getServletPath().contains("/api/auth")) { - filterChain.doFilter(request, response); - return; - } - final String authHeader = request.getHeader("Authorization"); - final String jwt; - final String userEmail; - if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, "Bearer ")) { - filterChain.doFilter(request, response); - return; - } - jwt = authHeader.substring(7); - userEmail = jwtService.extractUserName(jwt); - if (StringUtils.isNotEmpty(userEmail) - && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail); - if (jwtService.isTokenValid(jwt, userDetails)) { - SecurityContext context = SecurityContextHolder.createEmptyContext(); - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - context.setAuthentication(authToken); - SecurityContextHolder.setContext(context); - } - } - filterChain.doFilter(request, response); - } +package com.dne.ems.security; + +import java.io.IOException; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.dne.ems.service.JwtService; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +/** + * JWT认证过滤器,处理每个请求的JWT认证 + * + *

工作流程: + *

    + *
  1. 从Authorization头提取JWT令牌
  2. + *
  3. 验证令牌有效性
  4. + *
  5. 加载用户详情
  6. + *
  7. 设置安全上下文
  8. + *
+ * + *

安全特性: + *

    + *
  • 跳过认证相关路径(/api/auth)
  • + *
  • 验证令牌签名和有效期
  • + *
  • 不处理无效或过期的令牌
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024-03 + * @see com.dne.ems.service.JwtService JWT服务 + * @see org.springframework.security.core.userdetails.UserDetailsService 用户详情服务 + */ +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + + /** + * 执行JWT认证过滤逻辑 + * + *

处理步骤: + *

    + *
  1. 检查请求路径是否需要跳过认证
  2. + *
  3. 从Authorization头提取Bearer令牌
  4. + *
  5. 验证令牌并提取用户名
  6. + *
  7. 加载用户详情并设置认证上下文
  8. + *
+ * + * @param request HTTP请求 + * @param response HTTP响应 + * @param filterChain 过滤器链 + * @throws ServletException 如果发生servlet异常 + * @throws IOException 如果发生I/O异常 + */ + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) + throws ServletException, IOException { + // 如果请求路径是认证相关的,则直接跳过此过滤器 + if (request.getServletPath().contains("/api/auth")) { + filterChain.doFilter(request, response); + return; + } + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String userEmail; + if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, "Bearer ")) { + filterChain.doFilter(request, response); + return; + } + jwt = authHeader.substring(7); + userEmail = jwtService.extractUserName(jwt); + if (StringUtils.isNotEmpty(userEmail) + && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail); + if (jwtService.isTokenValid(jwt, userDetails)) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + context.setAuthentication(authToken); + SecurityContextHolder.setContext(context); + } + } + filterChain.doFilter(request, response); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/security/SecurityConfig.java b/ems-backend/src/main/java/com/dne/ems/security/SecurityConfig.java index 8534411..cafe226 100644 --- a/ems-backend/src/main/java/com/dne/ems/security/SecurityConfig.java +++ b/ems-backend/src/main/java/com/dne/ems/security/SecurityConfig.java @@ -1,155 +1,155 @@ -package com.dne.ems.security; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; // <--- 最可能需要添加的 -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import lombok.RequiredArgsConstructor; - -/** - * 安全配置类,定义系统安全策略 - * - *

主要配置: - *

    - *
  • HTTP安全过滤器链
  • - *
  • CORS跨域配置
  • - *
  • 认证管理器
  • - *
  • 密码编码器
  • - *
- * - *

安全特性: - *

    - *
  • 基于JWT的无状态认证
  • - *
  • CSRF防护禁用(因使用JWT)
  • - *
  • 方法级权限控制
  • - *
  • 细粒度URL访问控制
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024-03 - */ -@Configuration -@EnableWebSecurity -@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true, proxyTargetClass = true) -@RequiredArgsConstructor -public class SecurityConfig { - - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - /** - * 配置HTTP安全过滤器链 - * - *

安全规则: - *

    - *
  • 禁用CSRF(因使用JWT)
  • - *
  • 启用CORS跨域支持
  • - *
  • 设置无状态会话
  • - *
  • 配置URL访问权限
  • - *
  • 添加JWT认证过滤器
  • - *
- * - * @param http HTTP安全构建器 - * @return 安全过滤器链 - * @throws Exception 如果配置出错 - */ - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .authorizeHttpRequests(authorize -> authorize - // 允许所有OPTIONS预检请求 - .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - // 明确允许匿名访问的端点 - .requestMatchers("/api/auth/**").permitAll() - .requestMatchers( - "/api/auth/login", - "/api/auth/signup", - "/api/auth/send-verification-code", - "/api/auth/send-password-reset-code", - "/api/auth/reset-password-with-code" - ).permitAll() - // 明确地将logout端点设置为需要认证 - .requestMatchers("/api/auth/logout").hasAnyRole("ADMIN", "DECISION_MAKER", "GRID_WORKER") - // 公共数据和API - .requestMatchers( - "/api/public/**", - "/api/map/**", // 允许访问地图数据 - "/api/pathfinding/**", // 允许访问寻路功能 - "/api/files/view/**", - "/api/files/download/**", - "/swagger-ui/**", - "/v3/api-docs/**", - "/swagger-ui.html" - ).permitAll() - .requestMatchers("/api/files/**").permitAll() - // 添加操作日志端点权限控制 - .requestMatchers("/api/logs/my-logs").authenticated() - .requestMatchers("/api/logs").hasRole("ADMIN") - // 明确保护Dashboard端点 - .requestMatchers( - "/api/dashboard/stats", - "/api/dashboard/reports/aqi-distribution", - // ... (other dashboard endpoints) - "/api/dashboard/reports/aqi-trends-24h" - ).hasAnyRole("ADMIN", "DECISION_MAKER") - // 其他所有请求都需要认证 - .anyRequest().authenticated() // <--- 确保这一行存在且位于末尾 - ) - .sessionManagement(session -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - ) - - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } - - /** - * 配置CORS跨域资源共享 - * - *

允许规则: - *

    - *
  • 所有来源(*)
  • - *
  • 所有HTTP方法
  • - *
  • 所有HTTP头
  • - *
- * - * @return CORS配置源 - */ - @Bean - CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.addAllowedOrigin("*"); // 允许所有来源 - configuration.addAllowedMethod("*"); // 允许所有HTTP方法 - configuration.addAllowedHeader("*"); // 允许所有HTTP头 - // configuration.setAllowCredentials(true); // 如果需要携带凭证(如cookies),请取消注释 - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); // 对所有路径应用CORS配置 - return source; - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { - return config.getAuthenticationManager(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } +package com.dne.ems.security; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; // <--- 最可能需要添加的 +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import lombok.RequiredArgsConstructor; + +/** + * 安全配置类,定义系统安全策略 + * + *

主要配置: + *

    + *
  • HTTP安全过滤器链
  • + *
  • CORS跨域配置
  • + *
  • 认证管理器
  • + *
  • 密码编码器
  • + *
+ * + *

安全特性: + *

    + *
  • 基于JWT的无状态认证
  • + *
  • CSRF防护禁用(因使用JWT)
  • + *
  • 方法级权限控制
  • + *
  • 细粒度URL访问控制
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024-03 + */ +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true, proxyTargetClass = true) +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + /** + * 配置HTTP安全过滤器链 + * + *

安全规则: + *

    + *
  • 禁用CSRF(因使用JWT)
  • + *
  • 启用CORS跨域支持
  • + *
  • 设置无状态会话
  • + *
  • 配置URL访问权限
  • + *
  • 添加JWT认证过滤器
  • + *
+ * + * @param http HTTP安全构建器 + * @return 安全过滤器链 + * @throws Exception 如果配置出错 + */ + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests(authorize -> authorize + // 允许所有OPTIONS预检请求 + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + // 明确允许匿名访问的端点 + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers( + "/api/auth/login", + "/api/auth/signup", + "/api/auth/send-verification-code", + "/api/auth/send-password-reset-code", + "/api/auth/reset-password-with-code" + ).permitAll() + // 明确地将logout端点设置为需要认证 + .requestMatchers("/api/auth/logout").hasAnyRole("ADMIN", "DECISION_MAKER", "GRID_WORKER") + // 公共数据和API + .requestMatchers( + "/api/public/**", + "/api/map/**", // 允许访问地图数据 + "/api/pathfinding/**", // 允许访问寻路功能 + "/api/files/view/**", + "/api/files/download/**", + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-ui.html" + ).permitAll() + .requestMatchers("/api/files/**").permitAll() + // 添加操作日志端点权限控制 + .requestMatchers("/api/logs/my-logs").authenticated() + .requestMatchers("/api/logs").hasRole("ADMIN") + // 明确保护Dashboard端点 + .requestMatchers( + "/api/dashboard/stats", + "/api/dashboard/reports/aqi-distribution", + // ... (other dashboard endpoints) + "/api/dashboard/reports/aqi-trends-24h" + ).hasAnyRole("ADMIN", "DECISION_MAKER") + // 其他所有请求都需要认证 + .anyRequest().authenticated() // <--- 确保这一行存在且位于末尾 + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + /** + * 配置CORS跨域资源共享 + * + *

允许规则: + *

    + *
  • 所有来源(*)
  • + *
  • 所有HTTP方法
  • + *
  • 所有HTTP头
  • + *
+ * + * @return CORS配置源 + */ + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOrigin("*"); // 允许所有来源 + configuration.addAllowedMethod("*"); // 允许所有HTTP方法 + configuration.addAllowedHeader("*"); // 允许所有HTTP头 + // configuration.setAllowCredentials(true); // 如果需要携带凭证(如cookies),请取消注释 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); // 对所有路径应用CORS配置 + return source; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/security/UserDetailsServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/security/UserDetailsServiceImpl.java index 9f3676e..4605073 100644 --- a/ems-backend/src/main/java/com/dne/ems/security/UserDetailsServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/security/UserDetailsServiceImpl.java @@ -1,45 +1,45 @@ -package com.dne.ems.security; - -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import com.dne.ems.model.UserAccount; -import com.dne.ems.repository.UserAccountRepository; - -import lombok.RequiredArgsConstructor; -import org.springframework.security.authentication.LockedException; -import java.time.LocalDateTime; - -/** - * Service to load user-specific data. - * This is used by Spring Security to handle authentication. - */ -@Service -@RequiredArgsConstructor -public class UserDetailsServiceImpl implements UserDetailsService { - - private final UserAccountRepository userAccountRepository; - - /** - * Locates the user based on the username. In our case, the username is the email. - * - * @param username the username (email) identifying the user whose data is required. - * @return a fully populated user record (never {@code null}) - * @throws UsernameNotFoundException if the user could not be found or the user has no - * GrantedAuthority - */ - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - UserAccount userAccount = userAccountRepository.findByEmail(username) - .orElseThrow(() -> - new UsernameNotFoundException("未找到邮箱为 " + username + " 的用户。")); - - if (userAccount.getLockoutEndTime() != null && userAccount.getLockoutEndTime().isAfter(LocalDateTime.now())) { - throw new LockedException("该账户已被锁定,请稍后再试。"); - } - - return new CustomUserDetails(userAccount); - } +package com.dne.ems.security; + +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import com.dne.ems.model.UserAccount; +import com.dne.ems.repository.UserAccountRepository; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.LockedException; +import java.time.LocalDateTime; + +/** + * Service to load user-specific data. + * This is used by Spring Security to handle authentication. + */ +@Service +@RequiredArgsConstructor +public class UserDetailsServiceImpl implements UserDetailsService { + + private final UserAccountRepository userAccountRepository; + + /** + * Locates the user based on the username. In our case, the username is the email. + * + * @param username the username (email) identifying the user whose data is required. + * @return a fully populated user record (never {@code null}) + * @throws UsernameNotFoundException if the user could not be found or the user has no + * GrantedAuthority + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + UserAccount userAccount = userAccountRepository.findByEmail(username) + .orElseThrow(() -> + new UsernameNotFoundException("未找到邮箱为 " + username + " 的用户。")); + + if (userAccount.getLockoutEndTime() != null && userAccount.getLockoutEndTime().isAfter(LocalDateTime.now())) { + throw new LockedException("该账户已被锁定,请稍后再试。"); + } + + return new CustomUserDetails(userAccount); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/AiReviewService.java b/ems-backend/src/main/java/com/dne/ems/service/AiReviewService.java index 856c81a..7418b50 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/AiReviewService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/AiReviewService.java @@ -1,15 +1,15 @@ -package com.dne.ems.service; - -import com.dne.ems.model.Feedback; - -public interface AiReviewService { - - /** - * Reviews the given feedback using an AI model. - * This method will contain the logic to call the external AI service - * and update the feedback status based on the result. - * - * @param feedback The feedback entity to be reviewed. - */ - void reviewFeedback(Feedback feedback); +package com.dne.ems.service; + +import com.dne.ems.model.Feedback; + +public interface AiReviewService { + + /** + * Reviews the given feedback using an AI model. + * This method will contain the logic to call the external AI service + * and update the feedback status based on the result. + * + * @param feedback The feedback entity to be reviewed. + */ + void reviewFeedback(Feedback feedback); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/AuthService.java b/ems-backend/src/main/java/com/dne/ems/service/AuthService.java index ad6740f..e8cb42a 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/AuthService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/AuthService.java @@ -1,110 +1,110 @@ -package com.dne.ems.service; - -import com.dne.ems.dto.JwtAuthenticationResponse; -import com.dne.ems.dto.LoginRequest; -import com.dne.ems.dto.SignUpRequest; - -/** - * 认证服务接口,提供用户认证和安全相关的核心功能 - * - *

主要功能包括: - *

    - *
  • 用户注册与登录
  • - *
  • 密码重置与验证
  • - *
  • JWT令牌生成
  • - *
- * - *

典型使用示例: - *

{@code
- * // 用户注册
- * authService.registerUser(signUpRequest);
- *
- * // 用户登录
- * JwtAuthenticationResponse response = authService.signIn(loginRequest);
- * }
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -public interface AuthService { - - /** - * 请求密码重置,发送重置验证码到用户邮箱 - * - *

安全流程: - *

    - *
  1. 验证邮箱格式
  2. - *
  3. 如果邮箱已注册,发送6位数字验证码(有效期5分钟)
  4. - *
  5. 无论用户是否存在都返回成功,防止用户枚举攻击
  6. - *
- * - * @param email 用户注册邮箱(必须符合name@domain.com格式) - * @throws IllegalArgumentException 如果邮箱格式无效 - * @see com.dne.ems.service.VerificationCodeService 验证码服务 - */ - void requestPasswordReset(String email); - - /** - * 使用令牌重置密码(旧方法,保留以兼容历史代码) - * - * @param token 密码重置令牌 - * @param newPassword 新密码 - * @deprecated 使用基于验证码的 {@link #resetPasswordWithCode(String, String, String)} 替代 - */ - @Deprecated - void resetPassword(String token, String newPassword); - - /** - * 使用验证码重置密码 - * - *

重置流程: - *

    - *
  1. 验证邮箱是否注册
  2. - *
  3. 验证验证码是否正确且在有效期内(5分钟)
  4. - *
  5. 验证新密码复杂度
  6. - *
  7. 更新密码并清除验证码
  8. - *
- * - *

密码复杂度要求: - *

    - *
  • 长度8-20位
  • - *
  • 包含大小写字母和数字
  • - *
  • 至少一个特殊字符(!@#$%^&*)
  • - *
  • 不能与旧密码相同
  • - *
- * - * @param email 用户注册邮箱 - * @param code 验证码(6位数字) - * @param newPassword 新密码,必须符合密码复杂度要求 - * @throws ResourceNotFoundException 如果邮箱未注册 - * @throws InvalidOperationException 如果验证码无效或过期 - * @throws IllegalArgumentException 如果新密码不符合要求 - * @see com.dne.ems.validation.PasswordValidator 密码验证器 - */ - void resetPasswordWithCode(String email, String code, String newPassword); - - /** - * 注册新用户 - * - * @param signUpRequest 包含用户名、邮箱、密码等注册信息 - * @throws UserAlreadyExistsException 如果用户名或邮箱已被注册 - * @throws IllegalArgumentException 如果注册信息无效 - */ - void registerUser(SignUpRequest signUpRequest); - - /** - * 用户登录 - * - * @param request 包含用户名/邮箱和密码的登录请求 - * @return 包含JWT令牌的认证响应,令牌有效期24小时 - * @throws ResourceNotFoundException 如果用户不存在 - * @throws InvalidOperationException 如果密码不匹配 - */ - JwtAuthenticationResponse signIn(LoginRequest request); - - /** - * 处理用户登出逻辑 - */ - void logout(); +package com.dne.ems.service; + +import com.dne.ems.dto.JwtAuthenticationResponse; +import com.dne.ems.dto.LoginRequest; +import com.dne.ems.dto.SignUpRequest; + +/** + * 认证服务接口,提供用户认证和安全相关的核心功能 + * + *

主要功能包括: + *

    + *
  • 用户注册与登录
  • + *
  • 密码重置与验证
  • + *
  • JWT令牌生成
  • + *
+ * + *

典型使用示例: + *

{@code
+ * // 用户注册
+ * authService.registerUser(signUpRequest);
+ *
+ * // 用户登录
+ * JwtAuthenticationResponse response = authService.signIn(loginRequest);
+ * }
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +public interface AuthService { + + /** + * 请求密码重置,发送重置验证码到用户邮箱 + * + *

安全流程: + *

    + *
  1. 验证邮箱格式
  2. + *
  3. 如果邮箱已注册,发送6位数字验证码(有效期5分钟)
  4. + *
  5. 无论用户是否存在都返回成功,防止用户枚举攻击
  6. + *
+ * + * @param email 用户注册邮箱(必须符合name@domain.com格式) + * @throws IllegalArgumentException 如果邮箱格式无效 + * @see com.dne.ems.service.VerificationCodeService 验证码服务 + */ + void requestPasswordReset(String email); + + /** + * 使用令牌重置密码(旧方法,保留以兼容历史代码) + * + * @param token 密码重置令牌 + * @param newPassword 新密码 + * @deprecated 使用基于验证码的 {@link #resetPasswordWithCode(String, String, String)} 替代 + */ + @Deprecated + void resetPassword(String token, String newPassword); + + /** + * 使用验证码重置密码 + * + *

重置流程: + *

    + *
  1. 验证邮箱是否注册
  2. + *
  3. 验证验证码是否正确且在有效期内(5分钟)
  4. + *
  5. 验证新密码复杂度
  6. + *
  7. 更新密码并清除验证码
  8. + *
+ * + *

密码复杂度要求: + *

    + *
  • 长度8-20位
  • + *
  • 包含大小写字母和数字
  • + *
  • 至少一个特殊字符(!@#$%^&*)
  • + *
  • 不能与旧密码相同
  • + *
+ * + * @param email 用户注册邮箱 + * @param code 验证码(6位数字) + * @param newPassword 新密码,必须符合密码复杂度要求 + * @throws ResourceNotFoundException 如果邮箱未注册 + * @throws InvalidOperationException 如果验证码无效或过期 + * @throws IllegalArgumentException 如果新密码不符合要求 + * @see com.dne.ems.validation.PasswordValidator 密码验证器 + */ + void resetPasswordWithCode(String email, String code, String newPassword); + + /** + * 注册新用户 + * + * @param signUpRequest 包含用户名、邮箱、密码等注册信息 + * @throws UserAlreadyExistsException 如果用户名或邮箱已被注册 + * @throws IllegalArgumentException 如果注册信息无效 + */ + void registerUser(SignUpRequest signUpRequest); + + /** + * 用户登录 + * + * @param request 包含用户名/邮箱和密码的登录请求 + * @return 包含JWT令牌的认证响应,令牌有效期24小时 + * @throws ResourceNotFoundException 如果用户不存在 + * @throws InvalidOperationException 如果密码不匹配 + */ + JwtAuthenticationResponse signIn(LoginRequest request); + + /** + * 处理用户登出逻辑 + */ + void logout(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/DashboardService.java b/ems-backend/src/main/java/com/dne/ems/service/DashboardService.java index ae97e4b..eb99588 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/DashboardService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/DashboardService.java @@ -1,146 +1,146 @@ -package com.dne.ems.service; - -import java.util.List; -import java.util.Map; - -import com.dne.ems.dto.AqiDistributionDTO; -import com.dne.ems.dto.AqiHeatmapPointDTO; -import com.dne.ems.dto.DashboardStatsDTO; -import com.dne.ems.dto.GridCoverageDTO; -import com.dne.ems.dto.HeatmapPointDTO; -import com.dne.ems.dto.PollutantThresholdDTO; -import com.dne.ems.dto.PollutionStatsDTO; -import com.dne.ems.dto.TaskStatsDTO; -import com.dne.ems.dto.TrendDataPointDTO; - -/** - * 仪表盘服务接口,提供各类环境监测数据的统计和可视化数据 - * - *

主要功能: - *

    - *
  • 环境质量指标统计
  • - *
  • AQI数据分布和热力图
  • - *
  • 污染物阈值管理
  • - *
  • 任务完成情况统计
  • - *
  • 各类趋势分析数据
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024-03 - */ -public interface DashboardService { - - /** - * 获取污染物统计数据 - * - *

统计规则: - *

    - *
  • 按污染类型分组统计
  • - *
  • 包含过去24小时数据
  • - *
  • 按严重程度排序
  • - *
- * - * @return 污染物统计DTO列表 - * @see com.dne.ems.dto.PollutionStatsDTO - */ - List getPollutionStats(); - - /** - * 获取任务完成统计数据 - * - *

统计指标: - *

    - *
  • 任务总数
  • - *
  • 已完成任务数
  • - *
  • 超期任务数
  • - *
  • 平均完成时间
  • - *
- * - * @return 任务统计DTO - * @see com.dne.ems.dto.TaskStatsDTO - */ - TaskStatsDTO getTaskCompletionStats(); - - /** - * 获取仪表盘核心统计数据 - * - *

包含数据: - *

    - *
  • 实时AQI指数
  • - *
  • 待处理反馈数
  • - *
  • 待分配任务数
  • - *
  • 网格覆盖率
  • - *
  • 告警事件数
  • - *
- * - * @return 仪表盘统计DTO - * @see com.dne.ems.dto.DashboardStatsDTO - */ - DashboardStatsDTO getDashboardStats(); - - /** - * Gathers AQI distribution data for a pie/bar chart. - * - * @return A list of {@link AqiDistributionDTO} objects, each representing an AQI level and its count. - */ - List getAqiDistribution(); - - /** - * Gathers data for the monthly air quality exceedance trend chart. - * - * @return A list of {@link TrendDataPointDTO} for the last 12 months. - */ - List getMonthlyExceedanceTrend(); - - /** - * Gathers grid coverage data, grouped by city. - * - * @return A list of {@link GridCoverageDTO} objects, each representing a city and its grid count. - */ - List getGridCoverageByCity(); - - /** - * Gathers data for the pollution source heatmap. - * - * @return A list of {@link HeatmapPointDTO} objects, each representing a grid cell and its event intensity. - */ - List getHeatmapData(); - - /** - * Gathers data for the AQI heatmap. - * - * @return A list of {@link AqiHeatmapPointDTO} objects. - */ - List getAqiHeatmapData(); - - /** - * 获取所有污染物的阈值设置 - * - * @return 污染物阈值列表 - */ - List getPollutantThresholds(); - - /** - * 根据污染物名称获取阈值设置 - * - * @param pollutantName 污染物名称 - * @return 阈值设置,如果不存在则返回null - */ - PollutantThresholdDTO getPollutantThreshold(String pollutantName); - - /** - * 保存或更新污染物阈值设置 - * - * @param thresholdDTO 阈值设置 - * @return 保存后的阈值设置 - */ - PollutantThresholdDTO savePollutantThreshold(PollutantThresholdDTO thresholdDTO); - - /** - * 获取每种污染物的月度趋势数据 - * - * @return 每种污染物的月度趋势数据 - */ - Map> getPollutantMonthlyTrends(); +package com.dne.ems.service; + +import java.util.List; +import java.util.Map; + +import com.dne.ems.dto.AqiDistributionDTO; +import com.dne.ems.dto.AqiHeatmapPointDTO; +import com.dne.ems.dto.DashboardStatsDTO; +import com.dne.ems.dto.GridCoverageDTO; +import com.dne.ems.dto.HeatmapPointDTO; +import com.dne.ems.dto.PollutantThresholdDTO; +import com.dne.ems.dto.PollutionStatsDTO; +import com.dne.ems.dto.TaskStatsDTO; +import com.dne.ems.dto.TrendDataPointDTO; + +/** + * 仪表盘服务接口,提供各类环境监测数据的统计和可视化数据 + * + *

主要功能: + *

    + *
  • 环境质量指标统计
  • + *
  • AQI数据分布和热力图
  • + *
  • 污染物阈值管理
  • + *
  • 任务完成情况统计
  • + *
  • 各类趋势分析数据
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024-03 + */ +public interface DashboardService { + + /** + * 获取污染物统计数据 + * + *

统计规则: + *

    + *
  • 按污染类型分组统计
  • + *
  • 包含过去24小时数据
  • + *
  • 按严重程度排序
  • + *
+ * + * @return 污染物统计DTO列表 + * @see com.dne.ems.dto.PollutionStatsDTO + */ + List getPollutionStats(); + + /** + * 获取任务完成统计数据 + * + *

统计指标: + *

    + *
  • 任务总数
  • + *
  • 已完成任务数
  • + *
  • 超期任务数
  • + *
  • 平均完成时间
  • + *
+ * + * @return 任务统计DTO + * @see com.dne.ems.dto.TaskStatsDTO + */ + TaskStatsDTO getTaskCompletionStats(); + + /** + * 获取仪表盘核心统计数据 + * + *

包含数据: + *

    + *
  • 实时AQI指数
  • + *
  • 待处理反馈数
  • + *
  • 待分配任务数
  • + *
  • 网格覆盖率
  • + *
  • 告警事件数
  • + *
+ * + * @return 仪表盘统计DTO + * @see com.dne.ems.dto.DashboardStatsDTO + */ + DashboardStatsDTO getDashboardStats(); + + /** + * Gathers AQI distribution data for a pie/bar chart. + * + * @return A list of {@link AqiDistributionDTO} objects, each representing an AQI level and its count. + */ + List getAqiDistribution(); + + /** + * Gathers data for the monthly air quality exceedance trend chart. + * + * @return A list of {@link TrendDataPointDTO} for the last 12 months. + */ + List getMonthlyExceedanceTrend(); + + /** + * Gathers grid coverage data, grouped by city. + * + * @return A list of {@link GridCoverageDTO} objects, each representing a city and its grid count. + */ + List getGridCoverageByCity(); + + /** + * Gathers data for the pollution source heatmap. + * + * @return A list of {@link HeatmapPointDTO} objects, each representing a grid cell and its event intensity. + */ + List getHeatmapData(); + + /** + * Gathers data for the AQI heatmap. + * + * @return A list of {@link AqiHeatmapPointDTO} objects. + */ + List getAqiHeatmapData(); + + /** + * 获取所有污染物的阈值设置 + * + * @return 污染物阈值列表 + */ + List getPollutantThresholds(); + + /** + * 根据污染物名称获取阈值设置 + * + * @param pollutantName 污染物名称 + * @return 阈值设置,如果不存在则返回null + */ + PollutantThresholdDTO getPollutantThreshold(String pollutantName); + + /** + * 保存或更新污染物阈值设置 + * + * @param thresholdDTO 阈值设置 + * @return 保存后的阈值设置 + */ + PollutantThresholdDTO savePollutantThreshold(PollutantThresholdDTO thresholdDTO); + + /** + * 获取每种污染物的月度趋势数据 + * + * @return 每种污染物的月度趋势数据 + */ + Map> getPollutantMonthlyTrends(); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/FeedbackService.java b/ems-backend/src/main/java/com/dne/ems/service/FeedbackService.java index a53bec1..3e32dc9 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/FeedbackService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/FeedbackService.java @@ -1,132 +1,132 @@ -package com.dne.ems.service; -import java.time.LocalDate; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.web.multipart.MultipartFile; - -import com.dne.ems.dto.FeedbackResponseDTO; -import com.dne.ems.dto.FeedbackStatsResponse; -import com.dne.ems.dto.FeedbackSubmissionRequest; -import com.dne.ems.dto.ProcessFeedbackRequest; -import com.dne.ems.dto.PublicFeedbackRequest; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.security.CustomUserDetails; - -/** - * 反馈服务接口,处理环境问题反馈的提交和管理 - * - *

主要功能: - *

    - *
  • 用户反馈提交(认证用户和匿名用户)
  • - *
  • 反馈信息查询与过滤
  • - *
  • 反馈数据统计分析
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -public interface FeedbackService { - - /** - * 提交用户反馈(认证用户) - * - *

流程: - *

    - *
  1. 从安全上下文获取用户信息
  2. - *
  3. 创建反馈记录
  4. - *
  5. 生成唯一事件ID
  6. - *
  7. 触发AI初步处理
  8. - *
- * - * @param request 包含反馈信息的请求体 - * @param files 上传的附件(可选) - * @return 新创建的反馈实体 - * @throws ResourceNotFoundException 如果用户不存在 - */ - Feedback submitFeedback(FeedbackSubmissionRequest request, MultipartFile[] files); - - /** - * 获取所有反馈(多条件过滤) - * - *

支持以下过滤条件组合查询: - *

    - *
  • 状态:PENDING_REVIEW/PROCESSED/REJECTED
  • - *
  • 污染类型:AIR/WATER/SOIL/NOISE等
  • - *
  • 严重程度:LOW/MEDIUM/HIGH/CRITICAL
  • - *
  • 地理位置:城市/区县
  • - *
  • 时间范围:开始日期至结束日期
  • - *
  • 关键词:标题/描述模糊匹配
  • - *
- * - * @param userDetails 当前认证用户信息 - * @param status 反馈状态 - * @param pollutionType 污染类型 - * @param severityLevel 严重程度 - * @param cityName 城市名称 - * @param districtName 区县名称 - * @param startDate 开始日期(包含) - * @param endDate 结束日期(包含) - * @param keyword 关键词(标题/描述模糊匹配) - * @param pageable 分页参数(页号从0开始) - * @return 分页反馈列表(按创建时间降序) - * @see com.dne.ems.model.Feedback 反馈实体 - * @see com.dne.ems.model.enums.FeedbackStatus 反馈状态枚举 - * @see com.dne.ems.model.enums.PollutionType 污染类型枚举 - * @see com.dne.ems.model.enums.SeverityLevel 严重程度枚举 - */ - Page getFeedback( - CustomUserDetails userDetails, - FeedbackStatus status, - PollutionType pollutionType, - SeverityLevel severityLevel, - String cityName, - String districtName, - LocalDate startDate, - LocalDate endDate, - String keyword, - Pageable pageable); - - /** - * 根据ID获取反馈详情 - * @param id 反馈ID - * @return 反馈详情 - */ - Feedback getFeedbackById(Long id); - - /** - * 根据ID获取反馈详情DTO - * @param id 反馈ID - * @return 反馈详情DTO - */ - FeedbackResponseDTO getFeedbackDetail(Long id); - - /** - * 获取反馈统计数据 - * @param userDetails 当前认证用户信息 - * @return 反馈统计响应 - */ - FeedbackStatsResponse getFeedbackStats(CustomUserDetails userDetails); - - /** - * 提交公共反馈(匿名用户) - * - *

流程: - *

    - *
  1. 创建匿名反馈记录
  2. - *
  3. 生成唯一事件ID
  4. - *
  5. 触发AI初步处理
  6. - *
- * - * @param request 包含反馈信息的请求体 - * @param files 上传的附件(可选) - * @return 新创建的反馈实体 - */ - Feedback createPublicFeedback(PublicFeedbackRequest request, MultipartFile[] files); - - FeedbackResponseDTO processFeedback(Long id, ProcessFeedbackRequest request); +package com.dne.ems.service; +import java.time.LocalDate; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; + +import com.dne.ems.dto.FeedbackResponseDTO; +import com.dne.ems.dto.FeedbackStatsResponse; +import com.dne.ems.dto.FeedbackSubmissionRequest; +import com.dne.ems.dto.ProcessFeedbackRequest; +import com.dne.ems.dto.PublicFeedbackRequest; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.security.CustomUserDetails; + +/** + * 反馈服务接口,处理环境问题反馈的提交和管理 + * + *

主要功能: + *

    + *
  • 用户反馈提交(认证用户和匿名用户)
  • + *
  • 反馈信息查询与过滤
  • + *
  • 反馈数据统计分析
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +public interface FeedbackService { + + /** + * 提交用户反馈(认证用户) + * + *

流程: + *

    + *
  1. 从安全上下文获取用户信息
  2. + *
  3. 创建反馈记录
  4. + *
  5. 生成唯一事件ID
  6. + *
  7. 触发AI初步处理
  8. + *
+ * + * @param request 包含反馈信息的请求体 + * @param files 上传的附件(可选) + * @return 新创建的反馈实体 + * @throws ResourceNotFoundException 如果用户不存在 + */ + Feedback submitFeedback(FeedbackSubmissionRequest request, MultipartFile[] files); + + /** + * 获取所有反馈(多条件过滤) + * + *

支持以下过滤条件组合查询: + *

    + *
  • 状态:PENDING_REVIEW/PROCESSED/REJECTED
  • + *
  • 污染类型:AIR/WATER/SOIL/NOISE等
  • + *
  • 严重程度:LOW/MEDIUM/HIGH/CRITICAL
  • + *
  • 地理位置:城市/区县
  • + *
  • 时间范围:开始日期至结束日期
  • + *
  • 关键词:标题/描述模糊匹配
  • + *
+ * + * @param userDetails 当前认证用户信息 + * @param status 反馈状态 + * @param pollutionType 污染类型 + * @param severityLevel 严重程度 + * @param cityName 城市名称 + * @param districtName 区县名称 + * @param startDate 开始日期(包含) + * @param endDate 结束日期(包含) + * @param keyword 关键词(标题/描述模糊匹配) + * @param pageable 分页参数(页号从0开始) + * @return 分页反馈列表(按创建时间降序) + * @see com.dne.ems.model.Feedback 反馈实体 + * @see com.dne.ems.model.enums.FeedbackStatus 反馈状态枚举 + * @see com.dne.ems.model.enums.PollutionType 污染类型枚举 + * @see com.dne.ems.model.enums.SeverityLevel 严重程度枚举 + */ + Page getFeedback( + CustomUserDetails userDetails, + FeedbackStatus status, + PollutionType pollutionType, + SeverityLevel severityLevel, + String cityName, + String districtName, + LocalDate startDate, + LocalDate endDate, + String keyword, + Pageable pageable); + + /** + * 根据ID获取反馈详情 + * @param id 反馈ID + * @return 反馈详情 + */ + Feedback getFeedbackById(Long id); + + /** + * 根据ID获取反馈详情DTO + * @param id 反馈ID + * @return 反馈详情DTO + */ + FeedbackResponseDTO getFeedbackDetail(Long id); + + /** + * 获取反馈统计数据 + * @param userDetails 当前认证用户信息 + * @return 反馈统计响应 + */ + FeedbackStatsResponse getFeedbackStats(CustomUserDetails userDetails); + + /** + * 提交公共反馈(匿名用户) + * + *

流程: + *

    + *
  1. 创建匿名反馈记录
  2. + *
  3. 生成唯一事件ID
  4. + *
  5. 触发AI初步处理
  6. + *
+ * + * @param request 包含反馈信息的请求体 + * @param files 上传的附件(可选) + * @return 新创建的反馈实体 + */ + Feedback createPublicFeedback(PublicFeedbackRequest request, MultipartFile[] files); + + FeedbackResponseDTO processFeedback(Long id, ProcessFeedbackRequest request); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/FileStorageService.java b/ems-backend/src/main/java/com/dne/ems/service/FileStorageService.java index bc44413..b3bec1c 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/FileStorageService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/FileStorageService.java @@ -1,70 +1,70 @@ -package com.dne.ems.service; - -import org.springframework.core.io.Resource; -import org.springframework.web.multipart.MultipartFile; - -import com.dne.ems.model.Attachment; -import com.dne.ems.model.Feedback; - -/** - * 文件存储服务接口,处理文件上传和下载 - * - *

主要功能: - *

    - *
  • 安全存储上传文件
  • - *
  • 管理文件与业务实体的关联
  • - *
  • 提供文件下载功能
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -public interface FileStorageService { - - /** - * 存储文件并将其与反馈实体关联 - * - *

功能说明: - *

    - *
  • 保存上传的文件到文件系统
  • - *
  • 创建文件与反馈的关联关系
  • - *
  • 返回创建的附件实体
  • - *
- * - * @param file 要存储的文件 - * @param feedback 关联的反馈实体 - * @return 创建的附件实体 - * @throws com.dne.ems.exception.FileStorageException 如果文件存储失败 - * @deprecated 请使用更通用的 storeFile(file) 方法,并手动创建关联关系 - */ - @Deprecated - Attachment storeFileAndCreateAttachment(MultipartFile file, Feedback feedback); - - /** - * 存储文件并返回其唯一存储名称 - * - *

安全措施: - *

    - *
  • 验证文件类型和扩展名
  • - *
  • 生成唯一文件名防止冲突
  • - *
  • 检查路径遍历漏洞
  • - *
  • 限制允许的文件类型
  • - *
- * - * @param file 要存储的文件 - * @return 存储文件的唯一名称 - * @throws com.dne.ems.exception.FileStorageException 如果文件存储失败 - */ - String storeFile(MultipartFile file); - - /** - * 从存储位置加载文件资源 - * - * @param fileName 要加载的文件名 - * @return 文件资源 - * @throws com.dne.ems.exception.FileStorageException 如果文件不存在或不可读 - */ - Resource loadFileAsResource(String fileName); - +package com.dne.ems.service; + +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; + +import com.dne.ems.model.Attachment; +import com.dne.ems.model.Feedback; + +/** + * 文件存储服务接口,处理文件上传和下载 + * + *

主要功能: + *

    + *
  • 安全存储上传文件
  • + *
  • 管理文件与业务实体的关联
  • + *
  • 提供文件下载功能
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +public interface FileStorageService { + + /** + * 存储文件并将其与反馈实体关联 + * + *

功能说明: + *

    + *
  • 保存上传的文件到文件系统
  • + *
  • 创建文件与反馈的关联关系
  • + *
  • 返回创建的附件实体
  • + *
+ * + * @param file 要存储的文件 + * @param feedback 关联的反馈实体 + * @return 创建的附件实体 + * @throws com.dne.ems.exception.FileStorageException 如果文件存储失败 + * @deprecated 请使用更通用的 storeFile(file) 方法,并手动创建关联关系 + */ + @Deprecated + Attachment storeFileAndCreateAttachment(MultipartFile file, Feedback feedback); + + /** + * 存储文件并返回其唯一存储名称 + * + *

安全措施: + *

    + *
  • 验证文件类型和扩展名
  • + *
  • 生成唯一文件名防止冲突
  • + *
  • 检查路径遍历漏洞
  • + *
  • 限制允许的文件类型
  • + *
+ * + * @param file 要存储的文件 + * @return 存储文件的唯一名称 + * @throws com.dne.ems.exception.FileStorageException 如果文件存储失败 + */ + String storeFile(MultipartFile file); + + /** + * 从存储位置加载文件资源 + * + * @param fileName 要加载的文件名 + * @return 文件资源 + * @throws com.dne.ems.exception.FileStorageException 如果文件不存在或不可读 + */ + Resource loadFileAsResource(String fileName); + } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/GridService.java b/ems-backend/src/main/java/com/dne/ems/service/GridService.java index 2ab2153..acc66cc 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/GridService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/GridService.java @@ -1,87 +1,87 @@ -package com.dne.ems.service; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import com.dne.ems.dto.GridUpdateRequest; -import com.dne.ems.model.Grid; -import com.dne.ems.model.UserAccount; - -import java.util.List; -import java.util.Optional; - -/** - * 网格服务接口,处理网格数据的查询和更新 - * - *

主要功能: - *

    - *
  • 网格数据查询与过滤
  • - *
  • 网格信息更新
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2025 - */ -public interface GridService { - - /** - * 获取所有网格的列表. - * @return a list of all grids. - */ - List getAllGrids(); - - /** - * Finds a grid by its ID. - * @param gridId the ID of the grid. - * @return an Optional containing the grid if found. - */ - Optional getGridById(Long gridId); - - /** - * Assigns a worker to a specific grid. - * @param gridId the ID of the grid. - * @param userId the ID of the user (worker). - * @return the updated UserAccount of the worker. - */ - UserAccount assignWorkerToGrid(Long gridId, Long userId); - - /** - * Unassigns any worker from a specific grid. - * @param gridId the ID of the grid. - */ - void unassignWorkerFromGrid(Long gridId); - - /** - * 获取网格分页列表,支持按城市和区县过滤 - * - *

查询规则: - *

    - *
  • 支持按城市名称过滤
  • - *
  • 支持按区县名称过滤
  • - *
  • 返回分页结果
  • - *
- * - * @param cityName 城市名称过滤条件(可选) - * @param districtName 区县名称过滤条件(可选) - * @param pageable 分页参数 - * @return 网格分页列表 - */ - Page getGrids(String cityName, String districtName, Pageable pageable); - - /** - * 更新网格信息 - * - *

更新规则: - *

    - *
  • 可更新障碍物状态
  • - *
  • 可更新描述信息
  • - *
- * - * @param gridId 要更新的网格ID - * @param request 包含更新数据的请求体 - * @return 更新后的网格实体 - * @throws jakarta.persistence.EntityNotFoundException 如果网格不存在 - */ - Grid updateGrid(Long gridId, GridUpdateRequest request); +package com.dne.ems.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.dne.ems.dto.GridUpdateRequest; +import com.dne.ems.model.Grid; +import com.dne.ems.model.UserAccount; + +import java.util.List; +import java.util.Optional; + +/** + * 网格服务接口,处理网格数据的查询和更新 + * + *

主要功能: + *

    + *
  • 网格数据查询与过滤
  • + *
  • 网格信息更新
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2025 + */ +public interface GridService { + + /** + * 获取所有网格的列表. + * @return a list of all grids. + */ + List getAllGrids(); + + /** + * Finds a grid by its ID. + * @param gridId the ID of the grid. + * @return an Optional containing the grid if found. + */ + Optional getGridById(Long gridId); + + /** + * Assigns a worker to a specific grid. + * @param gridId the ID of the grid. + * @param userId the ID of the user (worker). + * @return the updated UserAccount of the worker. + */ + UserAccount assignWorkerToGrid(Long gridId, Long userId); + + /** + * Unassigns any worker from a specific grid. + * @param gridId the ID of the grid. + */ + void unassignWorkerFromGrid(Long gridId); + + /** + * 获取网格分页列表,支持按城市和区县过滤 + * + *

查询规则: + *

    + *
  • 支持按城市名称过滤
  • + *
  • 支持按区县名称过滤
  • + *
  • 返回分页结果
  • + *
+ * + * @param cityName 城市名称过滤条件(可选) + * @param districtName 区县名称过滤条件(可选) + * @param pageable 分页参数 + * @return 网格分页列表 + */ + Page getGrids(String cityName, String districtName, Pageable pageable); + + /** + * 更新网格信息 + * + *

更新规则: + *

    + *
  • 可更新障碍物状态
  • + *
  • 可更新描述信息
  • + *
+ * + * @param gridId 要更新的网格ID + * @param request 包含更新数据的请求体 + * @return 更新后的网格实体 + * @throws jakarta.persistence.EntityNotFoundException 如果网格不存在 + */ + Grid updateGrid(Long gridId, GridUpdateRequest request); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/GridWorkerTaskService.java b/ems-backend/src/main/java/com/dne/ems/service/GridWorkerTaskService.java index 298871e..838d25d 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/GridWorkerTaskService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/GridWorkerTaskService.java @@ -1,108 +1,108 @@ -package com.dne.ems.service; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.web.multipart.MultipartFile; - -import com.dne.ems.dto.TaskDetailDTO; -import com.dne.ems.dto.TaskSubmissionRequest; -import com.dne.ems.dto.TaskSummaryDTO; -import com.dne.ems.model.enums.TaskStatus; - -import java.util.List; - -/** - * 网格员任务服务接口,处理网格员的任务管理操作 - * - *

主要功能: - *

    - *
  • 查询分配的任务列表
  • - *
  • 处理任务状态流转
  • - *
  • 提交任务完成情况
  • - *
  • 获取任务详情
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024-03 - */ -public interface GridWorkerTaskService { - - /** - * 获取分配给指定网格员的任务列表 - * - *

查询规则: - *

    - *
  • 只返回分配给该网格员的任务
  • - *
  • 可按任务状态过滤
  • - *
  • 默认按截止时间升序排序
  • - *
- * - * @param workerId 网格员ID - * @param status 任务状态过滤条件(可选) - * @param pageable 分页参数 - * @return 任务摘要分页列表 - * @see com.dne.ems.dto.TaskSummaryDTO - * @see com.dne.ems.model.enums.TaskStatus - */ - Page getAssignedTasks(Long workerId, TaskStatus status, Pageable pageable); - - /** - * 网格员接受任务 - * - *

状态流转: - *

    - *
  • ASSIGNED -> IN_PROGRESS
  • - *
  • 验证任务确实分配给该网格员
  • - *
  • 记录接受时间
  • - *
- * - * @param taskId 任务ID - * @param workerId 网格员ID(用于验证) - * @return 更新后的任务摘要 - * @throws ResourceNotFoundException 如果任务不存在 - * @throws InvalidOperationException 如果任务状态不允许接受 - * @see com.dne.ems.dto.TaskSummaryDTO - */ - TaskSummaryDTO acceptTask(Long taskId, Long workerId); - /** - * 提交任务完成情况 - * - *

状态流转: - *

    - *
  • IN_PROGRESS -> PENDING_REVIEW
  • - *
  • 验证任务确实分配给该网格员
  • - *
  • 记录提交时间和完成说明
  • - *
- * - * @param taskId 任务ID - * @param workerId 网格员ID(用于验证) - * @param request 包含完成详情的请求 - * @return 更新后的任务摘要 - * @throws ResourceNotFoundException 如果任务不存在 - * @throws InvalidOperationException 如果任务状态不允许提交 - * @see com.dne.ems.dto.TaskSubmissionRequest - * @see com.dne.ems.dto.TaskSummaryDTO - */ - TaskSummaryDTO submitTaskCompletion(Long taskId, Long workerId, TaskSubmissionRequest request); - - /** - * Submits the completion details for a task, including file attachments. - * - * @param taskId The ID of the task. - * @param workerId The ID of the worker submitting the task. - * @param request The task submission details. - * @param files A list of files attached to the submission. - * @return A summary of the updated task. - */ - TaskSummaryDTO submitTaskCompletion(Long taskId, Long workerId, TaskSubmissionRequest request, List files); - - /** - * Retrieves the details of a specific task assigned to a worker. - * - * @param taskId The ID of the task. - * @param workerId The ID of the worker requesting the details, for validation. - * @return The full details of the task. - */ - TaskDetailDTO getTaskDetails(Long taskId, Long workerId); +package com.dne.ems.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; + +import com.dne.ems.dto.TaskDetailDTO; +import com.dne.ems.dto.TaskSubmissionRequest; +import com.dne.ems.dto.TaskSummaryDTO; +import com.dne.ems.model.enums.TaskStatus; + +import java.util.List; + +/** + * 网格员任务服务接口,处理网格员的任务管理操作 + * + *

主要功能: + *

    + *
  • 查询分配的任务列表
  • + *
  • 处理任务状态流转
  • + *
  • 提交任务完成情况
  • + *
  • 获取任务详情
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024-03 + */ +public interface GridWorkerTaskService { + + /** + * 获取分配给指定网格员的任务列表 + * + *

查询规则: + *

    + *
  • 只返回分配给该网格员的任务
  • + *
  • 可按任务状态过滤
  • + *
  • 默认按截止时间升序排序
  • + *
+ * + * @param workerId 网格员ID + * @param status 任务状态过滤条件(可选) + * @param pageable 分页参数 + * @return 任务摘要分页列表 + * @see com.dne.ems.dto.TaskSummaryDTO + * @see com.dne.ems.model.enums.TaskStatus + */ + Page getAssignedTasks(Long workerId, TaskStatus status, Pageable pageable); + + /** + * 网格员接受任务 + * + *

状态流转: + *

    + *
  • ASSIGNED -> IN_PROGRESS
  • + *
  • 验证任务确实分配给该网格员
  • + *
  • 记录接受时间
  • + *
+ * + * @param taskId 任务ID + * @param workerId 网格员ID(用于验证) + * @return 更新后的任务摘要 + * @throws ResourceNotFoundException 如果任务不存在 + * @throws InvalidOperationException 如果任务状态不允许接受 + * @see com.dne.ems.dto.TaskSummaryDTO + */ + TaskSummaryDTO acceptTask(Long taskId, Long workerId); + /** + * 提交任务完成情况 + * + *

状态流转: + *

    + *
  • IN_PROGRESS -> PENDING_REVIEW
  • + *
  • 验证任务确实分配给该网格员
  • + *
  • 记录提交时间和完成说明
  • + *
+ * + * @param taskId 任务ID + * @param workerId 网格员ID(用于验证) + * @param request 包含完成详情的请求 + * @return 更新后的任务摘要 + * @throws ResourceNotFoundException 如果任务不存在 + * @throws InvalidOperationException 如果任务状态不允许提交 + * @see com.dne.ems.dto.TaskSubmissionRequest + * @see com.dne.ems.dto.TaskSummaryDTO + */ + TaskSummaryDTO submitTaskCompletion(Long taskId, Long workerId, TaskSubmissionRequest request); + + /** + * Submits the completion details for a task, including file attachments. + * + * @param taskId The ID of the task. + * @param workerId The ID of the worker submitting the task. + * @param request The task submission details. + * @param files A list of files attached to the submission. + * @return A summary of the updated task. + */ + TaskSummaryDTO submitTaskCompletion(Long taskId, Long workerId, TaskSubmissionRequest request, List files); + + /** + * Retrieves the details of a specific task assigned to a worker. + * + * @param taskId The ID of the task. + * @param workerId The ID of the worker requesting the details, for validation. + * @return The full details of the task. + */ + TaskDetailDTO getTaskDetails(Long taskId, Long workerId); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/JsonStorageService.java b/ems-backend/src/main/java/com/dne/ems/service/JsonStorageService.java index 23aabbf..c176e34 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/JsonStorageService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/JsonStorageService.java @@ -1,106 +1,106 @@ -package com.dne.ems.service; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - -/** - * 通用服务,用于将数据以JSON文件形式存储和读取。 - * 提供线程安全的读写操作。 - */ -@Service -public class JsonStorageService { - - private static final Logger log = LoggerFactory.getLogger(JsonStorageService.class); - private final ObjectMapper objectMapper; - private final Path storagePath; - private final ConcurrentHashMap fileLocks = new ConcurrentHashMap<>(); - - /** - * 构造函数,初始化ObjectMapper和存储路径。 - */ - public JsonStorageService() { - this.objectMapper = new ObjectMapper(); - // 支持Java 8日期时间类型 - this.objectMapper.registerModule(new JavaTimeModule()); - // 禁用将日期写为时间戳(即数字),而是使用ISO-8601字符串格式 - this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - // 格式化输出JSON - this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); - - try { - // 将数据库文件存放在项目的 'json-db' 目录下 - File dbDir = new File("json-db"); - if (!dbDir.exists()) { - dbDir.mkdirs(); - } - this.storagePath = dbDir.toPath(); - log.info("JSON storage path initialized at: {}", storagePath.toAbsolutePath()); - } catch (Exception e) { - log.error("Failed to initialize JSON storage directory.", e); - throw new RuntimeException("Could not initialize JSON storage directory", e); - } - } - - /** - * 从指定的JSON文件读取数据列表。 - * - * @param filename 文件名 (例如 "grids.json") - * @param typeReference Jackson用于反序列化的类型引用 - * @param 列表中的对象类型 - * @return 如果文件存在且非空,则返回数据列表;否则返回空列表。 - */ - public List readData(String filename, TypeReference> typeReference) { - Lock lock = fileLocks.computeIfAbsent(filename, k -> new ReentrantLock()); - lock.lock(); - try { - Path filePath = storagePath.resolve(filename); - if (!Files.exists(filePath) || Files.size(filePath) == 0) { - log.warn("File {} not found or is empty. Returning empty list.", filename); - return Collections.emptyList(); - } - return objectMapper.readValue(filePath.toFile(), typeReference); - } catch (IOException e) { - log.error("Failed to read data from {}", filename, e); - return Collections.emptyList(); - } finally { - lock.unlock(); - } - } - - /** - * 将数据列表写入指定的JSON文件。 - * - * @param filename 文件名 (例如 "grids.json") - * @param data 要写入的数据列表 - * @param 列表中的对象类型 - */ - public void writeData(String filename, List data) { - Lock lock = fileLocks.computeIfAbsent(filename, k -> new ReentrantLock()); - lock.lock(); - try { - Path filePath = storagePath.resolve(filename); - objectMapper.writeValue(filePath.toFile(), data); - log.info("Successfully wrote {} records to {}", data.size(), filename); - } catch (IOException e) { - log.error("Failed to write data to {}", filename, e); - } finally { - lock.unlock(); - } - } +package com.dne.ems.service; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +/** + * 通用服务,用于将数据以JSON文件形式存储和读取。 + * 提供线程安全的读写操作。 + */ +@Service +public class JsonStorageService { + + private static final Logger log = LoggerFactory.getLogger(JsonStorageService.class); + private final ObjectMapper objectMapper; + private final Path storagePath; + private final ConcurrentHashMap fileLocks = new ConcurrentHashMap<>(); + + /** + * 构造函数,初始化ObjectMapper和存储路径。 + */ + public JsonStorageService() { + this.objectMapper = new ObjectMapper(); + // 支持Java 8日期时间类型 + this.objectMapper.registerModule(new JavaTimeModule()); + // 禁用将日期写为时间戳(即数字),而是使用ISO-8601字符串格式 + this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + // 格式化输出JSON + this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + + try { + // 将数据库文件存放在项目的 'json-db' 目录下 + File dbDir = new File("json-db"); + if (!dbDir.exists()) { + dbDir.mkdirs(); + } + this.storagePath = dbDir.toPath(); + log.info("JSON storage path initialized at: {}", storagePath.toAbsolutePath()); + } catch (Exception e) { + log.error("Failed to initialize JSON storage directory.", e); + throw new RuntimeException("Could not initialize JSON storage directory", e); + } + } + + /** + * 从指定的JSON文件读取数据列表。 + * + * @param filename 文件名 (例如 "grids.json") + * @param typeReference Jackson用于反序列化的类型引用 + * @param 列表中的对象类型 + * @return 如果文件存在且非空,则返回数据列表;否则返回空列表。 + */ + public List readData(String filename, TypeReference> typeReference) { + Lock lock = fileLocks.computeIfAbsent(filename, k -> new ReentrantLock()); + lock.lock(); + try { + Path filePath = storagePath.resolve(filename); + if (!Files.exists(filePath) || Files.size(filePath) == 0) { + log.warn("File {} not found or is empty. Returning empty list.", filename); + return Collections.emptyList(); + } + return objectMapper.readValue(filePath.toFile(), typeReference); + } catch (IOException e) { + log.error("Failed to read data from {}", filename, e); + return Collections.emptyList(); + } finally { + lock.unlock(); + } + } + + /** + * 将数据列表写入指定的JSON文件。 + * + * @param filename 文件名 (例如 "grids.json") + * @param data 要写入的数据列表 + * @param 列表中的对象类型 + */ + public void writeData(String filename, List data) { + Lock lock = fileLocks.computeIfAbsent(filename, k -> new ReentrantLock()); + lock.lock(); + try { + Path filePath = storagePath.resolve(filename); + objectMapper.writeValue(filePath.toFile(), data); + log.info("Successfully wrote {} records to {}", data.size(), filename); + } catch (IOException e) { + log.error("Failed to write data to {}", filename, e); + } finally { + lock.unlock(); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/JwtService.java b/ems-backend/src/main/java/com/dne/ems/service/JwtService.java index 2dc29cd..15f96ba 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/JwtService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/JwtService.java @@ -1,55 +1,55 @@ -package com.dne.ems.service; - -import org.springframework.security.core.userdetails.UserDetails; - -import io.jsonwebtoken.Claims; - -/** - * JWT服务接口,处理JWT令牌的生成、解析和验证 - * - *

安全特性: - *

    - *
  • 使用HS256算法签名
  • - *
  • 令牌有效期24小时
  • - *
  • 包含用户角色和权限声明
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024-03 - */ -public interface JwtService { - /** - * 从JWT令牌中提取用户名 - * - * @param token JWT令牌字符串 - * @return 令牌中包含的用户名 - * @throws IllegalArgumentException 如果令牌无效或过期 - */ - String extractUserName(String token); - - /** - * 提取JWT令牌的所有声明 - * - * @param token JWT令牌字符串 - * @return 包含所有声明的Claims对象 - */ - Claims extractAllClaims(String token); - - /** - * 为用户生成JWT令牌 - * - * @param userDetails 用户详情,包含用户名和权限 - * @return 生成的JWT令牌(有效期24小时) - */ - String generateToken(UserDetails userDetails); - - /** - * 验证JWT令牌有效性 - * - * @param token 待验证的JWT令牌 - * @param userDetails 用户详情用于验证 - * @return true如果令牌有效且匹配用户,false否则 - */ - boolean isTokenValid(String token, UserDetails userDetails); +package com.dne.ems.service; + +import org.springframework.security.core.userdetails.UserDetails; + +import io.jsonwebtoken.Claims; + +/** + * JWT服务接口,处理JWT令牌的生成、解析和验证 + * + *

安全特性: + *

    + *
  • 使用HS256算法签名
  • + *
  • 令牌有效期24小时
  • + *
  • 包含用户角色和权限声明
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024-03 + */ +public interface JwtService { + /** + * 从JWT令牌中提取用户名 + * + * @param token JWT令牌字符串 + * @return 令牌中包含的用户名 + * @throws IllegalArgumentException 如果令牌无效或过期 + */ + String extractUserName(String token); + + /** + * 提取JWT令牌的所有声明 + * + * @param token JWT令牌字符串 + * @return 包含所有声明的Claims对象 + */ + Claims extractAllClaims(String token); + + /** + * 为用户生成JWT令牌 + * + * @param userDetails 用户详情,包含用户名和权限 + * @return 生成的JWT令牌(有效期24小时) + */ + String generateToken(UserDetails userDetails); + + /** + * 验证JWT令牌有效性 + * + * @param token 待验证的JWT令牌 + * @param userDetails 用户详情用于验证 + * @return true如果令牌有效且匹配用户,false否则 + */ + boolean isTokenValid(String token, UserDetails userDetails); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/LoginAttemptService.java b/ems-backend/src/main/java/com/dne/ems/service/LoginAttemptService.java index 8486c0b..4f44867 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/LoginAttemptService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/LoginAttemptService.java @@ -1,23 +1,23 @@ -package com.dne.ems.service; - -public interface LoginAttemptService { - - /** - * 当用户登录成功时调用,清除失败尝试记录 - * @param key 通常是用户名或IP地址 - */ - void loginSucceeded(String key); - - /** - * 当用户登录失败时调用,增加失败尝试次数 - * @param key 通常是用户名或IP地址 - */ - void loginFailed(String key); - - /** - * 检查用户是否因为失败次数过多而被锁定 - * @param key 通常是用户名或IP地址 - * @return 如果被锁定,则返回true - */ - boolean isBlocked(String key); +package com.dne.ems.service; + +public interface LoginAttemptService { + + /** + * 当用户登录成功时调用,清除失败尝试记录 + * @param key 通常是用户名或IP地址 + */ + void loginSucceeded(String key); + + /** + * 当用户登录失败时调用,增加失败尝试次数 + * @param key 通常是用户名或IP地址 + */ + void loginFailed(String key); + + /** + * 检查用户是否因为失败次数过多而被锁定 + * @param key 通常是用户名或IP地址 + * @return 如果被锁定,则返回true + */ + boolean isBlocked(String key); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/MailService.java b/ems-backend/src/main/java/com/dne/ems/service/MailService.java index 4e05ff4..72d5aac 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/MailService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/MailService.java @@ -1,49 +1,49 @@ -package com.dne.ems.service; - -/** - * 邮件服务接口,提供发送各类邮件的功能 - * - *

主要功能: - *

    - *
  • 发送密码重置验证码邮件
  • - *
  • 发送账号注册验证码邮件
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -public interface MailService { - - /** - * 发送密码重置验证码邮件 - * - *

邮件内容: - *

    - *
  • 包含6位数字验证码
  • - *
  • 有效期5分钟
  • - *
  • 安全提示信息
  • - *
- * - * @param to 目标邮箱地址 - * @param code 6位数字验证码 - * @throws RuntimeException 如果邮件发送失败 - */ - void sendPasswordResetEmail(String to, String code); - - /** - * 发送账号注册验证码邮件 - * - *

邮件内容: - *

    - *
  • 包含6位数字验证码
  • - *
  • 有效期5分钟
  • - *
  • 欢迎信息
  • - *
- * - * @param to 目标邮箱地址 - * @param code 6位数字验证码 - * @throws RuntimeException 如果邮件发送失败 - */ - void sendVerificationCodeEmail(String to, String code); +package com.dne.ems.service; + +/** + * 邮件服务接口,提供发送各类邮件的功能 + * + *

主要功能: + *

    + *
  • 发送密码重置验证码邮件
  • + *
  • 发送账号注册验证码邮件
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +public interface MailService { + + /** + * 发送密码重置验证码邮件 + * + *

邮件内容: + *

    + *
  • 包含6位数字验证码
  • + *
  • 有效期5分钟
  • + *
  • 安全提示信息
  • + *
+ * + * @param to 目标邮箱地址 + * @param code 6位数字验证码 + * @throws RuntimeException 如果邮件发送失败 + */ + void sendPasswordResetEmail(String to, String code); + + /** + * 发送账号注册验证码邮件 + * + *

邮件内容: + *

    + *
  • 包含6位数字验证码
  • + *
  • 有效期5分钟
  • + *
  • 欢迎信息
  • + *
+ * + * @param to 目标邮箱地址 + * @param code 6位数字验证码 + * @throws RuntimeException 如果邮件发送失败 + */ + void sendVerificationCodeEmail(String to, String code); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/OperationLogService.java b/ems-backend/src/main/java/com/dne/ems/service/OperationLogService.java index 67a933b..2be7b04 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/OperationLogService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/OperationLogService.java @@ -1,81 +1,81 @@ -package com.dne.ems.service; - -import java.time.LocalDateTime; -import java.util.List; - -import com.dne.ems.dto.OperationLogDTO; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.OperationType; - -/** - * 操作日志服务接口,提供记录和查询用户操作日志的功能。 - * - * @version 1.0 - * @since 2025-06-20 - */ -public interface OperationLogService { - - /** - * 记录用户操作 - * - * @param operationType 操作类型 - * @param description 操作描述 - * @param targetId 操作对象ID(可选) - * @param targetType 操作对象类型(可选) - */ - void recordOperation(OperationType operationType, String description, String targetId, String targetType); - - /** - * 记录特定用户的操作 - * - * @param user 执行操作的用户 - * @param operationType 操作类型 - * @param description 操作描述 - * @param targetId 操作对象ID(可选) - * @param targetType 操作对象类型(可选) - */ - void recordOperation(UserAccount user, OperationType operationType, String description, String targetId, String targetType); - - /** - * 记录用户登录操作 - * - * @param userId 用户ID - * @param ipAddress IP地址 - * @param success 是否成功 - */ - void recordLogin(Long userId, String ipAddress, boolean success); - - /** - * 记录用户登出操作 - * - * @param userId 用户ID - */ - void recordLogout(Long userId); - - /** - * 获取指定用户的操作日志 - * - * @param userId 用户ID - * @return 操作日志DTO列表 - */ - List getUserOperationLogs(Long userId); - - /** - * 获取所有操作日志 - * - * @return 操作日志DTO列表 - */ - List getAllOperationLogs(); - - /** - * 根据条件查询操作日志 - * - * @param userId 用户ID(可选) - * @param operationType 操作类型(可选) - * @param startTime 开始时间(可选) - * @param endTime 结束时间(可选) - * @return 操作日志DTO列表 - */ - List queryOperationLogs(Long userId, OperationType operationType, - LocalDateTime startTime, LocalDateTime endTime); +package com.dne.ems.service; + +import java.time.LocalDateTime; +import java.util.List; + +import com.dne.ems.dto.OperationLogDTO; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.OperationType; + +/** + * 操作日志服务接口,提供记录和查询用户操作日志的功能。 + * + * @version 1.0 + * @since 2025-06-20 + */ +public interface OperationLogService { + + /** + * 记录用户操作 + * + * @param operationType 操作类型 + * @param description 操作描述 + * @param targetId 操作对象ID(可选) + * @param targetType 操作对象类型(可选) + */ + void recordOperation(OperationType operationType, String description, String targetId, String targetType); + + /** + * 记录特定用户的操作 + * + * @param user 执行操作的用户 + * @param operationType 操作类型 + * @param description 操作描述 + * @param targetId 操作对象ID(可选) + * @param targetType 操作对象类型(可选) + */ + void recordOperation(UserAccount user, OperationType operationType, String description, String targetId, String targetType); + + /** + * 记录用户登录操作 + * + * @param userId 用户ID + * @param ipAddress IP地址 + * @param success 是否成功 + */ + void recordLogin(Long userId, String ipAddress, boolean success); + + /** + * 记录用户登出操作 + * + * @param userId 用户ID + */ + void recordLogout(Long userId); + + /** + * 获取指定用户的操作日志 + * + * @param userId 用户ID + * @return 操作日志DTO列表 + */ + List getUserOperationLogs(Long userId); + + /** + * 获取所有操作日志 + * + * @return 操作日志DTO列表 + */ + List getAllOperationLogs(); + + /** + * 根据条件查询操作日志 + * + * @param userId 用户ID(可选) + * @param operationType 操作类型(可选) + * @param startTime 开始时间(可选) + * @param endTime 结束时间(可选) + * @return 操作日志DTO列表 + */ + List queryOperationLogs(Long userId, OperationType operationType, + LocalDateTime startTime, LocalDateTime endTime); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/PersonnelService.java b/ems-backend/src/main/java/com/dne/ems/service/PersonnelService.java index 6cc3c16..e05d101 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/PersonnelService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/PersonnelService.java @@ -1,49 +1,49 @@ -package com.dne.ems.service; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import com.dne.ems.dto.UserCreationRequest; -import com.dne.ems.dto.UserUpdateRequest; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.Role; - -/** - * Service interface for personnel management operations. - */ -public interface PersonnelService { - - /** - * Retrieves a paginated and filtered list of users. - * - * @param role Optional filter by user role. - * @param name Optional filter by user name (case-insensitive search). - * @param pageable Pagination and sorting information. - * @return A {@link Page} of {@link UserAccount} objects. - */ - Page getUsers(Role role, String name, Pageable pageable); - - /** - * Updates a user's information. - * - * @param userId The ID of the user to update. - * @param request The request containing the fields to update. - * @return The updated {@link UserAccount}. - */ - /** - * Creates a new user account. - * - * @param request The request containing the new user's details. - * @return The created {@link UserAccount}. - */ - UserAccount createUser(UserCreationRequest request); - UserAccount updateUser(Long userId, UserUpdateRequest request); - - /** - * Deletes a user by marking them as inactive. - * - * @param userId The ID of the user to delete. - */ - void deleteUser(Long userId); - - UserAccount updateOwnProfile(String currentUserEmail, UserUpdateRequest request); +package com.dne.ems.service; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.dne.ems.dto.UserCreationRequest; +import com.dne.ems.dto.UserUpdateRequest; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.Role; + +/** + * Service interface for personnel management operations. + */ +public interface PersonnelService { + + /** + * Retrieves a paginated and filtered list of users. + * + * @param role Optional filter by user role. + * @param name Optional filter by user name (case-insensitive search). + * @param pageable Pagination and sorting information. + * @return A {@link Page} of {@link UserAccount} objects. + */ + Page getUsers(Role role, String name, Pageable pageable); + + /** + * Updates a user's information. + * + * @param userId The ID of the user to update. + * @param request The request containing the fields to update. + * @return The updated {@link UserAccount}. + */ + /** + * Creates a new user account. + * + * @param request The request containing the new user's details. + * @return The created {@link UserAccount}. + */ + UserAccount createUser(UserCreationRequest request); + UserAccount updateUser(Long userId, UserUpdateRequest request); + + /** + * Deletes a user by marking them as inactive. + * + * @param userId The ID of the user to delete. + */ + void deleteUser(Long userId); + + UserAccount updateOwnProfile(String currentUserEmail, UserUpdateRequest request); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/SupervisorService.java b/ems-backend/src/main/java/com/dne/ems/service/SupervisorService.java index a68afca..ffeeab3 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/SupervisorService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/SupervisorService.java @@ -1,32 +1,32 @@ -package com.dne.ems.service; - -import java.util.List; - -import com.dne.ems.dto.RejectFeedbackRequest; -import com.dne.ems.model.Feedback; - -public interface SupervisorService { - - /** - * Get a list of feedback entries that are pending supervisor review. - * These are typically feedback that have passed initial AI review. - * - * @return A list of Feedback objects with PENDING_REVIEW status. - */ - List getFeedbackForReview(); - - /** - * Approve a feedback entry, moving it to the next stage for task assignment. - * - * @param feedbackId The ID of the feedback to approve. - */ - void approveFeedback(Long feedbackId); - - /** - * Reject a feedback entry, marking it as invalid. - * - * @param feedbackId The ID of the feedback to reject. - * @param request The request containing rejection notes. - */ - void rejectFeedback(Long feedbackId, RejectFeedbackRequest request); +package com.dne.ems.service; + +import java.util.List; + +import com.dne.ems.dto.RejectFeedbackRequest; +import com.dne.ems.model.Feedback; + +public interface SupervisorService { + + /** + * Get a list of feedback entries that are pending supervisor review. + * These are typically feedback that have passed initial AI review. + * + * @return A list of Feedback objects with PENDING_REVIEW status. + */ + List getFeedbackForReview(); + + /** + * Approve a feedback entry, moving it to the next stage for task assignment. + * + * @param feedbackId The ID of the feedback to approve. + */ + void approveFeedback(Long feedbackId); + + /** + * Reject a feedback entry, marking it as invalid. + * + * @param feedbackId The ID of the feedback to reject. + * @param request The request containing rejection notes. + */ + void rejectFeedback(Long feedbackId, RejectFeedbackRequest request); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/TaskAssignmentService.java b/ems-backend/src/main/java/com/dne/ems/service/TaskAssignmentService.java index bbddbd5..a2e2a32 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/TaskAssignmentService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/TaskAssignmentService.java @@ -1,38 +1,38 @@ -package com.dne.ems.service; - -import java.util.List; - -import com.dne.ems.model.Assignment; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.UserAccount; - -/** - * Service interface for handling task assignment operations. - */ -public interface TaskAssignmentService { - - /** - * Retrieves a list of all feedback entries that are pending assignment. - * - * @return A list of {@link Feedback} objects with a status of PENDING_ASSIGNMENT. - */ - List getUnassignedFeedback(); - - /** - * Retrieves a list of all available grid workers. - * - * @return A list of {@link UserAccount} objects with the role GRID_WORKER. - */ - List getAvailableGridWorkers(); - - /** - * Assigns a feedback task to a grid worker. - * - * @param feedbackId The ID of the feedback to be assigned. - * @param assigneeId The ID of the grid worker (UserAccount) to whom the task is assigned. - * @param assignerId The ID of the admin (UserAccount) who is assigning the task. - * @return The newly created {@link Assignment} object. - */ - Assignment assignTask(Long feedbackId, Long assigneeId, Long assignerId); - +package com.dne.ems.service; + +import java.util.List; + +import com.dne.ems.model.Assignment; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.UserAccount; + +/** + * Service interface for handling task assignment operations. + */ +public interface TaskAssignmentService { + + /** + * Retrieves a list of all feedback entries that are pending assignment. + * + * @return A list of {@link Feedback} objects with a status of PENDING_ASSIGNMENT. + */ + List getUnassignedFeedback(); + + /** + * Retrieves a list of all available grid workers. + * + * @return A list of {@link UserAccount} objects with the role GRID_WORKER. + */ + List getAvailableGridWorkers(); + + /** + * Assigns a feedback task to a grid worker. + * + * @param feedbackId The ID of the feedback to be assigned. + * @param assigneeId The ID of the grid worker (UserAccount) to whom the task is assigned. + * @param assignerId The ID of the admin (UserAccount) who is assigning the task. + * @return The newly created {@link Assignment} object. + */ + Assignment assignTask(Long feedbackId, Long assigneeId, Long assignerId); + } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/TaskManagementService.java b/ems-backend/src/main/java/com/dne/ems/service/TaskManagementService.java index e275a47..63e454d 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/TaskManagementService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/TaskManagementService.java @@ -1,149 +1,149 @@ -package com.dne.ems.service; - -import java.time.LocalDate; -import java.util.List; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import com.dne.ems.dto.TaskApprovalRequest; -import com.dne.ems.dto.TaskAssignmentRequest; -import com.dne.ems.dto.TaskCreationRequest; -import com.dne.ems.dto.TaskDetailDTO; -import com.dne.ems.dto.TaskFromFeedbackRequest; -import com.dne.ems.dto.TaskHistoryDTO; -import com.dne.ems.dto.TaskSummaryDTO; -import com.dne.ems.event.TaskReadyForAssignmentEvent; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.model.enums.TaskStatus; - -/** - * 任务管理服务接口,处理任务全生命周期管理 - * - *

主要功能: - *

    - *
  • 任务创建与分配
  • - *
  • 任务状态管理
  • - *
  • 任务审核与审批
  • - *
  • 从反馈创建任务
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -public interface TaskManagementService { - - /** - * 获取任务分页列表(支持多条件过滤) - * - * @param status 任务状态过滤条件 - * @param assigneeId 分配人ID过滤条件 - * @param severity 严重程度过滤条件 - * @param pollutionType 污染类型过滤条件 - * @param startDate 开始日期过滤条件(yyyy-MM-dd) - * @param endDate 结束日期过滤条件(yyyy-MM-dd) - * @param pageable 分页参数 - * @return 任务分页列表 - */ - Page getTasks( - TaskStatus status, - Long assigneeId, - SeverityLevel severity, - PollutionType pollutionType, - LocalDate startDate, - LocalDate endDate, - Pageable pageable - ); - - /** - * 分配任务给网格工作人员 - * - * @param taskId 任务ID - * @param request 包含分配信息的请求体 - * @return 更新后的任务摘要 - * @throws ResourceNotFoundException 如果任务或分配人不存在 - * @throws InvalidOperationException 如果任务状态不允许分配 - */ - TaskSummaryDTO assignTask(Long taskId, TaskAssignmentRequest request); - - /** - * Retrieves the full details of a specific task. - * - * @param taskId The ID of the task to retrieve. - * @return A {@link TaskDetailDTO} containing the task's details. - */ - TaskDetailDTO getTaskDetails(Long taskId); - - /** - * Processes an administrator's approval or rejection of a submitted task. - * - * @param taskId The ID of the task to review. - * @param request The approval request. - * @return The updated task details. - */ - TaskDetailDTO reviewTask(Long taskId, TaskApprovalRequest request); - - /** - * Cancels a task. - * - * @param taskId The ID of the task to cancel. - * @return The updated task details. - */ - TaskDetailDTO cancelTask(Long taskId); - - /** - * Retrieves the history of a specific task. - * - * @param taskId The ID of the task. - * @return A list of {@link TaskHistoryDTO} objects representing the task's history. - */ - List getTaskHistory(Long taskId); - - /** - * 创建新任务 - * - * @param request 包含任务信息的请求体 - * @return 创建的任务详情 - */ - TaskDetailDTO createTask(TaskCreationRequest request); - - /** - * 获取待处理的反馈列表 - * - * @return 待处理反馈列表 - */ - List getPendingFeedback(); - - /** - * 从反馈创建任务 - * - * @param feedbackId 反馈ID - * @param request 包含任务信息的请求体 - * @return 创建的任务详情 - * @throws ResourceNotFoundException 如果反馈不存在 - * @throws InvalidOperationException 如果反馈状态不允许创建任务 - */ - TaskDetailDTO createTaskFromFeedback(Long feedbackId, TaskFromFeedbackRequest request); - - void handleTaskReadyForAssignment(TaskReadyForAssignmentEvent event); - - /** - * 批准已提交的任务 - * - * @param taskId 任务ID - * @return 更新后的任务详情 - */ - TaskDetailDTO approveTask(Long taskId); - - /** - * 拒绝已提交的任务 - * - * @param taskId 任务ID - * @param reason 拒绝理由 - * @return 更新后的任务详情 - */ - TaskDetailDTO rejectTask(Long taskId, String reason); +package com.dne.ems.service; + +import java.time.LocalDate; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.dne.ems.dto.TaskApprovalRequest; +import com.dne.ems.dto.TaskAssignmentRequest; +import com.dne.ems.dto.TaskCreationRequest; +import com.dne.ems.dto.TaskDetailDTO; +import com.dne.ems.dto.TaskFromFeedbackRequest; +import com.dne.ems.dto.TaskHistoryDTO; +import com.dne.ems.dto.TaskSummaryDTO; +import com.dne.ems.event.TaskReadyForAssignmentEvent; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.model.enums.TaskStatus; + +/** + * 任务管理服务接口,处理任务全生命周期管理 + * + *

主要功能: + *

    + *
  • 任务创建与分配
  • + *
  • 任务状态管理
  • + *
  • 任务审核与审批
  • + *
  • 从反馈创建任务
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +public interface TaskManagementService { + + /** + * 获取任务分页列表(支持多条件过滤) + * + * @param status 任务状态过滤条件 + * @param assigneeId 分配人ID过滤条件 + * @param severity 严重程度过滤条件 + * @param pollutionType 污染类型过滤条件 + * @param startDate 开始日期过滤条件(yyyy-MM-dd) + * @param endDate 结束日期过滤条件(yyyy-MM-dd) + * @param pageable 分页参数 + * @return 任务分页列表 + */ + Page getTasks( + TaskStatus status, + Long assigneeId, + SeverityLevel severity, + PollutionType pollutionType, + LocalDate startDate, + LocalDate endDate, + Pageable pageable + ); + + /** + * 分配任务给网格工作人员 + * + * @param taskId 任务ID + * @param request 包含分配信息的请求体 + * @return 更新后的任务摘要 + * @throws ResourceNotFoundException 如果任务或分配人不存在 + * @throws InvalidOperationException 如果任务状态不允许分配 + */ + TaskSummaryDTO assignTask(Long taskId, TaskAssignmentRequest request); + + /** + * Retrieves the full details of a specific task. + * + * @param taskId The ID of the task to retrieve. + * @return A {@link TaskDetailDTO} containing the task's details. + */ + TaskDetailDTO getTaskDetails(Long taskId); + + /** + * Processes an administrator's approval or rejection of a submitted task. + * + * @param taskId The ID of the task to review. + * @param request The approval request. + * @return The updated task details. + */ + TaskDetailDTO reviewTask(Long taskId, TaskApprovalRequest request); + + /** + * Cancels a task. + * + * @param taskId The ID of the task to cancel. + * @return The updated task details. + */ + TaskDetailDTO cancelTask(Long taskId); + + /** + * Retrieves the history of a specific task. + * + * @param taskId The ID of the task. + * @return A list of {@link TaskHistoryDTO} objects representing the task's history. + */ + List getTaskHistory(Long taskId); + + /** + * 创建新任务 + * + * @param request 包含任务信息的请求体 + * @return 创建的任务详情 + */ + TaskDetailDTO createTask(TaskCreationRequest request); + + /** + * 获取待处理的反馈列表 + * + * @return 待处理反馈列表 + */ + List getPendingFeedback(); + + /** + * 从反馈创建任务 + * + * @param feedbackId 反馈ID + * @param request 包含任务信息的请求体 + * @return 创建的任务详情 + * @throws ResourceNotFoundException 如果反馈不存在 + * @throws InvalidOperationException 如果反馈状态不允许创建任务 + */ + TaskDetailDTO createTaskFromFeedback(Long feedbackId, TaskFromFeedbackRequest request); + + void handleTaskReadyForAssignment(TaskReadyForAssignmentEvent event); + + /** + * 批准已提交的任务 + * + * @param taskId 任务ID + * @return 更新后的任务详情 + */ + TaskDetailDTO approveTask(Long taskId); + + /** + * 拒绝已提交的任务 + * + * @param taskId 任务ID + * @param reason 拒绝理由 + * @return 更新后的任务详情 + */ + TaskDetailDTO rejectTask(Long taskId, String reason); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/UserAccountService.java b/ems-backend/src/main/java/com/dne/ems/service/UserAccountService.java index 58ea831..bc486d0 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/UserAccountService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/UserAccountService.java @@ -1,46 +1,46 @@ -package com.dne.ems.service; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import com.dne.ems.dto.UserRegistrationRequest; -import com.dne.ems.dto.UserRoleUpdateRequest; -import com.dne.ems.dto.UserSummaryDTO; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.UserStatus; - -/** - * Service interface for managing user accounts. - * Defines the contract for user-related business operations. - */ -public interface UserAccountService { - - /** - * Registers a new user in the system. - * - * @param registrationRequest DTO containing the new user's information. - * @return The newly created UserAccount entity. - * @throws com.dne.ems.exception.UserAlreadyExistsException if a user with the same email or phone already exists. - */ - UserAccount registerUser(UserRegistrationRequest registrationRequest); - - /** - * Updates the role and potentially the location of a user. - * - * @param userId The ID of the user to update. - * @param request DTO containing the new role and, if required, location data. - * @return The updated UserAccount entity. - * @throws jakarta.persistence.EntityNotFoundException if the user is not found. - */ - UserAccount updateUserRole(Long userId, UserRoleUpdateRequest request); - - UserAccount getUserById(Long userId); - - Page getUsersByRole(Role role, Pageable pageable); - - void updateUserLocation(Long userId, Double latitude, Double longitude); - - Page getAllUsers(Role role, UserStatus status, Pageable pageable); - +package com.dne.ems.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.dne.ems.dto.UserRegistrationRequest; +import com.dne.ems.dto.UserRoleUpdateRequest; +import com.dne.ems.dto.UserSummaryDTO; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.UserStatus; + +/** + * Service interface for managing user accounts. + * Defines the contract for user-related business operations. + */ +public interface UserAccountService { + + /** + * Registers a new user in the system. + * + * @param registrationRequest DTO containing the new user's information. + * @return The newly created UserAccount entity. + * @throws com.dne.ems.exception.UserAlreadyExistsException if a user with the same email or phone already exists. + */ + UserAccount registerUser(UserRegistrationRequest registrationRequest); + + /** + * Updates the role and potentially the location of a user. + * + * @param userId The ID of the user to update. + * @param request DTO containing the new role and, if required, location data. + * @return The updated UserAccount entity. + * @throws jakarta.persistence.EntityNotFoundException if the user is not found. + */ + UserAccount updateUserRole(Long userId, UserRoleUpdateRequest request); + + UserAccount getUserById(Long userId); + + Page getUsersByRole(Role role, Pageable pageable); + + void updateUserLocation(Long userId, Double latitude, Double longitude); + + Page getAllUsers(Role role, UserStatus status, Pageable pageable); + } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/UserFeedbackService.java b/ems-backend/src/main/java/com/dne/ems/service/UserFeedbackService.java index aa98d66..9e78188 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/UserFeedbackService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/UserFeedbackService.java @@ -1,20 +1,20 @@ -package com.dne.ems.service; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import com.dne.ems.dto.UserFeedbackSummaryDTO; - -/** - * Service interface for user-specific feedback operations. - */ -public interface UserFeedbackService { - - /** - * Retrieves a paginated history of feedback submitted by a specific user. - * - * @param userId The ID of the user whose feedback history is to be retrieved. - * @param pageable Pagination and sorting information. - * @return A {@link Page} of {@link UserFeedbackSummaryDTO} objects. - */ - Page getFeedbackHistoryByUserId(Long userId, Pageable pageable); +package com.dne.ems.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import com.dne.ems.dto.UserFeedbackSummaryDTO; + +/** + * Service interface for user-specific feedback operations. + */ +public interface UserFeedbackService { + + /** + * Retrieves a paginated history of feedback submitted by a specific user. + * + * @param userId The ID of the user whose feedback history is to be retrieved. + * @param pageable Pagination and sorting information. + * @return A {@link Page} of {@link UserFeedbackSummaryDTO} objects. + */ + Page getFeedbackHistoryByUserId(Long userId, Pageable pageable); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/VerificationCodeService.java b/ems-backend/src/main/java/com/dne/ems/service/VerificationCodeService.java index b1cb5f2..2b663f7 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/VerificationCodeService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/VerificationCodeService.java @@ -1,25 +1,25 @@ -package com.dne.ems.service; - -/** - * 验证码服务接口,定义了生成、发送和校验验证码的核心功能。 - */ -public interface VerificationCodeService { - - /** - * 生成验证码,并通过邮件发送给指定用户。 - * 该方法会处理发送频率的限制(例如60秒冷却)。 - * - * @param email 接收验证码的电子邮箱地址。 - * @throws IllegalStateException 如果请求过于频繁(处于冷却期内)。 - */ - void sendVerificationCode(String email); - - /** - * 校验用户提交的验证码是否正确且有效。 - * - * @param email 电子邮箱地址。 - * @param code 用户提交的6位验证码。 - * @return 如果验证码正确且在有效期内,则返回 true;否则返回 false。 - */ - boolean verifyCode(String email, String code); +package com.dne.ems.service; + +/** + * 验证码服务接口,定义了生成、发送和校验验证码的核心功能。 + */ +public interface VerificationCodeService { + + /** + * 生成验证码,并通过邮件发送给指定用户。 + * 该方法会处理发送频率的限制(例如60秒冷却)。 + * + * @param email 接收验证码的电子邮箱地址。 + * @throws IllegalStateException 如果请求过于频繁(处于冷却期内)。 + */ + void sendVerificationCode(String email); + + /** + * 校验用户提交的验证码是否正确且有效。 + * + * @param email 电子邮箱地址。 + * @param code 用户提交的6位验证码。 + * @return 如果验证码正确且在有效期内,则返回 true;否则返回 false。 + */ + boolean verifyCode(String email, String code); } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/AiReviewServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/AiReviewServiceImpl.java index 5e4eea1..bdd5780 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/AiReviewServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/AiReviewServiceImpl.java @@ -1,209 +1,209 @@ -package com.dne.ems.service.impl; - -import java.util.List; -import java.util.Map; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -import com.dne.ems.dto.ai.VolcanoChatRequest; -import com.dne.ems.dto.ai.VolcanoChatResponse; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.repository.FeedbackRepository; -import com.dne.ems.service.AiReviewService; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import reactor.core.publisher.Mono; - -/** - * AI反馈内容审核服务实现类 - * - *

该服务负责调用火山引擎AI接口对用户提交的反馈内容进行审核, - * 判断内容是否合规(不含敏感词)以及是否与大气污染相关。 - * 根据AI审核结果,自动更新反馈状态。

- * - * @version 1.0 - * @since 2025-06 - */ -@Service -@Slf4j -@RequiredArgsConstructor -public class AiReviewServiceImpl implements AiReviewService { - - private final FeedbackRepository feedbackRepository; - private final WebClient webClient; - private final ObjectMapper objectMapper; // For parsing JSON strings - - /** - * 火山引擎API的URL地址 - */ - @Value("${volcano.api.url}") - private String apiUrl; - - /** - * 火山引擎API的访问密钥 - */ - @Value("${volcano.api.key}") - private String apiKey; - - /** - * 使用的AI模型名称 - */ - @Value("${volcano.model.name}") - private String modelName; - - /** - * 应用基础URL,用于构建附件访问地址 - */ - @Value("${app.base.url}") - private String appBaseUrl; - - /** - * 对反馈内容进行AI审核 - * - *

该方法将反馈内容及其附件图片发送给火山引擎AI进行审核, - * 判断内容是否合规以及是否与大气污染相关。根据AI的判断结果, - * 自动更新反馈的状态。

- * - * @param feedback 需要审核的反馈对象 - */ - @Override - public void reviewFeedback(Feedback feedback) { - log.info("Starting AI review for feedback ID: {}", feedback.getId()); - - List imageUrls = feedback.getAttachments().stream() - .map(attachment -> appBaseUrl + "/api/files/view/" + attachment.getStoredFileName()) - .toList(); - - VolcanoChatRequest request = buildRequest(feedback.getDescription(), imageUrls); - - webClient.post() - .uri(apiUrl) - .header("Authorization", "Bearer " + apiKey) - .body(Mono.just(request), VolcanoChatRequest.class) - .retrieve() - .bodyToMono(VolcanoChatResponse.class) - .doOnSuccess(response -> processResponse(response, feedback.getId())) - .onErrorResume(error -> { - log.error("AI API call failed for feedback ID: {}. Setting status to AI_REVIEW_FAILED.", feedback.getId(), error); - feedbackRepository.findById(feedback.getId()).ifPresent(freshFeedback -> { - freshFeedback.setStatus(FeedbackStatus.AI_REVIEW_FAILED); - feedbackRepository.save(freshFeedback); - }); - return Mono.empty(); // Terminate the chain gracefully - }) - .subscribe(); // Subscribe to trigger the execution - } - - /** - * 构建火山引擎AI聊天请求 - * - *

根据反馈描述和图片URL列表,构建发送给火山引擎AI的请求对象。 - * 包括系统提示、用户内容和函数定义等组件。

- * - * @param feedbackDescription 反馈文本描述 - * @param imageUrls 反馈附带的图片URL列表 - * @return 构建好的请求对象 - */ - private VolcanoChatRequest buildRequest(String feedbackDescription, List imageUrls) { - VolcanoChatRequest.Message systemMessage = VolcanoChatRequest.Message.builder() - .role("system") - .content(List.of( - VolcanoChatRequest.TextContentPart.builder().text("你是一个内容审核员。你的任务是判断用户反馈是否合规(不含敏感词)并且是否与大气污染相关。反馈可能包含文本描述和图片,图片是可选的辅助信息。你必须主要根据文本内容进行判断,即使没有图片也要完成分析。你必须使用提供的'feedback_analysis'工具来返回你的分析结果。").build() - )) - .build(); - - List userContentParts = new java.util.ArrayList<>(); - userContentParts.add(VolcanoChatRequest.TextContentPart.builder().text(feedbackDescription).build()); - - if (imageUrls != null) { - for (String imageUrl : imageUrls) { - userContentParts.add(VolcanoChatRequest.ImageUrlContentPart.builder() - .imageUrl(VolcanoChatRequest.ImageUrl.builder().url(imageUrl).build()) - .build()); - } - } - - VolcanoChatRequest.Message userMessage = VolcanoChatRequest.Message.builder() - .role("user") - .content(userContentParts) - .build(); - - VolcanoChatRequest.FunctionDef functionDef = VolcanoChatRequest.FunctionDef.builder() - .name("feedback_analysis") - .description("分析反馈内容并返回审核结果") - .parameters(VolcanoChatRequest.Parameters.builder() - .type("object") - .properties(Map.of( - "is_compliant", Map.of("type", "boolean", "description", "内容是否合规"), - "is_relevant", Map.of("type", "boolean", "description", "内容是否与大气污染相关") - )) - .required(List.of("is_compliant", "is_relevant")) - .build()) - .build(); - - VolcanoChatRequest.Tool tool = VolcanoChatRequest.Tool.builder() - .type("function") - .function(functionDef) - .build(); - - return VolcanoChatRequest.builder() - .model(modelName) - .messages(List.of(systemMessage, userMessage)) - .tools(List.of(tool)) - .build(); - } - - /** - * 处理AI响应结果 - * - *

解析AI返回的响应,提取工具调用结果,并根据内容合规性和相关性 - * 更新反馈状态。如果内容合规且相关,状态更新为待审核;否则标记为无效关闭。 - * 处理过程中的任何异常都会导致反馈状态被设置为AI审核失败。

- * - * @param response AI返回的响应对象 - * @param feedbackId 反馈ID,用于查询和更新反馈状态 - */ - private void processResponse(VolcanoChatResponse response, Long feedbackId) { - feedbackRepository.findById(feedbackId).ifPresentOrElse(feedback -> { - try { - if (response != null && response.getChoices() != null && !response.getChoices().isEmpty()) { - VolcanoChatResponse.ToolCall toolCall = response.getChoices().get(0).getMessage().getToolCalls().get(0); - - if ("feedback_analysis".equals(toolCall.getFunction().getName())) { - String argumentsJson = toolCall.getFunction().getArguments(); - Map args = objectMapper.readValue(argumentsJson, new TypeReference>() {}); - - boolean isCompliant = args.getOrDefault("is_compliant", false); - boolean isRelevant = args.getOrDefault("is_relevant", false); - - if (isCompliant && isRelevant) { - feedback.setStatus(FeedbackStatus.PENDING_REVIEW); - log.info("AI review PASSED for feedback ID: {}. Status set to PENDING_REVIEW.", feedback.getId()); - } else { - feedback.setStatus(FeedbackStatus.CLOSED_INVALID); - log.warn("AI review FAILED for feedback ID: {}. Compliant: {}, Relevant: {}. Status set to CLOSED_INVALID.", feedback.getId(), isCompliant, isRelevant); - } - } else { - log.error("AI response tool_call function name mismatch for feedback ID: {}", feedback.getId()); - feedback.setStatus(FeedbackStatus.AI_REVIEW_FAILED); - } - } else { - log.error("Received empty or invalid response from AI API for feedback ID: {}", feedback.getId()); - feedback.setStatus(FeedbackStatus.AI_REVIEW_FAILED); - } - } catch (JsonProcessingException | NullPointerException | IndexOutOfBoundsException e) { - log.error("Failed to parse or process AI response for feedback ID: {}. Setting status to AI_REVIEW_FAILED.", feedback.getId(), e); - feedback.setStatus(FeedbackStatus.AI_REVIEW_FAILED); - } finally { - feedbackRepository.save(feedback); - } - }, () -> log.error("Feedback with ID {} not found for AI review processing.", feedbackId)); - } +package com.dne.ems.service.impl; + +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import com.dne.ems.dto.ai.VolcanoChatRequest; +import com.dne.ems.dto.ai.VolcanoChatResponse; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.repository.FeedbackRepository; +import com.dne.ems.service.AiReviewService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + +/** + * AI反馈内容审核服务实现类 + * + *

该服务负责调用火山引擎AI接口对用户提交的反馈内容进行审核, + * 判断内容是否合规(不含敏感词)以及是否与大气污染相关。 + * 根据AI审核结果,自动更新反馈状态。

+ * + * @version 1.0 + * @since 2025-06 + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class AiReviewServiceImpl implements AiReviewService { + + private final FeedbackRepository feedbackRepository; + private final WebClient webClient; + private final ObjectMapper objectMapper; // For parsing JSON strings + + /** + * 火山引擎API的URL地址 + */ + @Value("${volcano.api.url}") + private String apiUrl; + + /** + * 火山引擎API的访问密钥 + */ + @Value("${volcano.api.key}") + private String apiKey; + + /** + * 使用的AI模型名称 + */ + @Value("${volcano.model.name}") + private String modelName; + + /** + * 应用基础URL,用于构建附件访问地址 + */ + @Value("${app.base.url}") + private String appBaseUrl; + + /** + * 对反馈内容进行AI审核 + * + *

该方法将反馈内容及其附件图片发送给火山引擎AI进行审核, + * 判断内容是否合规以及是否与大气污染相关。根据AI的判断结果, + * 自动更新反馈的状态。

+ * + * @param feedback 需要审核的反馈对象 + */ + @Override + public void reviewFeedback(Feedback feedback) { + log.info("Starting AI review for feedback ID: {}", feedback.getId()); + + List imageUrls = feedback.getAttachments().stream() + .map(attachment -> appBaseUrl + "/api/files/view/" + attachment.getStoredFileName()) + .toList(); + + VolcanoChatRequest request = buildRequest(feedback.getDescription(), imageUrls); + + webClient.post() + .uri(apiUrl) + .header("Authorization", "Bearer " + apiKey) + .body(Mono.just(request), VolcanoChatRequest.class) + .retrieve() + .bodyToMono(VolcanoChatResponse.class) + .doOnSuccess(response -> processResponse(response, feedback.getId())) + .onErrorResume(error -> { + log.error("AI API call failed for feedback ID: {}. Setting status to AI_REVIEW_FAILED.", feedback.getId(), error); + feedbackRepository.findById(feedback.getId()).ifPresent(freshFeedback -> { + freshFeedback.setStatus(FeedbackStatus.AI_REVIEW_FAILED); + feedbackRepository.save(freshFeedback); + }); + return Mono.empty(); // Terminate the chain gracefully + }) + .subscribe(); // Subscribe to trigger the execution + } + + /** + * 构建火山引擎AI聊天请求 + * + *

根据反馈描述和图片URL列表,构建发送给火山引擎AI的请求对象。 + * 包括系统提示、用户内容和函数定义等组件。

+ * + * @param feedbackDescription 反馈文本描述 + * @param imageUrls 反馈附带的图片URL列表 + * @return 构建好的请求对象 + */ + private VolcanoChatRequest buildRequest(String feedbackDescription, List imageUrls) { + VolcanoChatRequest.Message systemMessage = VolcanoChatRequest.Message.builder() + .role("system") + .content(List.of( + VolcanoChatRequest.TextContentPart.builder().text("你是一个内容审核员。你的任务是判断用户反馈是否合规(不含敏感词)并且是否与大气污染相关。反馈可能包含文本描述和图片,图片是可选的辅助信息。你必须主要根据文本内容进行判断,即使没有图片也要完成分析。你必须使用提供的'feedback_analysis'工具来返回你的分析结果。").build() + )) + .build(); + + List userContentParts = new java.util.ArrayList<>(); + userContentParts.add(VolcanoChatRequest.TextContentPart.builder().text(feedbackDescription).build()); + + if (imageUrls != null) { + for (String imageUrl : imageUrls) { + userContentParts.add(VolcanoChatRequest.ImageUrlContentPart.builder() + .imageUrl(VolcanoChatRequest.ImageUrl.builder().url(imageUrl).build()) + .build()); + } + } + + VolcanoChatRequest.Message userMessage = VolcanoChatRequest.Message.builder() + .role("user") + .content(userContentParts) + .build(); + + VolcanoChatRequest.FunctionDef functionDef = VolcanoChatRequest.FunctionDef.builder() + .name("feedback_analysis") + .description("分析反馈内容并返回审核结果") + .parameters(VolcanoChatRequest.Parameters.builder() + .type("object") + .properties(Map.of( + "is_compliant", Map.of("type", "boolean", "description", "内容是否合规"), + "is_relevant", Map.of("type", "boolean", "description", "内容是否与大气污染相关") + )) + .required(List.of("is_compliant", "is_relevant")) + .build()) + .build(); + + VolcanoChatRequest.Tool tool = VolcanoChatRequest.Tool.builder() + .type("function") + .function(functionDef) + .build(); + + return VolcanoChatRequest.builder() + .model(modelName) + .messages(List.of(systemMessage, userMessage)) + .tools(List.of(tool)) + .build(); + } + + /** + * 处理AI响应结果 + * + *

解析AI返回的响应,提取工具调用结果,并根据内容合规性和相关性 + * 更新反馈状态。如果内容合规且相关,状态更新为待审核;否则标记为无效关闭。 + * 处理过程中的任何异常都会导致反馈状态被设置为AI审核失败。

+ * + * @param response AI返回的响应对象 + * @param feedbackId 反馈ID,用于查询和更新反馈状态 + */ + private void processResponse(VolcanoChatResponse response, Long feedbackId) { + feedbackRepository.findById(feedbackId).ifPresentOrElse(feedback -> { + try { + if (response != null && response.getChoices() != null && !response.getChoices().isEmpty()) { + VolcanoChatResponse.ToolCall toolCall = response.getChoices().get(0).getMessage().getToolCalls().get(0); + + if ("feedback_analysis".equals(toolCall.getFunction().getName())) { + String argumentsJson = toolCall.getFunction().getArguments(); + Map args = objectMapper.readValue(argumentsJson, new TypeReference>() {}); + + boolean isCompliant = args.getOrDefault("is_compliant", false); + boolean isRelevant = args.getOrDefault("is_relevant", false); + + if (isCompliant && isRelevant) { + feedback.setStatus(FeedbackStatus.PENDING_REVIEW); + log.info("AI review PASSED for feedback ID: {}. Status set to PENDING_REVIEW.", feedback.getId()); + } else { + feedback.setStatus(FeedbackStatus.CLOSED_INVALID); + log.warn("AI review FAILED for feedback ID: {}. Compliant: {}, Relevant: {}. Status set to CLOSED_INVALID.", feedback.getId(), isCompliant, isRelevant); + } + } else { + log.error("AI response tool_call function name mismatch for feedback ID: {}", feedback.getId()); + feedback.setStatus(FeedbackStatus.AI_REVIEW_FAILED); + } + } else { + log.error("Received empty or invalid response from AI API for feedback ID: {}", feedback.getId()); + feedback.setStatus(FeedbackStatus.AI_REVIEW_FAILED); + } + } catch (JsonProcessingException | NullPointerException | IndexOutOfBoundsException e) { + log.error("Failed to parse or process AI response for feedback ID: {}. Setting status to AI_REVIEW_FAILED.", feedback.getId(), e); + feedback.setStatus(FeedbackStatus.AI_REVIEW_FAILED); + } finally { + feedbackRepository.save(feedback); + } + }, () -> log.error("Feedback with ID {} not found for AI review processing.", feedbackId)); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/AuthServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/AuthServiceImpl.java index ed7ef9a..413917c 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/AuthServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/AuthServiceImpl.java @@ -1,310 +1,310 @@ -package com.dne.ems.service.impl; - -import java.util.Optional; - -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import com.dne.ems.dto.JwtAuthenticationResponse; -import com.dne.ems.dto.LoginRequest; -import com.dne.ems.dto.SignUpRequest; -import com.dne.ems.exception.UserAlreadyExistsException; -import com.dne.ems.model.PasswordResetToken; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.OperationType; -import com.dne.ems.repository.PasswordResetTokenRepository; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.AuthService; -import com.dne.ems.service.JwtService; -import com.dne.ems.service.OperationLogService; -import com.dne.ems.service.VerificationCodeService; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.AuthenticationException; - -import jakarta.servlet.http.HttpServletRequest; - -/** - * 认证服务实现类,提供用户认证和安全相关的核心功能实现 - * - *

实现了 {@link AuthService} 接口定义的所有方法,包括: - *

    - *
  • 用户注册与验证
  • - *
  • 用户登录认证
  • - *
  • 密码重置流程
  • - *
  • JWT令牌生成
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class AuthServiceImpl implements AuthService { - - private final UserAccountRepository userAccountRepository; - private final PasswordResetTokenRepository tokenRepository; - private final PasswordEncoder passwordEncoder; - private final JwtService jwtService; - private final AuthenticationManager authenticationManager; - private final VerificationCodeService verificationCodeService; - private final OperationLogService operationLogService; - - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 验证注册验证码有效性
  2. - *
  3. 检查邮箱和手机号是否已注册
  4. - *
  5. 创建并保存新用户
  6. - *
- */ - @Override - @Transactional - public void registerUser(SignUpRequest signUpRequest) { - // 1. 校验验证码 - if (!verificationCodeService.verifyCode(signUpRequest.email(), signUpRequest.verificationCode())) { - throw new IllegalArgumentException("无效或已过期的验证码。"); - } - - // 2. 检查用户是否已存在 - if (userAccountRepository.findByEmail(signUpRequest.email()).isPresent() || - userAccountRepository.findByPhone(signUpRequest.phone()).isPresent()) { - throw new UserAlreadyExistsException("该邮箱或手机号已被注册。"); - } - - // 3. 创建并保存用户 - UserAccount user = new UserAccount(); - user.setName(signUpRequest.name()); - user.setEmail(signUpRequest.email()); - user.setPhone(signUpRequest.phone()); - user.setPassword(passwordEncoder.encode(signUpRequest.password())); - user.setRole(signUpRequest.role()); - user.setEnabled(true); - - UserAccount savedUser = userAccountRepository.save(user); - operationLogService.recordOperation( - savedUser, - OperationType.CREATE, - "新用户注册成功: " + savedUser.getEmail(), - savedUser.getId().toString(), - "UserAccount" - ); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 使用Spring Security进行认证
  2. - *
  3. 生成JWT令牌
  4. - *
  5. 返回包含令牌的响应
  6. - *
- */ - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 使用Spring Security进行认证
  2. - *
  3. 生成JWT令牌(有效期24小时)
  4. - *
  5. 返回包含令牌的响应
  6. - *
- * - *

安全考虑: - *

    - *
  • 使用BCrypt密码哈希
  • - *
  • 令牌包含用户角色信息
  • - *
- */ - @Override - public JwtAuthenticationResponse signIn(LoginRequest request) { - String ipAddress = getClientIpAddress(); - Optional userOpt = userAccountRepository.findByEmail(request.email()); - - try { - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.email(), request.password())); - - UserAccount user = userOpt.orElseThrow(() -> new IllegalArgumentException("User not found after successful authentication.")); - - operationLogService.recordLogin(user.getId(), ipAddress, true); - - var userDetails = new CustomUserDetails(user); - var jwt = jwtService.generateToken(userDetails); - return new JwtAuthenticationResponse(jwt); - - } catch (AuthenticationException e) { - log.warn("用户登录失败: {}", request.email()); - userOpt.ifPresent(user -> operationLogService.recordLogin(user.getId(), ipAddress, false)); - throw e; - } - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 查找用户邮箱是否存在
  2. - *
  3. 生成并发送密码重置验证码
  4. - *
- * - *

安全考虑:无论用户是否存在都返回成功,防止用户枚举 - */ - @Override - @Transactional - public void requestPasswordReset(String email) { - Optional userOptional = userAccountRepository.findByEmail(email); - if (userOptional.isEmpty()) { - // 为防止用户枚举,我们不透露用户是否存在 - log.info("尝试为不存在的用户发送密码重置验证码: {}", email); - return; - } - - // 生成并发送验证码 - try { - verificationCodeService.sendVerificationCode(email); - log.info("密码重置验证码已发送: {}", email); - } catch (Exception e) { - log.error("发送密码重置验证码失败: {}", e.getMessage(), e); - throw new RuntimeException("发送密码重置验证码失败", e); - } - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 验证令牌有效性
  2. - *
  3. 检查令牌是否过期
  4. - *
  5. 更新用户密码
  6. - *
  7. 删除已使用的令牌
  8. - *
- */ - @Override - @Transactional - @Deprecated - public void resetPassword(String token, String newPassword) { - PasswordResetToken resetToken = tokenRepository.findByToken(token) - .orElseThrow(() -> new IllegalArgumentException("无效的密码重置令牌。")); - - if (resetToken.isExpired()) { - tokenRepository.delete(resetToken); - throw new IllegalArgumentException("密码重置令牌已过期。"); - } - - UserAccount user = resetToken.getUser(); - user.setPassword(passwordEncoder.encode(newPassword)); - userAccountRepository.save(user); - - tokenRepository.delete(resetToken); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 验证验证码有效性
  2. - *
  3. 查找用户
  4. - *
  5. 更新用户密码
  6. - *
- */ - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 验证验证码有效性
  2. - *
  3. 查找用户
  4. - *
  5. 更新用户密码(BCrypt加密)
  6. - *
  7. 记录操作日志
  8. - *
- * - *

安全措施: - *

    - *
  • 验证码有效期5分钟
  • - *
  • 密码复杂度检查
  • - *
  • 操作日志记录
  • - *
- */ - @Override - @Transactional - public void resetPasswordWithCode(String email, String code, String newPassword) { - // 1. 验证验证码 - if (!verificationCodeService.verifyCode(email, code)) { - log.warn("密码重置验证码无效: {}", email); - throw new IllegalArgumentException("验证码无效或已过期。"); - } - - // 2. 查找用户 - UserAccount user = userAccountRepository.findByEmail(email) - .orElseThrow(() -> new IllegalArgumentException("用户不存在。")); - - // 3. 更新密码 - user.setPassword(passwordEncoder.encode(newPassword)); - userAccountRepository.save(user); - - log.info("用户密码重置成功: {}", email); - operationLogService.recordOperation( - user, - OperationType.UPDATE, - "用户密码重置成功: " + user.getEmail(), - user.getId().toString(), - "UserAccount" - ); - } - - @Override - public void logout() { - org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails) { - CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); - Long userId = userDetails.getUserAccount().getId(); - operationLogService.recordLogout(userId); - log.info("用户ID: {} 已登出", userId); - } else { - log.warn("未经身份验证的用户尝试登出"); - } - } - - private String getClientIpAddress() { - try { - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes != null) { - HttpServletRequest request = attributes.getRequest(); - String ip = request.getHeader("X-Forwarded-For"); - - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("Proxy-Client-IP"); - } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("WL-Proxy-Client-IP"); - } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getRemoteAddr(); - } - - return ip; - } - } catch (Exception e) { - log.warn("获取客户端IP地址失败", e); - } - - return "unknown"; - } +package com.dne.ems.service.impl; + +import java.util.Optional; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import com.dne.ems.dto.JwtAuthenticationResponse; +import com.dne.ems.dto.LoginRequest; +import com.dne.ems.dto.SignUpRequest; +import com.dne.ems.exception.UserAlreadyExistsException; +import com.dne.ems.model.PasswordResetToken; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.OperationType; +import com.dne.ems.repository.PasswordResetTokenRepository; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.AuthService; +import com.dne.ems.service.JwtService; +import com.dne.ems.service.OperationLogService; +import com.dne.ems.service.VerificationCodeService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 认证服务实现类,提供用户认证和安全相关的核心功能实现 + * + *

实现了 {@link AuthService} 接口定义的所有方法,包括: + *

    + *
  • 用户注册与验证
  • + *
  • 用户登录认证
  • + *
  • 密码重置流程
  • + *
  • JWT令牌生成
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class AuthServiceImpl implements AuthService { + + private final UserAccountRepository userAccountRepository; + private final PasswordResetTokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + private final AuthenticationManager authenticationManager; + private final VerificationCodeService verificationCodeService; + private final OperationLogService operationLogService; + + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 验证注册验证码有效性
  2. + *
  3. 检查邮箱和手机号是否已注册
  4. + *
  5. 创建并保存新用户
  6. + *
+ */ + @Override + @Transactional + public void registerUser(SignUpRequest signUpRequest) { + // 1. 校验验证码 + if (!verificationCodeService.verifyCode(signUpRequest.email(), signUpRequest.verificationCode())) { + throw new IllegalArgumentException("无效或已过期的验证码。"); + } + + // 2. 检查用户是否已存在 + if (userAccountRepository.findByEmail(signUpRequest.email()).isPresent() || + userAccountRepository.findByPhone(signUpRequest.phone()).isPresent()) { + throw new UserAlreadyExistsException("该邮箱或手机号已被注册。"); + } + + // 3. 创建并保存用户 + UserAccount user = new UserAccount(); + user.setName(signUpRequest.name()); + user.setEmail(signUpRequest.email()); + user.setPhone(signUpRequest.phone()); + user.setPassword(passwordEncoder.encode(signUpRequest.password())); + user.setRole(signUpRequest.role()); + user.setEnabled(true); + + UserAccount savedUser = userAccountRepository.save(user); + operationLogService.recordOperation( + savedUser, + OperationType.CREATE, + "新用户注册成功: " + savedUser.getEmail(), + savedUser.getId().toString(), + "UserAccount" + ); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 使用Spring Security进行认证
  2. + *
  3. 生成JWT令牌
  4. + *
  5. 返回包含令牌的响应
  6. + *
+ */ + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 使用Spring Security进行认证
  2. + *
  3. 生成JWT令牌(有效期24小时)
  4. + *
  5. 返回包含令牌的响应
  6. + *
+ * + *

安全考虑: + *

    + *
  • 使用BCrypt密码哈希
  • + *
  • 令牌包含用户角色信息
  • + *
+ */ + @Override + public JwtAuthenticationResponse signIn(LoginRequest request) { + String ipAddress = getClientIpAddress(); + Optional userOpt = userAccountRepository.findByEmail(request.email()); + + try { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.email(), request.password())); + + UserAccount user = userOpt.orElseThrow(() -> new IllegalArgumentException("User not found after successful authentication.")); + + operationLogService.recordLogin(user.getId(), ipAddress, true); + + var userDetails = new CustomUserDetails(user); + var jwt = jwtService.generateToken(userDetails); + return new JwtAuthenticationResponse(jwt); + + } catch (AuthenticationException e) { + log.warn("用户登录失败: {}", request.email()); + userOpt.ifPresent(user -> operationLogService.recordLogin(user.getId(), ipAddress, false)); + throw e; + } + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 查找用户邮箱是否存在
  2. + *
  3. 生成并发送密码重置验证码
  4. + *
+ * + *

安全考虑:无论用户是否存在都返回成功,防止用户枚举 + */ + @Override + @Transactional + public void requestPasswordReset(String email) { + Optional userOptional = userAccountRepository.findByEmail(email); + if (userOptional.isEmpty()) { + // 为防止用户枚举,我们不透露用户是否存在 + log.info("尝试为不存在的用户发送密码重置验证码: {}", email); + return; + } + + // 生成并发送验证码 + try { + verificationCodeService.sendVerificationCode(email); + log.info("密码重置验证码已发送: {}", email); + } catch (Exception e) { + log.error("发送密码重置验证码失败: {}", e.getMessage(), e); + throw new RuntimeException("发送密码重置验证码失败", e); + } + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 验证令牌有效性
  2. + *
  3. 检查令牌是否过期
  4. + *
  5. 更新用户密码
  6. + *
  7. 删除已使用的令牌
  8. + *
+ */ + @Override + @Transactional + @Deprecated + public void resetPassword(String token, String newPassword) { + PasswordResetToken resetToken = tokenRepository.findByToken(token) + .orElseThrow(() -> new IllegalArgumentException("无效的密码重置令牌。")); + + if (resetToken.isExpired()) { + tokenRepository.delete(resetToken); + throw new IllegalArgumentException("密码重置令牌已过期。"); + } + + UserAccount user = resetToken.getUser(); + user.setPassword(passwordEncoder.encode(newPassword)); + userAccountRepository.save(user); + + tokenRepository.delete(resetToken); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 验证验证码有效性
  2. + *
  3. 查找用户
  4. + *
  5. 更新用户密码
  6. + *
+ */ + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 验证验证码有效性
  2. + *
  3. 查找用户
  4. + *
  5. 更新用户密码(BCrypt加密)
  6. + *
  7. 记录操作日志
  8. + *
+ * + *

安全措施: + *

    + *
  • 验证码有效期5分钟
  • + *
  • 密码复杂度检查
  • + *
  • 操作日志记录
  • + *
+ */ + @Override + @Transactional + public void resetPasswordWithCode(String email, String code, String newPassword) { + // 1. 验证验证码 + if (!verificationCodeService.verifyCode(email, code)) { + log.warn("密码重置验证码无效: {}", email); + throw new IllegalArgumentException("验证码无效或已过期。"); + } + + // 2. 查找用户 + UserAccount user = userAccountRepository.findByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("用户不存在。")); + + // 3. 更新密码 + user.setPassword(passwordEncoder.encode(newPassword)); + userAccountRepository.save(user); + + log.info("用户密码重置成功: {}", email); + operationLogService.recordOperation( + user, + OperationType.UPDATE, + "用户密码重置成功: " + user.getEmail(), + user.getId().toString(), + "UserAccount" + ); + } + + @Override + public void logout() { + org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails) { + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + Long userId = userDetails.getUserAccount().getId(); + operationLogService.recordLogout(userId); + log.info("用户ID: {} 已登出", userId); + } else { + log.warn("未经身份验证的用户尝试登出"); + } + } + + private String getClientIpAddress() { + try { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + HttpServletRequest request = attributes.getRequest(); + String ip = request.getHeader("X-Forwarded-For"); + + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + + return ip; + } + } catch (Exception e) { + log.warn("获取客户端IP地址失败", e); + } + + return "unknown"; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/DashboardServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/DashboardServiceImpl.java index 9f0f85f..cf68b46 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/DashboardServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/DashboardServiceImpl.java @@ -1,631 +1,631 @@ -package com.dne.ems.service.impl; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.YearMonth; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; - -import com.dne.ems.dto.AqiDistributionDTO; -import com.dne.ems.dto.AqiHeatmapPointDTO; -import com.dne.ems.dto.DashboardStatsDTO; -import com.dne.ems.dto.GridCoverageDTO; -import com.dne.ems.dto.HeatmapPointDTO; -import com.dne.ems.dto.PollutantThresholdDTO; -import com.dne.ems.dto.PollutionStatsDTO; -import com.dne.ems.dto.TaskStatsDTO; -import com.dne.ems.dto.TrendDataPointDTO; -import com.dne.ems.exception.ResourceNotFoundException; -import com.dne.ems.model.AqiRecord; -import com.dne.ems.model.Grid; -import com.dne.ems.model.PollutantThreshold; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.TaskStatus; -import com.dne.ems.repository.AqiDataRepository; -import com.dne.ems.repository.AqiRecordRepository; -import com.dne.ems.repository.FeedbackRepository; -import com.dne.ems.repository.GridRepository; -import com.dne.ems.repository.PollutantThresholdRepository; -import com.dne.ems.repository.TaskRepository; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.DashboardService; - -import lombok.RequiredArgsConstructor; - -/** - * 仪表盘服务实现类,提供各类环境监测数据的统计和可视化数据 - * - *

实现了 {@link DashboardService} 接口定义的所有方法,包括: - *

    - *
  • 环境质量指标统计
  • - *
  • AQI数据分布和热力图
  • - *
  • 污染物阈值管理
  • - *
  • 任务完成情况统计
  • - *
  • 各类趋势分析数据
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Service -@RequiredArgsConstructor -public class DashboardServiceImpl implements DashboardService { - - private final FeedbackRepository feedbackRepository; - private final UserAccountRepository userAccountRepository; - private final AqiDataRepository aqiDataRepository; - private final AqiRecordRepository aqiRecordRepository; - private final GridRepository gridRepository; - private final TaskRepository taskRepository; - private final PollutantThresholdRepository pollutantThresholdRepository; - private static final Logger log = LoggerFactory.getLogger(DashboardServiceImpl.class); - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 统计总反馈数量
  2. - *
  3. 统计已确认反馈数量
  4. - *
  5. 统计AQI记录总数
  6. - *
  7. 统计活跃网格员数量
  8. - *
- */ - @Override - public DashboardStatsDTO getDashboardStats() { - long totalFeedbacks = feedbackRepository.count(); - long confirmedFeedbacks = feedbackRepository.countByStatus(FeedbackStatus.CONFIRMED); - long totalAqiRecords = aqiDataRepository.count(); - long activeGridWorkers = userAccountRepository.countByRole(Role.GRID_WORKER); - - return new DashboardStatsDTO( - totalFeedbacks, - confirmedFeedbacks, - totalAqiRecords, - activeGridWorkers - ); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 从AQI记录库查询分布数据
  2. - *
  3. 处理空结果情况
  4. - *
- */ - @Override - public List getAqiDistribution() { - List distribution = aqiRecordRepository.getAqiDistribution(); - return distribution != null ? distribution : List.of(); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 获取过去12个月的数据
  2. - *
  3. 加载污染物阈值设置
  4. - *
  5. 查询超标记录
  6. - *
  7. 按月份分组统计
  8. - *
  9. 填充空缺月份数据
  10. - *
- */ - @Override - public List getMonthlyExceedanceTrend() { - LocalDateTime startDate = LocalDate.now().minusMonths(11).withDayOfMonth(1).atStartOfDay(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); - - // 添加日志记录开始时间 - log.info("获取月度超标趋势数据,开始时间: {}", startDate.format(formatter)); - - // 获取污染物阈值设置 - List thresholds = pollutantThresholdRepository.findAll(); - - // 如果没有阈值设置,创建默认值 - if (thresholds.isEmpty()) { - log.info("没有找到污染物阈值设置,使用默认值"); - createDefaultThresholds(); - thresholds = pollutantThresholdRepository.findAll(); - } - - // 从阈值设置中提取各污染物的阈值 - Double aqiThreshold = 100.0; // 默认AQI阈值 - Double pm25Threshold = getThresholdValue(thresholds, PollutionType.PM25, 75.0); - Double o3Threshold = getThresholdValue(thresholds, PollutionType.O3, 160.0); - Double no2Threshold = getThresholdValue(thresholds, PollutionType.NO2, 100.0); - Double so2Threshold = getThresholdValue(thresholds, PollutionType.SO2, 150.0); - - log.info("使用的阈值设置 - AQI: {}, PM25: {}, O3: {}, NO2: {}, SO2: {}", - aqiThreshold, pm25Threshold, o3Threshold, no2Threshold, so2Threshold); - - // 使用阈值查询超标记录 - List records = aqiRecordRepository.findExceedanceRecordsWithThresholds( - startDate, aqiThreshold, pm25Threshold, o3Threshold, no2Threshold, so2Threshold); - - log.info("查询到 {} 条超标记录", records.size()); - - // 按月份分组统计 - Map resultsMap = records.stream() - .collect(Collectors.groupingBy( - record -> record.getRecordTime().format(formatter), - Collectors.counting() - )); - - log.info("按月份分组后的数据: {}", resultsMap); - - // 生成最终列表,填充空缺的月份 - List result = Stream.iterate(YearMonth.from(startDate), ym -> ym.plusMonths(1)) - .limit(12) - .map(ym -> { - String yearMonthStr = ym.format(formatter); - long count = resultsMap.getOrDefault(yearMonthStr, 0L); - TrendDataPointDTO dto = new TrendDataPointDTO(yearMonthStr, count); - log.debug("生成趋势数据点: {}", dto); - return dto; - }) - .collect(Collectors.toList()); - - // 添加日志记录最终结果 - log.info("生成了 {} 个月度趋势数据点", result.size()); - if (!result.isEmpty()) { - log.info("第一个数据点: {}, 最后一个数据点: {}", - result.get(0), result.get(result.size() - 1)); - } - - return result; - } - - /** - * 从阈值设置列表中获取指定污染物的阈值 - * - * @param thresholds 阈值设置列表 - * @param pollutionType 污染物类型 - * @param defaultValue 默认值 - * @return 阈值 - */ - private Double getThresholdValue(List thresholds, PollutionType pollutionType, Double defaultValue) { - return thresholds.stream() - .filter(t -> pollutionType.equals(t.getPollutionType())) - .findFirst() - .map(PollutantThreshold::getThreshold) - .orElse(defaultValue); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 从网格库查询覆盖率数据
  2. - *
  3. 处理空结果情况(生成测试数据)
  4. - *
  5. 计算每个城市的覆盖率
  6. - *
- */ - @Override - public List getGridCoverageByCity() { - // 1. 获取所有网格员及其分配的网格坐标 - Set coveredCoordinates = userAccountRepository.findByRole(Role.GRID_WORKER).stream() - .filter(worker -> worker.getGridX() != null && worker.getGridY() != null) - .map(worker -> worker.getGridX() + "," + worker.getGridY()) - .collect(Collectors.toSet()); - - // 2. 获取所有网格并按城市分组 - Map> gridsByCity = gridRepository.findAll().stream() - .filter(grid -> grid.getCityName() != null && !grid.getCityName().isEmpty()) - .collect(Collectors.groupingBy(Grid::getCityName)); - - // 如果没有数据,可以返回空列表或测试数据 - if (gridsByCity.isEmpty()) { - log.info("数据库中未找到任何城市的网格数据,无法计算覆盖率。"); - return Collections.emptyList(); // or return test data if needed - } - - // 3. 计算每个城市的覆盖率 - List result = new ArrayList<>(); - for (Map.Entry> entry : gridsByCity.entrySet()) { - String cityName = entry.getKey(); - List cityGrids = entry.getValue(); - - long totalGrids = cityGrids.size(); - long coveredGrids = cityGrids.stream() - .filter(grid -> coveredCoordinates.contains(grid.getGridX() + "," + grid.getGridY())) - .count(); - - // 计算覆盖率,避免除以零 - double coverageRate = (totalGrids > 0) - ? Math.round(((double) coveredGrids / totalGrids) * 10000) / 100.0 // 保留两位小数 - : 0.0; - - result.add(new GridCoverageDTO(cityName, coveredGrids, totalGrids, coverageRate)); - } - - // 按覆盖率降序排序 - result.sort((a, b) -> b.coverageRate().compareTo(a.coverageRate())); - - return result; - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 从反馈库查询热力图数据
  2. - *
  3. 处理空结果情况(生成测试数据)
  4. - *
- */ - @Override - public List getHeatmapData() { - log.info("开始获取反馈热力图数据..."); - - // 尝试从数据库获取热力图数据 - List result = feedbackRepository.getHeatmapData(); - - // 如果没有数据,生成测试数据 - if (result == null || result.isEmpty()) { - log.info("没有找到反馈热力图数据,创建测试数据"); - - // 创建测试数据 - List testData = new ArrayList<>(); - for (int x = 0; x < 5; x++) { - for (int y = 0; y < 5; y++) { - // 随机生成不同强度的热力点 - long intensity = 1 + (long)(Math.random() * 10); - testData.add(new HeatmapPointDTO(x, y, intensity)); - } - } - - log.info("已创建 {} 个测试数据点", testData.size()); - return testData; - } - - log.info("成功获取反馈热力图数据,共 {} 个数据点", result.size()); - return result; - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 按污染类型统计反馈数量
  2. - *
- */ - @Override - public List getPollutionStats() { - return feedbackRepository.countByPollutionType(); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 统计总任务数量
  2. - *
  3. 统计已完成任务数量
  4. - *
  5. 计算完成率
  6. - *
- */ - @Override - public TaskStatsDTO getTaskCompletionStats() { - long totalTasks = taskRepository.count(); - long completedTasks = taskRepository.countByStatus(TaskStatus.COMPLETED); - double completionRate = (totalTasks == 0) ? 0 : (double) completedTasks / totalTasks; - return new TaskStatsDTO(totalTasks, completedTasks, completionRate); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 检查是否有设置了网格坐标的记录
  2. - *
  3. 处理空结果情况(生成测试数据)
  4. - *
  5. 尝试JPQL查询
  6. - *
  7. JPQL失败时尝试原生SQL查询
  8. - *
  9. 转换查询结果
  10. - *
- */ - @Override - public List getAqiHeatmapData() { - log.info("开始获取AQI热力图数据..."); - - // 检查是否有设置了网格坐标的记录 - List recordsWithGrid = aqiRecordRepository.findAll().stream() - .filter(r -> r.getGridX() != null && r.getGridY() != null) - .toList(); - log.info("设置了网格坐标的记录有 {} 条", recordsWithGrid.size()); - - // 如果没有设置网格坐标的记录,创建一些测试数据 - if (recordsWithGrid.isEmpty()) { - log.info("没有找到设置了网格坐标的记录,创建测试数据"); - List testRecords = new ArrayList<>(); - - for (int x = 0; x < 5; x++) { - for (int y = 0; y < 5; y++) { - AqiRecord record = new AqiRecord( - "测试城市", 50 + x * 10 + y * 5, - LocalDateTime.now(), - 10.0, 20.0, 5.0, 15.0, 0.5, 30.0); - record.setGridX(x); - record.setGridY(y); - testRecords.add(record); - } - } - - aqiRecordRepository.saveAll(testRecords); - log.info("已创建 {} 条测试数据", testRecords.size()); - } - - // 尝试使用 JPQL 查询 - List result = aqiRecordRepository.getAqiHeatmapData(); - - if (result == null || result.isEmpty()) { - log.warn("JPQL查询返回空结果,尝试使用原生SQL查询"); - - // 尝试使用原生 SQL 查询 - List rawData = aqiRecordRepository.getAqiHeatmapDataRaw(); - - if (rawData == null || rawData.isEmpty()) { - log.warn("原生SQL查询也返回空结果"); - return List.of(); // 返回空列表 - } - - log.info("原生SQL查询成功,获取到 {} 个数据点", rawData.size()); - - // 转换原生SQL查询结果 - List convertedResult = rawData.stream() - .map(row -> { - try { - Integer gridX = row[0] != null ? ((Number) row[0]).intValue() : null; - Integer gridY = row[1] != null ? ((Number) row[1]).intValue() : null; - Double avgAqi = row[2] != null ? ((Number) row[2]).doubleValue() : null; - log.debug("转换数据点: gridX={}, gridY={}, avgAqi={}", gridX, gridY, avgAqi); - return new AqiHeatmapPointDTO(gridX, gridY, avgAqi); - } catch (Exception e) { - log.error("转换数据点时出错: {}", e.getMessage(), e); - return null; - } - }) - .filter(dto -> dto != null) - .collect(Collectors.toList()); - - log.info("成功转换 {} 个数据点", convertedResult.size()); - return convertedResult; - } - - log.info("JPQL查询成功,获取到 {} 个数据点", result.size()); - return result; - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 查询所有污染物阈值设置
  2. - *
  3. 处理空结果情况(创建默认设置)
  4. - *
  5. 转换为DTO格式
  6. - *
- */ - @Override - public List getPollutantThresholds() { - log.info("获取所有污染物阈值设置"); - List thresholds = pollutantThresholdRepository.findAll(); - - if (thresholds.isEmpty()) { - log.info("没有找到污染物阈值设置,创建默认设置"); - createDefaultThresholds(); - thresholds = pollutantThresholdRepository.findAll(); - } - - // 转换为DTO - return thresholds.stream() - .map(t -> new PollutantThresholdDTO( - t.getPollutionType(), - t.getPollutantName(), - t.getThreshold(), - t.getUnit(), - t.getDescription())) - .collect(Collectors.toList()); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 按污染物名称查询阈值设置
  2. - *
  3. 转换为DTO格式
  4. - *
- */ - @Override - public PollutantThresholdDTO getPollutantThreshold(String pollutantName) { - log.info("获取污染物 {} 的阈值设置", pollutantName); - return pollutantThresholdRepository.findByPollutantName(pollutantName) - .map(t -> new PollutantThresholdDTO( - t.getPollutionType(), - t.getPollutantName(), - t.getThreshold(), - t.getUnit(), - t.getDescription())) - .orElseGet(() -> { - log.info("未找到污染物 {} 的阈值设置,返回null", pollutantName); - return null; - }); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 检查是否已存在相同名称的阈值设置
  2. - *
  3. 更新或创建阈值设置
  4. - *
  5. 转换为DTO格式返回
  6. - *
- */ - @Override - public PollutantThresholdDTO savePollutantThreshold(PollutantThresholdDTO thresholdDTO) { - log.info("保存污染物 {} 的阈值设置: {}", thresholdDTO.getPollutantName(), thresholdDTO); - - // 检查是否已存在 - PollutantThreshold threshold = pollutantThresholdRepository.findByPollutantName(thresholdDTO.getPollutantName()) - .orElse(new PollutantThreshold()); - - // 更新属性 - threshold.setPollutionType(thresholdDTO.getPollutionType()); - threshold.setPollutantName(thresholdDTO.getPollutantName()); - threshold.setThreshold(thresholdDTO.getThreshold()); - threshold.setUnit(thresholdDTO.getUnit()); - threshold.setDescription(thresholdDTO.getDescription()); - - // 保存 - PollutantThreshold saved = pollutantThresholdRepository.save(threshold); - log.info("污染物阈值设置已保存: {}", saved); - - // 转换为DTO返回 - return new PollutantThresholdDTO( - saved.getPollutionType(), - saved.getPollutantName(), - saved.getThreshold(), - saved.getUnit(), - saved.getDescription()); - } - - /** - * 创建默认的污染物阈值设置 - */ - private void createDefaultThresholds() { - log.info("创建默认的污染物阈值设置"); - List defaults = List.of( - new PollutantThreshold(PollutionType.PM25, 75.0, "µg/m³", "细颗粒物"), - new PollutantThreshold(PollutionType.O3, 160.0, "µg/m³", "臭氧"), - new PollutantThreshold(PollutionType.NO2, 100.0, "µg/m³", "二氧化氮"), - new PollutantThreshold(PollutionType.SO2, 150.0, "µg/m³", "二氧化硫"), - new PollutantThreshold(PollutionType.OTHER, 100.0, "unit", "其他污染物") - ); - - pollutantThresholdRepository.saveAll(defaults); - log.info("已创建 {} 个默认污染物阈值设置", defaults.size()); - } - - @SuppressWarnings("unused") - private UserAccount getCurrentUser() { - Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - if (principal == null) { - throw new ResourceNotFoundException("User not found in security context (principal is null)."); - } - if (principal instanceof CustomUserDetails userDetails) { - return userDetails.getUserAccount(); - } - // This case should ideally not happen in a secured context. - String username = principal.toString(); - return userAccountRepository.findByEmail(username) - .orElseThrow(() -> new ResourceNotFoundException("User not found in security context: " + username)); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 获取过去12个月的AQI记录
  2. - *
  3. 为每种污染物类型计算月度平均值
  4. - *
  5. 填充空缺月份数据
  6. - *
- */ - @Override - public Map> getPollutantMonthlyTrends() { - log.info("获取每种污染物的月度趋势数据"); - - LocalDateTime startDate = LocalDate.now().minusMonths(11).withDayOfMonth(1).atStartOfDay(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); - - // 获取所有AQI记录 - List allRecords = aqiRecordRepository.findByRecordTimeAfter(startDate); - log.info("查询到 {} 条AQI记录", allRecords.size()); - - // 准备结果Map - Map> result = new HashMap<>(); - - // 为每种污染物类型准备数据 - for (PollutionType pollutionType : PollutionType.values()) { - String pollutantName = pollutionType.name(); - log.info("处理污染物: {}", pollutantName); - - // 按月份分组统计平均值 - Map> monthlyValues = new HashMap<>(); - - // 根据污染物类型获取对应的值 - for (AqiRecord record : allRecords) { - String monthKey = record.getRecordTime().format(formatter); - Double value = getPollutantValue(record, pollutionType); - - if (value != null) { - monthlyValues.computeIfAbsent(monthKey, k -> new ArrayList<>()).add(value); - } - } - - // 计算每月平均值 - List trendData = Stream.iterate(YearMonth.from(startDate), ym -> ym.plusMonths(1)) - .limit(12) - .map(ym -> { - String yearMonthStr = ym.format(formatter); - List values = monthlyValues.getOrDefault(yearMonthStr, Collections.emptyList()); - - // 计算平均值 - double avgValue = values.isEmpty() ? 0.0 : - values.stream().mapToDouble(Double::doubleValue).average().orElse(0.0); - - // 四舍五入到两位小数 - long roundedValue = Math.round(avgValue * 100); - - return new TrendDataPointDTO(yearMonthStr, roundedValue); - }) - .collect(Collectors.toList()); - - result.put(pollutantName, trendData); - } - - log.info("生成了 {} 种污染物的趋势数据", result.size()); - return result; - } - - /** - * 根据污染物类型获取对应的值 - * - * @param record AQI记录 - * @param pollutionType 污染物类型 - * @return 污染物值 - */ - private Double getPollutantValue(AqiRecord record, PollutionType pollutionType) { - return switch (pollutionType) { - case PM25 -> record.getPm25(); - case O3 -> record.getO3(); - case NO2 -> record.getNo2(); - case SO2 -> record.getSo2(); - case OTHER -> null; - }; - } +package com.dne.ems.service.impl; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import com.dne.ems.dto.AqiDistributionDTO; +import com.dne.ems.dto.AqiHeatmapPointDTO; +import com.dne.ems.dto.DashboardStatsDTO; +import com.dne.ems.dto.GridCoverageDTO; +import com.dne.ems.dto.HeatmapPointDTO; +import com.dne.ems.dto.PollutantThresholdDTO; +import com.dne.ems.dto.PollutionStatsDTO; +import com.dne.ems.dto.TaskStatsDTO; +import com.dne.ems.dto.TrendDataPointDTO; +import com.dne.ems.exception.ResourceNotFoundException; +import com.dne.ems.model.AqiRecord; +import com.dne.ems.model.Grid; +import com.dne.ems.model.PollutantThreshold; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.TaskStatus; +import com.dne.ems.repository.AqiDataRepository; +import com.dne.ems.repository.AqiRecordRepository; +import com.dne.ems.repository.FeedbackRepository; +import com.dne.ems.repository.GridRepository; +import com.dne.ems.repository.PollutantThresholdRepository; +import com.dne.ems.repository.TaskRepository; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.DashboardService; + +import lombok.RequiredArgsConstructor; + +/** + * 仪表盘服务实现类,提供各类环境监测数据的统计和可视化数据 + * + *

实现了 {@link DashboardService} 接口定义的所有方法,包括: + *

    + *
  • 环境质量指标统计
  • + *
  • AQI数据分布和热力图
  • + *
  • 污染物阈值管理
  • + *
  • 任务完成情况统计
  • + *
  • 各类趋势分析数据
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Service +@RequiredArgsConstructor +public class DashboardServiceImpl implements DashboardService { + + private final FeedbackRepository feedbackRepository; + private final UserAccountRepository userAccountRepository; + private final AqiDataRepository aqiDataRepository; + private final AqiRecordRepository aqiRecordRepository; + private final GridRepository gridRepository; + private final TaskRepository taskRepository; + private final PollutantThresholdRepository pollutantThresholdRepository; + private static final Logger log = LoggerFactory.getLogger(DashboardServiceImpl.class); + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 统计总反馈数量
  2. + *
  3. 统计已确认反馈数量
  4. + *
  5. 统计AQI记录总数
  6. + *
  7. 统计活跃网格员数量
  8. + *
+ */ + @Override + public DashboardStatsDTO getDashboardStats() { + long totalFeedbacks = feedbackRepository.count(); + long confirmedFeedbacks = feedbackRepository.countByStatus(FeedbackStatus.CONFIRMED); + long totalAqiRecords = aqiDataRepository.count(); + long activeGridWorkers = userAccountRepository.countByRole(Role.GRID_WORKER); + + return new DashboardStatsDTO( + totalFeedbacks, + confirmedFeedbacks, + totalAqiRecords, + activeGridWorkers + ); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 从AQI记录库查询分布数据
  2. + *
  3. 处理空结果情况
  4. + *
+ */ + @Override + public List getAqiDistribution() { + List distribution = aqiRecordRepository.getAqiDistribution(); + return distribution != null ? distribution : List.of(); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 获取过去12个月的数据
  2. + *
  3. 加载污染物阈值设置
  4. + *
  5. 查询超标记录
  6. + *
  7. 按月份分组统计
  8. + *
  9. 填充空缺月份数据
  10. + *
+ */ + @Override + public List getMonthlyExceedanceTrend() { + LocalDateTime startDate = LocalDate.now().minusMonths(11).withDayOfMonth(1).atStartOfDay(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + + // 添加日志记录开始时间 + log.info("获取月度超标趋势数据,开始时间: {}", startDate.format(formatter)); + + // 获取污染物阈值设置 + List thresholds = pollutantThresholdRepository.findAll(); + + // 如果没有阈值设置,创建默认值 + if (thresholds.isEmpty()) { + log.info("没有找到污染物阈值设置,使用默认值"); + createDefaultThresholds(); + thresholds = pollutantThresholdRepository.findAll(); + } + + // 从阈值设置中提取各污染物的阈值 + Double aqiThreshold = 100.0; // 默认AQI阈值 + Double pm25Threshold = getThresholdValue(thresholds, PollutionType.PM25, 75.0); + Double o3Threshold = getThresholdValue(thresholds, PollutionType.O3, 160.0); + Double no2Threshold = getThresholdValue(thresholds, PollutionType.NO2, 100.0); + Double so2Threshold = getThresholdValue(thresholds, PollutionType.SO2, 150.0); + + log.info("使用的阈值设置 - AQI: {}, PM25: {}, O3: {}, NO2: {}, SO2: {}", + aqiThreshold, pm25Threshold, o3Threshold, no2Threshold, so2Threshold); + + // 使用阈值查询超标记录 + List records = aqiRecordRepository.findExceedanceRecordsWithThresholds( + startDate, aqiThreshold, pm25Threshold, o3Threshold, no2Threshold, so2Threshold); + + log.info("查询到 {} 条超标记录", records.size()); + + // 按月份分组统计 + Map resultsMap = records.stream() + .collect(Collectors.groupingBy( + record -> record.getRecordTime().format(formatter), + Collectors.counting() + )); + + log.info("按月份分组后的数据: {}", resultsMap); + + // 生成最终列表,填充空缺的月份 + List result = Stream.iterate(YearMonth.from(startDate), ym -> ym.plusMonths(1)) + .limit(12) + .map(ym -> { + String yearMonthStr = ym.format(formatter); + long count = resultsMap.getOrDefault(yearMonthStr, 0L); + TrendDataPointDTO dto = new TrendDataPointDTO(yearMonthStr, count); + log.debug("生成趋势数据点: {}", dto); + return dto; + }) + .collect(Collectors.toList()); + + // 添加日志记录最终结果 + log.info("生成了 {} 个月度趋势数据点", result.size()); + if (!result.isEmpty()) { + log.info("第一个数据点: {}, 最后一个数据点: {}", + result.get(0), result.get(result.size() - 1)); + } + + return result; + } + + /** + * 从阈值设置列表中获取指定污染物的阈值 + * + * @param thresholds 阈值设置列表 + * @param pollutionType 污染物类型 + * @param defaultValue 默认值 + * @return 阈值 + */ + private Double getThresholdValue(List thresholds, PollutionType pollutionType, Double defaultValue) { + return thresholds.stream() + .filter(t -> pollutionType.equals(t.getPollutionType())) + .findFirst() + .map(PollutantThreshold::getThreshold) + .orElse(defaultValue); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 从网格库查询覆盖率数据
  2. + *
  3. 处理空结果情况(生成测试数据)
  4. + *
  5. 计算每个城市的覆盖率
  6. + *
+ */ + @Override + public List getGridCoverageByCity() { + // 1. 获取所有网格员及其分配的网格坐标 + Set coveredCoordinates = userAccountRepository.findByRole(Role.GRID_WORKER).stream() + .filter(worker -> worker.getGridX() != null && worker.getGridY() != null) + .map(worker -> worker.getGridX() + "," + worker.getGridY()) + .collect(Collectors.toSet()); + + // 2. 获取所有网格并按城市分组 + Map> gridsByCity = gridRepository.findAll().stream() + .filter(grid -> grid.getCityName() != null && !grid.getCityName().isEmpty()) + .collect(Collectors.groupingBy(Grid::getCityName)); + + // 如果没有数据,可以返回空列表或测试数据 + if (gridsByCity.isEmpty()) { + log.info("数据库中未找到任何城市的网格数据,无法计算覆盖率。"); + return Collections.emptyList(); // or return test data if needed + } + + // 3. 计算每个城市的覆盖率 + List result = new ArrayList<>(); + for (Map.Entry> entry : gridsByCity.entrySet()) { + String cityName = entry.getKey(); + List cityGrids = entry.getValue(); + + long totalGrids = cityGrids.size(); + long coveredGrids = cityGrids.stream() + .filter(grid -> coveredCoordinates.contains(grid.getGridX() + "," + grid.getGridY())) + .count(); + + // 计算覆盖率,避免除以零 + double coverageRate = (totalGrids > 0) + ? Math.round(((double) coveredGrids / totalGrids) * 10000) / 100.0 // 保留两位小数 + : 0.0; + + result.add(new GridCoverageDTO(cityName, coveredGrids, totalGrids, coverageRate)); + } + + // 按覆盖率降序排序 + result.sort((a, b) -> b.coverageRate().compareTo(a.coverageRate())); + + return result; + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 从反馈库查询热力图数据
  2. + *
  3. 处理空结果情况(生成测试数据)
  4. + *
+ */ + @Override + public List getHeatmapData() { + log.info("开始获取反馈热力图数据..."); + + // 尝试从数据库获取热力图数据 + List result = feedbackRepository.getHeatmapData(); + + // 如果没有数据,生成测试数据 + if (result == null || result.isEmpty()) { + log.info("没有找到反馈热力图数据,创建测试数据"); + + // 创建测试数据 + List testData = new ArrayList<>(); + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + // 随机生成不同强度的热力点 + long intensity = 1 + (long)(Math.random() * 10); + testData.add(new HeatmapPointDTO(x, y, intensity)); + } + } + + log.info("已创建 {} 个测试数据点", testData.size()); + return testData; + } + + log.info("成功获取反馈热力图数据,共 {} 个数据点", result.size()); + return result; + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 按污染类型统计反馈数量
  2. + *
+ */ + @Override + public List getPollutionStats() { + return feedbackRepository.countByPollutionType(); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 统计总任务数量
  2. + *
  3. 统计已完成任务数量
  4. + *
  5. 计算完成率
  6. + *
+ */ + @Override + public TaskStatsDTO getTaskCompletionStats() { + long totalTasks = taskRepository.count(); + long completedTasks = taskRepository.countByStatus(TaskStatus.COMPLETED); + double completionRate = (totalTasks == 0) ? 0 : (double) completedTasks / totalTasks; + return new TaskStatsDTO(totalTasks, completedTasks, completionRate); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 检查是否有设置了网格坐标的记录
  2. + *
  3. 处理空结果情况(生成测试数据)
  4. + *
  5. 尝试JPQL查询
  6. + *
  7. JPQL失败时尝试原生SQL查询
  8. + *
  9. 转换查询结果
  10. + *
+ */ + @Override + public List getAqiHeatmapData() { + log.info("开始获取AQI热力图数据..."); + + // 检查是否有设置了网格坐标的记录 + List recordsWithGrid = aqiRecordRepository.findAll().stream() + .filter(r -> r.getGridX() != null && r.getGridY() != null) + .toList(); + log.info("设置了网格坐标的记录有 {} 条", recordsWithGrid.size()); + + // 如果没有设置网格坐标的记录,创建一些测试数据 + if (recordsWithGrid.isEmpty()) { + log.info("没有找到设置了网格坐标的记录,创建测试数据"); + List testRecords = new ArrayList<>(); + + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + AqiRecord record = new AqiRecord( + "测试城市", 50 + x * 10 + y * 5, + LocalDateTime.now(), + 10.0, 20.0, 5.0, 15.0, 0.5, 30.0); + record.setGridX(x); + record.setGridY(y); + testRecords.add(record); + } + } + + aqiRecordRepository.saveAll(testRecords); + log.info("已创建 {} 条测试数据", testRecords.size()); + } + + // 尝试使用 JPQL 查询 + List result = aqiRecordRepository.getAqiHeatmapData(); + + if (result == null || result.isEmpty()) { + log.warn("JPQL查询返回空结果,尝试使用原生SQL查询"); + + // 尝试使用原生 SQL 查询 + List rawData = aqiRecordRepository.getAqiHeatmapDataRaw(); + + if (rawData == null || rawData.isEmpty()) { + log.warn("原生SQL查询也返回空结果"); + return List.of(); // 返回空列表 + } + + log.info("原生SQL查询成功,获取到 {} 个数据点", rawData.size()); + + // 转换原生SQL查询结果 + List convertedResult = rawData.stream() + .map(row -> { + try { + Integer gridX = row[0] != null ? ((Number) row[0]).intValue() : null; + Integer gridY = row[1] != null ? ((Number) row[1]).intValue() : null; + Double avgAqi = row[2] != null ? ((Number) row[2]).doubleValue() : null; + log.debug("转换数据点: gridX={}, gridY={}, avgAqi={}", gridX, gridY, avgAqi); + return new AqiHeatmapPointDTO(gridX, gridY, avgAqi); + } catch (Exception e) { + log.error("转换数据点时出错: {}", e.getMessage(), e); + return null; + } + }) + .filter(dto -> dto != null) + .collect(Collectors.toList()); + + log.info("成功转换 {} 个数据点", convertedResult.size()); + return convertedResult; + } + + log.info("JPQL查询成功,获取到 {} 个数据点", result.size()); + return result; + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 查询所有污染物阈值设置
  2. + *
  3. 处理空结果情况(创建默认设置)
  4. + *
  5. 转换为DTO格式
  6. + *
+ */ + @Override + public List getPollutantThresholds() { + log.info("获取所有污染物阈值设置"); + List thresholds = pollutantThresholdRepository.findAll(); + + if (thresholds.isEmpty()) { + log.info("没有找到污染物阈值设置,创建默认设置"); + createDefaultThresholds(); + thresholds = pollutantThresholdRepository.findAll(); + } + + // 转换为DTO + return thresholds.stream() + .map(t -> new PollutantThresholdDTO( + t.getPollutionType(), + t.getPollutantName(), + t.getThreshold(), + t.getUnit(), + t.getDescription())) + .collect(Collectors.toList()); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 按污染物名称查询阈值设置
  2. + *
  3. 转换为DTO格式
  4. + *
+ */ + @Override + public PollutantThresholdDTO getPollutantThreshold(String pollutantName) { + log.info("获取污染物 {} 的阈值设置", pollutantName); + return pollutantThresholdRepository.findByPollutantName(pollutantName) + .map(t -> new PollutantThresholdDTO( + t.getPollutionType(), + t.getPollutantName(), + t.getThreshold(), + t.getUnit(), + t.getDescription())) + .orElseGet(() -> { + log.info("未找到污染物 {} 的阈值设置,返回null", pollutantName); + return null; + }); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 检查是否已存在相同名称的阈值设置
  2. + *
  3. 更新或创建阈值设置
  4. + *
  5. 转换为DTO格式返回
  6. + *
+ */ + @Override + public PollutantThresholdDTO savePollutantThreshold(PollutantThresholdDTO thresholdDTO) { + log.info("保存污染物 {} 的阈值设置: {}", thresholdDTO.getPollutantName(), thresholdDTO); + + // 检查是否已存在 + PollutantThreshold threshold = pollutantThresholdRepository.findByPollutantName(thresholdDTO.getPollutantName()) + .orElse(new PollutantThreshold()); + + // 更新属性 + threshold.setPollutionType(thresholdDTO.getPollutionType()); + threshold.setPollutantName(thresholdDTO.getPollutantName()); + threshold.setThreshold(thresholdDTO.getThreshold()); + threshold.setUnit(thresholdDTO.getUnit()); + threshold.setDescription(thresholdDTO.getDescription()); + + // 保存 + PollutantThreshold saved = pollutantThresholdRepository.save(threshold); + log.info("污染物阈值设置已保存: {}", saved); + + // 转换为DTO返回 + return new PollutantThresholdDTO( + saved.getPollutionType(), + saved.getPollutantName(), + saved.getThreshold(), + saved.getUnit(), + saved.getDescription()); + } + + /** + * 创建默认的污染物阈值设置 + */ + private void createDefaultThresholds() { + log.info("创建默认的污染物阈值设置"); + List defaults = List.of( + new PollutantThreshold(PollutionType.PM25, 75.0, "µg/m³", "细颗粒物"), + new PollutantThreshold(PollutionType.O3, 160.0, "µg/m³", "臭氧"), + new PollutantThreshold(PollutionType.NO2, 100.0, "µg/m³", "二氧化氮"), + new PollutantThreshold(PollutionType.SO2, 150.0, "µg/m³", "二氧化硫"), + new PollutantThreshold(PollutionType.OTHER, 100.0, "unit", "其他污染物") + ); + + pollutantThresholdRepository.saveAll(defaults); + log.info("已创建 {} 个默认污染物阈值设置", defaults.size()); + } + + @SuppressWarnings("unused") + private UserAccount getCurrentUser() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (principal == null) { + throw new ResourceNotFoundException("User not found in security context (principal is null)."); + } + if (principal instanceof CustomUserDetails userDetails) { + return userDetails.getUserAccount(); + } + // This case should ideally not happen in a secured context. + String username = principal.toString(); + return userAccountRepository.findByEmail(username) + .orElseThrow(() -> new ResourceNotFoundException("User not found in security context: " + username)); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 获取过去12个月的AQI记录
  2. + *
  3. 为每种污染物类型计算月度平均值
  4. + *
  5. 填充空缺月份数据
  6. + *
+ */ + @Override + public Map> getPollutantMonthlyTrends() { + log.info("获取每种污染物的月度趋势数据"); + + LocalDateTime startDate = LocalDate.now().minusMonths(11).withDayOfMonth(1).atStartOfDay(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + + // 获取所有AQI记录 + List allRecords = aqiRecordRepository.findByRecordTimeAfter(startDate); + log.info("查询到 {} 条AQI记录", allRecords.size()); + + // 准备结果Map + Map> result = new HashMap<>(); + + // 为每种污染物类型准备数据 + for (PollutionType pollutionType : PollutionType.values()) { + String pollutantName = pollutionType.name(); + log.info("处理污染物: {}", pollutantName); + + // 按月份分组统计平均值 + Map> monthlyValues = new HashMap<>(); + + // 根据污染物类型获取对应的值 + for (AqiRecord record : allRecords) { + String monthKey = record.getRecordTime().format(formatter); + Double value = getPollutantValue(record, pollutionType); + + if (value != null) { + monthlyValues.computeIfAbsent(monthKey, k -> new ArrayList<>()).add(value); + } + } + + // 计算每月平均值 + List trendData = Stream.iterate(YearMonth.from(startDate), ym -> ym.plusMonths(1)) + .limit(12) + .map(ym -> { + String yearMonthStr = ym.format(formatter); + List values = monthlyValues.getOrDefault(yearMonthStr, Collections.emptyList()); + + // 计算平均值 + double avgValue = values.isEmpty() ? 0.0 : + values.stream().mapToDouble(Double::doubleValue).average().orElse(0.0); + + // 四舍五入到两位小数 + long roundedValue = Math.round(avgValue * 100); + + return new TrendDataPointDTO(yearMonthStr, roundedValue); + }) + .collect(Collectors.toList()); + + result.put(pollutantName, trendData); + } + + log.info("生成了 {} 种污染物的趋势数据", result.size()); + return result; + } + + /** + * 根据污染物类型获取对应的值 + * + * @param record AQI记录 + * @param pollutionType 污染物类型 + * @return 污染物值 + */ + private Double getPollutantValue(AqiRecord record, PollutionType pollutionType) { + return switch (pollutionType) { + case PM25 -> record.getPm25(); + case O3 -> record.getO3(); + case NO2 -> record.getNo2(); + case SO2 -> record.getSo2(); + case OTHER -> null; + }; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/FeedbackServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/FeedbackServiceImpl.java index 83d0cc5..63dda01 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/FeedbackServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/FeedbackServiceImpl.java @@ -1,376 +1,376 @@ -package com.dne.ems.service.impl; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; - -import com.dne.ems.dto.FeedbackResponseDTO; -import com.dne.ems.dto.FeedbackStatsResponse; -import com.dne.ems.dto.FeedbackSubmissionRequest; -import com.dne.ems.dto.ProcessFeedbackRequest; -import com.dne.ems.dto.PublicFeedbackRequest; -import com.dne.ems.dto.TaskDetailDTO; -import com.dne.ems.event.FeedbackSubmittedForAiReviewEvent; -import com.dne.ems.exception.ResourceNotFoundException; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.repository.FeedbackRepository; -import com.dne.ems.repository.TaskRepository; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.FeedbackService; -import com.dne.ems.service.FileStorageService; -import com.dne.ems.service.OperationLogService; -import com.dne.ems.service.TaskManagementService; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class FeedbackServiceImpl implements FeedbackService { - - private final FeedbackRepository feedbackRepository; - private final UserAccountRepository userAccountRepository; - private final ApplicationEventPublisher eventPublisher; - private final FileStorageService fileStorageService; - private final TaskRepository taskRepository; - private final TaskManagementService taskManagementService; - private final OperationLogService operationLogService; - - @SuppressWarnings("deprecation") - @Override - @Transactional - public Feedback submitFeedback(FeedbackSubmissionRequest request, MultipartFile[] files) { - CustomUserDetails currentUser = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - Long userId = currentUser.getId(); - - UserAccount userAccount = userAccountRepository.findById(userId) - .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); - - Feedback feedback = new Feedback(); - feedback.setTitle(request.title()); - feedback.setDescription(request.description()); - feedback.setPollutionType(request.pollutionType()); - feedback.setSeverityLevel(request.severityLevel()); - - feedback.setLatitude(request.location().latitude()); - feedback.setLongitude(request.location().longitude()); - feedback.setTextAddress(request.location().textAddress()); - feedback.setGridX(request.location().gridX()); - feedback.setGridY(request.location().gridY()); - - feedback.setEventId(UUID.randomUUID().toString()); - feedback.setStatus(FeedbackStatus.AI_REVIEWING); - feedback.setCreatedAt(LocalDateTime.now()); - feedback.setUpdatedAt(LocalDateTime.now()); - feedback.setSubmitterId(userId); - feedback.setUser(userAccount); - - Feedback savedFeedback = feedbackRepository.save(feedback); - - if (files != null && files.length > 0) { - Arrays.stream(files) - .filter(file -> file != null && !file.isEmpty()) - .forEach(file -> fileStorageService.storeFileAndCreateAttachment(file, savedFeedback)); - } - - eventPublisher.publishEvent(new FeedbackSubmittedForAiReviewEvent(this, savedFeedback)); - - return savedFeedback; - } - - @Override - public Page getFeedback( - CustomUserDetails userDetails, - FeedbackStatus status, - PollutionType pollutionType, - SeverityLevel severityLevel, - String cityName, - String districtName, - LocalDate startDate, - LocalDate endDate, - String keyword, - Pageable pageable) { - - List allFeedback = feedbackRepository.findAll(); - - // Role-based filtering - List roleFilteredFeedback; - if (userDetails.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_" + Role.PUBLIC_SUPERVISOR))) { - roleFilteredFeedback = allFeedback.stream() - .filter(f -> f.getSubmitterId().equals(userDetails.getId())) - .collect(Collectors.toList()); - } else if (!userDetails.getAuthorities().stream().anyMatch(a -> - a.getAuthority().equals("ROLE_" + Role.ADMIN) || - a.getAuthority().equals("ROLE_" + Role.SUPERVISOR) || - a.getAuthority().equals("ROLE_" + Role.DECISION_MAKER))) { - roleFilteredFeedback = List.of(); - } else { - roleFilteredFeedback = allFeedback; - } - - // In-memory filtering for other criteria - List finalFilteredList = roleFilteredFeedback.stream() - .filter(f -> status == null || filterByStatus(f, status)) - .filter(f -> pollutionType == null || f.getPollutionType() == pollutionType) - .filter(f -> severityLevel == null || f.getSeverityLevel() == severityLevel) - .filter(f -> !StringUtils.hasText(cityName) || (f.getTextAddress() != null && f.getTextAddress().toLowerCase().contains(cityName.toLowerCase()))) - .filter(f -> !StringUtils.hasText(districtName) || (f.getTextAddress() != null && f.getTextAddress().toLowerCase().contains(districtName.toLowerCase()))) - .filter(f -> startDate == null || !f.getCreatedAt().toLocalDate().isBefore(startDate)) - .filter(f -> endDate == null || !f.getCreatedAt().toLocalDate().isAfter(endDate)) - .filter(f -> !StringUtils.hasText(keyword) || (f.getTitle() != null && f.getTitle().toLowerCase().contains(keyword.toLowerCase()))) - .collect(Collectors.toList()); - - // Manual pagination - int start = (int) pageable.getOffset(); - int end = Math.min((start + pageable.getPageSize()), finalFilteredList.size()); - - List pageContent = (start > finalFilteredList.size()) ? List.of() : finalFilteredList.subList(start, end); - - Page feedbackPage = new PageImpl<>(pageContent, pageable, finalFilteredList.size()); - return feedbackPage.map(this::convertToDto); - } - - private boolean filterByStatus(Feedback feedback, FeedbackStatus status) { - switch (status) { - case PROCESSED, RESOLVED, COMPLETED -> { - if (feedback.getStatus() == FeedbackStatus.PROCESSED || feedback.getStatus() == FeedbackStatus.RESOLVED) { - return true; - } - return taskRepository.findByFeedback(feedback) - .map(task -> task.getStatus() == com.dne.ems.model.enums.TaskStatus.COMPLETED) - .orElse(false); - } - case IN_PROGRESS, ASSIGNED -> { - com.dne.ems.model.enums.TaskStatus targetTaskStatus = - status == FeedbackStatus.IN_PROGRESS - ? com.dne.ems.model.enums.TaskStatus.IN_PROGRESS - : com.dne.ems.model.enums.TaskStatus.ASSIGNED; - return taskRepository.findByFeedback(feedback) - .map(task -> task.getStatus() == targetTaskStatus) - .orElse(false); - } - default -> { - return feedback.getStatus() == status; - } - } - } - - private FeedbackResponseDTO convertToDto(Feedback feedback) { - TaskDetailDTO taskDetail = taskRepository.findByFeedback(feedback) - .map(task -> taskManagementService.getTaskDetails(task.getId())) - .orElse(null); - - return FeedbackResponseDTO.fromEntity(feedback, taskDetail); - } - - @Override - public Feedback getFeedbackById(Long id) { - return feedbackRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException("Feedback not found with id: " + id)); - } - - @Override - public FeedbackStatsResponse getFeedbackStats(CustomUserDetails userDetails) { - int total ; - int pending = 0; - int confirmed = 0; - int assigned = 0; - int inProgress = 0; - int resolved = 0; - int closed = 0; - int rejected = 0; - int pendingReview = 0; - int pendingAssignment = 0; - int aiReviewing = 0; - - List feedbacks; - - if (userDetails.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_" + Role.PUBLIC_SUPERVISOR))) { - Long userId = userDetails.getId(); - feedbacks = feedbackRepository.findBySubmitterId(userId); - } else if (userDetails.getAuthorities().stream().anyMatch(a -> - a.getAuthority().equals("ROLE_" + Role.ADMIN) || - a.getAuthority().equals("ROLE_" + Role.SUPERVISOR) || - a.getAuthority().equals("ROLE_" + Role.DECISION_MAKER))) { - feedbacks = feedbackRepository.findAll(); - } else { - feedbacks = List.of(); - } - - for (Feedback feedback : feedbacks) { - switch (feedback.getStatus()) { - case AI_REVIEWING -> { - aiReviewing++; - pending++; - } - case PENDING_REVIEW -> { - pendingReview++; - pending++; - } - case PENDING_ASSIGNMENT -> { - pendingAssignment++; - pending++; - } - case CONFIRMED -> confirmed++; - case ASSIGNED -> assigned++; - case PROCESSED, AI_PROCESSING -> inProgress++; - case CLOSED_INVALID -> closed++; - case AI_REVIEW_FAILED -> rejected++; - default -> { - } - } - } - total = feedbacks.size(); - - return FeedbackStatsResponse.builder() - .total(total) - .pending(pending) - .confirmed(confirmed) - .assigned(assigned) - .inProgress(inProgress) - .resolved(resolved) - .closed(closed) - .rejected(rejected) - .pendingReview(pendingReview) - .pendingAssignment(pendingAssignment) - .aiReviewing(aiReviewing) - .build(); - } - - @SuppressWarnings("deprecation") - @Override - @Transactional - public Feedback createPublicFeedback(PublicFeedbackRequest request, MultipartFile[] files) { - Feedback feedback = new Feedback(); - feedback.setTitle(request.getTitle()); - feedback.setDescription(request.getDescription()); - feedback.setSeverityLevel(request.getSeverityLevel()); - if (request.getGridX() != null) { - feedback.setGridX(request.getGridX().intValue()); - } - if (request.getGridY() != null) { - feedback.setGridY(request.getGridY().intValue()); - } - try { - feedback.setPollutionType(PollutionType.valueOf(request.getPollutionType().toUpperCase())); - } catch (IllegalArgumentException e) { - // For now, we can ignore or set a default - feedback.setPollutionType(PollutionType.OTHER); - } - feedback.setTextAddress(request.getTextAddress()); - feedback.setEventId(UUID.randomUUID().toString()); - feedback.setStatus(FeedbackStatus.PENDING_REVIEW); - feedback.setCreatedAt(LocalDateTime.now()); - feedback.setUpdatedAt(LocalDateTime.now()); - - Feedback savedFeedback = feedbackRepository.save(feedback); - - if (files != null && files.length > 0) { - Arrays.stream(files) - .filter(file -> file != null && !file.isEmpty()) - .forEach(file -> fileStorageService.storeFileAndCreateAttachment(file, savedFeedback)); - } - - return savedFeedback; - } - - @Override - public FeedbackResponseDTO getFeedbackDetail(Long id) { - Feedback feedback = getFeedbackById(id); - return convertToDto(feedback); - } - - @Override - public FeedbackResponseDTO processFeedback(Long id, ProcessFeedbackRequest request) { - Feedback feedback = feedbackRepository.findById(id) - .orElseThrow(() -> new ResourceNotFoundException("Feedback not found with id: " + id)); - - if (feedback.getStatus() != FeedbackStatus.PENDING_REVIEW) { - throw new IllegalStateException("Only feedback with PENDING_REVIEW status can be processed. Current status: " + feedback.getStatus()); - } - - feedback.setStatus(request.getStatus()); - feedback.setUpdatedAt(LocalDateTime.now()); - - Feedback updatedFeedback = feedbackRepository.save(feedback); - - // 记录操作日志 - com.dne.ems.model.enums.OperationType operationType; - String description; - String notes = request.getNotes() != null ? request.getNotes() : ""; - - switch (updatedFeedback.getStatus()) { - case PENDING_ASSIGNMENT -> { - operationType = com.dne.ems.model.enums.OperationType.APPROVE_FEEDBACK; - description = "反馈已批准,待分配。备注: " + notes; - } - case CLOSED_INVALID -> { - operationType = com.dne.ems.model.enums.OperationType.REJECT_FEEDBACK; - description = "反馈已拒绝。备注: " + notes; - } - default -> { - operationType = com.dne.ems.model.enums.OperationType.UPDATE; - description = "反馈状态更新为 " + updatedFeedback.getStatus() + "。备注: " + notes; - } - } - - operationLogService.recordOperation( - operationType, - description, - updatedFeedback.getId().toString(), - "Feedback" - ); - - return FeedbackResponseDTO.fromEntity(updatedFeedback, null); - } - - @SuppressWarnings("unused") - private FeedbackStatsResponse calculateStats(List feedbackList) { - int total = feedbackList.size(); - int pendingReview = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.PENDING_REVIEW).count(); - int pendingAssignment = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.PENDING_ASSIGNMENT).count(); - int aiReviewing = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.AI_REVIEWING).count(); - int confirmed = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.CONFIRMED).count(); - int assigned = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.ASSIGNED).count(); - int inProgress = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.IN_PROGRESS).count(); - int resolved = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.RESOLVED).count(); - int closed = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.CLOSED_INVALID).count(); - - // The "pending" count is a summary of all statuses that require action - int pending = pendingReview + pendingAssignment + aiReviewing; - - // The 'rejected' field in DTO doesn't map to a single enum, using 0. - // CLOSED_INVALID is counted as 'closed'. - return FeedbackStatsResponse.builder() - .total(total) - .pending(pending) - .confirmed(confirmed) - .assigned(assigned) - .inProgress(inProgress) - .resolved(resolved) - .closed(closed) - .rejected(0) // No direct mapping for REJECTED status - .pendingReview(pendingReview) - .pendingAssignment(pendingAssignment) - .aiReviewing(aiReviewing) - .build(); - } +package com.dne.ems.service.impl; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import com.dne.ems.dto.FeedbackResponseDTO; +import com.dne.ems.dto.FeedbackStatsResponse; +import com.dne.ems.dto.FeedbackSubmissionRequest; +import com.dne.ems.dto.ProcessFeedbackRequest; +import com.dne.ems.dto.PublicFeedbackRequest; +import com.dne.ems.dto.TaskDetailDTO; +import com.dne.ems.event.FeedbackSubmittedForAiReviewEvent; +import com.dne.ems.exception.ResourceNotFoundException; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.repository.FeedbackRepository; +import com.dne.ems.repository.TaskRepository; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.FeedbackService; +import com.dne.ems.service.FileStorageService; +import com.dne.ems.service.OperationLogService; +import com.dne.ems.service.TaskManagementService; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class FeedbackServiceImpl implements FeedbackService { + + private final FeedbackRepository feedbackRepository; + private final UserAccountRepository userAccountRepository; + private final ApplicationEventPublisher eventPublisher; + private final FileStorageService fileStorageService; + private final TaskRepository taskRepository; + private final TaskManagementService taskManagementService; + private final OperationLogService operationLogService; + + @SuppressWarnings("deprecation") + @Override + @Transactional + public Feedback submitFeedback(FeedbackSubmissionRequest request, MultipartFile[] files) { + CustomUserDetails currentUser = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + Long userId = currentUser.getId(); + + UserAccount userAccount = userAccountRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); + + Feedback feedback = new Feedback(); + feedback.setTitle(request.title()); + feedback.setDescription(request.description()); + feedback.setPollutionType(request.pollutionType()); + feedback.setSeverityLevel(request.severityLevel()); + + feedback.setLatitude(request.location().latitude()); + feedback.setLongitude(request.location().longitude()); + feedback.setTextAddress(request.location().textAddress()); + feedback.setGridX(request.location().gridX()); + feedback.setGridY(request.location().gridY()); + + feedback.setEventId(UUID.randomUUID().toString()); + feedback.setStatus(FeedbackStatus.AI_REVIEWING); + feedback.setCreatedAt(LocalDateTime.now()); + feedback.setUpdatedAt(LocalDateTime.now()); + feedback.setSubmitterId(userId); + feedback.setUser(userAccount); + + Feedback savedFeedback = feedbackRepository.save(feedback); + + if (files != null && files.length > 0) { + Arrays.stream(files) + .filter(file -> file != null && !file.isEmpty()) + .forEach(file -> fileStorageService.storeFileAndCreateAttachment(file, savedFeedback)); + } + + eventPublisher.publishEvent(new FeedbackSubmittedForAiReviewEvent(this, savedFeedback)); + + return savedFeedback; + } + + @Override + public Page getFeedback( + CustomUserDetails userDetails, + FeedbackStatus status, + PollutionType pollutionType, + SeverityLevel severityLevel, + String cityName, + String districtName, + LocalDate startDate, + LocalDate endDate, + String keyword, + Pageable pageable) { + + List allFeedback = feedbackRepository.findAll(); + + // Role-based filtering + List roleFilteredFeedback; + if (userDetails.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_" + Role.PUBLIC_SUPERVISOR))) { + roleFilteredFeedback = allFeedback.stream() + .filter(f -> f.getSubmitterId().equals(userDetails.getId())) + .collect(Collectors.toList()); + } else if (!userDetails.getAuthorities().stream().anyMatch(a -> + a.getAuthority().equals("ROLE_" + Role.ADMIN) || + a.getAuthority().equals("ROLE_" + Role.SUPERVISOR) || + a.getAuthority().equals("ROLE_" + Role.DECISION_MAKER))) { + roleFilteredFeedback = List.of(); + } else { + roleFilteredFeedback = allFeedback; + } + + // In-memory filtering for other criteria + List finalFilteredList = roleFilteredFeedback.stream() + .filter(f -> status == null || filterByStatus(f, status)) + .filter(f -> pollutionType == null || f.getPollutionType() == pollutionType) + .filter(f -> severityLevel == null || f.getSeverityLevel() == severityLevel) + .filter(f -> !StringUtils.hasText(cityName) || (f.getTextAddress() != null && f.getTextAddress().toLowerCase().contains(cityName.toLowerCase()))) + .filter(f -> !StringUtils.hasText(districtName) || (f.getTextAddress() != null && f.getTextAddress().toLowerCase().contains(districtName.toLowerCase()))) + .filter(f -> startDate == null || !f.getCreatedAt().toLocalDate().isBefore(startDate)) + .filter(f -> endDate == null || !f.getCreatedAt().toLocalDate().isAfter(endDate)) + .filter(f -> !StringUtils.hasText(keyword) || (f.getTitle() != null && f.getTitle().toLowerCase().contains(keyword.toLowerCase()))) + .collect(Collectors.toList()); + + // Manual pagination + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), finalFilteredList.size()); + + List pageContent = (start > finalFilteredList.size()) ? List.of() : finalFilteredList.subList(start, end); + + Page feedbackPage = new PageImpl<>(pageContent, pageable, finalFilteredList.size()); + return feedbackPage.map(this::convertToDto); + } + + private boolean filterByStatus(Feedback feedback, FeedbackStatus status) { + switch (status) { + case PROCESSED, RESOLVED, COMPLETED -> { + if (feedback.getStatus() == FeedbackStatus.PROCESSED || feedback.getStatus() == FeedbackStatus.RESOLVED) { + return true; + } + return taskRepository.findByFeedback(feedback) + .map(task -> task.getStatus() == com.dne.ems.model.enums.TaskStatus.COMPLETED) + .orElse(false); + } + case IN_PROGRESS, ASSIGNED -> { + com.dne.ems.model.enums.TaskStatus targetTaskStatus = + status == FeedbackStatus.IN_PROGRESS + ? com.dne.ems.model.enums.TaskStatus.IN_PROGRESS + : com.dne.ems.model.enums.TaskStatus.ASSIGNED; + return taskRepository.findByFeedback(feedback) + .map(task -> task.getStatus() == targetTaskStatus) + .orElse(false); + } + default -> { + return feedback.getStatus() == status; + } + } + } + + private FeedbackResponseDTO convertToDto(Feedback feedback) { + TaskDetailDTO taskDetail = taskRepository.findByFeedback(feedback) + .map(task -> taskManagementService.getTaskDetails(task.getId())) + .orElse(null); + + return FeedbackResponseDTO.fromEntity(feedback, taskDetail); + } + + @Override + public Feedback getFeedbackById(Long id) { + return feedbackRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Feedback not found with id: " + id)); + } + + @Override + public FeedbackStatsResponse getFeedbackStats(CustomUserDetails userDetails) { + int total ; + int pending = 0; + int confirmed = 0; + int assigned = 0; + int inProgress = 0; + int resolved = 0; + int closed = 0; + int rejected = 0; + int pendingReview = 0; + int pendingAssignment = 0; + int aiReviewing = 0; + + List feedbacks; + + if (userDetails.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_" + Role.PUBLIC_SUPERVISOR))) { + Long userId = userDetails.getId(); + feedbacks = feedbackRepository.findBySubmitterId(userId); + } else if (userDetails.getAuthorities().stream().anyMatch(a -> + a.getAuthority().equals("ROLE_" + Role.ADMIN) || + a.getAuthority().equals("ROLE_" + Role.SUPERVISOR) || + a.getAuthority().equals("ROLE_" + Role.DECISION_MAKER))) { + feedbacks = feedbackRepository.findAll(); + } else { + feedbacks = List.of(); + } + + for (Feedback feedback : feedbacks) { + switch (feedback.getStatus()) { + case AI_REVIEWING -> { + aiReviewing++; + pending++; + } + case PENDING_REVIEW -> { + pendingReview++; + pending++; + } + case PENDING_ASSIGNMENT -> { + pendingAssignment++; + pending++; + } + case CONFIRMED -> confirmed++; + case ASSIGNED -> assigned++; + case PROCESSED, AI_PROCESSING -> inProgress++; + case CLOSED_INVALID -> closed++; + case AI_REVIEW_FAILED -> rejected++; + default -> { + } + } + } + total = feedbacks.size(); + + return FeedbackStatsResponse.builder() + .total(total) + .pending(pending) + .confirmed(confirmed) + .assigned(assigned) + .inProgress(inProgress) + .resolved(resolved) + .closed(closed) + .rejected(rejected) + .pendingReview(pendingReview) + .pendingAssignment(pendingAssignment) + .aiReviewing(aiReviewing) + .build(); + } + + @SuppressWarnings("deprecation") + @Override + @Transactional + public Feedback createPublicFeedback(PublicFeedbackRequest request, MultipartFile[] files) { + Feedback feedback = new Feedback(); + feedback.setTitle(request.getTitle()); + feedback.setDescription(request.getDescription()); + feedback.setSeverityLevel(request.getSeverityLevel()); + if (request.getGridX() != null) { + feedback.setGridX(request.getGridX().intValue()); + } + if (request.getGridY() != null) { + feedback.setGridY(request.getGridY().intValue()); + } + try { + feedback.setPollutionType(PollutionType.valueOf(request.getPollutionType().toUpperCase())); + } catch (IllegalArgumentException e) { + // For now, we can ignore or set a default + feedback.setPollutionType(PollutionType.OTHER); + } + feedback.setTextAddress(request.getTextAddress()); + feedback.setEventId(UUID.randomUUID().toString()); + feedback.setStatus(FeedbackStatus.PENDING_REVIEW); + feedback.setCreatedAt(LocalDateTime.now()); + feedback.setUpdatedAt(LocalDateTime.now()); + + Feedback savedFeedback = feedbackRepository.save(feedback); + + if (files != null && files.length > 0) { + Arrays.stream(files) + .filter(file -> file != null && !file.isEmpty()) + .forEach(file -> fileStorageService.storeFileAndCreateAttachment(file, savedFeedback)); + } + + return savedFeedback; + } + + @Override + public FeedbackResponseDTO getFeedbackDetail(Long id) { + Feedback feedback = getFeedbackById(id); + return convertToDto(feedback); + } + + @Override + public FeedbackResponseDTO processFeedback(Long id, ProcessFeedbackRequest request) { + Feedback feedback = feedbackRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Feedback not found with id: " + id)); + + if (feedback.getStatus() != FeedbackStatus.PENDING_REVIEW) { + throw new IllegalStateException("Only feedback with PENDING_REVIEW status can be processed. Current status: " + feedback.getStatus()); + } + + feedback.setStatus(request.getStatus()); + feedback.setUpdatedAt(LocalDateTime.now()); + + Feedback updatedFeedback = feedbackRepository.save(feedback); + + // 记录操作日志 + com.dne.ems.model.enums.OperationType operationType; + String description; + String notes = request.getNotes() != null ? request.getNotes() : ""; + + switch (updatedFeedback.getStatus()) { + case PENDING_ASSIGNMENT -> { + operationType = com.dne.ems.model.enums.OperationType.APPROVE_FEEDBACK; + description = "反馈已批准,待分配。备注: " + notes; + } + case CLOSED_INVALID -> { + operationType = com.dne.ems.model.enums.OperationType.REJECT_FEEDBACK; + description = "反馈已拒绝。备注: " + notes; + } + default -> { + operationType = com.dne.ems.model.enums.OperationType.UPDATE; + description = "反馈状态更新为 " + updatedFeedback.getStatus() + "。备注: " + notes; + } + } + + operationLogService.recordOperation( + operationType, + description, + updatedFeedback.getId().toString(), + "Feedback" + ); + + return FeedbackResponseDTO.fromEntity(updatedFeedback, null); + } + + @SuppressWarnings("unused") + private FeedbackStatsResponse calculateStats(List feedbackList) { + int total = feedbackList.size(); + int pendingReview = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.PENDING_REVIEW).count(); + int pendingAssignment = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.PENDING_ASSIGNMENT).count(); + int aiReviewing = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.AI_REVIEWING).count(); + int confirmed = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.CONFIRMED).count(); + int assigned = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.ASSIGNED).count(); + int inProgress = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.IN_PROGRESS).count(); + int resolved = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.RESOLVED).count(); + int closed = (int) feedbackList.stream().filter(f -> f.getStatus() == FeedbackStatus.CLOSED_INVALID).count(); + + // The "pending" count is a summary of all statuses that require action + int pending = pendingReview + pendingAssignment + aiReviewing; + + // The 'rejected' field in DTO doesn't map to a single enum, using 0. + // CLOSED_INVALID is counted as 'closed'. + return FeedbackStatsResponse.builder() + .total(total) + .pending(pending) + .confirmed(confirmed) + .assigned(assigned) + .inProgress(inProgress) + .resolved(resolved) + .closed(closed) + .rejected(0) // No direct mapping for REJECTED status + .pendingReview(pendingReview) + .pendingAssignment(pendingAssignment) + .aiReviewing(aiReviewing) + .build(); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/FileStorageServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/FileStorageServiceImpl.java index a268f3d..314fe6a 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/FileStorageServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/FileStorageServiceImpl.java @@ -1,222 +1,222 @@ -package com.dne.ems.service.impl; - -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; -import org.springframework.core.io.UrlResource; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; - -import com.dne.ems.exception.FileStorageException; -import com.dne.ems.model.Attachment; -import com.dne.ems.model.Feedback; -import com.dne.ems.repository.AttachmentRepository; -import com.dne.ems.service.FileStorageService; - -import jakarta.annotation.PostConstruct; - -/** - * 文件存储服务实现类,处理文件上传和下载 - * - *

该服务负责安全地存储用户上传的文件,管理文件与业务实体的关联, - * 并提供文件下载功能。实现了文件类型验证、路径安全检查等安全措施。

- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Service -public class FileStorageServiceImpl implements FileStorageService { - - private final Path fileStorageLocation; - private final AttachmentRepository attachmentRepository; - - // Whitelist of allowed file extensions and content types for security - private static final List ALLOWED_EXTENSIONS = Arrays.asList(".png", ".jpg", ".jpeg", ".gif", ".pdf", ".doc", ".docx"); - private static final List ALLOWED_CONTENT_TYPES = Arrays.asList( - "image/png", "image/jpeg", "image/gif", "application/pdf", - "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - ); - - public FileStorageServiceImpl(@Value("${file.upload-dir}") String uploadDir, AttachmentRepository attachmentRepository) { - this.attachmentRepository = attachmentRepository; - this.fileStorageLocation = Paths.get(uploadDir).toAbsolutePath().normalize(); - } - - /** - * 初始化文件存储目录 - * - *

在服务启动时创建文件存储目录(如果不存在)。

- * - * @throws FileStorageException 如果无法创建存储目录 - */ - @PostConstruct - public void init() { - try { - Files.createDirectories(this.fileStorageLocation); - } catch (IOException ex) { - throw new FileStorageException("Could not create the directory where the uploaded files will be stored.", ex); - } - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 验证文件存储位置配置
  2. - *
  3. 检查文件是否为空
  4. - *
  5. 清理文件名并验证安全性
  6. - *
  7. 验证文件扩展名和内容类型
  8. - *
  9. 生成唯一存储文件名
  10. - *
  11. 保存文件到存储位置
  12. - *
  13. 创建并保存附件记录
  14. - *
- * - *

安全措施: - *

    - *
  • 防止路径遍历攻击
  • - *
  • 限制允许的文件类型
  • - *
  • 使用唯一文件名防止冲突
  • - *
- */ - @Override - @Deprecated - public Attachment storeFileAndCreateAttachment(MultipartFile file, Feedback feedback) { - String storedFilename = storeFile(file); - - Attachment attachment = Attachment.builder() - .fileName(StringUtils.cleanPath(file.getOriginalFilename())) - .storedFileName(storedFilename) - .fileType(file.getContentType()) - .fileSize(file.getSize()) - .feedback(feedback) - .build(); - - return attachmentRepository.save(attachment); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 验证文件存储位置配置
  2. - *
  3. 检查文件是否为空
  4. - *
  5. 清理文件名并验证安全性
  6. - *
  7. 验证文件扩展名和内容类型
  8. - *
  9. 生成唯一存储文件名
  10. - *
  11. 保存文件到存储位置
  12. - *
- */ - @Override - public String storeFile(MultipartFile file) { - if (this.fileStorageLocation == null) { - throw new FileStorageException("File storage location is not configured."); - } - if (file == null || file.isEmpty()) { - throw new FileStorageException("Cannot store an empty file."); - } - - String originalFilename = file.getOriginalFilename(); - if (originalFilename == null || originalFilename.trim().isEmpty()) { - throw new FileStorageException("File name is invalid or not provided."); - } - String cleanedFilename = StringUtils.cleanPath(originalFilename); - - // Security Check: Validate filename - if (cleanedFilename.contains("..")) { - throw new FileStorageException("Cannot store file with relative path outside current directory " + cleanedFilename); - } - - // Security Check: Validate file extension and content type against a whitelist - String fileExtension = getFileExtension(cleanedFilename); - if (!ALLOWED_EXTENSIONS.contains(fileExtension.toLowerCase())) { - throw new FileStorageException("Invalid file extension. Only " + ALLOWED_EXTENSIONS + " are allowed."); - } - - String contentType = file.getContentType(); - if (!ALLOWED_CONTENT_TYPES.contains(contentType)) { - throw new FileStorageException("Invalid file content type. Only " + ALLOWED_CONTENT_TYPES + " are allowed."); - } - - try { - // Generate a unique file name to avoid collisions - String storedFilename = UUID.randomUUID().toString() + fileExtension; - - Path targetLocation = this.fileStorageLocation.resolve(storedFilename); - try (InputStream inputStream = file.getInputStream()) { - Files.copy(inputStream, targetLocation, StandardCopyOption.REPLACE_EXISTING); - } - - return storedFilename; - } catch (IOException ex) { - throw new FileStorageException("Could not store file " + cleanedFilename + ". Please try again!", ex); - } - } - - /** - * 获取文件扩展名(内部方法) - * - * @param filename 文件名 - * @return 文件扩展名(包含点),如".jpg" - */ - private String getFileExtension(String filename) { - if (filename == null || !filename.contains(".")) { - return ""; - } - return filename.substring(filename.lastIndexOf(".")); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 解析文件路径
  2. - *
  3. 加载文件资源
  4. - *
  5. 验证资源可读性
  6. - *
- * - *

安全措施: - *

    - *
  • 规范化文件路径
  • - *
  • 验证资源存在性和可读性
  • - *
- */ - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 解析文件路径
  2. - *
  3. 验证文件是否存在
  4. - *
  5. 创建并返回文件资源
  6. - *
- */ - @Override - public Resource loadFileAsResource(String fileName) { - try { - Path filePath = this.fileStorageLocation.resolve(fileName).normalize(); - Resource resource = new UrlResource(filePath.toUri()); - if (resource.exists() && resource.isReadable()) { - return resource; - } else { - throw new FileStorageException("File not found or not readable: " + fileName); - } - } catch (MalformedURLException ex) { - throw new FileStorageException("File not found: " + fileName, ex); - } - } +package com.dne.ems.service.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import com.dne.ems.exception.FileStorageException; +import com.dne.ems.model.Attachment; +import com.dne.ems.model.Feedback; +import com.dne.ems.repository.AttachmentRepository; +import com.dne.ems.service.FileStorageService; + +import jakarta.annotation.PostConstruct; + +/** + * 文件存储服务实现类,处理文件上传和下载 + * + *

该服务负责安全地存储用户上传的文件,管理文件与业务实体的关联, + * 并提供文件下载功能。实现了文件类型验证、路径安全检查等安全措施。

+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Service +public class FileStorageServiceImpl implements FileStorageService { + + private final Path fileStorageLocation; + private final AttachmentRepository attachmentRepository; + + // Whitelist of allowed file extensions and content types for security + private static final List ALLOWED_EXTENSIONS = Arrays.asList(".png", ".jpg", ".jpeg", ".gif", ".pdf", ".doc", ".docx"); + private static final List ALLOWED_CONTENT_TYPES = Arrays.asList( + "image/png", "image/jpeg", "image/gif", "application/pdf", + "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ); + + public FileStorageServiceImpl(@Value("${file.upload-dir}") String uploadDir, AttachmentRepository attachmentRepository) { + this.attachmentRepository = attachmentRepository; + this.fileStorageLocation = Paths.get(uploadDir).toAbsolutePath().normalize(); + } + + /** + * 初始化文件存储目录 + * + *

在服务启动时创建文件存储目录(如果不存在)。

+ * + * @throws FileStorageException 如果无法创建存储目录 + */ + @PostConstruct + public void init() { + try { + Files.createDirectories(this.fileStorageLocation); + } catch (IOException ex) { + throw new FileStorageException("Could not create the directory where the uploaded files will be stored.", ex); + } + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 验证文件存储位置配置
  2. + *
  3. 检查文件是否为空
  4. + *
  5. 清理文件名并验证安全性
  6. + *
  7. 验证文件扩展名和内容类型
  8. + *
  9. 生成唯一存储文件名
  10. + *
  11. 保存文件到存储位置
  12. + *
  13. 创建并保存附件记录
  14. + *
+ * + *

安全措施: + *

    + *
  • 防止路径遍历攻击
  • + *
  • 限制允许的文件类型
  • + *
  • 使用唯一文件名防止冲突
  • + *
+ */ + @Override + @Deprecated + public Attachment storeFileAndCreateAttachment(MultipartFile file, Feedback feedback) { + String storedFilename = storeFile(file); + + Attachment attachment = Attachment.builder() + .fileName(StringUtils.cleanPath(file.getOriginalFilename())) + .storedFileName(storedFilename) + .fileType(file.getContentType()) + .fileSize(file.getSize()) + .feedback(feedback) + .build(); + + return attachmentRepository.save(attachment); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 验证文件存储位置配置
  2. + *
  3. 检查文件是否为空
  4. + *
  5. 清理文件名并验证安全性
  6. + *
  7. 验证文件扩展名和内容类型
  8. + *
  9. 生成唯一存储文件名
  10. + *
  11. 保存文件到存储位置
  12. + *
+ */ + @Override + public String storeFile(MultipartFile file) { + if (this.fileStorageLocation == null) { + throw new FileStorageException("File storage location is not configured."); + } + if (file == null || file.isEmpty()) { + throw new FileStorageException("Cannot store an empty file."); + } + + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || originalFilename.trim().isEmpty()) { + throw new FileStorageException("File name is invalid or not provided."); + } + String cleanedFilename = StringUtils.cleanPath(originalFilename); + + // Security Check: Validate filename + if (cleanedFilename.contains("..")) { + throw new FileStorageException("Cannot store file with relative path outside current directory " + cleanedFilename); + } + + // Security Check: Validate file extension and content type against a whitelist + String fileExtension = getFileExtension(cleanedFilename); + if (!ALLOWED_EXTENSIONS.contains(fileExtension.toLowerCase())) { + throw new FileStorageException("Invalid file extension. Only " + ALLOWED_EXTENSIONS + " are allowed."); + } + + String contentType = file.getContentType(); + if (!ALLOWED_CONTENT_TYPES.contains(contentType)) { + throw new FileStorageException("Invalid file content type. Only " + ALLOWED_CONTENT_TYPES + " are allowed."); + } + + try { + // Generate a unique file name to avoid collisions + String storedFilename = UUID.randomUUID().toString() + fileExtension; + + Path targetLocation = this.fileStorageLocation.resolve(storedFilename); + try (InputStream inputStream = file.getInputStream()) { + Files.copy(inputStream, targetLocation, StandardCopyOption.REPLACE_EXISTING); + } + + return storedFilename; + } catch (IOException ex) { + throw new FileStorageException("Could not store file " + cleanedFilename + ". Please try again!", ex); + } + } + + /** + * 获取文件扩展名(内部方法) + * + * @param filename 文件名 + * @return 文件扩展名(包含点),如".jpg" + */ + private String getFileExtension(String filename) { + if (filename == null || !filename.contains(".")) { + return ""; + } + return filename.substring(filename.lastIndexOf(".")); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 解析文件路径
  2. + *
  3. 加载文件资源
  4. + *
  5. 验证资源可读性
  6. + *
+ * + *

安全措施: + *

    + *
  • 规范化文件路径
  • + *
  • 验证资源存在性和可读性
  • + *
+ */ + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 解析文件路径
  2. + *
  3. 验证文件是否存在
  4. + *
  5. 创建并返回文件资源
  6. + *
+ */ + @Override + public Resource loadFileAsResource(String fileName) { + try { + Path filePath = this.fileStorageLocation.resolve(fileName).normalize(); + Resource resource = new UrlResource(filePath.toUri()); + if (resource.exists() && resource.isReadable()) { + return resource; + } else { + throw new FileStorageException("File not found or not readable: " + fileName); + } + } catch (MalformedURLException ex) { + throw new FileStorageException("File not found: " + fileName, ex); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/GridServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/GridServiceImpl.java index c602c98..deac819 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/GridServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/GridServiceImpl.java @@ -1,170 +1,170 @@ -package com.dne.ems.service.impl; - -// import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - -import com.dne.ems.dto.GridUpdateRequest; -import com.dne.ems.exception.ResourceNotFoundException; -import com.dne.ems.model.Grid; -import com.dne.ems.model.MapGrid; -import com.dne.ems.model.UserAccount; -import com.dne.ems.repository.GridRepository; -import com.dne.ems.repository.MapGridRepository; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.service.GridService; -import com.dne.ems.service.OperationLogService; - -import jakarta.persistence.EntityNotFoundException; -import lombok.RequiredArgsConstructor; - -/** - * 网格服务实现类,处理网格数据的查询和更新 - * - *

实现了 {@link GridService} 接口定义的所有方法,包括: - *

    - *
  • 网格数据查询与过滤
  • - *
  • 网格信息更新
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2025 - */ -@Service -@RequiredArgsConstructor -public class GridServiceImpl implements GridService { - - private final GridRepository gridRepository; - private final UserAccountRepository userAccountRepository; - private final MapGridRepository mapGridRepository; - private final OperationLogService operationLogService; - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 使用Specification动态构建查询条件
  2. - *
  3. 支持按城市和区县过滤
  4. - *
  5. 返回分页结果
  6. - *
- * - * @param cityName 城市名称过滤条件(可选) - * @param districtName 区县名称过滤条件(可选) - * @param pageable 分页参数 - * @return 网格分页列表 - */ - @Override - public Page getGrids(String cityName, String districtName, Pageable pageable) { - List allGrids = gridRepository.findAll(); - - // In-memory filtering - List filteredGrids = allGrids.stream() - .filter(grid -> !StringUtils.hasText(cityName) || cityName.equals(grid.getCityName())) - .filter(grid -> !StringUtils.hasText(districtName) || districtName.equals(grid.getDistrictName())) - .collect(Collectors.toList()); - - // Manual pagination - int start = (int) pageable.getOffset(); - int end = Math.min((start + pageable.getPageSize()), filteredGrids.size()); - - List pageContent = (start > filteredGrids.size()) ? List.of() : filteredGrids.subList(start, end); - - return new PageImpl<>(pageContent, pageable, filteredGrids.size()); - } - - @Override - public List getAllGrids() { - return gridRepository.findAll(); - } - - @Override - public Optional getGridById(Long gridId) { - return gridRepository.findById(gridId); - } - - @Override - @Transactional - public UserAccount assignWorkerToGrid(Long gridId, Long userId) { - Grid grid = gridRepository.findById(gridId) - .orElseThrow(() -> new ResourceNotFoundException("Grid not found with id: " + gridId)); - UserAccount user = userAccountRepository.findById(userId) - .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); - - // Unassign from any previous grid - unassignWorkerFromAnyGrid(userId); - - user.setGridX(grid.getGridX()); - user.setGridY(grid.getGridY()); - return userAccountRepository.save(user); - } - - @Override - @Transactional - public void unassignWorkerFromGrid(Long gridId) { - Grid grid = gridRepository.findById(gridId) - .orElseThrow(() -> new ResourceNotFoundException("Grid not found with id: " + gridId)); - - userAccountRepository.findByGridXAndGridY(grid.getGridX(), grid.getGridY()).ifPresent(user -> { - user.setGridX(null); - user.setGridY(null); - userAccountRepository.save(user); - }); - } - - private void unassignWorkerFromAnyGrid(Long userId) { - UserAccount user = userAccountRepository.findById(userId) - .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); - if (user.getGridX() != null && user.getGridY() != null) { - user.setGridX(null); - user.setGridY(null); - userAccountRepository.save(user); - } - } - - @Override - @Transactional - public Grid updateGrid(Long gridId, GridUpdateRequest request) { - Grid grid = gridRepository.findById(gridId) - .orElseThrow(() -> new EntityNotFoundException("Grid not found with id: " + gridId)); - - // Sync obstacle status to MapGrid if it's being updated - if (request.getIsObstacle() != null && !request.getIsObstacle().equals(grid.getIsObstacle())) { - grid.setIsObstacle(request.getIsObstacle()); - - MapGrid mapGrid = mapGridRepository.findByXAndY(grid.getGridX(), grid.getGridY()) - .orElseGet(() -> MapGrid.builder() - .x(grid.getGridX()) - .y(grid.getGridY()) - .cityName(grid.getCityName()) - .build()); - - mapGrid.setObstacle(request.getIsObstacle()); - mapGridRepository.save(mapGrid); - } - - if (request.getDescription() != null) { - grid.setDescription(request.getDescription()); - } - - Grid updatedGrid = gridRepository.save(grid); - - operationLogService.recordOperation( - com.dne.ems.model.enums.OperationType.UPDATE, - "更新了网格信息, 网格ID: " + gridId, - gridId.toString(), - "Grid" - ); - - return updatedGrid; - } +package com.dne.ems.service.impl; + +// import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import com.dne.ems.dto.GridUpdateRequest; +import com.dne.ems.exception.ResourceNotFoundException; +import com.dne.ems.model.Grid; +import com.dne.ems.model.MapGrid; +import com.dne.ems.model.UserAccount; +import com.dne.ems.repository.GridRepository; +import com.dne.ems.repository.MapGridRepository; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.service.GridService; +import com.dne.ems.service.OperationLogService; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; + +/** + * 网格服务实现类,处理网格数据的查询和更新 + * + *

实现了 {@link GridService} 接口定义的所有方法,包括: + *

    + *
  • 网格数据查询与过滤
  • + *
  • 网格信息更新
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2025 + */ +@Service +@RequiredArgsConstructor +public class GridServiceImpl implements GridService { + + private final GridRepository gridRepository; + private final UserAccountRepository userAccountRepository; + private final MapGridRepository mapGridRepository; + private final OperationLogService operationLogService; + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 使用Specification动态构建查询条件
  2. + *
  3. 支持按城市和区县过滤
  4. + *
  5. 返回分页结果
  6. + *
+ * + * @param cityName 城市名称过滤条件(可选) + * @param districtName 区县名称过滤条件(可选) + * @param pageable 分页参数 + * @return 网格分页列表 + */ + @Override + public Page getGrids(String cityName, String districtName, Pageable pageable) { + List allGrids = gridRepository.findAll(); + + // In-memory filtering + List filteredGrids = allGrids.stream() + .filter(grid -> !StringUtils.hasText(cityName) || cityName.equals(grid.getCityName())) + .filter(grid -> !StringUtils.hasText(districtName) || districtName.equals(grid.getDistrictName())) + .collect(Collectors.toList()); + + // Manual pagination + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), filteredGrids.size()); + + List pageContent = (start > filteredGrids.size()) ? List.of() : filteredGrids.subList(start, end); + + return new PageImpl<>(pageContent, pageable, filteredGrids.size()); + } + + @Override + public List getAllGrids() { + return gridRepository.findAll(); + } + + @Override + public Optional getGridById(Long gridId) { + return gridRepository.findById(gridId); + } + + @Override + @Transactional + public UserAccount assignWorkerToGrid(Long gridId, Long userId) { + Grid grid = gridRepository.findById(gridId) + .orElseThrow(() -> new ResourceNotFoundException("Grid not found with id: " + gridId)); + UserAccount user = userAccountRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); + + // Unassign from any previous grid + unassignWorkerFromAnyGrid(userId); + + user.setGridX(grid.getGridX()); + user.setGridY(grid.getGridY()); + return userAccountRepository.save(user); + } + + @Override + @Transactional + public void unassignWorkerFromGrid(Long gridId) { + Grid grid = gridRepository.findById(gridId) + .orElseThrow(() -> new ResourceNotFoundException("Grid not found with id: " + gridId)); + + userAccountRepository.findByGridXAndGridY(grid.getGridX(), grid.getGridY()).ifPresent(user -> { + user.setGridX(null); + user.setGridY(null); + userAccountRepository.save(user); + }); + } + + private void unassignWorkerFromAnyGrid(Long userId) { + UserAccount user = userAccountRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); + if (user.getGridX() != null && user.getGridY() != null) { + user.setGridX(null); + user.setGridY(null); + userAccountRepository.save(user); + } + } + + @Override + @Transactional + public Grid updateGrid(Long gridId, GridUpdateRequest request) { + Grid grid = gridRepository.findById(gridId) + .orElseThrow(() -> new EntityNotFoundException("Grid not found with id: " + gridId)); + + // Sync obstacle status to MapGrid if it's being updated + if (request.getIsObstacle() != null && !request.getIsObstacle().equals(grid.getIsObstacle())) { + grid.setIsObstacle(request.getIsObstacle()); + + MapGrid mapGrid = mapGridRepository.findByXAndY(grid.getGridX(), grid.getGridY()) + .orElseGet(() -> MapGrid.builder() + .x(grid.getGridX()) + .y(grid.getGridY()) + .cityName(grid.getCityName()) + .build()); + + mapGrid.setObstacle(request.getIsObstacle()); + mapGridRepository.save(mapGrid); + } + + if (request.getDescription() != null) { + grid.setDescription(request.getDescription()); + } + + Grid updatedGrid = gridRepository.save(grid); + + operationLogService.recordOperation( + com.dne.ems.model.enums.OperationType.UPDATE, + "更新了网格信息, 网格ID: " + gridId, + gridId.toString(), + "Grid" + ); + + return updatedGrid; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/GridWorkerTaskServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/GridWorkerTaskServiceImpl.java index 30576c3..fd5648e 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/GridWorkerTaskServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/GridWorkerTaskServiceImpl.java @@ -1,383 +1,383 @@ -package com.dne.ems.service.impl; - -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import com.dne.ems.dto.AttachmentDTO; -import com.dne.ems.dto.TaskDetailDTO; -import com.dne.ems.dto.TaskDetailDTO.SubmissionInfoDTO; -import com.dne.ems.dto.TaskHistoryDTO; -import com.dne.ems.dto.TaskSubmissionRequest; -import com.dne.ems.dto.TaskSummaryDTO; -import com.dne.ems.exception.InvalidOperationException; -import com.dne.ems.exception.ResourceNotFoundException; -import com.dne.ems.model.Attachment; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.Task; -import com.dne.ems.model.TaskHistory; -import com.dne.ems.model.TaskSubmission; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.model.enums.TaskStatus; -import com.dne.ems.repository.AttachmentRepository; -import com.dne.ems.repository.TaskHistoryRepository; -import com.dne.ems.repository.TaskRepository; -import com.dne.ems.repository.TaskSubmissionRepository; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.FileStorageService; -import com.dne.ems.service.GridWorkerTaskService; -import com.dne.ems.service.OperationLogService; - -import lombok.RequiredArgsConstructor; - -/** - * 网格员任务服务实现类,处理网格员的任务管理操作 - * - *

实现了 {@link GridWorkerTaskService} 接口定义的所有方法,包括: - *

    - *
  • 获取分配的任务列表
  • - *
  • 接受任务
  • - *
  • 提交任务完成情况
  • - *
  • 查看任务详情
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Service -@RequiredArgsConstructor -public class GridWorkerTaskServiceImpl implements GridWorkerTaskService { - - private final TaskRepository taskRepository; - private final TaskHistoryRepository taskHistoryRepository; - private final TaskSubmissionRepository taskSubmissionRepository; - // private final UserAccountRepository userAccountRepository; - private final FileStorageService fileStorageService; - private final AttachmentRepository attachmentRepository; - private final OperationLogService operationLogService; - - @Value("${app.base.url}") - private String appBaseUrl; - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 验证分页排序参数,默认按创建时间降序
  2. - *
  3. 构建动态查询条件
  4. - *
  5. 查询并转换结果
  6. - *
- * - * @param workerId 网格员ID - * @param status 任务状态过滤条件(可选) - * @param pageable 分页参数 - * @return 任务摘要分页列表 - */ - @Override - public Page getAssignedTasks(Long workerId, TaskStatus status, Pageable pageable) { - if (pageable.getSort().isUnsorted()) { - pageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("createdAt").descending()); - } - - List allTasks = taskRepository.findAll(); - - List filteredTasks = allTasks.stream() - .filter(task -> task.getAssignee() != null && task.getAssignee().getId().equals(workerId)) - .filter(task -> status == null || task.getStatus() == status) - .collect(Collectors.toList()); - - // Manual sorting - if (pageable.getSort().isSorted() && pageable.getSort().getOrderFor("createdAt") != null && pageable.getSort().getOrderFor("createdAt").isDescending()) { - filteredTasks.sort((t1, t2) -> t2.getCreatedAt().compareTo(t1.getCreatedAt())); - } - - // Manual pagination - int start = (int) pageable.getOffset(); - int end = Math.min((start + pageable.getPageSize()), filteredTasks.size()); - - List pageContent = (start > filteredTasks.size()) ? List.of() : filteredTasks.subList(start, end); - - Page taskPage = new PageImpl<>(pageContent, pageable, filteredTasks.size()); - return taskPage.map(this::convertToSummaryDto); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 验证任务所有权
  2. - *
  3. 检查任务状态是否可接受
  4. - *
  5. 更新任务状态
  6. - *
  7. 记录任务历史
  8. - *
- * - * @param taskId 任务ID - * @param workerId 网格员ID - * @return 更新后的任务摘要 - * @throws InvalidOperationException 如果任务状态不允许接受 - */ - @Override - public TaskSummaryDTO acceptTask(Long taskId, Long workerId) { - Task task = validateTaskOwnership(taskId, workerId); - if (task.getStatus() != TaskStatus.ASSIGNED) { - throw new InvalidOperationException("Task cannot be accepted. Current status: " + task.getStatus()); - } - TaskStatus oldStatus = task.getStatus(); - task.setStatus(TaskStatus.IN_PROGRESS); - Task updatedTask = taskRepository.save(task); - logTaskHistory(updatedTask, oldStatus, updatedTask.getStatus(), "Task accepted by worker."); - - operationLogService.recordOperation( - com.dne.ems.model.enums.OperationType.UPDATE, - "接受了任务 (ID: " + taskId + ")", - taskId.toString(), - "Task" - ); - - return convertToSummaryDto(updatedTask); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 验证任务所有权
  2. - *
  3. 检查任务状态是否可提交
  4. - *
  5. 创建任务提交记录
  6. - *
  7. 更新任务状态
  8. - *
  9. 记录任务历史
  10. - *
- * - * @param taskId 任务ID - * @param workerId 网格员ID - * @param request 包含提交说明的请求体 - * @return 更新后的任务摘要 - * @throws InvalidOperationException 如果任务状态不允许提交 - */ - @Override - public TaskSummaryDTO submitTaskCompletion(Long taskId, Long workerId, TaskSubmissionRequest request) { - return submitTaskCompletion(taskId, workerId, request, null); - } - - @Override - @Transactional - public TaskSummaryDTO submitTaskCompletion(Long taskId, Long workerId, TaskSubmissionRequest request, List files) { - Task task = validateTaskOwnership(taskId, workerId); - if (task.getStatus() != TaskStatus.IN_PROGRESS && task.getStatus() != TaskStatus.ASSIGNED) { - throw new InvalidOperationException("Task must be IN_PROGRESS or ASSIGNED to be submitted. Current status: " + task.getStatus()); - } - - // 1. 先更新并保存任务状态 - TaskStatus oldStatus = task.getStatus(); - task.setStatus(TaskStatus.SUBMITTED); - Task updatedTask = taskRepository.save(task); - - // 2. 然后创建并保存提交记录 - TaskSubmission submission = new TaskSubmission(updatedTask, request.comments()); - taskSubmissionRepository.save(submission); - - // 3. 最后处理附件 - if (files != null && !files.isEmpty()) { - for (MultipartFile file : files) { - String storedFileName = fileStorageService.storeFile(file); - Attachment attachment = Attachment.builder() - .fileName(file.getOriginalFilename()) - .fileType(file.getContentType()) - .fileSize(file.getSize()) - .storedFileName(storedFileName) - .taskSubmission(submission) - .build(); - attachmentRepository.save(attachment); - } - } - - // 4. 记录历史 - logTaskHistory(updatedTask, oldStatus, updatedTask.getStatus(), "Worker submitted comments: " + request.comments()); - - operationLogService.recordOperation( - com.dne.ems.model.enums.OperationType.SUBMIT_TASK, - "提交了任务 (ID: " + taskId + ") 的完成情况", - taskId.toString(), - "Task" - ); - - return convertToSummaryDto(updatedTask); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 验证任务所有权
  2. - *
  3. 转换任务详情DTO
  4. - *
- * - * @param taskId 任务ID - * @param workerId 网格员ID - * @return 任务详情DTO - */ - @Override - public TaskDetailDTO getTaskDetails(Long taskId, Long workerId) { - Task task = validateTaskOwnership(taskId, workerId); - return convertToDetailDto(task); - } - - /** - * 验证任务所有权 - * - * @param taskId 任务ID - * @param workerId 网格员ID - * @return 验证通过的任务实体 - * @throws ResourceNotFoundException 如果任务不存在 - * @throws AccessDeniedException 如果任务不属于当前网格员 - */ - private Task validateTaskOwnership(Long taskId, Long workerId) { - Task task = taskRepository.findById(taskId).orElseThrow(() -> new ResourceNotFoundException("Task not found")); - if (task.getAssignee() == null || !Objects.equals(task.getAssignee().getId(), workerId)) { - throw new AccessDeniedException("This task is not assigned to you."); - } - return task; - } - - /** - * 记录任务历史 - * - * @param task 任务实体 - * @param oldStatus 旧状态 - * @param newStatus 新状态 - * @param comments 备注信息 - */ - private void logTaskHistory(Task task, TaskStatus oldStatus, TaskStatus newStatus, String comments) { - UserAccount currentUser = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserAccount(); - TaskHistory history = new TaskHistory(task, oldStatus, newStatus, currentUser, comments); - taskHistoryRepository.save(history); - } - - /** - * 转换任务实体为摘要DTO - * - * @param task 任务实体 - * @return 任务摘要DTO - */ - private TaskSummaryDTO convertToSummaryDto(Task task) { - SeverityLevel severity = task.getFeedback() != null ? task.getFeedback().getSeverityLevel() : task.getSeverityLevel(); - String imageUrl = ""; - if (task.getFeedback() != null && task.getFeedback().getAttachments() != null && !task.getFeedback().getAttachments().isEmpty()) { - imageUrl = appBaseUrl + "/api/files/view/" + task.getFeedback().getAttachments().get(0).getStoredFileName(); - } - return new TaskSummaryDTO( - task.getId(), - task.getTitle(), - task.getDescription(), - task.getStatus(), - task.getAssignee() != null ? task.getAssignee().getName() : null, - task.getCreatedAt(), - task.getTextAddress(), - imageUrl, - severity - ); - } - - /** - * 转换任务实体为详情DTO - * - * @param task 任务实体 - * @return 任务详情DTO - */ - private TaskDetailDTO convertToDetailDto(Task task) { - TaskDetailDTO.FeedbackDTO feedbackDto = null; - if (task.getFeedback() != null) { - Feedback feedback = task.getFeedback(); - List imageUrls = feedback.getAttachments().stream() - .map(att -> appBaseUrl + "/api/files/" + att.getStoredFileName()) - .collect(Collectors.toList()); - - feedbackDto = new TaskDetailDTO.FeedbackDTO( - feedback.getId(), - feedback.getEventId(), - feedback.getTitle(), - feedback.getDescription(), - feedback.getSeverityLevel(), - feedback.getTextAddress(), - feedback.getSubmitterId(), - null, // Submitter name - can be fetched if needed - feedback.getCreatedAt(), - imageUrls - ); - } - - TaskDetailDTO.AssigneeDTO assigneeDto = null; - if (task.getAssignee() != null) { - assigneeDto = new TaskDetailDTO.AssigneeDTO(task.getAssignee().getId(), task.getAssignee().getName(), task.getAssignee().getPhone()); - } - - List historyDtos = task.getHistory().stream() - .map(this::convertToHistoryDto) - .collect(Collectors.toList()); - - // Fetch and build SubmissionInfoDTO - TaskSubmission latestSubmission = taskSubmissionRepository.findFirstByTaskOrderBySubmittedAtDesc(task); - SubmissionInfoDTO submissionInfoDto = null; - if (latestSubmission != null) { - List attachments = attachmentRepository.findByTaskSubmission(latestSubmission); - List attachmentDtos = attachments.stream() - .map(AttachmentDTO::fromEntity) - .collect(Collectors.toList()); - submissionInfoDto = new SubmissionInfoDTO(latestSubmission.getNotes(), attachmentDtos); - } - - return new TaskDetailDTO( - task.getId(), - feedbackDto, - task.getTitle(), - task.getDescription(), - task.getSeverityLevel(), - task.getPollutionType(), - task.getTextAddress(), - task.getGridX(), - task.getGridY(), - task.getStatus(), - assigneeDto, - task.getAssignedAt(), - task.getCompletedAt(), - historyDtos, - submissionInfoDto - ); - } - - /** - * 转换任务历史实体为DTO - * - * @param history 任务历史实体 - * @return 任务历史DTO - */ - private TaskHistoryDTO convertToHistoryDto(TaskHistory history) { - UserAccount changedBy = history.getChangedBy(); - return new TaskHistoryDTO( - history.getId(), - history.getOldStatus(), - history.getNewStatus(), - history.getComments(), - history.getChangedAt(), - new TaskHistoryDTO.UserDTO(changedBy.getId(), changedBy.getName()) - ); - } +package com.dne.ems.service.impl; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import com.dne.ems.dto.AttachmentDTO; +import com.dne.ems.dto.TaskDetailDTO; +import com.dne.ems.dto.TaskDetailDTO.SubmissionInfoDTO; +import com.dne.ems.dto.TaskHistoryDTO; +import com.dne.ems.dto.TaskSubmissionRequest; +import com.dne.ems.dto.TaskSummaryDTO; +import com.dne.ems.exception.InvalidOperationException; +import com.dne.ems.exception.ResourceNotFoundException; +import com.dne.ems.model.Attachment; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.Task; +import com.dne.ems.model.TaskHistory; +import com.dne.ems.model.TaskSubmission; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.model.enums.TaskStatus; +import com.dne.ems.repository.AttachmentRepository; +import com.dne.ems.repository.TaskHistoryRepository; +import com.dne.ems.repository.TaskRepository; +import com.dne.ems.repository.TaskSubmissionRepository; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.FileStorageService; +import com.dne.ems.service.GridWorkerTaskService; +import com.dne.ems.service.OperationLogService; + +import lombok.RequiredArgsConstructor; + +/** + * 网格员任务服务实现类,处理网格员的任务管理操作 + * + *

实现了 {@link GridWorkerTaskService} 接口定义的所有方法,包括: + *

    + *
  • 获取分配的任务列表
  • + *
  • 接受任务
  • + *
  • 提交任务完成情况
  • + *
  • 查看任务详情
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Service +@RequiredArgsConstructor +public class GridWorkerTaskServiceImpl implements GridWorkerTaskService { + + private final TaskRepository taskRepository; + private final TaskHistoryRepository taskHistoryRepository; + private final TaskSubmissionRepository taskSubmissionRepository; + // private final UserAccountRepository userAccountRepository; + private final FileStorageService fileStorageService; + private final AttachmentRepository attachmentRepository; + private final OperationLogService operationLogService; + + @Value("${app.base.url}") + private String appBaseUrl; + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 验证分页排序参数,默认按创建时间降序
  2. + *
  3. 构建动态查询条件
  4. + *
  5. 查询并转换结果
  6. + *
+ * + * @param workerId 网格员ID + * @param status 任务状态过滤条件(可选) + * @param pageable 分页参数 + * @return 任务摘要分页列表 + */ + @Override + public Page getAssignedTasks(Long workerId, TaskStatus status, Pageable pageable) { + if (pageable.getSort().isUnsorted()) { + pageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("createdAt").descending()); + } + + List allTasks = taskRepository.findAll(); + + List filteredTasks = allTasks.stream() + .filter(task -> task.getAssignee() != null && task.getAssignee().getId().equals(workerId)) + .filter(task -> status == null || task.getStatus() == status) + .collect(Collectors.toList()); + + // Manual sorting + if (pageable.getSort().isSorted() && pageable.getSort().getOrderFor("createdAt") != null && pageable.getSort().getOrderFor("createdAt").isDescending()) { + filteredTasks.sort((t1, t2) -> t2.getCreatedAt().compareTo(t1.getCreatedAt())); + } + + // Manual pagination + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), filteredTasks.size()); + + List pageContent = (start > filteredTasks.size()) ? List.of() : filteredTasks.subList(start, end); + + Page taskPage = new PageImpl<>(pageContent, pageable, filteredTasks.size()); + return taskPage.map(this::convertToSummaryDto); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 验证任务所有权
  2. + *
  3. 检查任务状态是否可接受
  4. + *
  5. 更新任务状态
  6. + *
  7. 记录任务历史
  8. + *
+ * + * @param taskId 任务ID + * @param workerId 网格员ID + * @return 更新后的任务摘要 + * @throws InvalidOperationException 如果任务状态不允许接受 + */ + @Override + public TaskSummaryDTO acceptTask(Long taskId, Long workerId) { + Task task = validateTaskOwnership(taskId, workerId); + if (task.getStatus() != TaskStatus.ASSIGNED) { + throw new InvalidOperationException("Task cannot be accepted. Current status: " + task.getStatus()); + } + TaskStatus oldStatus = task.getStatus(); + task.setStatus(TaskStatus.IN_PROGRESS); + Task updatedTask = taskRepository.save(task); + logTaskHistory(updatedTask, oldStatus, updatedTask.getStatus(), "Task accepted by worker."); + + operationLogService.recordOperation( + com.dne.ems.model.enums.OperationType.UPDATE, + "接受了任务 (ID: " + taskId + ")", + taskId.toString(), + "Task" + ); + + return convertToSummaryDto(updatedTask); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 验证任务所有权
  2. + *
  3. 检查任务状态是否可提交
  4. + *
  5. 创建任务提交记录
  6. + *
  7. 更新任务状态
  8. + *
  9. 记录任务历史
  10. + *
+ * + * @param taskId 任务ID + * @param workerId 网格员ID + * @param request 包含提交说明的请求体 + * @return 更新后的任务摘要 + * @throws InvalidOperationException 如果任务状态不允许提交 + */ + @Override + public TaskSummaryDTO submitTaskCompletion(Long taskId, Long workerId, TaskSubmissionRequest request) { + return submitTaskCompletion(taskId, workerId, request, null); + } + + @Override + @Transactional + public TaskSummaryDTO submitTaskCompletion(Long taskId, Long workerId, TaskSubmissionRequest request, List files) { + Task task = validateTaskOwnership(taskId, workerId); + if (task.getStatus() != TaskStatus.IN_PROGRESS && task.getStatus() != TaskStatus.ASSIGNED) { + throw new InvalidOperationException("Task must be IN_PROGRESS or ASSIGNED to be submitted. Current status: " + task.getStatus()); + } + + // 1. 先更新并保存任务状态 + TaskStatus oldStatus = task.getStatus(); + task.setStatus(TaskStatus.SUBMITTED); + Task updatedTask = taskRepository.save(task); + + // 2. 然后创建并保存提交记录 + TaskSubmission submission = new TaskSubmission(updatedTask, request.comments()); + taskSubmissionRepository.save(submission); + + // 3. 最后处理附件 + if (files != null && !files.isEmpty()) { + for (MultipartFile file : files) { + String storedFileName = fileStorageService.storeFile(file); + Attachment attachment = Attachment.builder() + .fileName(file.getOriginalFilename()) + .fileType(file.getContentType()) + .fileSize(file.getSize()) + .storedFileName(storedFileName) + .taskSubmission(submission) + .build(); + attachmentRepository.save(attachment); + } + } + + // 4. 记录历史 + logTaskHistory(updatedTask, oldStatus, updatedTask.getStatus(), "Worker submitted comments: " + request.comments()); + + operationLogService.recordOperation( + com.dne.ems.model.enums.OperationType.SUBMIT_TASK, + "提交了任务 (ID: " + taskId + ") 的完成情况", + taskId.toString(), + "Task" + ); + + return convertToSummaryDto(updatedTask); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 验证任务所有权
  2. + *
  3. 转换任务详情DTO
  4. + *
+ * + * @param taskId 任务ID + * @param workerId 网格员ID + * @return 任务详情DTO + */ + @Override + public TaskDetailDTO getTaskDetails(Long taskId, Long workerId) { + Task task = validateTaskOwnership(taskId, workerId); + return convertToDetailDto(task); + } + + /** + * 验证任务所有权 + * + * @param taskId 任务ID + * @param workerId 网格员ID + * @return 验证通过的任务实体 + * @throws ResourceNotFoundException 如果任务不存在 + * @throws AccessDeniedException 如果任务不属于当前网格员 + */ + private Task validateTaskOwnership(Long taskId, Long workerId) { + Task task = taskRepository.findById(taskId).orElseThrow(() -> new ResourceNotFoundException("Task not found")); + if (task.getAssignee() == null || !Objects.equals(task.getAssignee().getId(), workerId)) { + throw new AccessDeniedException("This task is not assigned to you."); + } + return task; + } + + /** + * 记录任务历史 + * + * @param task 任务实体 + * @param oldStatus 旧状态 + * @param newStatus 新状态 + * @param comments 备注信息 + */ + private void logTaskHistory(Task task, TaskStatus oldStatus, TaskStatus newStatus, String comments) { + UserAccount currentUser = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserAccount(); + TaskHistory history = new TaskHistory(task, oldStatus, newStatus, currentUser, comments); + taskHistoryRepository.save(history); + } + + /** + * 转换任务实体为摘要DTO + * + * @param task 任务实体 + * @return 任务摘要DTO + */ + private TaskSummaryDTO convertToSummaryDto(Task task) { + SeverityLevel severity = task.getFeedback() != null ? task.getFeedback().getSeverityLevel() : task.getSeverityLevel(); + String imageUrl = ""; + if (task.getFeedback() != null && task.getFeedback().getAttachments() != null && !task.getFeedback().getAttachments().isEmpty()) { + imageUrl = appBaseUrl + "/api/files/view/" + task.getFeedback().getAttachments().get(0).getStoredFileName(); + } + return new TaskSummaryDTO( + task.getId(), + task.getTitle(), + task.getDescription(), + task.getStatus(), + task.getAssignee() != null ? task.getAssignee().getName() : null, + task.getCreatedAt(), + task.getTextAddress(), + imageUrl, + severity + ); + } + + /** + * 转换任务实体为详情DTO + * + * @param task 任务实体 + * @return 任务详情DTO + */ + private TaskDetailDTO convertToDetailDto(Task task) { + TaskDetailDTO.FeedbackDTO feedbackDto = null; + if (task.getFeedback() != null) { + Feedback feedback = task.getFeedback(); + List imageUrls = feedback.getAttachments().stream() + .map(att -> appBaseUrl + "/api/files/" + att.getStoredFileName()) + .collect(Collectors.toList()); + + feedbackDto = new TaskDetailDTO.FeedbackDTO( + feedback.getId(), + feedback.getEventId(), + feedback.getTitle(), + feedback.getDescription(), + feedback.getSeverityLevel(), + feedback.getTextAddress(), + feedback.getSubmitterId(), + null, // Submitter name - can be fetched if needed + feedback.getCreatedAt(), + imageUrls + ); + } + + TaskDetailDTO.AssigneeDTO assigneeDto = null; + if (task.getAssignee() != null) { + assigneeDto = new TaskDetailDTO.AssigneeDTO(task.getAssignee().getId(), task.getAssignee().getName(), task.getAssignee().getPhone()); + } + + List historyDtos = task.getHistory().stream() + .map(this::convertToHistoryDto) + .collect(Collectors.toList()); + + // Fetch and build SubmissionInfoDTO + TaskSubmission latestSubmission = taskSubmissionRepository.findFirstByTaskOrderBySubmittedAtDesc(task); + SubmissionInfoDTO submissionInfoDto = null; + if (latestSubmission != null) { + List attachments = attachmentRepository.findByTaskSubmission(latestSubmission); + List attachmentDtos = attachments.stream() + .map(AttachmentDTO::fromEntity) + .collect(Collectors.toList()); + submissionInfoDto = new SubmissionInfoDTO(latestSubmission.getNotes(), attachmentDtos); + } + + return new TaskDetailDTO( + task.getId(), + feedbackDto, + task.getTitle(), + task.getDescription(), + task.getSeverityLevel(), + task.getPollutionType(), + task.getTextAddress(), + task.getGridX(), + task.getGridY(), + task.getStatus(), + assigneeDto, + task.getAssignedAt(), + task.getCompletedAt(), + historyDtos, + submissionInfoDto + ); + } + + /** + * 转换任务历史实体为DTO + * + * @param history 任务历史实体 + * @return 任务历史DTO + */ + private TaskHistoryDTO convertToHistoryDto(TaskHistory history) { + UserAccount changedBy = history.getChangedBy(); + return new TaskHistoryDTO( + history.getId(), + history.getOldStatus(), + history.getNewStatus(), + history.getComments(), + history.getChangedAt(), + new TaskHistoryDTO.UserDTO(changedBy.getId(), changedBy.getName()) + ); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/JwtServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/JwtServiceImpl.java index 4cf62c3..17afa8c 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/JwtServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/JwtServiceImpl.java @@ -1,183 +1,183 @@ -package com.dne.ems.service.impl; - -import java.security.Key; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; - -import com.dne.ems.service.JwtService; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; - -/** - * JWT服务实现类,处理JWT令牌的生成、解析和验证 - * - *

该服务负责实现JWT(JSON Web Token)相关的所有功能,包括: - *

    - *
  • 生成包含用户信息的JWT令牌
  • - *
  • 验证令牌的有效性和完整性
  • - *
  • 从令牌中提取用户信息和声明
  • - *
  • 处理令牌的签名和过期时间
  • - *
- * - * @author DNE开发团队 - * @version 1.0 - * @since 2024 - */ -@Service -public class JwtServiceImpl implements JwtService { - @Value("${token.signing.key}") - private String jwtSigningKey; - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 从令牌中提取所有声明
  2. - *
  3. 获取主题声明(Subject)作为用户名
  4. - *
- */ - @Override - public String extractUserName(String token) { - return extractClaim(token, Claims::getSubject); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 检查是否为自定义用户详情类型
  2. - *
  3. 提取用户信息作为额外声明
  4. - *
  5. 添加用户权限到声明中
  6. - *
  7. 生成带有签名的JWT令牌
  8. - *
- */ - @Override - public String generateToken(UserDetails userDetails) { - // 使用 instanceof 模式匹配 (Java 16+) 来简化代码 - if (userDetails instanceof com.dne.ems.security.CustomUserDetails customUserDetails) { - Map extraClaims = new HashMap<>(); - - // 将所有需要暴露给前端的用户信息放入 "claims" - // 注意:我们不应放入密码等敏感信息 - extraClaims.put("id", customUserDetails.getId()); - extraClaims.put("name", customUserDetails.getName()); - extraClaims.put("email", customUserDetails.getUsername()); // getUsername() 返回的是 email - extraClaims.put("role", customUserDetails.getRole()); - extraClaims.put("phone", customUserDetails.getPhone()); - extraClaims.put("status", customUserDetails.getStatus()); - extraClaims.put("region", customUserDetails.getRegion()); - extraClaims.put("skills", customUserDetails.getSkills()); - - // 关键修复:将用户的权限(GrantedAuthority)转换为字符串列表并添加到 claims 中 - // Spring Security 的 hasAuthority() 会检查这个 "authorities" 列表 - extraClaims.put("authorities", customUserDetails.getAuthorities().stream() - .map(auth -> auth.getAuthority()) - .collect(java.util.stream.Collectors.toList())); - - // 使用重载的方法生成带有额外信息的 Token - return generateToken(extraClaims, userDetails); - } - - // 如果不是我们的 CustomUserDetails 类型,则生成一个只包含基本信息的 Token - return generateToken(new HashMap<>(), userDetails); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 从令牌中提取用户名
  2. - *
  3. 验证用户名是否匹配
  4. - *
  5. 检查令牌是否已过期
  6. - *
- */ - @Override - public boolean isTokenValid(String token, UserDetails userDetails) { - final String userName = extractUserName(token); - return (userName.equals(userDetails.getUsername())) && !isTokenExpired(token); - } - - /** - * 从令牌中提取特定声明 - * - * @param token 令牌字符串 - * @param claimsResolvers 声明解析函数 - * @return 解析后的声明值 - */ - private T extractClaim(String token, Function claimsResolvers) { - final Claims claims = extractAllClaims(token); - return claimsResolvers.apply(claims); - } - - /** - * 生成JWT令牌 - * - * @param extraClaims 额外的声明信息 - * @param userDetails 用户详情 - * @return 生成的JWT令牌字符串 - */ - private String generateToken(Map extraClaims, UserDetails userDetails) { - return Jwts.builder().setClaims(extraClaims).setSubject(userDetails.getUsername()) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 24)) // 1 day - .signWith(getSigningKey(), SignatureAlgorithm.HS256).compact(); - } - - /** - * 检查令牌是否已过期 - * - * @param token 令牌字符串 - * @return 如果令牌已过期则返回true,否则返回false - */ - private boolean isTokenExpired(String token) { - return extractExpiration(token).before(new Date()); - } - - /** - * 从令牌中提取过期时间 - * - * @param token 令牌字符串 - * @return 令牌的过期时间 - */ - private Date extractExpiration(String token) { - return extractClaim(token, Claims::getExpiration); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 使用签名密钥解析令牌
  2. - *
  3. 提取令牌主体中的所有声明
  4. - *
- */ - @Override - public Claims extractAllClaims(String token) { - return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token) - .getBody(); - } - - /** - * 获取用于签名的密钥 - * - * @return 用于JWT签名的密钥对象 - */ - private Key getSigningKey() { - byte[] keyBytes = Decoders.BASE64.decode(jwtSigningKey); - return Keys.hmacShaKeyFor(keyBytes); - } +package com.dne.ems.service.impl; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import com.dne.ems.service.JwtService; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; + +/** + * JWT服务实现类,处理JWT令牌的生成、解析和验证 + * + *

该服务负责实现JWT(JSON Web Token)相关的所有功能,包括: + *

    + *
  • 生成包含用户信息的JWT令牌
  • + *
  • 验证令牌的有效性和完整性
  • + *
  • 从令牌中提取用户信息和声明
  • + *
  • 处理令牌的签名和过期时间
  • + *
+ * + * @author DNE开发团队 + * @version 1.0 + * @since 2024 + */ +@Service +public class JwtServiceImpl implements JwtService { + @Value("${token.signing.key}") + private String jwtSigningKey; + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 从令牌中提取所有声明
  2. + *
  3. 获取主题声明(Subject)作为用户名
  4. + *
+ */ + @Override + public String extractUserName(String token) { + return extractClaim(token, Claims::getSubject); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 检查是否为自定义用户详情类型
  2. + *
  3. 提取用户信息作为额外声明
  4. + *
  5. 添加用户权限到声明中
  6. + *
  7. 生成带有签名的JWT令牌
  8. + *
+ */ + @Override + public String generateToken(UserDetails userDetails) { + // 使用 instanceof 模式匹配 (Java 16+) 来简化代码 + if (userDetails instanceof com.dne.ems.security.CustomUserDetails customUserDetails) { + Map extraClaims = new HashMap<>(); + + // 将所有需要暴露给前端的用户信息放入 "claims" + // 注意:我们不应放入密码等敏感信息 + extraClaims.put("id", customUserDetails.getId()); + extraClaims.put("name", customUserDetails.getName()); + extraClaims.put("email", customUserDetails.getUsername()); // getUsername() 返回的是 email + extraClaims.put("role", customUserDetails.getRole()); + extraClaims.put("phone", customUserDetails.getPhone()); + extraClaims.put("status", customUserDetails.getStatus()); + extraClaims.put("region", customUserDetails.getRegion()); + extraClaims.put("skills", customUserDetails.getSkills()); + + // 关键修复:将用户的权限(GrantedAuthority)转换为字符串列表并添加到 claims 中 + // Spring Security 的 hasAuthority() 会检查这个 "authorities" 列表 + extraClaims.put("authorities", customUserDetails.getAuthorities().stream() + .map(auth -> auth.getAuthority()) + .collect(java.util.stream.Collectors.toList())); + + // 使用重载的方法生成带有额外信息的 Token + return generateToken(extraClaims, userDetails); + } + + // 如果不是我们的 CustomUserDetails 类型,则生成一个只包含基本信息的 Token + return generateToken(new HashMap<>(), userDetails); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 从令牌中提取用户名
  2. + *
  3. 验证用户名是否匹配
  4. + *
  5. 检查令牌是否已过期
  6. + *
+ */ + @Override + public boolean isTokenValid(String token, UserDetails userDetails) { + final String userName = extractUserName(token); + return (userName.equals(userDetails.getUsername())) && !isTokenExpired(token); + } + + /** + * 从令牌中提取特定声明 + * + * @param token 令牌字符串 + * @param claimsResolvers 声明解析函数 + * @return 解析后的声明值 + */ + private T extractClaim(String token, Function claimsResolvers) { + final Claims claims = extractAllClaims(token); + return claimsResolvers.apply(claims); + } + + /** + * 生成JWT令牌 + * + * @param extraClaims 额外的声明信息 + * @param userDetails 用户详情 + * @return 生成的JWT令牌字符串 + */ + private String generateToken(Map extraClaims, UserDetails userDetails) { + return Jwts.builder().setClaims(extraClaims).setSubject(userDetails.getUsername()) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 24)) // 1 day + .signWith(getSigningKey(), SignatureAlgorithm.HS256).compact(); + } + + /** + * 检查令牌是否已过期 + * + * @param token 令牌字符串 + * @return 如果令牌已过期则返回true,否则返回false + */ + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + /** + * 从令牌中提取过期时间 + * + * @param token 令牌字符串 + * @return 令牌的过期时间 + */ + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 使用签名密钥解析令牌
  2. + *
  3. 提取令牌主体中的所有声明
  4. + *
+ */ + @Override + public Claims extractAllClaims(String token) { + return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token) + .getBody(); + } + + /** + * 获取用于签名的密钥 + * + * @return 用于JWT签名的密钥对象 + */ + private Key getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(jwtSigningKey); + return Keys.hmacShaKeyFor(keyBytes); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/LoginAttemptServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/LoginAttemptServiceImpl.java index ac2e45f..e7cb8d2 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/LoginAttemptServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/LoginAttemptServiceImpl.java @@ -1,59 +1,59 @@ -package com.dne.ems.service.impl; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; - -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Service; - -import com.dne.ems.service.LoginAttemptService; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; - -@Service -public class LoginAttemptServiceImpl implements LoginAttemptService { - - public static final int MAX_ATTEMPT = 5; // 最大尝试次数 - @SuppressWarnings("FieldMayBeFinal") - private LoadingCache attemptsCache; - - public LoginAttemptServiceImpl() { - super(); - attemptsCache = CacheBuilder.newBuilder() - .expireAfterWrite(1, TimeUnit.DAYS) - .build(new CacheLoader() { - @Override - public Integer load(@SuppressWarnings("null") @NonNull String key) { - return 0; - } - }); - } - - @Override - public void loginSucceeded(String key) { - attemptsCache.invalidate(key); - } - - @Override - public void loginFailed(String key) { - int attempts; - try { - attempts = attemptsCache.get(key); - } catch (ExecutionException e) { - // Log the exception or handle it as per application's error handling policy - attempts = 0; - } - attempts++; - attemptsCache.put(key, attempts); - } - - @Override - public boolean isBlocked(String key) { - try { - return attemptsCache.get(key) >= MAX_ATTEMPT; - } catch (ExecutionException e) { - return false; - } - } -} +package com.dne.ems.service.impl; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; + +import com.dne.ems.service.LoginAttemptService; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +@Service +public class LoginAttemptServiceImpl implements LoginAttemptService { + + public static final int MAX_ATTEMPT = 5; // 最大尝试次数 + @SuppressWarnings("FieldMayBeFinal") + private LoadingCache attemptsCache; + + public LoginAttemptServiceImpl() { + super(); + attemptsCache = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.DAYS) + .build(new CacheLoader() { + @Override + public Integer load(@SuppressWarnings("null") @NonNull String key) { + return 0; + } + }); + } + + @Override + public void loginSucceeded(String key) { + attemptsCache.invalidate(key); + } + + @Override + public void loginFailed(String key) { + int attempts; + try { + attempts = attemptsCache.get(key); + } catch (ExecutionException e) { + // Log the exception or handle it as per application's error handling policy + attempts = 0; + } + attempts++; + attemptsCache.put(key, attempts); + } + + @Override + public boolean isBlocked(String key) { + try { + return attemptsCache.get(key) >= MAX_ATTEMPT; + } catch (ExecutionException e) { + return false; + } + } +} diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/MailServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/MailServiceImpl.java index 66e54d8..75dfca0 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/MailServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/MailServiceImpl.java @@ -1,121 +1,121 @@ -package com.dne.ems.service.impl; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.MimeMessageHelper; -import org.springframework.stereotype.Service; - -import com.dne.ems.service.MailService; - -import jakarta.mail.MessagingException; -import jakarta.mail.internet.MimeMessage; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * 邮件服务实现类,负责通过 JavaMailSender 发送各类邮件 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class MailServiceImpl implements MailService { - - private final JavaMailSender mailSender; - - - @Value("${spring.mail.username}") - private String fromEmail; - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 构建HTML邮件内容
  2. - *
  3. 设置邮件主题
  4. - *
  5. 调用sendHtmlEmail发送邮件
  6. - *
  7. 记录发送日志
  8. - *
- */ - @Override - public void sendPasswordResetEmail(String to, String code) { - String subject = "【EMS系统】您的密码重置验证码"; - String content = "" - + "

您好,

" - + "

我们收到了您的密码重置请求。以下是您的验证码:

" - + "
" - + code - + "
" - + "

验证码有效期为5分钟,请勿将验证码泄露给他人。

" - + "

如果您没有请求重置密码,请忽略此邮件。

" - + "

谢谢,
EMS系统团队

" - + ""; - - sendHtmlEmail(to, subject, content); - log.info("密码重置验证码已发送到邮箱: {}", to); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 构建HTML邮件内容
  2. - *
  3. 设置邮件主题
  4. - *
  5. 调用sendHtmlEmail发送邮件
  6. - *
  7. 记录发送日志
  8. - *
- */ - @Override - public void sendVerificationCodeEmail(String to, String code) { - String subject = "【EMS系统】您的注册验证码"; - String content = "" - + "

欢迎注册EMS系统

" - + "

您的邮箱验证码是:

" - + "
" - + code - + "
" - + "

该验证码5分钟内有效,请勿泄露给他人。

" - + ""; - - sendHtmlEmail(to, subject, content); - log.info("注册验证码已发送到邮箱: {}", to); - } - - /** - * 发送HTML格式的邮件 - * - * @param to 收件人邮箱 - * @param subject 邮件主题 - * @param htmlContent HTML格式的邮件内容 - */ - /** - * 发送HTML格式邮件(内部方法) - * - *

实现细节: - *

    - *
  1. 创建MIME消息
  2. - *
  3. 设置发件人、收件人、主题
  4. - *
  5. 设置HTML内容并发送
  6. - *
- * - * @param to 收件人邮箱 - * @param subject 邮件主题 - * @param htmlContent HTML格式内容 - * @throws RuntimeException 如果邮件发送失败 - */ - private void sendHtmlEmail(String to, String subject, String htmlContent) { - MimeMessage mimeMessage = mailSender.createMimeMessage(); - try { - MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); - helper.setFrom(fromEmail); - helper.setTo(to); - helper.setSubject(subject); - helper.setText(htmlContent, true); // true表示支持HTML内容 - mailSender.send(mimeMessage); - } catch (MessagingException e) { - log.error("发送邮件失败: {}", e.getMessage(), e); - throw new RuntimeException("发送邮件失败", e); - } - } +package com.dne.ems.service.impl; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import com.dne.ems.service.MailService; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 邮件服务实现类,负责通过 JavaMailSender 发送各类邮件 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class MailServiceImpl implements MailService { + + private final JavaMailSender mailSender; + + + @Value("${spring.mail.username}") + private String fromEmail; + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 构建HTML邮件内容
  2. + *
  3. 设置邮件主题
  4. + *
  5. 调用sendHtmlEmail发送邮件
  6. + *
  7. 记录发送日志
  8. + *
+ */ + @Override + public void sendPasswordResetEmail(String to, String code) { + String subject = "【EMS系统】您的密码重置验证码"; + String content = "" + + "

您好,

" + + "

我们收到了您的密码重置请求。以下是您的验证码:

" + + "
" + + code + + "
" + + "

验证码有效期为5分钟,请勿将验证码泄露给他人。

" + + "

如果您没有请求重置密码,请忽略此邮件。

" + + "

谢谢,
EMS系统团队

" + + ""; + + sendHtmlEmail(to, subject, content); + log.info("密码重置验证码已发送到邮箱: {}", to); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 构建HTML邮件内容
  2. + *
  3. 设置邮件主题
  4. + *
  5. 调用sendHtmlEmail发送邮件
  6. + *
  7. 记录发送日志
  8. + *
+ */ + @Override + public void sendVerificationCodeEmail(String to, String code) { + String subject = "【EMS系统】您的注册验证码"; + String content = "" + + "

欢迎注册EMS系统

" + + "

您的邮箱验证码是:

" + + "
" + + code + + "
" + + "

该验证码5分钟内有效,请勿泄露给他人。

" + + ""; + + sendHtmlEmail(to, subject, content); + log.info("注册验证码已发送到邮箱: {}", to); + } + + /** + * 发送HTML格式的邮件 + * + * @param to 收件人邮箱 + * @param subject 邮件主题 + * @param htmlContent HTML格式的邮件内容 + */ + /** + * 发送HTML格式邮件(内部方法) + * + *

实现细节: + *

    + *
  1. 创建MIME消息
  2. + *
  3. 设置发件人、收件人、主题
  4. + *
  5. 设置HTML内容并发送
  6. + *
+ * + * @param to 收件人邮箱 + * @param subject 邮件主题 + * @param htmlContent HTML格式内容 + * @throws RuntimeException 如果邮件发送失败 + */ + private void sendHtmlEmail(String to, String subject, String htmlContent) { + MimeMessage mimeMessage = mailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); + helper.setFrom(fromEmail); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(htmlContent, true); // true表示支持HTML内容 + mailSender.send(mimeMessage); + } catch (MessagingException e) { + log.error("发送邮件失败: {}", e.getMessage(), e); + throw new RuntimeException("发送邮件失败", e); + } + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/OperationLogServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/OperationLogServiceImpl.java index 43c17e0..e5b894e 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/OperationLogServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/OperationLogServiceImpl.java @@ -1,232 +1,232 @@ -package com.dne.ems.service.impl; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import com.dne.ems.dto.OperationLogDTO; -import com.dne.ems.model.OperationLog; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.OperationType; -import com.dne.ems.repository.OperationLogRepository; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.OperationLogService; - -import jakarta.servlet.http.HttpServletRequest; - -/** - * 操作日志服务实现类,提供记录和查询用户操作日志的功能。 - * - * @version 1.0 - * @since 2025-06-20 - */ -@Service -public class OperationLogServiceImpl implements OperationLogService { - - private static final Logger log = LoggerFactory.getLogger(OperationLogServiceImpl.class); - - @Autowired - private OperationLogRepository operationLogRepository; - - @Autowired - private UserAccountRepository userAccountRepository; - - @Override - public void recordOperation(OperationType operationType, String description, String targetId, String targetType) { - UserAccount currentUser = getCurrentUser(); - if (currentUser == null) { - log.warn("无法记录操作日志:未找到当前用户,操作描述: {}", description); - return; - } - recordOperation(currentUser, operationType, description, targetId, targetType); - } - - @Override - public void recordOperation(UserAccount user, OperationType operationType, String description, String targetId, String targetType) { - try { - if (user == null) { - log.warn("无法记录操作日志:用户为null"); - return; - } - - String ipAddress = getClientIpAddress(); - - OperationLog operationLog = new OperationLog( - user, - operationType, - description, - targetId, - targetType, - ipAddress - ); - - operationLogRepository.save(operationLog); - log.debug("已记录操作日志:用户={}, 操作={}, 描述={}", - user.getName(), operationType, description); - } catch (Exception e) { - log.error("记录操作日志失败", e); - } - } - - @Override - public void recordLogin(Long userId, String ipAddress, boolean success) { - try { - UserAccount user = userAccountRepository.findById(userId).orElse(null); - if (user == null) { - log.warn("无法记录登录操作:未找到用户ID={}", userId); - return; - } - - String description = success ? "用户登录成功" : "用户登录失败"; - - OperationLog operationLog = new OperationLog( - user, - OperationType.LOGIN, - description, - userId.toString(), - "UserAccount", - ipAddress - ); - - operationLogRepository.save(operationLog); - log.debug("已记录登录操作:用户={}, 结果={}", user.getName(), success ? "成功" : "失败"); - } catch (Exception e) { - log.error("记录登录操作失败", e); - } - } - - @Override - public void recordLogout(Long userId) { - try { - UserAccount user = userAccountRepository.findById(userId).orElse(null); - if (user == null) { - log.warn("无法记录登出操作:未找到用户ID={}", userId); - return; - } - - String ipAddress = getClientIpAddress(); - - OperationLog operationLog = new OperationLog( - user, - OperationType.LOGOUT, - "用户登出系统", - userId.toString(), - "UserAccount", - ipAddress - ); - - operationLogRepository.save(operationLog); - log.debug("已记录登出操作:用户={}", user.getName()); - } catch (Exception e) { - log.error("记录登出操作失败", e); - } - } - - @Override - public List getUserOperationLogs(Long userId) { - List logs = operationLogRepository.findByUserId(userId); - return convertToDTO(logs); - } - - @Override - public List getAllOperationLogs() { - List logs = operationLogRepository.findAll(); - return convertToDTO(logs); - } - - @Override - public List queryOperationLogs(Long userId, OperationType operationType, - LocalDateTime startTime, LocalDateTime endTime) { - List logs = operationLogRepository.findByConditions(userId, operationType, startTime, endTime); - return convertToDTO(logs); - } - - /** - * 将操作日志实体列表转换为DTO列表 - * - * @param logs 操作日志实体列表 - * @return 操作日志DTO列表 - */ - private List convertToDTO(List logs) { - return logs.stream() - .map(this::convertToDTO) - .collect(Collectors.toList()); - } - - /** - * 将操作日志实体转换为DTO - * - * @param log 操作日志实体 - * @return 操作日志DTO - */ - private OperationLogDTO convertToDTO(OperationLog log) { - UserAccount user = log.getUser(); - - return OperationLogDTO.builder() - .id(log.getId()) - .userId(user != null ? user.getId() : null) - .userName(user != null ? user.getName() : "未知用户") - .operationType(log.getOperationType()) - .operationTypeDesc(OperationLogDTO.getOperationTypeDesc(log.getOperationType())) - .description(log.getDescription()) - .targetId(log.getTargetId()) - .targetType(log.getTargetType()) - .ipAddress(log.getIpAddress()) - .createdAt(log.getCreatedAt()) - .build(); - } - - /** - * 获取当前登录用户 - * - * @return 当前用户实体,如果未登录则返回null - */ - private UserAccount getCurrentUser() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails) { - return ((CustomUserDetails) authentication.getPrincipal()).getUserAccount(); - } - return null; - } - - /** - * 获取客户端IP地址 - * - * @return IP地址字符串 - */ - private String getClientIpAddress() { - try { - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes != null) { - HttpServletRequest request = attributes.getRequest(); - String ip = request.getHeader("X-Forwarded-For"); - - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("Proxy-Client-IP"); - } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("WL-Proxy-Client-IP"); - } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getRemoteAddr(); - } - - return ip; - } - } catch (Exception e) { - log.warn("获取客户端IP地址失败", e); - } - - return "unknown"; - } +package com.dne.ems.service.impl; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import com.dne.ems.dto.OperationLogDTO; +import com.dne.ems.model.OperationLog; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.OperationType; +import com.dne.ems.repository.OperationLogRepository; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.OperationLogService; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 操作日志服务实现类,提供记录和查询用户操作日志的功能。 + * + * @version 1.0 + * @since 2025-06-20 + */ +@Service +public class OperationLogServiceImpl implements OperationLogService { + + private static final Logger log = LoggerFactory.getLogger(OperationLogServiceImpl.class); + + @Autowired + private OperationLogRepository operationLogRepository; + + @Autowired + private UserAccountRepository userAccountRepository; + + @Override + public void recordOperation(OperationType operationType, String description, String targetId, String targetType) { + UserAccount currentUser = getCurrentUser(); + if (currentUser == null) { + log.warn("无法记录操作日志:未找到当前用户,操作描述: {}", description); + return; + } + recordOperation(currentUser, operationType, description, targetId, targetType); + } + + @Override + public void recordOperation(UserAccount user, OperationType operationType, String description, String targetId, String targetType) { + try { + if (user == null) { + log.warn("无法记录操作日志:用户为null"); + return; + } + + String ipAddress = getClientIpAddress(); + + OperationLog operationLog = new OperationLog( + user, + operationType, + description, + targetId, + targetType, + ipAddress + ); + + operationLogRepository.save(operationLog); + log.debug("已记录操作日志:用户={}, 操作={}, 描述={}", + user.getName(), operationType, description); + } catch (Exception e) { + log.error("记录操作日志失败", e); + } + } + + @Override + public void recordLogin(Long userId, String ipAddress, boolean success) { + try { + UserAccount user = userAccountRepository.findById(userId).orElse(null); + if (user == null) { + log.warn("无法记录登录操作:未找到用户ID={}", userId); + return; + } + + String description = success ? "用户登录成功" : "用户登录失败"; + + OperationLog operationLog = new OperationLog( + user, + OperationType.LOGIN, + description, + userId.toString(), + "UserAccount", + ipAddress + ); + + operationLogRepository.save(operationLog); + log.debug("已记录登录操作:用户={}, 结果={}", user.getName(), success ? "成功" : "失败"); + } catch (Exception e) { + log.error("记录登录操作失败", e); + } + } + + @Override + public void recordLogout(Long userId) { + try { + UserAccount user = userAccountRepository.findById(userId).orElse(null); + if (user == null) { + log.warn("无法记录登出操作:未找到用户ID={}", userId); + return; + } + + String ipAddress = getClientIpAddress(); + + OperationLog operationLog = new OperationLog( + user, + OperationType.LOGOUT, + "用户登出系统", + userId.toString(), + "UserAccount", + ipAddress + ); + + operationLogRepository.save(operationLog); + log.debug("已记录登出操作:用户={}", user.getName()); + } catch (Exception e) { + log.error("记录登出操作失败", e); + } + } + + @Override + public List getUserOperationLogs(Long userId) { + List logs = operationLogRepository.findByUserId(userId); + return convertToDTO(logs); + } + + @Override + public List getAllOperationLogs() { + List logs = operationLogRepository.findAll(); + return convertToDTO(logs); + } + + @Override + public List queryOperationLogs(Long userId, OperationType operationType, + LocalDateTime startTime, LocalDateTime endTime) { + List logs = operationLogRepository.findByConditions(userId, operationType, startTime, endTime); + return convertToDTO(logs); + } + + /** + * 将操作日志实体列表转换为DTO列表 + * + * @param logs 操作日志实体列表 + * @return 操作日志DTO列表 + */ + private List convertToDTO(List logs) { + return logs.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * 将操作日志实体转换为DTO + * + * @param log 操作日志实体 + * @return 操作日志DTO + */ + private OperationLogDTO convertToDTO(OperationLog log) { + UserAccount user = log.getUser(); + + return OperationLogDTO.builder() + .id(log.getId()) + .userId(user != null ? user.getId() : null) + .userName(user != null ? user.getName() : "未知用户") + .operationType(log.getOperationType()) + .operationTypeDesc(OperationLogDTO.getOperationTypeDesc(log.getOperationType())) + .description(log.getDescription()) + .targetId(log.getTargetId()) + .targetType(log.getTargetType()) + .ipAddress(log.getIpAddress()) + .createdAt(log.getCreatedAt()) + .build(); + } + + /** + * 获取当前登录用户 + * + * @return 当前用户实体,如果未登录则返回null + */ + private UserAccount getCurrentUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails) { + return ((CustomUserDetails) authentication.getPrincipal()).getUserAccount(); + } + return null; + } + + /** + * 获取客户端IP地址 + * + * @return IP地址字符串 + */ + private String getClientIpAddress() { + try { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + HttpServletRequest request = attributes.getRequest(); + String ip = request.getHeader("X-Forwarded-For"); + + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + + return ip; + } + } catch (Exception e) { + log.warn("获取客户端IP地址失败", e); + } + + return "unknown"; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/PersonnelServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/PersonnelServiceImpl.java index 9f54845..4962a0c 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/PersonnelServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/PersonnelServiceImpl.java @@ -1,163 +1,163 @@ -package com.dne.ems.service.impl; - -// import java.util.List; -// import java.util.stream.Collectors; - -import org.springframework.data.domain.Page; -// import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -import com.dne.ems.dto.UserCreationRequest; -import com.dne.ems.dto.UserUpdateRequest; -import com.dne.ems.exception.UserAlreadyExistsException; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.UserStatus; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.service.OperationLogService; -import com.dne.ems.service.PersonnelService; - -import jakarta.persistence.EntityNotFoundException; - -@Service -public class PersonnelServiceImpl implements PersonnelService { - - private final UserAccountRepository userAccountRepository; - private final PasswordEncoder passwordEncoder; - private final OperationLogService operationLogService; - - public PersonnelServiceImpl(UserAccountRepository userAccountRepository, - PasswordEncoder passwordEncoder, - OperationLogService operationLogService) { - this.userAccountRepository = userAccountRepository; - this.passwordEncoder = passwordEncoder; - this.operationLogService = operationLogService; - } - - @Override - public Page getUsers(Role role, String name, Pageable pageable) { - return userAccountRepository.findUsers(role, name, pageable); - } - - @Override - public UserAccount createUser(UserCreationRequest request) { - if (userAccountRepository.findByEmail(request.email()).isPresent()) { - throw new UserAlreadyExistsException("Account with email " + request.email() + " already exists."); - } - if (userAccountRepository.findByPhone(request.phone()).isPresent()) { - throw new UserAlreadyExistsException("Account with phone " + request.phone() + " already exists."); - } - - UserAccount user = new UserAccount(); - user.setName(request.name()); - user.setEmail(request.email()); - user.setPhone(request.phone()); - user.setPassword(passwordEncoder.encode(request.password())); - user.setRole(request.role()); - user.setRegion(request.region()); - user.setEnabled(true); // Or based on some logic, default to active - user.setStatus(UserStatus.ACTIVE); // Default to active status - - return userAccountRepository.save(user); - } - - @Override - public UserAccount updateUser(Long userId, UserUpdateRequest request) { - UserAccount user = userAccountRepository.findById(userId) - .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + userId)); - - if (StringUtils.hasText(request.name())) { - user.setName(request.name()); - } - - if (StringUtils.hasText(request.phone())) { - // Optional: Add validation to ensure new phone isn't already taken by another user - user.setPhone(request.phone()); - } - - if (StringUtils.hasText(request.region())) { - user.setRegion(request.region()); - } - - if (request.role() != null) { - user.setRole(request.role()); - } - - if (request.status() != null) { - user.setStatus(request.status()); - } - - if (request.gender() != null) { - user.setGender(request.gender()); - } - - if (request.gridX() != null) { - user.setGridX(request.gridX()); - } - - if (request.gridY() != null) { - user.setGridY(request.gridY()); - } - - if (request.level() != null) { - user.setLevel(request.level()); - } - - if (request.skills() != null && !request.skills().isEmpty()) { - user.setSkills(request.skills()); - } - - if (request.currentLatitude() != null) { - user.setCurrentLatitude(request.currentLatitude()); - } - - if (request.currentLongitude() != null) { - user.setCurrentLongitude(request.currentLongitude()); - } - - UserAccount updatedUser = userAccountRepository.save(user); - - operationLogService.recordOperation( - com.dne.ems.model.enums.OperationType.UPDATE, - "更新了用户信息, 用户: " + updatedUser.getName() + " (ID: " + userId + ")", - userId.toString(), - "UserAccount" - ); - - return updatedUser; - } - - @Override - public void deleteUser(Long userId) { - UserAccount user = userAccountRepository.findById(userId) - .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + userId)); - - user.setStatus(UserStatus.INACTIVE); - userAccountRepository.save(user); - } - - @Override - public UserAccount updateOwnProfile(String currentUserEmail, UserUpdateRequest request) { - UserAccount user = userAccountRepository.findByEmail(currentUserEmail) - .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + currentUserEmail)); - - // User can update their own name, phone, and region. - // Role and status changes are ignored. - if (StringUtils.hasText(request.name())) { - user.setName(request.name()); - } - - if (StringUtils.hasText(request.phone())) { - user.setPhone(request.phone()); - } - - if (StringUtils.hasText(request.region())) { - user.setRegion(request.region()); - } - - return userAccountRepository.save(user); - } +package com.dne.ems.service.impl; + +// import java.util.List; +// import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +// import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import com.dne.ems.dto.UserCreationRequest; +import com.dne.ems.dto.UserUpdateRequest; +import com.dne.ems.exception.UserAlreadyExistsException; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.UserStatus; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.service.OperationLogService; +import com.dne.ems.service.PersonnelService; + +import jakarta.persistence.EntityNotFoundException; + +@Service +public class PersonnelServiceImpl implements PersonnelService { + + private final UserAccountRepository userAccountRepository; + private final PasswordEncoder passwordEncoder; + private final OperationLogService operationLogService; + + public PersonnelServiceImpl(UserAccountRepository userAccountRepository, + PasswordEncoder passwordEncoder, + OperationLogService operationLogService) { + this.userAccountRepository = userAccountRepository; + this.passwordEncoder = passwordEncoder; + this.operationLogService = operationLogService; + } + + @Override + public Page getUsers(Role role, String name, Pageable pageable) { + return userAccountRepository.findUsers(role, name, pageable); + } + + @Override + public UserAccount createUser(UserCreationRequest request) { + if (userAccountRepository.findByEmail(request.email()).isPresent()) { + throw new UserAlreadyExistsException("Account with email " + request.email() + " already exists."); + } + if (userAccountRepository.findByPhone(request.phone()).isPresent()) { + throw new UserAlreadyExistsException("Account with phone " + request.phone() + " already exists."); + } + + UserAccount user = new UserAccount(); + user.setName(request.name()); + user.setEmail(request.email()); + user.setPhone(request.phone()); + user.setPassword(passwordEncoder.encode(request.password())); + user.setRole(request.role()); + user.setRegion(request.region()); + user.setEnabled(true); // Or based on some logic, default to active + user.setStatus(UserStatus.ACTIVE); // Default to active status + + return userAccountRepository.save(user); + } + + @Override + public UserAccount updateUser(Long userId, UserUpdateRequest request) { + UserAccount user = userAccountRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + userId)); + + if (StringUtils.hasText(request.name())) { + user.setName(request.name()); + } + + if (StringUtils.hasText(request.phone())) { + // Optional: Add validation to ensure new phone isn't already taken by another user + user.setPhone(request.phone()); + } + + if (StringUtils.hasText(request.region())) { + user.setRegion(request.region()); + } + + if (request.role() != null) { + user.setRole(request.role()); + } + + if (request.status() != null) { + user.setStatus(request.status()); + } + + if (request.gender() != null) { + user.setGender(request.gender()); + } + + if (request.gridX() != null) { + user.setGridX(request.gridX()); + } + + if (request.gridY() != null) { + user.setGridY(request.gridY()); + } + + if (request.level() != null) { + user.setLevel(request.level()); + } + + if (request.skills() != null && !request.skills().isEmpty()) { + user.setSkills(request.skills()); + } + + if (request.currentLatitude() != null) { + user.setCurrentLatitude(request.currentLatitude()); + } + + if (request.currentLongitude() != null) { + user.setCurrentLongitude(request.currentLongitude()); + } + + UserAccount updatedUser = userAccountRepository.save(user); + + operationLogService.recordOperation( + com.dne.ems.model.enums.OperationType.UPDATE, + "更新了用户信息, 用户: " + updatedUser.getName() + " (ID: " + userId + ")", + userId.toString(), + "UserAccount" + ); + + return updatedUser; + } + + @Override + public void deleteUser(Long userId) { + UserAccount user = userAccountRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + userId)); + + user.setStatus(UserStatus.INACTIVE); + userAccountRepository.save(user); + } + + @Override + public UserAccount updateOwnProfile(String currentUserEmail, UserUpdateRequest request) { + UserAccount user = userAccountRepository.findByEmail(currentUserEmail) + .orElseThrow(() -> new EntityNotFoundException("User not found with email: " + currentUserEmail)); + + // User can update their own name, phone, and region. + // Role and status changes are ignored. + if (StringUtils.hasText(request.name())) { + user.setName(request.name()); + } + + if (StringUtils.hasText(request.phone())) { + user.setPhone(request.phone()); + } + + if (StringUtils.hasText(request.region())) { + user.setRegion(request.region()); + } + + return userAccountRepository.save(user); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/SupervisorServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/SupervisorServiceImpl.java index 44a209e..8880ada 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/SupervisorServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/SupervisorServiceImpl.java @@ -1,103 +1,103 @@ -package com.dne.ems.service.impl; - -import com.dne.ems.dto.RejectFeedbackRequest; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.repository.FeedbackRepository; -import com.dne.ems.service.SupervisorService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -/** - * 主管反馈审核服务实现类 - * - *

该服务负责处理主管对用户提交的反馈进行审核的相关功能, - * 包括获取待审核的反馈列表、批准有效反馈和拒绝无效反馈。 - * 审核结果将直接影响反馈的后续处理流程。

- * - * @version 1.0 - * @since 2025-06 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class SupervisorServiceImpl implements SupervisorService { - - private final FeedbackRepository feedbackRepository; - - /** - * 获取所有待审核的反馈 - * - *

查询状态为PENDING_REVIEW的所有反馈记录,这些反馈已通过AI初步审核, - * 需要主管进行人工审核确认。

- * - * @return 待审核的反馈列表 - */ - @Override - public List getFeedbackForReview() { - log.info("Fetching feedback for supervisor review with status PENDING_REVIEW."); - return feedbackRepository.findByStatus(FeedbackStatus.PENDING_REVIEW); - } - - /** - * 批准反馈 - * - *

将指定ID的反馈状态从PENDING_REVIEW更新为PENDING_ASSIGNMENT, - * 表示该反馈已通过主管审核,可以进入任务分配阶段。

- * - * @param feedbackId 需要批准的反馈ID - * @throws IllegalArgumentException 如果找不到指定ID的反馈 - * @throws IllegalStateException 如果反馈状态不是PENDING_REVIEW - */ - @Override - @Transactional - public void approveFeedback(Long feedbackId) { - log.info("Attempting to approve feedback with ID: {}", feedbackId); - Feedback feedback = feedbackRepository.findById(feedbackId) - .orElseThrow(() -> new IllegalArgumentException("Feedback not found with ID: " + feedbackId)); - - if (feedback.getStatus() != FeedbackStatus.PENDING_REVIEW) { - log.warn("Feedback {} cannot be approved because its status is {} instead of PENDING_REVIEW.", feedbackId, feedback.getStatus()); - throw new IllegalStateException("Feedback is not in a PENDING_REVIEW state."); - } - - feedback.setStatus(FeedbackStatus.PENDING_ASSIGNMENT); - feedbackRepository.save(feedback); - log.info("Feedback {} approved successfully. Status changed to PENDING_ASSIGNMENT.", feedbackId); - } - - /** - * 拒绝反馈 - * - *

将指定ID的反馈状态从PENDING_REVIEW更新为CLOSED_INVALID, - * 表示该反馈未通过主管审核,将被标记为无效并关闭。

- * - * @param feedbackId 需要拒绝的反馈ID - * @param request 拒绝反馈的请求 - * @throws IllegalArgumentException 如果找不到指定ID的反馈 - * @throws IllegalStateException 如果反馈状态不是PENDING_REVIEW - */ - @Override - @Transactional - public void rejectFeedback(Long feedbackId, RejectFeedbackRequest request) { - log.info("Attempting to reject feedback with ID: {}. Reason: {}", feedbackId, request.getNotes()); - Feedback feedback = feedbackRepository.findById(feedbackId) - .orElseThrow(() -> new IllegalArgumentException("Feedback not found with ID: " + feedbackId)); - - if (feedback.getStatus() != FeedbackStatus.PENDING_REVIEW) { - log.warn("Feedback {} cannot be rejected because its status is {} instead of PENDING_REVIEW.", feedbackId, feedback.getStatus()); - throw new IllegalStateException("Feedback is not in a PENDING_REVIEW state."); - } - - feedback.setStatus(FeedbackStatus.CLOSED_INVALID); - // Although we don't have a dedicated field for rejection notes, - // we can log it for audit purposes. A future enhancement could be to add a field. - log.info("Feedback {} rejected successfully by user. Reason: {}", feedbackId, request.getNotes()); - feedbackRepository.save(feedback); - log.info("Feedback {} status changed to CLOSED_INVALID.", feedbackId); - } +package com.dne.ems.service.impl; + +import com.dne.ems.dto.RejectFeedbackRequest; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.repository.FeedbackRepository; +import com.dne.ems.service.SupervisorService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 主管反馈审核服务实现类 + * + *

该服务负责处理主管对用户提交的反馈进行审核的相关功能, + * 包括获取待审核的反馈列表、批准有效反馈和拒绝无效反馈。 + * 审核结果将直接影响反馈的后续处理流程。

+ * + * @version 1.0 + * @since 2025-06 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class SupervisorServiceImpl implements SupervisorService { + + private final FeedbackRepository feedbackRepository; + + /** + * 获取所有待审核的反馈 + * + *

查询状态为PENDING_REVIEW的所有反馈记录,这些反馈已通过AI初步审核, + * 需要主管进行人工审核确认。

+ * + * @return 待审核的反馈列表 + */ + @Override + public List getFeedbackForReview() { + log.info("Fetching feedback for supervisor review with status PENDING_REVIEW."); + return feedbackRepository.findByStatus(FeedbackStatus.PENDING_REVIEW); + } + + /** + * 批准反馈 + * + *

将指定ID的反馈状态从PENDING_REVIEW更新为PENDING_ASSIGNMENT, + * 表示该反馈已通过主管审核,可以进入任务分配阶段。

+ * + * @param feedbackId 需要批准的反馈ID + * @throws IllegalArgumentException 如果找不到指定ID的反馈 + * @throws IllegalStateException 如果反馈状态不是PENDING_REVIEW + */ + @Override + @Transactional + public void approveFeedback(Long feedbackId) { + log.info("Attempting to approve feedback with ID: {}", feedbackId); + Feedback feedback = feedbackRepository.findById(feedbackId) + .orElseThrow(() -> new IllegalArgumentException("Feedback not found with ID: " + feedbackId)); + + if (feedback.getStatus() != FeedbackStatus.PENDING_REVIEW) { + log.warn("Feedback {} cannot be approved because its status is {} instead of PENDING_REVIEW.", feedbackId, feedback.getStatus()); + throw new IllegalStateException("Feedback is not in a PENDING_REVIEW state."); + } + + feedback.setStatus(FeedbackStatus.PENDING_ASSIGNMENT); + feedbackRepository.save(feedback); + log.info("Feedback {} approved successfully. Status changed to PENDING_ASSIGNMENT.", feedbackId); + } + + /** + * 拒绝反馈 + * + *

将指定ID的反馈状态从PENDING_REVIEW更新为CLOSED_INVALID, + * 表示该反馈未通过主管审核,将被标记为无效并关闭。

+ * + * @param feedbackId 需要拒绝的反馈ID + * @param request 拒绝反馈的请求 + * @throws IllegalArgumentException 如果找不到指定ID的反馈 + * @throws IllegalStateException 如果反馈状态不是PENDING_REVIEW + */ + @Override + @Transactional + public void rejectFeedback(Long feedbackId, RejectFeedbackRequest request) { + log.info("Attempting to reject feedback with ID: {}. Reason: {}", feedbackId, request.getNotes()); + Feedback feedback = feedbackRepository.findById(feedbackId) + .orElseThrow(() -> new IllegalArgumentException("Feedback not found with ID: " + feedbackId)); + + if (feedback.getStatus() != FeedbackStatus.PENDING_REVIEW) { + log.warn("Feedback {} cannot be rejected because its status is {} instead of PENDING_REVIEW.", feedbackId, feedback.getStatus()); + throw new IllegalStateException("Feedback is not in a PENDING_REVIEW state."); + } + + feedback.setStatus(FeedbackStatus.CLOSED_INVALID); + // Although we don't have a dedicated field for rejection notes, + // we can log it for audit purposes. A future enhancement could be to add a field. + log.info("Feedback {} rejected successfully by user. Reason: {}", feedbackId, request.getNotes()); + feedbackRepository.save(feedback); + log.info("Feedback {} status changed to CLOSED_INVALID.", feedbackId); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/TaskAssignmentServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/TaskAssignmentServiceImpl.java index 891ff32..ca4b222 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/TaskAssignmentServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/TaskAssignmentServiceImpl.java @@ -1,174 +1,174 @@ -package com.dne.ems.service.impl; - -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.dne.ems.model.Assignment; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.Task; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.AssignmentStatus; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.TaskStatus; -import com.dne.ems.repository.AssignmentRepository; -import com.dne.ems.repository.FeedbackRepository; -import com.dne.ems.repository.TaskRepository; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.service.OperationLogService; -import com.dne.ems.service.TaskAssignmentService; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * 任务分配服务实现类 - * - *

该服务负责处理环境问题反馈的任务分配流程, - * 包括获取待分配的反馈列表、查询可用的网格员, - * 以及将反馈分配给网格员形成具体任务。

- * - * @version 1.0 - * @since 2025-06 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class TaskAssignmentServiceImpl implements TaskAssignmentService { - - private final FeedbackRepository feedbackRepository; - private final UserAccountRepository userAccountRepository; - private final AssignmentRepository assignmentRepository; - private final TaskRepository taskRepository; - private final OperationLogService operationLogService; - - /** - * 获取所有未分配的反馈 - * - *

查询状态为PENDING_ASSIGNMENT的所有反馈记录,这些反馈已通过主管审核, - * 等待分配给网格员处理。

- * - * @return 待分配的反馈列表 - */ - @Override - public List getUnassignedFeedback() { - // Unassigned feedback are those that have been approved and are awaiting assignment. - return feedbackRepository.findByStatus(FeedbackStatus.PENDING_ASSIGNMENT); - } - - /** - * 获取所有可用的网格员 - * - *

查询系统中所有角色为GRID_WORKER的用户账号,作为任务分配的候选人。

- * - * @return 可用网格员列表 - * @throws RuntimeException 如果数据库查询过程中发生异常 - */ - @Override - public List getAvailableGridWorkers() { - log.info("正在查询角色为 GRID_WORKER 的用户"); - try { - List workers = userAccountRepository.findByRole(Role.GRID_WORKER); - log.info("数据库查询成功,返回 {} 名网格员", workers.size()); - return workers; - } catch (Exception e) { - log.error("从数据库查询网格员时发生异常", e); - throw e; // 重新抛出异常,由Controller层处理 - } - } - - /** - * 分配任务给网格员 - * - *

将指定的反馈分配给指定的网格员处理,创建新的任务和分配记录, - * 并更新反馈状态为已分配。整个过程在一个事务中完成,确保数据一致性。

- * - * @param feedbackId 反馈ID - * @param assigneeId 被分配的网格员ID - * @param assignerId 执行分配操作的管理员ID - * @return 新创建的分配记录 - * @throws IllegalArgumentException 如果找不到指定ID的反馈、网格员或管理员 - * @throws IllegalStateException 如果反馈状态不允许分配 - */ - @Override - @Transactional - public Assignment assignTask(Long feedbackId, Long assigneeId, Long assignerId) { - log.info("开始分配任务:feedbackId={}, assigneeId={}, assignerId={}", feedbackId, assigneeId, assignerId); - - // 1. 验证反馈是否存在且状态可分配 - Feedback feedback = feedbackRepository.findById(feedbackId) - .orElseThrow(() -> { - log.error("分配任务失败:找不到ID为 {} 的反馈", feedbackId); - return new IllegalArgumentException("Feedback not found with ID: " + feedbackId); - }); - - if (feedback.getStatus() != FeedbackStatus.CONFIRMED && feedback.getStatus() != FeedbackStatus.PENDING_ASSIGNMENT) { - log.error("分配任务失败:反馈ID {} 的状态为 {},无法分配", feedbackId, feedback.getStatus()); - throw new IllegalStateException("Feedback with ID " + feedbackId + " is not in a CONFIRMED or PENDING_ASSIGNMENT state and cannot be assigned."); - } - - // 2. 验证分配对象是否为有效的网格员 - UserAccount assignee = userAccountRepository.findById(assigneeId) - .orElseThrow(() -> { - log.error("分配任务失败:找不到ID为 {} 的用户", assigneeId); - return new IllegalArgumentException("Assignee user not found with ID: " + assigneeId); - }); - - if (assignee.getRole() != Role.GRID_WORKER) { - log.error("分配任务失败:用户ID {} 的角色为 {},不是网格员", assigneeId, assignee.getRole()); - throw new IllegalArgumentException("User with ID " + assigneeId + " is not a grid worker."); - } - - // 3. 验证分派人是否存在 - UserAccount assigner = userAccountRepository.findById(assignerId) - .orElseThrow(() -> { - log.error("分配任务失败:找不到ID为 {} 的分派人", assignerId); - return new IllegalArgumentException("Assigner user not found with ID: " + assignerId); - }); - - // 4. 从反馈创建任务 - Task newTask = new Task(); - newTask.setFeedback(feedback); - newTask.setAssignee(assignee); - newTask.setCreatedBy(assigner); - newTask.setStatus(TaskStatus.ASSIGNED); - newTask.setAssignedAt(java.time.LocalDateTime.now()); - newTask.setTitle(feedback.getTitle()); - newTask.setDescription(feedback.getDescription()); - newTask.setPollutionType(feedback.getPollutionType()); - newTask.setSeverityLevel(feedback.getSeverityLevel()); - newTask.setLatitude(feedback.getLatitude()); - newTask.setLongitude(feedback.getLongitude()); - newTask.setTextAddress(feedback.getTextAddress()); - newTask.setGridX(feedback.getGridX()); - newTask.setGridY(feedback.getGridY()); - - Task savedTask = taskRepository.save(newTask); - log.info("新任务已创建并保存,ID:{}", savedTask.getId()); - - // 5. 创建并保存新的分配记录 - Assignment newAssignment = new Assignment(); - newAssignment.setTask(savedTask); - newAssignment.setAssigner(assigner); - newAssignment.setStatus(AssignmentStatus.PENDING); - Assignment savedAssignment = assignmentRepository.save(newAssignment); - log.info("新分配记录已创建并保存,ID:{}", savedAssignment.getId()); - - // 6. 更新反馈状态为"已分配" - feedback.setStatus(FeedbackStatus.ASSIGNED); - feedbackRepository.save(feedback); - log.info("反馈ID {} 的状态已更新为 ASSIGNED", feedback.getId()); - - // 记录操作日志 - operationLogService.recordOperation( - com.dne.ems.model.enums.OperationType.ASSIGN_TASK, - "任务(ID: " + savedTask.getId() + ") 已分配给网格员 " + assignee.getName() + " (ID: " + assignee.getId() + ")", - savedTask.getId().toString(), - "Task" - ); - - return savedAssignment; - } +package com.dne.ems.service.impl; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dne.ems.model.Assignment; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.Task; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.AssignmentStatus; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.TaskStatus; +import com.dne.ems.repository.AssignmentRepository; +import com.dne.ems.repository.FeedbackRepository; +import com.dne.ems.repository.TaskRepository; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.service.OperationLogService; +import com.dne.ems.service.TaskAssignmentService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 任务分配服务实现类 + * + *

该服务负责处理环境问题反馈的任务分配流程, + * 包括获取待分配的反馈列表、查询可用的网格员, + * 以及将反馈分配给网格员形成具体任务。

+ * + * @version 1.0 + * @since 2025-06 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class TaskAssignmentServiceImpl implements TaskAssignmentService { + + private final FeedbackRepository feedbackRepository; + private final UserAccountRepository userAccountRepository; + private final AssignmentRepository assignmentRepository; + private final TaskRepository taskRepository; + private final OperationLogService operationLogService; + + /** + * 获取所有未分配的反馈 + * + *

查询状态为PENDING_ASSIGNMENT的所有反馈记录,这些反馈已通过主管审核, + * 等待分配给网格员处理。

+ * + * @return 待分配的反馈列表 + */ + @Override + public List getUnassignedFeedback() { + // Unassigned feedback are those that have been approved and are awaiting assignment. + return feedbackRepository.findByStatus(FeedbackStatus.PENDING_ASSIGNMENT); + } + + /** + * 获取所有可用的网格员 + * + *

查询系统中所有角色为GRID_WORKER的用户账号,作为任务分配的候选人。

+ * + * @return 可用网格员列表 + * @throws RuntimeException 如果数据库查询过程中发生异常 + */ + @Override + public List getAvailableGridWorkers() { + log.info("正在查询角色为 GRID_WORKER 的用户"); + try { + List workers = userAccountRepository.findByRole(Role.GRID_WORKER); + log.info("数据库查询成功,返回 {} 名网格员", workers.size()); + return workers; + } catch (Exception e) { + log.error("从数据库查询网格员时发生异常", e); + throw e; // 重新抛出异常,由Controller层处理 + } + } + + /** + * 分配任务给网格员 + * + *

将指定的反馈分配给指定的网格员处理,创建新的任务和分配记录, + * 并更新反馈状态为已分配。整个过程在一个事务中完成,确保数据一致性。

+ * + * @param feedbackId 反馈ID + * @param assigneeId 被分配的网格员ID + * @param assignerId 执行分配操作的管理员ID + * @return 新创建的分配记录 + * @throws IllegalArgumentException 如果找不到指定ID的反馈、网格员或管理员 + * @throws IllegalStateException 如果反馈状态不允许分配 + */ + @Override + @Transactional + public Assignment assignTask(Long feedbackId, Long assigneeId, Long assignerId) { + log.info("开始分配任务:feedbackId={}, assigneeId={}, assignerId={}", feedbackId, assigneeId, assignerId); + + // 1. 验证反馈是否存在且状态可分配 + Feedback feedback = feedbackRepository.findById(feedbackId) + .orElseThrow(() -> { + log.error("分配任务失败:找不到ID为 {} 的反馈", feedbackId); + return new IllegalArgumentException("Feedback not found with ID: " + feedbackId); + }); + + if (feedback.getStatus() != FeedbackStatus.CONFIRMED && feedback.getStatus() != FeedbackStatus.PENDING_ASSIGNMENT) { + log.error("分配任务失败:反馈ID {} 的状态为 {},无法分配", feedbackId, feedback.getStatus()); + throw new IllegalStateException("Feedback with ID " + feedbackId + " is not in a CONFIRMED or PENDING_ASSIGNMENT state and cannot be assigned."); + } + + // 2. 验证分配对象是否为有效的网格员 + UserAccount assignee = userAccountRepository.findById(assigneeId) + .orElseThrow(() -> { + log.error("分配任务失败:找不到ID为 {} 的用户", assigneeId); + return new IllegalArgumentException("Assignee user not found with ID: " + assigneeId); + }); + + if (assignee.getRole() != Role.GRID_WORKER) { + log.error("分配任务失败:用户ID {} 的角色为 {},不是网格员", assigneeId, assignee.getRole()); + throw new IllegalArgumentException("User with ID " + assigneeId + " is not a grid worker."); + } + + // 3. 验证分派人是否存在 + UserAccount assigner = userAccountRepository.findById(assignerId) + .orElseThrow(() -> { + log.error("分配任务失败:找不到ID为 {} 的分派人", assignerId); + return new IllegalArgumentException("Assigner user not found with ID: " + assignerId); + }); + + // 4. 从反馈创建任务 + Task newTask = new Task(); + newTask.setFeedback(feedback); + newTask.setAssignee(assignee); + newTask.setCreatedBy(assigner); + newTask.setStatus(TaskStatus.ASSIGNED); + newTask.setAssignedAt(java.time.LocalDateTime.now()); + newTask.setTitle(feedback.getTitle()); + newTask.setDescription(feedback.getDescription()); + newTask.setPollutionType(feedback.getPollutionType()); + newTask.setSeverityLevel(feedback.getSeverityLevel()); + newTask.setLatitude(feedback.getLatitude()); + newTask.setLongitude(feedback.getLongitude()); + newTask.setTextAddress(feedback.getTextAddress()); + newTask.setGridX(feedback.getGridX()); + newTask.setGridY(feedback.getGridY()); + + Task savedTask = taskRepository.save(newTask); + log.info("新任务已创建并保存,ID:{}", savedTask.getId()); + + // 5. 创建并保存新的分配记录 + Assignment newAssignment = new Assignment(); + newAssignment.setTask(savedTask); + newAssignment.setAssigner(assigner); + newAssignment.setStatus(AssignmentStatus.PENDING); + Assignment savedAssignment = assignmentRepository.save(newAssignment); + log.info("新分配记录已创建并保存,ID:{}", savedAssignment.getId()); + + // 6. 更新反馈状态为"已分配" + feedback.setStatus(FeedbackStatus.ASSIGNED); + feedbackRepository.save(feedback); + log.info("反馈ID {} 的状态已更新为 ASSIGNED", feedback.getId()); + + // 记录操作日志 + operationLogService.recordOperation( + com.dne.ems.model.enums.OperationType.ASSIGN_TASK, + "任务(ID: " + savedTask.getId() + ") 已分配给网格员 " + assignee.getName() + " (ID: " + assignee.getId() + ")", + savedTask.getId().toString(), + "Task" + ); + + return savedAssignment; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/TaskManagementServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/TaskManagementServiceImpl.java index 50297ae..71f55a4 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/TaskManagementServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/TaskManagementServiceImpl.java @@ -1,467 +1,467 @@ -package com.dne.ems.service.impl; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.scheduling.annotation.Async; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.event.TransactionPhase; -import org.springframework.transaction.event.TransactionalEventListener; - -import com.dne.ems.dto.AttachmentDTO; -import com.dne.ems.dto.TaskApprovalRequest; -import com.dne.ems.dto.TaskAssignmentRequest; -import com.dne.ems.dto.TaskCreationRequest; -import com.dne.ems.dto.TaskDetailDTO; -import com.dne.ems.dto.TaskFromFeedbackRequest; -import com.dne.ems.dto.TaskHistoryDTO; -import com.dne.ems.dto.TaskSummaryDTO; -import com.dne.ems.event.TaskReadyForAssignmentEvent; -import com.dne.ems.exception.InvalidOperationException; -import com.dne.ems.exception.ResourceNotFoundException; -import com.dne.ems.model.Attachment; -import com.dne.ems.model.Feedback; -import com.dne.ems.model.Task; -import com.dne.ems.model.TaskHistory; -import com.dne.ems.model.TaskSubmission; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.FeedbackStatus; -import com.dne.ems.model.enums.PollutionType; -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.SeverityLevel; -import com.dne.ems.model.enums.TaskStatus; -import com.dne.ems.repository.AttachmentRepository; -import com.dne.ems.repository.FeedbackRepository; -import com.dne.ems.repository.TaskHistoryRepository; -import com.dne.ems.repository.TaskRepository; -import com.dne.ems.repository.TaskSubmissionRepository; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.security.CustomUserDetails; -import com.dne.ems.service.OperationLogService; -import com.dne.ems.service.TaskManagementService; -import com.dne.ems.service.pathfinding.AStarService; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; - -@Service -@RequiredArgsConstructor -@Slf4j -public class TaskManagementServiceImpl implements TaskManagementService { - - private final TaskRepository taskRepository; - private final UserAccountRepository userAccountRepository; - private final TaskHistoryRepository taskHistoryRepository; - private final ApplicationEventPublisher eventPublisher; - private final FeedbackRepository feedbackRepository; - private final TaskSubmissionRepository taskSubmissionRepository; - private final AttachmentRepository attachmentRepository; - private final OperationLogService operationLogService; - @SuppressWarnings("unused") - private final AStarService aStarService; - - @Value("${app.base.url}") - private String appBaseUrl; - - @Override - public Page getTasks( - TaskStatus status, Long assigneeId, SeverityLevel severity, - PollutionType pollutionType, LocalDate startDate, LocalDate endDate, Pageable pageable - ) { - List allTasks = taskRepository.findAll(); - - List filteredTasks = allTasks.stream() - .filter(task -> status == null || task.getStatus() == status) - .filter(task -> assigneeId == null || (task.getAssignee() != null && task.getAssignee().getId().equals(assigneeId))) - .filter(task -> severity == null || (task.getSeverityLevel() == severity || (task.getFeedback() != null && task.getFeedback().getSeverityLevel() == severity))) - .filter(task -> pollutionType == null || (task.getPollutionType() == pollutionType || (task.getFeedback() != null && task.getFeedback().getPollutionType() == pollutionType))) - .filter(task -> startDate == null || !task.getCreatedAt().toLocalDate().isBefore(startDate)) - .filter(task -> endDate == null || !task.getCreatedAt().toLocalDate().isAfter(endDate)) - .collect(Collectors.toList()); - - // Manual sorting - if (pageable.getSort().isSorted()) { - // This is a simplified sort implementation. A full implementation would parse the Sort object. - // For now, we just support the default createdAt descending. - if (pageable.getSort().getOrderFor("createdAt") != null && pageable.getSort().getOrderFor("createdAt").isDescending()) { - filteredTasks.sort((t1, t2) -> t2.getCreatedAt().compareTo(t1.getCreatedAt())); - } - } else { - // Default sort - filteredTasks.sort((t1, t2) -> t2.getCreatedAt().compareTo(t1.getCreatedAt())); - } - - // Manual pagination - int start = (int) pageable.getOffset(); - int end = Math.min((start + pageable.getPageSize()), filteredTasks.size()); - - List pageContent = (start > filteredTasks.size()) ? List.of() : filteredTasks.subList(start, end); - - Page taskPage = new PageImpl<>(pageContent, pageable, filteredTasks.size()); - return taskPage.map(this::convertToSummaryDto); - } - - @Override - public TaskSummaryDTO assignTask(Long taskId, TaskAssignmentRequest request) { - Task task = taskRepository.findById(taskId).orElseThrow(() -> new ResourceNotFoundException("Task not found")); - if (task.getStatus() != TaskStatus.PENDING_ASSIGNMENT && task.getStatus() != TaskStatus.ASSIGNED) { - throw new InvalidOperationException("Task must be pending or already assigned."); - } - UserAccount assignee = userAccountRepository.findById(request.assigneeId()).orElseThrow(() -> new ResourceNotFoundException("Assignee not found")); - if (assignee.getRole() != Role.GRID_WORKER) { - throw new InvalidOperationException("Cannot assign tasks to non-grid workers."); - } - TaskStatus oldStatus = task.getStatus(); - task.setAssignee(assignee); - task.setStatus(TaskStatus.ASSIGNED); - task.setAssignedAt(LocalDateTime.now()); - Task updatedTask = taskRepository.save(task); - logTaskHistory(updatedTask, oldStatus, updatedTask.getStatus(), "Task assigned to " + assignee.getName()); - - operationLogService.recordOperation( - com.dne.ems.model.enums.OperationType.ASSIGN_TASK, - "将任务 (ID: " + taskId + ") 分配给 " + assignee.getName(), - taskId.toString(), - "Task" - ); - - return convertToSummaryDto(updatedTask); - } - - @Override - public TaskDetailDTO getTaskDetails(Long taskId) { - return taskRepository.findById(taskId).map(this::convertToDetailDto).orElseThrow(() -> new ResourceNotFoundException("Task not found")); - } - - @Override - public TaskDetailDTO reviewTask(Long taskId, TaskApprovalRequest request) { - Task task = taskRepository.findById(taskId).orElseThrow(() -> new ResourceNotFoundException("Task not found")); - if (task.getStatus() != TaskStatus.SUBMITTED) { - throw new InvalidOperationException("Task must be SUBMITTED to be reviewed."); - } - TaskStatus oldStatus = task.getStatus(); - task.setStatus(request.approved() ? TaskStatus.COMPLETED : TaskStatus.ASSIGNED); - if (request.approved()) { - task.setCompletedAt(LocalDateTime.now()); - Feedback feedback = task.getFeedback(); - if (feedback != null) { - log.info("Task {} approved. Updating associated feedback {} from {} to PROCESSED.", - task.getId(), feedback.getId(), feedback.getStatus()); - feedback.setStatus(FeedbackStatus.PROCESSED); - feedbackRepository.save(feedback); - } - } - Task updatedTask = taskRepository.save(task); - logTaskHistory(updatedTask, oldStatus, updatedTask.getStatus(), request.comments()); - return convertToDetailDto(updatedTask); - } - - @Override - public TaskDetailDTO cancelTask(Long taskId) { - Task task = taskRepository.findById(taskId).orElseThrow(() -> new ResourceNotFoundException("Task not found")); - if (task.getStatus() == TaskStatus.COMPLETED || task.getStatus() == TaskStatus.CANCELLED) { - throw new InvalidOperationException("Cannot cancel a task that is already completed or cancelled."); - } - TaskStatus oldStatus = task.getStatus(); - task.setStatus(TaskStatus.CANCELLED); - Task updatedTask = taskRepository.save(task); - logTaskHistory(updatedTask, oldStatus, updatedTask.getStatus(), "Task manually cancelled."); - return convertToDetailDto(updatedTask); - } - - @Override - public List getTaskHistory(Long taskId) { - if (!taskRepository.existsById(taskId)) throw new ResourceNotFoundException("Task not found"); - return taskHistoryRepository.findByTaskIdOrderByChangedAtDesc(taskId).stream().map(this::convertToHistoryDto).collect(Collectors.toList()); - } - - @Override - public TaskDetailDTO createTask(TaskCreationRequest request) { - Task task = new Task(); - task.setTitle(request.title()); - task.setDescription(request.description()); - task.setPollutionType(request.pollutionType()); - task.setSeverityLevel(request.severityLevel()); - task.setLatitude(request.location().latitude()); - task.setLongitude(request.location().longitude()); - task.setTextAddress(request.location().textAddress()); - task.setGridX(request.location().gridX()); - task.setGridY(request.location().gridY()); - task.setStatus(TaskStatus.PENDING_ASSIGNMENT); - task.setCreatedBy(getCurrentUser()); - task.setCreatedAt(LocalDateTime.now()); - Task createdTask = taskRepository.save(task); - logTaskHistory(createdTask, null, createdTask.getStatus(), "Task created manually."); - eventPublisher.publishEvent(new TaskReadyForAssignmentEvent(this, createdTask)); - return convertToDetailDto(createdTask); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 查询状态为PENDING_REVIEW的反馈
  2. - *
  3. 返回未处理的反馈列表
  4. - *
- */ - @Override - public List getPendingFeedback() { - return feedbackRepository.findByStatus(FeedbackStatus.PENDING_REVIEW); - } - - @Override - public TaskDetailDTO createTaskFromFeedback(Long feedbackId, TaskFromFeedbackRequest request) { - Feedback feedback = feedbackRepository.findById(feedbackId).orElseThrow(() -> new ResourceNotFoundException("Feedback not found")); - if (feedback.getStatus() != FeedbackStatus.PENDING_REVIEW) { - throw new InvalidOperationException("Feedback must be in PENDING_REVIEW state."); - } - Task task = new Task(); - task.setTitle(request.getTitle()); - task.setDescription(feedback.getDescription()); - task.setPollutionType(feedback.getPollutionType()); - task.setSeverityLevel(request.getSeverityLevel()); - task.setLatitude(feedback.getLatitude()); - task.setLongitude(feedback.getLongitude()); - task.setTextAddress(feedback.getTextAddress()); - task.setGridX(feedback.getGridX()); - task.setGridY(feedback.getGridY()); - task.setStatus(TaskStatus.PENDING_ASSIGNMENT); - task.setFeedback(feedback); - task.setCreatedBy(getCurrentUser()); - task.setCreatedAt(LocalDateTime.now()); - Task createdTask = taskRepository.save(task); - feedback.setStatus(FeedbackStatus.PROCESSED); - feedbackRepository.save(feedback); - logTaskHistory(createdTask, null, createdTask.getStatus(), "Task created from feedback ID: " + feedbackId); - eventPublisher.publishEvent(new TaskReadyForAssignmentEvent(this, createdTask)); - return convertToDetailDto(createdTask); - } - - /** - * {@inheritDoc} - * - *

实现细节: - *

    - *
  1. 异步处理任务分配事件
  2. - *
  3. 触发智能任务分配流程
  4. - *
- * - *

事务处理: - *

    - *
  • 在事务提交后执行
  • - *
  • 确保任务数据已持久化
  • - *
- */ - @Async - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - @Override - public void handleTaskReadyForAssignment(TaskReadyForAssignmentEvent event) { - log.info("Received task ready for assignment event for task id: {}", event.getTask().getId()); - intelligentAssignTask(event.getTask()); - } - - private void intelligentAssignTask(Task task) { - // Dummy implementation for now - log.warn("Intelligent task assignment not implemented. Task {} needs manual assignment.", task.getId()); - } - - private UserAccount getCurrentUser() { - Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - if (principal instanceof CustomUserDetails customUserDetails) { - return customUserDetails.getUserAccount(); - } - // This should not happen in a secured context - return null; - } - - private void logTaskHistory(Task task, TaskStatus oldStatus, TaskStatus newStatus, String comments) { - TaskHistory history = TaskHistory.builder() - .task(task) - .oldStatus(oldStatus) - .newStatus(newStatus) - .comments(comments) - .changedAt(LocalDateTime.now()) - .changedBy(getCurrentUser()) - .build(); - taskHistoryRepository.save(history); - } - - private TaskSummaryDTO convertToSummaryDto(Task task) { - Feedback feedback = task.getFeedback(); - SeverityLevel severity = feedback != null ? feedback.getSeverityLevel() : task.getSeverityLevel(); - String imageUrl = ""; - // Note: This is a simplified representation. A real implementation might need to - // query attachments associated with the feedback. For now, we assume no image - // or a placeholder logic is sufficient for the summary. - if (feedback != null) { - List attachments = attachmentRepository.findByFeedback(feedback); - if (attachments != null && !attachments.isEmpty()) { - imageUrl = appBaseUrl + "/api/files/view/" + attachments.get(0).getStoredFileName(); - } - } - - return new TaskSummaryDTO( - task.getId(), - task.getTitle(), - task.getDescription(), - task.getStatus(), - task.getAssignee() != null ? task.getAssignee().getName() : "N/A", - task.getCreatedAt(), - task.getTextAddress(), - imageUrl, - severity - ); - } - - private TaskDetailDTO convertToDetailDto(Task task) { - @SuppressWarnings("unused") - UserAccount creator = task.getCreatedBy(); - UserAccount assignee = task.getAssignee(); - Feedback feedback = task.getFeedback(); - - // Assignee DTO - TaskDetailDTO.AssigneeDTO assigneeDto = null; - if (assignee != null) { - assigneeDto = new TaskDetailDTO.AssigneeDTO(assignee.getId(), assignee.getName(), assignee.getPhone()); - } - - // Submission DTO - List submissions = taskSubmissionRepository.findByTaskId(task.getId()); - List submissionInfo = submissions.stream() - .map(s -> { - List submissionAttachments = attachmentRepository.findByTaskSubmission(s); - List attachmentDTOs = submissionAttachments.stream() - .map(a -> new AttachmentDTO( - a.getId(), - a.getFileName(), - a.getFileType(), - appBaseUrl + "/api/files/view/" + a.getStoredFileName(), - a.getUploadDate() - )) - .collect(Collectors.toList()); - return new TaskDetailDTO.SubmissionInfoDTO(s.getNotes(), attachmentDTOs); - }) - .collect(Collectors.toList()); - - // Feedback DTO - TaskDetailDTO.FeedbackDTO feedbackDto = null; - if (feedback != null) { - List feedbackImageUrls = attachmentRepository.findByFeedback(feedback).stream() - .map(a -> appBaseUrl + "/api/files/view/" + a.getStoredFileName()) - .collect(Collectors.toList()); - - feedbackDto = new TaskDetailDTO.FeedbackDTO( - feedback.getId(), - feedback.getEventId(), - feedback.getTitle(), - feedback.getDescription(), - feedback.getSeverityLevel(), - feedback.getTextAddress(), - feedback.getSubmitterId(), - null, // Submitter name can be fetched if needed - feedback.getCreatedAt(), - feedbackImageUrls - ); - } - - List historyDtos = taskHistoryRepository.findByTaskIdOrderByChangedAtDesc(task.getId()) - .stream() - .map(this::convertToHistoryDto) - .collect(Collectors.toList()); - - return new TaskDetailDTO( - task.getId(), - feedbackDto, - task.getTitle(), - task.getDescription(), - task.getSeverityLevel(), - task.getPollutionType(), - task.getTextAddress(), - task.getGridX(), - task.getGridY(), - task.getStatus(), - assigneeDto, - task.getAssignedAt(), - task.getCompletedAt(), - historyDtos, - submissionInfo.isEmpty() ? null : submissionInfo.get(0) // Assuming one submission for detail view - ); - } - - private TaskHistoryDTO convertToHistoryDto(TaskHistory history) { - UserAccount changedBy = history.getChangedBy(); - String changedByName = (changedBy != null) ? changedBy.getName() : "System"; - TaskHistoryDTO.UserDTO userDto = (changedBy != null) ? new TaskHistoryDTO.UserDTO(changedBy.getId(), changedByName) : new TaskHistoryDTO.UserDTO(null, changedByName); - - return new TaskHistoryDTO( - history.getId(), - history.getOldStatus(), - history.getNewStatus(), - history.getComments(), - history.getChangedAt(), - userDto - ); - } - - @Override - public TaskDetailDTO approveTask(Long taskId) { - Task task = taskRepository.findById(taskId) - .orElseThrow(() -> new ResourceNotFoundException("Task not found with id: " + taskId)); - - if (task.getStatus() != TaskStatus.SUBMITTED) { - throw new InvalidOperationException("Only submitted tasks can be approved."); - } - - task.setStatus(TaskStatus.COMPLETED); - task.setCompletedAt(LocalDateTime.now()); - - Task savedTask = taskRepository.save(task); - - logTaskHistory(savedTask, TaskStatus.SUBMITTED, TaskStatus.COMPLETED, "Task approved and completed."); - - operationLogService.recordOperation( - com.dne.ems.model.enums.OperationType.APPROVE_TASK, - "批准了任务 (ID: " + taskId + ")", - taskId.toString(), - "Task" - ); - - return convertToDetailDto(savedTask); - } - - @Override - public TaskDetailDTO rejectTask(Long taskId, String reason) { - Task task = taskRepository.findById(taskId) - .orElseThrow(() -> new ResourceNotFoundException("Task not found with id: " + taskId)); - - if (task.getStatus() != TaskStatus.SUBMITTED) { - throw new InvalidOperationException("Only submitted tasks can be rejected."); - } - - TaskStatus oldStatus = task.getStatus(); - - task.setStatus(TaskStatus.IN_PROGRESS); // or ASSIGNED, depends on business logic - Task savedTask = taskRepository.save(task); - - logTaskHistory(savedTask, oldStatus, task.getStatus(), "Task rejected. Reason: " + reason); - - operationLogService.recordOperation( - com.dne.ems.model.enums.OperationType.REJECT_TASK, - "拒绝了任务 (ID: " + taskId + "), 理由: " + reason, - taskId.toString(), - "Task" - ); - - return convertToDetailDto(savedTask); - } - +package com.dne.ems.service.impl; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import com.dne.ems.dto.AttachmentDTO; +import com.dne.ems.dto.TaskApprovalRequest; +import com.dne.ems.dto.TaskAssignmentRequest; +import com.dne.ems.dto.TaskCreationRequest; +import com.dne.ems.dto.TaskDetailDTO; +import com.dne.ems.dto.TaskFromFeedbackRequest; +import com.dne.ems.dto.TaskHistoryDTO; +import com.dne.ems.dto.TaskSummaryDTO; +import com.dne.ems.event.TaskReadyForAssignmentEvent; +import com.dne.ems.exception.InvalidOperationException; +import com.dne.ems.exception.ResourceNotFoundException; +import com.dne.ems.model.Attachment; +import com.dne.ems.model.Feedback; +import com.dne.ems.model.Task; +import com.dne.ems.model.TaskHistory; +import com.dne.ems.model.TaskSubmission; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.FeedbackStatus; +import com.dne.ems.model.enums.PollutionType; +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.SeverityLevel; +import com.dne.ems.model.enums.TaskStatus; +import com.dne.ems.repository.AttachmentRepository; +import com.dne.ems.repository.FeedbackRepository; +import com.dne.ems.repository.TaskHistoryRepository; +import com.dne.ems.repository.TaskRepository; +import com.dne.ems.repository.TaskSubmissionRepository; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.security.CustomUserDetails; +import com.dne.ems.service.OperationLogService; +import com.dne.ems.service.TaskManagementService; +import com.dne.ems.service.pathfinding.AStarService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TaskManagementServiceImpl implements TaskManagementService { + + private final TaskRepository taskRepository; + private final UserAccountRepository userAccountRepository; + private final TaskHistoryRepository taskHistoryRepository; + private final ApplicationEventPublisher eventPublisher; + private final FeedbackRepository feedbackRepository; + private final TaskSubmissionRepository taskSubmissionRepository; + private final AttachmentRepository attachmentRepository; + private final OperationLogService operationLogService; + @SuppressWarnings("unused") + private final AStarService aStarService; + + @Value("${app.base.url}") + private String appBaseUrl; + + @Override + public Page getTasks( + TaskStatus status, Long assigneeId, SeverityLevel severity, + PollutionType pollutionType, LocalDate startDate, LocalDate endDate, Pageable pageable + ) { + List allTasks = taskRepository.findAll(); + + List filteredTasks = allTasks.stream() + .filter(task -> status == null || task.getStatus() == status) + .filter(task -> assigneeId == null || (task.getAssignee() != null && task.getAssignee().getId().equals(assigneeId))) + .filter(task -> severity == null || (task.getSeverityLevel() == severity || (task.getFeedback() != null && task.getFeedback().getSeverityLevel() == severity))) + .filter(task -> pollutionType == null || (task.getPollutionType() == pollutionType || (task.getFeedback() != null && task.getFeedback().getPollutionType() == pollutionType))) + .filter(task -> startDate == null || !task.getCreatedAt().toLocalDate().isBefore(startDate)) + .filter(task -> endDate == null || !task.getCreatedAt().toLocalDate().isAfter(endDate)) + .collect(Collectors.toList()); + + // Manual sorting + if (pageable.getSort().isSorted()) { + // This is a simplified sort implementation. A full implementation would parse the Sort object. + // For now, we just support the default createdAt descending. + if (pageable.getSort().getOrderFor("createdAt") != null && pageable.getSort().getOrderFor("createdAt").isDescending()) { + filteredTasks.sort((t1, t2) -> t2.getCreatedAt().compareTo(t1.getCreatedAt())); + } + } else { + // Default sort + filteredTasks.sort((t1, t2) -> t2.getCreatedAt().compareTo(t1.getCreatedAt())); + } + + // Manual pagination + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), filteredTasks.size()); + + List pageContent = (start > filteredTasks.size()) ? List.of() : filteredTasks.subList(start, end); + + Page taskPage = new PageImpl<>(pageContent, pageable, filteredTasks.size()); + return taskPage.map(this::convertToSummaryDto); + } + + @Override + public TaskSummaryDTO assignTask(Long taskId, TaskAssignmentRequest request) { + Task task = taskRepository.findById(taskId).orElseThrow(() -> new ResourceNotFoundException("Task not found")); + if (task.getStatus() != TaskStatus.PENDING_ASSIGNMENT && task.getStatus() != TaskStatus.ASSIGNED) { + throw new InvalidOperationException("Task must be pending or already assigned."); + } + UserAccount assignee = userAccountRepository.findById(request.assigneeId()).orElseThrow(() -> new ResourceNotFoundException("Assignee not found")); + if (assignee.getRole() != Role.GRID_WORKER) { + throw new InvalidOperationException("Cannot assign tasks to non-grid workers."); + } + TaskStatus oldStatus = task.getStatus(); + task.setAssignee(assignee); + task.setStatus(TaskStatus.ASSIGNED); + task.setAssignedAt(LocalDateTime.now()); + Task updatedTask = taskRepository.save(task); + logTaskHistory(updatedTask, oldStatus, updatedTask.getStatus(), "Task assigned to " + assignee.getName()); + + operationLogService.recordOperation( + com.dne.ems.model.enums.OperationType.ASSIGN_TASK, + "将任务 (ID: " + taskId + ") 分配给 " + assignee.getName(), + taskId.toString(), + "Task" + ); + + return convertToSummaryDto(updatedTask); + } + + @Override + public TaskDetailDTO getTaskDetails(Long taskId) { + return taskRepository.findById(taskId).map(this::convertToDetailDto).orElseThrow(() -> new ResourceNotFoundException("Task not found")); + } + + @Override + public TaskDetailDTO reviewTask(Long taskId, TaskApprovalRequest request) { + Task task = taskRepository.findById(taskId).orElseThrow(() -> new ResourceNotFoundException("Task not found")); + if (task.getStatus() != TaskStatus.SUBMITTED) { + throw new InvalidOperationException("Task must be SUBMITTED to be reviewed."); + } + TaskStatus oldStatus = task.getStatus(); + task.setStatus(request.approved() ? TaskStatus.COMPLETED : TaskStatus.ASSIGNED); + if (request.approved()) { + task.setCompletedAt(LocalDateTime.now()); + Feedback feedback = task.getFeedback(); + if (feedback != null) { + log.info("Task {} approved. Updating associated feedback {} from {} to PROCESSED.", + task.getId(), feedback.getId(), feedback.getStatus()); + feedback.setStatus(FeedbackStatus.PROCESSED); + feedbackRepository.save(feedback); + } + } + Task updatedTask = taskRepository.save(task); + logTaskHistory(updatedTask, oldStatus, updatedTask.getStatus(), request.comments()); + return convertToDetailDto(updatedTask); + } + + @Override + public TaskDetailDTO cancelTask(Long taskId) { + Task task = taskRepository.findById(taskId).orElseThrow(() -> new ResourceNotFoundException("Task not found")); + if (task.getStatus() == TaskStatus.COMPLETED || task.getStatus() == TaskStatus.CANCELLED) { + throw new InvalidOperationException("Cannot cancel a task that is already completed or cancelled."); + } + TaskStatus oldStatus = task.getStatus(); + task.setStatus(TaskStatus.CANCELLED); + Task updatedTask = taskRepository.save(task); + logTaskHistory(updatedTask, oldStatus, updatedTask.getStatus(), "Task manually cancelled."); + return convertToDetailDto(updatedTask); + } + + @Override + public List getTaskHistory(Long taskId) { + if (!taskRepository.existsById(taskId)) throw new ResourceNotFoundException("Task not found"); + return taskHistoryRepository.findByTaskIdOrderByChangedAtDesc(taskId).stream().map(this::convertToHistoryDto).collect(Collectors.toList()); + } + + @Override + public TaskDetailDTO createTask(TaskCreationRequest request) { + Task task = new Task(); + task.setTitle(request.title()); + task.setDescription(request.description()); + task.setPollutionType(request.pollutionType()); + task.setSeverityLevel(request.severityLevel()); + task.setLatitude(request.location().latitude()); + task.setLongitude(request.location().longitude()); + task.setTextAddress(request.location().textAddress()); + task.setGridX(request.location().gridX()); + task.setGridY(request.location().gridY()); + task.setStatus(TaskStatus.PENDING_ASSIGNMENT); + task.setCreatedBy(getCurrentUser()); + task.setCreatedAt(LocalDateTime.now()); + Task createdTask = taskRepository.save(task); + logTaskHistory(createdTask, null, createdTask.getStatus(), "Task created manually."); + eventPublisher.publishEvent(new TaskReadyForAssignmentEvent(this, createdTask)); + return convertToDetailDto(createdTask); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 查询状态为PENDING_REVIEW的反馈
  2. + *
  3. 返回未处理的反馈列表
  4. + *
+ */ + @Override + public List getPendingFeedback() { + return feedbackRepository.findByStatus(FeedbackStatus.PENDING_REVIEW); + } + + @Override + public TaskDetailDTO createTaskFromFeedback(Long feedbackId, TaskFromFeedbackRequest request) { + Feedback feedback = feedbackRepository.findById(feedbackId).orElseThrow(() -> new ResourceNotFoundException("Feedback not found")); + if (feedback.getStatus() != FeedbackStatus.PENDING_REVIEW) { + throw new InvalidOperationException("Feedback must be in PENDING_REVIEW state."); + } + Task task = new Task(); + task.setTitle(request.getTitle()); + task.setDescription(feedback.getDescription()); + task.setPollutionType(feedback.getPollutionType()); + task.setSeverityLevel(request.getSeverityLevel()); + task.setLatitude(feedback.getLatitude()); + task.setLongitude(feedback.getLongitude()); + task.setTextAddress(feedback.getTextAddress()); + task.setGridX(feedback.getGridX()); + task.setGridY(feedback.getGridY()); + task.setStatus(TaskStatus.PENDING_ASSIGNMENT); + task.setFeedback(feedback); + task.setCreatedBy(getCurrentUser()); + task.setCreatedAt(LocalDateTime.now()); + Task createdTask = taskRepository.save(task); + feedback.setStatus(FeedbackStatus.PROCESSED); + feedbackRepository.save(feedback); + logTaskHistory(createdTask, null, createdTask.getStatus(), "Task created from feedback ID: " + feedbackId); + eventPublisher.publishEvent(new TaskReadyForAssignmentEvent(this, createdTask)); + return convertToDetailDto(createdTask); + } + + /** + * {@inheritDoc} + * + *

实现细节: + *

    + *
  1. 异步处理任务分配事件
  2. + *
  3. 触发智能任务分配流程
  4. + *
+ * + *

事务处理: + *

    + *
  • 在事务提交后执行
  • + *
  • 确保任务数据已持久化
  • + *
+ */ + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Override + public void handleTaskReadyForAssignment(TaskReadyForAssignmentEvent event) { + log.info("Received task ready for assignment event for task id: {}", event.getTask().getId()); + intelligentAssignTask(event.getTask()); + } + + private void intelligentAssignTask(Task task) { + // Dummy implementation for now + log.warn("Intelligent task assignment not implemented. Task {} needs manual assignment.", task.getId()); + } + + private UserAccount getCurrentUser() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (principal instanceof CustomUserDetails customUserDetails) { + return customUserDetails.getUserAccount(); + } + // This should not happen in a secured context + return null; + } + + private void logTaskHistory(Task task, TaskStatus oldStatus, TaskStatus newStatus, String comments) { + TaskHistory history = TaskHistory.builder() + .task(task) + .oldStatus(oldStatus) + .newStatus(newStatus) + .comments(comments) + .changedAt(LocalDateTime.now()) + .changedBy(getCurrentUser()) + .build(); + taskHistoryRepository.save(history); + } + + private TaskSummaryDTO convertToSummaryDto(Task task) { + Feedback feedback = task.getFeedback(); + SeverityLevel severity = feedback != null ? feedback.getSeverityLevel() : task.getSeverityLevel(); + String imageUrl = ""; + // Note: This is a simplified representation. A real implementation might need to + // query attachments associated with the feedback. For now, we assume no image + // or a placeholder logic is sufficient for the summary. + if (feedback != null) { + List attachments = attachmentRepository.findByFeedback(feedback); + if (attachments != null && !attachments.isEmpty()) { + imageUrl = appBaseUrl + "/api/files/view/" + attachments.get(0).getStoredFileName(); + } + } + + return new TaskSummaryDTO( + task.getId(), + task.getTitle(), + task.getDescription(), + task.getStatus(), + task.getAssignee() != null ? task.getAssignee().getName() : "N/A", + task.getCreatedAt(), + task.getTextAddress(), + imageUrl, + severity + ); + } + + private TaskDetailDTO convertToDetailDto(Task task) { + @SuppressWarnings("unused") + UserAccount creator = task.getCreatedBy(); + UserAccount assignee = task.getAssignee(); + Feedback feedback = task.getFeedback(); + + // Assignee DTO + TaskDetailDTO.AssigneeDTO assigneeDto = null; + if (assignee != null) { + assigneeDto = new TaskDetailDTO.AssigneeDTO(assignee.getId(), assignee.getName(), assignee.getPhone()); + } + + // Submission DTO + List submissions = taskSubmissionRepository.findByTaskId(task.getId()); + List submissionInfo = submissions.stream() + .map(s -> { + List submissionAttachments = attachmentRepository.findByTaskSubmission(s); + List attachmentDTOs = submissionAttachments.stream() + .map(a -> new AttachmentDTO( + a.getId(), + a.getFileName(), + a.getFileType(), + appBaseUrl + "/api/files/view/" + a.getStoredFileName(), + a.getUploadDate() + )) + .collect(Collectors.toList()); + return new TaskDetailDTO.SubmissionInfoDTO(s.getNotes(), attachmentDTOs); + }) + .collect(Collectors.toList()); + + // Feedback DTO + TaskDetailDTO.FeedbackDTO feedbackDto = null; + if (feedback != null) { + List feedbackImageUrls = attachmentRepository.findByFeedback(feedback).stream() + .map(a -> appBaseUrl + "/api/files/view/" + a.getStoredFileName()) + .collect(Collectors.toList()); + + feedbackDto = new TaskDetailDTO.FeedbackDTO( + feedback.getId(), + feedback.getEventId(), + feedback.getTitle(), + feedback.getDescription(), + feedback.getSeverityLevel(), + feedback.getTextAddress(), + feedback.getSubmitterId(), + null, // Submitter name can be fetched if needed + feedback.getCreatedAt(), + feedbackImageUrls + ); + } + + List historyDtos = taskHistoryRepository.findByTaskIdOrderByChangedAtDesc(task.getId()) + .stream() + .map(this::convertToHistoryDto) + .collect(Collectors.toList()); + + return new TaskDetailDTO( + task.getId(), + feedbackDto, + task.getTitle(), + task.getDescription(), + task.getSeverityLevel(), + task.getPollutionType(), + task.getTextAddress(), + task.getGridX(), + task.getGridY(), + task.getStatus(), + assigneeDto, + task.getAssignedAt(), + task.getCompletedAt(), + historyDtos, + submissionInfo.isEmpty() ? null : submissionInfo.get(0) // Assuming one submission for detail view + ); + } + + private TaskHistoryDTO convertToHistoryDto(TaskHistory history) { + UserAccount changedBy = history.getChangedBy(); + String changedByName = (changedBy != null) ? changedBy.getName() : "System"; + TaskHistoryDTO.UserDTO userDto = (changedBy != null) ? new TaskHistoryDTO.UserDTO(changedBy.getId(), changedByName) : new TaskHistoryDTO.UserDTO(null, changedByName); + + return new TaskHistoryDTO( + history.getId(), + history.getOldStatus(), + history.getNewStatus(), + history.getComments(), + history.getChangedAt(), + userDto + ); + } + + @Override + public TaskDetailDTO approveTask(Long taskId) { + Task task = taskRepository.findById(taskId) + .orElseThrow(() -> new ResourceNotFoundException("Task not found with id: " + taskId)); + + if (task.getStatus() != TaskStatus.SUBMITTED) { + throw new InvalidOperationException("Only submitted tasks can be approved."); + } + + task.setStatus(TaskStatus.COMPLETED); + task.setCompletedAt(LocalDateTime.now()); + + Task savedTask = taskRepository.save(task); + + logTaskHistory(savedTask, TaskStatus.SUBMITTED, TaskStatus.COMPLETED, "Task approved and completed."); + + operationLogService.recordOperation( + com.dne.ems.model.enums.OperationType.APPROVE_TASK, + "批准了任务 (ID: " + taskId + ")", + taskId.toString(), + "Task" + ); + + return convertToDetailDto(savedTask); + } + + @Override + public TaskDetailDTO rejectTask(Long taskId, String reason) { + Task task = taskRepository.findById(taskId) + .orElseThrow(() -> new ResourceNotFoundException("Task not found with id: " + taskId)); + + if (task.getStatus() != TaskStatus.SUBMITTED) { + throw new InvalidOperationException("Only submitted tasks can be rejected."); + } + + TaskStatus oldStatus = task.getStatus(); + + task.setStatus(TaskStatus.IN_PROGRESS); // or ASSIGNED, depends on business logic + Task savedTask = taskRepository.save(task); + + logTaskHistory(savedTask, oldStatus, task.getStatus(), "Task rejected. Reason: " + reason); + + operationLogService.recordOperation( + com.dne.ems.model.enums.OperationType.REJECT_TASK, + "拒绝了任务 (ID: " + taskId + "), 理由: " + reason, + taskId.toString(), + "Task" + ); + + return convertToDetailDto(savedTask); + } + } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/UserAccountServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/UserAccountServiceImpl.java index 8b75443..2e71d3b 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/UserAccountServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/UserAccountServiceImpl.java @@ -1,136 +1,136 @@ -package com.dne.ems.service.impl; - -import java.util.List; -import java.util.stream.Collectors; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.dne.ems.dto.UserRegistrationRequest; -import com.dne.ems.dto.UserRoleUpdateRequest; -import com.dne.ems.dto.UserSummaryDTO; -import com.dne.ems.exception.ResourceNotFoundException; -import com.dne.ems.exception.UserAlreadyExistsException; -import com.dne.ems.model.UserAccount; -import com.dne.ems.model.enums.Role; -import com.dne.ems.model.enums.UserStatus; -import com.dne.ems.repository.UserAccountRepository; -import com.dne.ems.service.UserAccountService; - -import lombok.RequiredArgsConstructor; - -/** - * Implementation of the {@link UserAccountService} interface. - * Handles the business logic for user account management. - */ -@Service -@RequiredArgsConstructor -public class UserAccountServiceImpl implements UserAccountService { - - private final UserAccountRepository userAccountRepository; - private final PasswordEncoder passwordEncoder; - - @Override - @Transactional - public UserAccount registerUser(UserRegistrationRequest request) { - // Check if a user with the given email or phone number already exists. - userAccountRepository.findByEmail(request.email()).ifPresent(user -> { - throw new UserAlreadyExistsException("邮箱 " + request.email() + " 已被注册。"); - }); - userAccountRepository.findByPhone(request.phone()).ifPresent(user -> { - throw new UserAlreadyExistsException("手机号 " + request.phone() + " 已被注册。"); - }); - - // Create a new UserAccount entity from the request DTO. - UserAccount newUser = new UserAccount(); - newUser.setName(request.name()); - newUser.setEmail(request.email()); - newUser.setPhone(request.phone()); - // Encode the password before saving. - newUser.setPassword(passwordEncoder.encode(request.password())); - // Set default values for new users. - newUser.setRole(Role.PUBLIC_SUPERVISOR); - newUser.setStatus(UserStatus.ACTIVE); - - // Save the new user to the database. - return userAccountRepository.save(newUser); - } - - @Override - @Transactional - public UserAccount updateUserRole(Long userId, UserRoleUpdateRequest request) { - UserAccount user = userAccountRepository.findById(userId) - .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); - - user.setRole(request.getRole()); - - // If the user is a grid worker, update their grid location. - if (request.getRole() == Role.GRID_WORKER) { - user.setGridX(request.getGridX()); - user.setGridY(request.getGridY()); - } - - return userAccountRepository.save(user); - } - - @Override - public UserAccount getUserById(Long userId) { - return userAccountRepository.findById(userId) - .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); - } - - @Override - public Page getUsersByRole(Role role, Pageable pageable) { - List users = userAccountRepository.findByRole(role); - - int start = (int) pageable.getOffset(); - int end = Math.min((start + pageable.getPageSize()), users.size()); - - List pageContent = (start > users.size()) ? List.of() : users.subList(start, end); - - return new PageImpl<>(pageContent, pageable, users.size()); - } - - @Override - public void updateUserLocation(Long userId, Double latitude, Double longitude) { - UserAccount user = getUserById(userId); - user.setCurrentLatitude(latitude); - user.setCurrentLongitude(longitude); - userAccountRepository.save(user); - } - - @Override - public Page getAllUsers(Role role, UserStatus status, Pageable pageable) { - List allUsers = userAccountRepository.findAll(); - - // In-memory filtering - List filteredUsers = allUsers.stream() - .filter(user -> role == null || user.getRole() == role) - .filter(user -> status == null || user.getStatus() == status) - .collect(Collectors.toList()); - - // Manual pagination - int start = (int) pageable.getOffset(); - int end = Math.min((start + pageable.getPageSize()), filteredUsers.size()); - - List pageContent = (start > filteredUsers.size()) ? List.of() : filteredUsers.subList(start, end); - - Page userPage = new PageImpl<>(pageContent, pageable, filteredUsers.size()); - return userPage.map(this::convertToSummaryDto); - } - - private UserSummaryDTO convertToSummaryDto(UserAccount user) { - return new UserSummaryDTO( - user.getId(), - user.getName(), - user.getEmail(), - user.getPhone(), - user.getRole(), - user.getStatus() - ); - } +package com.dne.ems.service.impl; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dne.ems.dto.UserRegistrationRequest; +import com.dne.ems.dto.UserRoleUpdateRequest; +import com.dne.ems.dto.UserSummaryDTO; +import com.dne.ems.exception.ResourceNotFoundException; +import com.dne.ems.exception.UserAlreadyExistsException; +import com.dne.ems.model.UserAccount; +import com.dne.ems.model.enums.Role; +import com.dne.ems.model.enums.UserStatus; +import com.dne.ems.repository.UserAccountRepository; +import com.dne.ems.service.UserAccountService; + +import lombok.RequiredArgsConstructor; + +/** + * Implementation of the {@link UserAccountService} interface. + * Handles the business logic for user account management. + */ +@Service +@RequiredArgsConstructor +public class UserAccountServiceImpl implements UserAccountService { + + private final UserAccountRepository userAccountRepository; + private final PasswordEncoder passwordEncoder; + + @Override + @Transactional + public UserAccount registerUser(UserRegistrationRequest request) { + // Check if a user with the given email or phone number already exists. + userAccountRepository.findByEmail(request.email()).ifPresent(user -> { + throw new UserAlreadyExistsException("邮箱 " + request.email() + " 已被注册。"); + }); + userAccountRepository.findByPhone(request.phone()).ifPresent(user -> { + throw new UserAlreadyExistsException("手机号 " + request.phone() + " 已被注册。"); + }); + + // Create a new UserAccount entity from the request DTO. + UserAccount newUser = new UserAccount(); + newUser.setName(request.name()); + newUser.setEmail(request.email()); + newUser.setPhone(request.phone()); + // Encode the password before saving. + newUser.setPassword(passwordEncoder.encode(request.password())); + // Set default values for new users. + newUser.setRole(Role.PUBLIC_SUPERVISOR); + newUser.setStatus(UserStatus.ACTIVE); + + // Save the new user to the database. + return userAccountRepository.save(newUser); + } + + @Override + @Transactional + public UserAccount updateUserRole(Long userId, UserRoleUpdateRequest request) { + UserAccount user = userAccountRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); + + user.setRole(request.getRole()); + + // If the user is a grid worker, update their grid location. + if (request.getRole() == Role.GRID_WORKER) { + user.setGridX(request.getGridX()); + user.setGridY(request.getGridY()); + } + + return userAccountRepository.save(user); + } + + @Override + public UserAccount getUserById(Long userId) { + return userAccountRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); + } + + @Override + public Page getUsersByRole(Role role, Pageable pageable) { + List users = userAccountRepository.findByRole(role); + + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), users.size()); + + List pageContent = (start > users.size()) ? List.of() : users.subList(start, end); + + return new PageImpl<>(pageContent, pageable, users.size()); + } + + @Override + public void updateUserLocation(Long userId, Double latitude, Double longitude) { + UserAccount user = getUserById(userId); + user.setCurrentLatitude(latitude); + user.setCurrentLongitude(longitude); + userAccountRepository.save(user); + } + + @Override + public Page getAllUsers(Role role, UserStatus status, Pageable pageable) { + List allUsers = userAccountRepository.findAll(); + + // In-memory filtering + List filteredUsers = allUsers.stream() + .filter(user -> role == null || user.getRole() == role) + .filter(user -> status == null || user.getStatus() == status) + .collect(Collectors.toList()); + + // Manual pagination + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), filteredUsers.size()); + + List pageContent = (start > filteredUsers.size()) ? List.of() : filteredUsers.subList(start, end); + + Page userPage = new PageImpl<>(pageContent, pageable, filteredUsers.size()); + return userPage.map(this::convertToSummaryDto); + } + + private UserSummaryDTO convertToSummaryDto(UserAccount user) { + return new UserSummaryDTO( + user.getId(), + user.getName(), + user.getEmail(), + user.getPhone(), + user.getRole(), + user.getStatus() + ); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/UserFeedbackServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/UserFeedbackServiceImpl.java index 49d1787..83781b3 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/UserFeedbackServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/UserFeedbackServiceImpl.java @@ -1,73 +1,73 @@ -package com.dne.ems.service.impl; - -import com.dne.ems.dto.UserFeedbackSummaryDTO; -import com.dne.ems.model.Feedback; -import com.dne.ems.repository.FeedbackRepository; -import com.dne.ems.service.UserFeedbackService; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; - -/** - * 用户反馈服务实现类 - * - *

该服务负责处理用户查询自己提交的反馈历史记录相关功能, - * 提供分页查询和数据转换功能,将反馈实体转换为前端所需的DTO对象。

- * - * @version 1.0 - * @since 2025-06 - */ -@Service -@RequiredArgsConstructor -public class UserFeedbackServiceImpl implements UserFeedbackService { - - private final FeedbackRepository feedbackRepository; - - /** - * 应用基础URL,用于构建附件访问地址 - */ - @Value("${app.base.url}") - private String appBaseUrl; - - /** - * 获取用户反馈历史记录 - * - *

根据用户ID查询该用户提交的所有反馈记录,并转换为前端所需的DTO格式, - * 支持分页查询。

- * - * @param userId 用户ID - * @param pageable 分页参数 - * @return 分页的用户反馈摘要DTO列表 - */ - @Override - public Page getFeedbackHistoryByUserId(Long userId, Pageable pageable) { - Page feedbackPage = feedbackRepository.findBySubmitterId(userId, pageable); - return feedbackPage.map(this::convertToDto); - } - - /** - * 将反馈实体转换为DTO对象 - * - *

提取反馈实体中的关键信息,构建前端所需的摘要DTO对象, - * 包括事件ID、标题、状态、创建时间和首张附件图片URL(如果有)。

- * - * @param feedback 反馈实体 - * @return 反馈摘要DTO - */ - private UserFeedbackSummaryDTO convertToDto(Feedback feedback) { - String imageUrl = null; - if (feedback.getAttachments() != null && !feedback.getAttachments().isEmpty()) { - imageUrl = appBaseUrl + "/api/files/view/" + feedback.getAttachments().get(0).getStoredFileName(); - } - - return new UserFeedbackSummaryDTO( - feedback.getEventId(), - feedback.getTitle(), - feedback.getStatus(), - feedback.getCreatedAt(), - imageUrl - ); - } +package com.dne.ems.service.impl; + +import com.dne.ems.dto.UserFeedbackSummaryDTO; +import com.dne.ems.model.Feedback; +import com.dne.ems.repository.FeedbackRepository; +import com.dne.ems.service.UserFeedbackService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +/** + * 用户反馈服务实现类 + * + *

该服务负责处理用户查询自己提交的反馈历史记录相关功能, + * 提供分页查询和数据转换功能,将反馈实体转换为前端所需的DTO对象。

+ * + * @version 1.0 + * @since 2025-06 + */ +@Service +@RequiredArgsConstructor +public class UserFeedbackServiceImpl implements UserFeedbackService { + + private final FeedbackRepository feedbackRepository; + + /** + * 应用基础URL,用于构建附件访问地址 + */ + @Value("${app.base.url}") + private String appBaseUrl; + + /** + * 获取用户反馈历史记录 + * + *

根据用户ID查询该用户提交的所有反馈记录,并转换为前端所需的DTO格式, + * 支持分页查询。

+ * + * @param userId 用户ID + * @param pageable 分页参数 + * @return 分页的用户反馈摘要DTO列表 + */ + @Override + public Page getFeedbackHistoryByUserId(Long userId, Pageable pageable) { + Page feedbackPage = feedbackRepository.findBySubmitterId(userId, pageable); + return feedbackPage.map(this::convertToDto); + } + + /** + * 将反馈实体转换为DTO对象 + * + *

提取反馈实体中的关键信息,构建前端所需的摘要DTO对象, + * 包括事件ID、标题、状态、创建时间和首张附件图片URL(如果有)。

+ * + * @param feedback 反馈实体 + * @return 反馈摘要DTO + */ + private UserFeedbackSummaryDTO convertToDto(Feedback feedback) { + String imageUrl = null; + if (feedback.getAttachments() != null && !feedback.getAttachments().isEmpty()) { + imageUrl = appBaseUrl + "/api/files/view/" + feedback.getAttachments().get(0).getStoredFileName(); + } + + return new UserFeedbackSummaryDTO( + feedback.getEventId(), + feedback.getTitle(), + feedback.getStatus(), + feedback.getCreatedAt(), + imageUrl + ); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/impl/VerificationCodeServiceImpl.java b/ems-backend/src/main/java/com/dne/ems/service/impl/VerificationCodeServiceImpl.java index b618464..8e3f32f 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/impl/VerificationCodeServiceImpl.java +++ b/ems-backend/src/main/java/com/dne/ems/service/impl/VerificationCodeServiceImpl.java @@ -1,96 +1,96 @@ -package com.dne.ems.service.impl; - -import java.security.SecureRandom; -import java.time.LocalDateTime; -import java.util.concurrent.TimeUnit; - -import org.springframework.stereotype.Service; - -import com.dne.ems.service.MailService; -import com.dne.ems.service.VerificationCodeService; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -/** - * 验证码服务实现类,负责生成、发送和验证验证码 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class VerificationCodeServiceImpl implements VerificationCodeService { - - // 使用Guava Cache存储验证码,5分钟后自动过期 - private final Cache verificationCodeCache = CacheBuilder.newBuilder() - .expireAfterWrite(5, TimeUnit.MINUTES) - .build(); - - // 使用Guava Cache存储最后一次发送时间,限制发送频率 - private final Cache lastSendTimeCache = CacheBuilder.newBuilder() - .expireAfterWrite(1, TimeUnit.MINUTES) - .build(); - - private final MailService mailService; - - @Override - public void sendVerificationCode(String email) { - // 检查发送频率限制 - LocalDateTime lastSendTime = lastSendTimeCache.getIfPresent(email); - if (lastSendTime != null) { - LocalDateTime now = LocalDateTime.now(); - // 60秒内只能发送一次验证码 - if (lastSendTime.plusSeconds(60).isAfter(now)) { - long secondsLeft = 60 - java.time.Duration.between(lastSendTime, now).getSeconds(); - log.warn("验证码请求过于频繁:{}, 剩余等待时间: {}秒", email, secondsLeft); - throw new IllegalStateException("请求过于频繁,请在" + secondsLeft + "秒后再试。"); - } - } - - // 生成6位随机验证码 - String code = generateCode(); - - // 存储验证码和发送时间 - verificationCodeCache.put(email, code); - lastSendTimeCache.put(email, LocalDateTime.now()); - - // 发送邮件 - mailService.sendVerificationCodeEmail(email, code); - log.info("验证码已发送到: {}", email); - } - - @Override - public boolean verifyCode(String email, String code) { - if (email == null || code == null) { - return false; - } - - // 从缓存中获取验证码 - String storedCode = verificationCodeCache.getIfPresent(email); - - if (storedCode == null) { - log.warn("验证码不存在或已过期: {}", email); - return false; - } - - // 验证成功后,从缓存中移除验证码,防止重复使用 - if (storedCode.equals(code)) { - verificationCodeCache.invalidate(email); - log.info("验证码验证成功: {}", email); - return true; - } - - log.warn("验证码不匹配: {}", email); - return false; - } - - /** - * 生成6位随机数字验证码 - */ - private String generateCode() { - SecureRandom random = new SecureRandom(); - int code = 100000 + random.nextInt(900000); // 生成100000-999999之间的随机数 - return String.valueOf(code); - } +package com.dne.ems.service.impl; + +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +import org.springframework.stereotype.Service; + +import com.dne.ems.service.MailService; +import com.dne.ems.service.VerificationCodeService; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 验证码服务实现类,负责生成、发送和验证验证码 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class VerificationCodeServiceImpl implements VerificationCodeService { + + // 使用Guava Cache存储验证码,5分钟后自动过期 + private final Cache verificationCodeCache = CacheBuilder.newBuilder() + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(); + + // 使用Guava Cache存储最后一次发送时间,限制发送频率 + private final Cache lastSendTimeCache = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.MINUTES) + .build(); + + private final MailService mailService; + + @Override + public void sendVerificationCode(String email) { + // 检查发送频率限制 + LocalDateTime lastSendTime = lastSendTimeCache.getIfPresent(email); + if (lastSendTime != null) { + LocalDateTime now = LocalDateTime.now(); + // 60秒内只能发送一次验证码 + if (lastSendTime.plusSeconds(60).isAfter(now)) { + long secondsLeft = 60 - java.time.Duration.between(lastSendTime, now).getSeconds(); + log.warn("验证码请求过于频繁:{}, 剩余等待时间: {}秒", email, secondsLeft); + throw new IllegalStateException("请求过于频繁,请在" + secondsLeft + "秒后再试。"); + } + } + + // 生成6位随机验证码 + String code = generateCode(); + + // 存储验证码和发送时间 + verificationCodeCache.put(email, code); + lastSendTimeCache.put(email, LocalDateTime.now()); + + // 发送邮件 + mailService.sendVerificationCodeEmail(email, code); + log.info("验证码已发送到: {}", email); + } + + @Override + public boolean verifyCode(String email, String code) { + if (email == null || code == null) { + return false; + } + + // 从缓存中获取验证码 + String storedCode = verificationCodeCache.getIfPresent(email); + + if (storedCode == null) { + log.warn("验证码不存在或已过期: {}", email); + return false; + } + + // 验证成功后,从缓存中移除验证码,防止重复使用 + if (storedCode.equals(code)) { + verificationCodeCache.invalidate(email); + log.info("验证码验证成功: {}", email); + return true; + } + + log.warn("验证码不匹配: {}", email); + return false; + } + + /** + * 生成6位随机数字验证码 + */ + private String generateCode() { + SecureRandom random = new SecureRandom(); + int code = 100000 + random.nextInt(900000); // 生成100000-999999之间的随机数 + return String.valueOf(code); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/service/pathfinding/AStarService.java b/ems-backend/src/main/java/com/dne/ems/service/pathfinding/AStarService.java index 49edd6d..ada2ce2 100644 --- a/ems-backend/src/main/java/com/dne/ems/service/pathfinding/AStarService.java +++ b/ems-backend/src/main/java/com/dne/ems/service/pathfinding/AStarService.java @@ -1,162 +1,162 @@ -package com.dne.ems.service.pathfinding; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.PriorityQueue; -import java.util.Set; - -import org.springframework.stereotype.Service; - -import com.dne.ems.dto.Point; -import com.dne.ems.model.MapGrid; -import com.dne.ems.repository.MapGridRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class AStarService { - - private final MapGridRepository mapGridRepository; - - private static class Node { - Point point; - int g; // cost from start to current node - int h; // heuristic: estimated cost from current node to end - int f; // g + h - Node parent; - - public Node(Point point, Node parent, int g, int h) { - this.point = point; - this.parent = parent; - this.g = g; - this.h = h; - this.f = g + h; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Node node = (Node) o; - return point.equals(node.point); - } - - @Override - public int hashCode() { - return Objects.hash(point); - } - } - - /** - * Finds the shortest path between two points on a grid, avoiding obstacles. - * The map, including dimensions and obstacles, is loaded dynamically from the database for each call. - * - * @param start The starting point. - * @param end The ending point. - * @return A list of points representing the path from start to end, or an empty list if no path is found. - */ - public List findPath(Point start, Point end) { - List mapGrids = mapGridRepository.findAll(); - if (mapGrids.isEmpty()) { - return Collections.emptyList(); // No map data - } - - // Dynamically determine map dimensions and obstacles - int maxX = 0; - int maxY = 0; - Set obstacles = new HashSet<>(); - for (MapGrid gridCell : mapGrids) { - // Treat cells that are explicit obstacles OR have no city name as obstacles - if (gridCell.isObstacle() || gridCell.getCityName() == null || gridCell.getCityName().trim().isEmpty()) { - obstacles.add(new Point(gridCell.getX(), gridCell.getY())); - } - if (gridCell.getX() > maxX) maxX = gridCell.getX(); - if (gridCell.getY() > maxY) maxY = gridCell.getY(); - } - final int mapWidth = maxX + 1; - final int mapHeight = maxY + 1; - - if (obstacles.contains(start) || obstacles.contains(end)) { - return Collections.emptyList(); // Start or end is on an obstacle - } - - PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f)); - Map allNodes = new HashMap<>(); - - Node startNode = new Node(start, null, 0, calculateHeuristic(start, end)); - openSet.add(startNode); - allNodes.put(start, startNode); - - while (!openSet.isEmpty()) { - Node currentNode = openSet.poll(); - - if (currentNode.point.equals(end)) { - return reconstructPath(currentNode); - } - - for (Point neighborPoint : getNeighbors(currentNode.point, mapWidth, mapHeight)) { - if (obstacles.contains(neighborPoint)) { - continue; - } - - int tentativeG = currentNode.g + 1; // Cost between neighbors is 1 - - Node neighborNode = allNodes.get(neighborPoint); - - if (neighborNode == null) { - int heuristic = calculateHeuristic(neighborPoint, end); - neighborNode = new Node(neighborPoint, currentNode, tentativeG, heuristic); - allNodes.put(neighborPoint, neighborNode); - openSet.add(neighborNode); - } else if (tentativeG < neighborNode.g) { - // Found a better path to this neighbor - neighborNode.parent = currentNode; - neighborNode.g = tentativeG; - neighborNode.f = tentativeG + neighborNode.h; - // Re-heapify the priority queue (this is a bit of a hack for standard Java PQ) - openSet.remove(neighborNode); - openSet.add(neighborNode); - } - } - } - return Collections.emptyList(); // No path found - } - - private int calculateHeuristic(Point a, Point b) { - // Manhattan distance: suitable for grid-based movement (up, down, left, right) - return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()); - } - - private List getNeighbors(Point point, int mapWidth, int mapHeight) { - List neighbors = new ArrayList<>(4); - int[] dx = {0, 0, 1, -1}; // Right, Left, Down, Up - int[] dy = {1, -1, 0, 0}; // Corresponds to dx indices - - for (int i = 0; i < 4; i++) { - int newX = point.x() + dx[i]; - int newY = point.y() + dy[i]; - - if (newX >= 0 && newX < mapWidth && newY >= 0 && newY < mapHeight) { - neighbors.add(new Point(newX, newY)); - } - } - return neighbors; - } - - private List reconstructPath(Node node) { - LinkedList path = new LinkedList<>(); - while (node != null) { - path.addFirst(node.point); - node = node.parent; - } - return path; - } +package com.dne.ems.service.pathfinding; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.Set; + +import org.springframework.stereotype.Service; + +import com.dne.ems.dto.Point; +import com.dne.ems.model.MapGrid; +import com.dne.ems.repository.MapGridRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AStarService { + + private final MapGridRepository mapGridRepository; + + private static class Node { + Point point; + int g; // cost from start to current node + int h; // heuristic: estimated cost from current node to end + int f; // g + h + Node parent; + + public Node(Point point, Node parent, int g, int h) { + this.point = point; + this.parent = parent; + this.g = g; + this.h = h; + this.f = g + h; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Node node = (Node) o; + return point.equals(node.point); + } + + @Override + public int hashCode() { + return Objects.hash(point); + } + } + + /** + * Finds the shortest path between two points on a grid, avoiding obstacles. + * The map, including dimensions and obstacles, is loaded dynamically from the database for each call. + * + * @param start The starting point. + * @param end The ending point. + * @return A list of points representing the path from start to end, or an empty list if no path is found. + */ + public List findPath(Point start, Point end) { + List mapGrids = mapGridRepository.findAll(); + if (mapGrids.isEmpty()) { + return Collections.emptyList(); // No map data + } + + // Dynamically determine map dimensions and obstacles + int maxX = 0; + int maxY = 0; + Set obstacles = new HashSet<>(); + for (MapGrid gridCell : mapGrids) { + // Treat cells that are explicit obstacles OR have no city name as obstacles + if (gridCell.isObstacle() || gridCell.getCityName() == null || gridCell.getCityName().trim().isEmpty()) { + obstacles.add(new Point(gridCell.getX(), gridCell.getY())); + } + if (gridCell.getX() > maxX) maxX = gridCell.getX(); + if (gridCell.getY() > maxY) maxY = gridCell.getY(); + } + final int mapWidth = maxX + 1; + final int mapHeight = maxY + 1; + + if (obstacles.contains(start) || obstacles.contains(end)) { + return Collections.emptyList(); // Start or end is on an obstacle + } + + PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f)); + Map allNodes = new HashMap<>(); + + Node startNode = new Node(start, null, 0, calculateHeuristic(start, end)); + openSet.add(startNode); + allNodes.put(start, startNode); + + while (!openSet.isEmpty()) { + Node currentNode = openSet.poll(); + + if (currentNode.point.equals(end)) { + return reconstructPath(currentNode); + } + + for (Point neighborPoint : getNeighbors(currentNode.point, mapWidth, mapHeight)) { + if (obstacles.contains(neighborPoint)) { + continue; + } + + int tentativeG = currentNode.g + 1; // Cost between neighbors is 1 + + Node neighborNode = allNodes.get(neighborPoint); + + if (neighborNode == null) { + int heuristic = calculateHeuristic(neighborPoint, end); + neighborNode = new Node(neighborPoint, currentNode, tentativeG, heuristic); + allNodes.put(neighborPoint, neighborNode); + openSet.add(neighborNode); + } else if (tentativeG < neighborNode.g) { + // Found a better path to this neighbor + neighborNode.parent = currentNode; + neighborNode.g = tentativeG; + neighborNode.f = tentativeG + neighborNode.h; + // Re-heapify the priority queue (this is a bit of a hack for standard Java PQ) + openSet.remove(neighborNode); + openSet.add(neighborNode); + } + } + } + return Collections.emptyList(); // No path found + } + + private int calculateHeuristic(Point a, Point b) { + // Manhattan distance: suitable for grid-based movement (up, down, left, right) + return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y()); + } + + private List getNeighbors(Point point, int mapWidth, int mapHeight) { + List neighbors = new ArrayList<>(4); + int[] dx = {0, 0, 1, -1}; // Right, Left, Down, Up + int[] dy = {1, -1, 0, 0}; // Corresponds to dx indices + + for (int i = 0; i < 4; i++) { + int newX = point.x() + dx[i]; + int newY = point.y() + dy[i]; + + if (newX >= 0 && newX < mapWidth && newY >= 0 && newY < mapHeight) { + neighbors.add(new Point(newX, newY)); + } + } + return neighbors; + } + + private List reconstructPath(Node node) { + LinkedList path = new LinkedList<>(); + while (node != null) { + path.addFirst(node.point); + node = node.parent; + } + return path; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/validation/GridWorkerLocationRequired.java b/ems-backend/src/main/java/com/dne/ems/validation/GridWorkerLocationRequired.java index 2802eac..790e5ec 100644 --- a/ems-backend/src/main/java/com/dne/ems/validation/GridWorkerLocationRequired.java +++ b/ems-backend/src/main/java/com/dne/ems/validation/GridWorkerLocationRequired.java @@ -1,19 +1,19 @@ -package com.dne.ems.validation; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Documented -@Constraint(validatedBy = GridWorkerLocationValidator.class) -@Target({ ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -public @interface GridWorkerLocationRequired { - String message() default "当角色为网格员时,必须提供X和Y坐标"; - Class[] groups() default {}; - Class[] payload() default {}; +package com.dne.ems.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Constraint(validatedBy = GridWorkerLocationValidator.class) +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface GridWorkerLocationRequired { + String message() default "当角色为网格员时,必须提供X和Y坐标"; + Class[] groups() default {}; + Class[] payload() default {}; } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/validation/GridWorkerLocationValidator.java b/ems-backend/src/main/java/com/dne/ems/validation/GridWorkerLocationValidator.java index 3ce81e4..e9d390d 100644 --- a/ems-backend/src/main/java/com/dne/ems/validation/GridWorkerLocationValidator.java +++ b/ems-backend/src/main/java/com/dne/ems/validation/GridWorkerLocationValidator.java @@ -1,24 +1,24 @@ -package com.dne.ems.validation; - -import com.dne.ems.dto.UserRoleUpdateRequest; -import com.dne.ems.model.enums.Role; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; - -public class GridWorkerLocationValidator implements ConstraintValidator { - - @Override - public boolean isValid(UserRoleUpdateRequest value, ConstraintValidatorContext context) { - if (value == null) { - return true; // Let other annotations handle null - } - - if (value.getRole() == Role.GRID_WORKER) { - return value.getGridX() != null && value.getGridY() != null; - } - - // For any other role, this validation does not apply. - return true; - } +package com.dne.ems.validation; + +import com.dne.ems.dto.UserRoleUpdateRequest; +import com.dne.ems.model.enums.Role; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class GridWorkerLocationValidator implements ConstraintValidator { + + @Override + public boolean isValid(UserRoleUpdateRequest value, ConstraintValidatorContext context) { + if (value == null) { + return true; // Let other annotations handle null + } + + if (value.getRole() == Role.GRID_WORKER) { + return value.getGridX() != null && value.getGridY() != null; + } + + // For any other role, this validation does not apply. + return true; + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/validation/PasswordValidator.java b/ems-backend/src/main/java/com/dne/ems/validation/PasswordValidator.java index b88026b..3f7e2e4 100644 --- a/ems-backend/src/main/java/com/dne/ems/validation/PasswordValidator.java +++ b/ems-backend/src/main/java/com/dne/ems/validation/PasswordValidator.java @@ -1,43 +1,43 @@ -package com.dne.ems.validation; - -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; -import java.util.regex.Pattern; - -/** - * 密码验证器,用于验证密码是否符合系统安全要求 - * - *

实现了Jakarta Validation的ConstraintValidator接口, - * 用于验证带有@ValidPassword注解的字段。密码必须满足以下条件:

- *
    - *
  • 至少8个字符长
  • - *
  • 至少包含一个数字
  • - *
  • 至少包含一个小写字母
  • - *
  • 至少包含一个大写字母
  • - *
  • 至少包含一个特殊字符
  • - *
  • 不允许包含空格
  • - *
- * - * @see ValidPassword - */ -public class PasswordValidator implements ConstraintValidator { - - // (?=.*[0-9]) a digit must occur at least once - // (?=.*[a-z]) a lower case letter must occur at least once - // (?=.*[A-Z]) an upper case letter must occur at least once - // (?=.*[!@#$%^&*()]) a special character must occur at least once - // (?=\\S+$) no whitespace allowed in the entire string - // .{8,} at least 8 characters - private static final String PASSWORD_PATTERN = - "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]).{8,}$"; - - private static final Pattern PATTERN = Pattern.compile(PASSWORD_PATTERN); - - @Override - public boolean isValid(String password, ConstraintValidatorContext context) { - if (password == null) { - return false; - } - return PATTERN.matcher(password).matches(); - } +package com.dne.ems.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +/** + * 密码验证器,用于验证密码是否符合系统安全要求 + * + *

实现了Jakarta Validation的ConstraintValidator接口, + * 用于验证带有@ValidPassword注解的字段。密码必须满足以下条件:

+ *
    + *
  • 至少8个字符长
  • + *
  • 至少包含一个数字
  • + *
  • 至少包含一个小写字母
  • + *
  • 至少包含一个大写字母
  • + *
  • 至少包含一个特殊字符
  • + *
  • 不允许包含空格
  • + *
+ * + * @see ValidPassword + */ +public class PasswordValidator implements ConstraintValidator { + + // (?=.*[0-9]) a digit must occur at least once + // (?=.*[a-z]) a lower case letter must occur at least once + // (?=.*[A-Z]) an upper case letter must occur at least once + // (?=.*[!@#$%^&*()]) a special character must occur at least once + // (?=\\S+$) no whitespace allowed in the entire string + // .{8,} at least 8 characters + private static final String PASSWORD_PATTERN = + "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]).{8,}$"; + + private static final Pattern PATTERN = Pattern.compile(PASSWORD_PATTERN); + + @Override + public boolean isValid(String password, ConstraintValidatorContext context) { + if (password == null) { + return false; + } + return PATTERN.matcher(password).matches(); + } } \ No newline at end of file diff --git a/ems-backend/src/main/java/com/dne/ems/validation/ValidPassword.java b/ems-backend/src/main/java/com/dne/ems/validation/ValidPassword.java index 0a7135c..8234323 100644 --- a/ems-backend/src/main/java/com/dne/ems/validation/ValidPassword.java +++ b/ems-backend/src/main/java/com/dne/ems/validation/ValidPassword.java @@ -1,35 +1,35 @@ -package com.dne.ems.validation; - -import jakarta.validation.Constraint; -import jakarta.validation.Payload; -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * 密码验证注解,用于标记需要进行密码规则验证的字段 - * - *

使用此注解的字段将由{@link PasswordValidator}进行验证, - * 确保密码符合系统的安全要求:至少8个字符长,包含大小写字母、数字和特殊字符。

- * - *

示例用法:

- *
- * public class UserDto {
- *     @ValidPassword
- *     private String password;
- * }
- * 
- * - * @see PasswordValidator 实际执行验证的验证器类 - */ -@Documented -@Constraint(validatedBy = PasswordValidator.class) -@Target({ ElementType.FIELD, ElementType.METHOD }) -@Retention(RetentionPolicy.RUNTIME) -public @interface ValidPassword { - String message() default "无效的密码:密码必须至少8个字符长,包含至少一个大写字母,一个小写字母,一个数字和一个特殊字符。"; - Class[] groups() default {}; - Class[] payload() default {}; +package com.dne.ems.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 密码验证注解,用于标记需要进行密码规则验证的字段 + * + *

使用此注解的字段将由{@link PasswordValidator}进行验证, + * 确保密码符合系统的安全要求:至少8个字符长,包含大小写字母、数字和特殊字符。

+ * + *

示例用法:

+ *
+ * public class UserDto {
+ *     @ValidPassword
+ *     private String password;
+ * }
+ * 
+ * + * @see PasswordValidator 实际执行验证的验证器类 + */ +@Documented +@Constraint(validatedBy = PasswordValidator.class) +@Target({ ElementType.FIELD, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidPassword { + String message() default "无效的密码:密码必须至少8个字符长,包含至少一个大写字母,一个小写字母,一个数字和一个特殊字符。"; + Class[] groups() default {}; + Class[] payload() default {}; } \ No newline at end of file diff --git a/ems-backend/src/main/resources/application.properties b/ems-backend/src/main/resources/application.properties index 9e2af0a..29724b5 100644 --- a/ems-backend/src/main/resources/application.properties +++ b/ems-backend/src/main/resources/application.properties @@ -1,73 +1,73 @@ -spring.application.name=ems-backend - -# =================================================================== -# 移除数据库相关配置,使用JSON文件存储 -# =================================================================== - -file.upload-dir=./uploads -# A secure key for signing JWT tokens. -# This should be a long, random, Base64-encoded string. -# For production, this should be stored securely (e.g., as an environment variable). +spring.application.name=ems-backend + +# =================================================================== +# 移除数据库相关配置,使用JSON文件存储 +# =================================================================== + +file.upload-dir=./uploads +# A secure key for signing JWT tokens. +# This should be a long, random, Base64-encoded string. +# For production, this should be stored securely (e.g., as an environment variable). token.signing.key=${TOKEN_SIGNING_KEY:dev-change-me} - -# =================================================================== -# c. Volcano Engine AI Service Configuration -# =================================================================== -volcano.api.url=https://ark.cn-beijing.volces.com/api/v3/chat/completions + +# =================================================================== +# c. Volcano Engine AI Service Configuration +# =================================================================== +volcano.api.url=https://ark.cn-beijing.volces.com/api/v3/chat/completions volcano.api.key=${VOLCANO_API_KEY:} -volcano.model.name=deepseek-v3-250324 - -# =================================================================== -# d. Spring AOP Configuration -# =================================================================== -# Force the use of CGLIB proxies for AOP -spring.aop.proxy-target-class=true - -# =================================================================== -# e. Application Base URL -# =================================================================== -# This URL is used to construct absolute URLs for resources like uploaded files. -# It should be the public-facing base URL of the application. +volcano.model.name=deepseek-v3-250324 + +# =================================================================== +# d. Spring AOP Configuration +# =================================================================== +# Force the use of CGLIB proxies for AOP +spring.aop.proxy-target-class=true + +# =================================================================== +# e. Application Base URL +# =================================================================== +# This URL is used to construct absolute URLs for resources like uploaded files. +# It should be the public-facing base URL of the application. app.base.url=${APP_BASE_URL:http://localhost:5173} - -# =================================================================== -# Mail Configuration for 163 SMTP -# =================================================================== -spring.mail.host=smtp.163.com -spring.mail.port=465 + +# =================================================================== +# Mail Configuration for 163 SMTP +# =================================================================== +spring.mail.host=smtp.163.com +spring.mail.port=465 spring.mail.username=${SPRING_MAIL_USERNAME:} spring.mail.password=${SPRING_MAIL_PASSWORD:} -spring.mail.properties.mail.smtp.auth=true -spring.mail.properties.mail.smtp.ssl.enable=true -spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory -spring.mail.properties.mail.smtp.socketFactory.fallback=false -spring.mail.properties.mail.smtp.socketFactory.port=465 -spring.mail.properties.mail.transport.protocol=smtp -spring.mail.properties.mail.debug=true - -# =================================================================== -# Server Configuration -# =================================================================== -server.port=8080 - -# =================================================================== -# Logging Configuration -# =================================================================== -logging.level.root=INFO -logging.level.com.dne.ems=DEBUG -logging.level.org.springframework.web=INFO -logging.level.org.hibernate=WARN - -# =================================================================== -# JWT Configuration -# =================================================================== +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.ssl.enable=true +spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory +spring.mail.properties.mail.smtp.socketFactory.fallback=false +spring.mail.properties.mail.smtp.socketFactory.port=465 +spring.mail.properties.mail.transport.protocol=smtp +spring.mail.properties.mail.debug=true + +# =================================================================== +# Server Configuration +# =================================================================== +server.port=8080 + +# =================================================================== +# Logging Configuration +# =================================================================== +logging.level.root=INFO +logging.level.com.dne.ems=DEBUG +logging.level.org.springframework.web=INFO +logging.level.org.hibernate=WARN + +# =================================================================== +# JWT Configuration +# =================================================================== jwt.secret=${JWT_SECRET:dev-change-me} -jwt.expiration=86400000 - -# =================================================================== -# Spring Task Configuration -# =================================================================== -spring.task.execution.pool.core-size=5 -spring.task.execution.pool.max-size=10 -spring.task.execution.pool.queue-capacity=25 +jwt.expiration=86400000 + +# =================================================================== +# Spring Task Configuration +# =================================================================== +spring.task.execution.pool.core-size=5 +spring.task.execution.pool.max-size=10 +spring.task.execution.pool.queue-capacity=25 spring.task.execution.thread-name-prefix=ems-async- diff --git a/ems-backend/target/classes/application.properties b/ems-backend/target/classes/application.properties index 9e2af0a..29724b5 100644 --- a/ems-backend/target/classes/application.properties +++ b/ems-backend/target/classes/application.properties @@ -1,73 +1,73 @@ -spring.application.name=ems-backend - -# =================================================================== -# 移除数据库相关配置,使用JSON文件存储 -# =================================================================== - -file.upload-dir=./uploads -# A secure key for signing JWT tokens. -# This should be a long, random, Base64-encoded string. -# For production, this should be stored securely (e.g., as an environment variable). +spring.application.name=ems-backend + +# =================================================================== +# 移除数据库相关配置,使用JSON文件存储 +# =================================================================== + +file.upload-dir=./uploads +# A secure key for signing JWT tokens. +# This should be a long, random, Base64-encoded string. +# For production, this should be stored securely (e.g., as an environment variable). token.signing.key=${TOKEN_SIGNING_KEY:dev-change-me} - -# =================================================================== -# c. Volcano Engine AI Service Configuration -# =================================================================== -volcano.api.url=https://ark.cn-beijing.volces.com/api/v3/chat/completions + +# =================================================================== +# c. Volcano Engine AI Service Configuration +# =================================================================== +volcano.api.url=https://ark.cn-beijing.volces.com/api/v3/chat/completions volcano.api.key=${VOLCANO_API_KEY:} -volcano.model.name=deepseek-v3-250324 - -# =================================================================== -# d. Spring AOP Configuration -# =================================================================== -# Force the use of CGLIB proxies for AOP -spring.aop.proxy-target-class=true - -# =================================================================== -# e. Application Base URL -# =================================================================== -# This URL is used to construct absolute URLs for resources like uploaded files. -# It should be the public-facing base URL of the application. +volcano.model.name=deepseek-v3-250324 + +# =================================================================== +# d. Spring AOP Configuration +# =================================================================== +# Force the use of CGLIB proxies for AOP +spring.aop.proxy-target-class=true + +# =================================================================== +# e. Application Base URL +# =================================================================== +# This URL is used to construct absolute URLs for resources like uploaded files. +# It should be the public-facing base URL of the application. app.base.url=${APP_BASE_URL:http://localhost:5173} - -# =================================================================== -# Mail Configuration for 163 SMTP -# =================================================================== -spring.mail.host=smtp.163.com -spring.mail.port=465 + +# =================================================================== +# Mail Configuration for 163 SMTP +# =================================================================== +spring.mail.host=smtp.163.com +spring.mail.port=465 spring.mail.username=${SPRING_MAIL_USERNAME:} spring.mail.password=${SPRING_MAIL_PASSWORD:} -spring.mail.properties.mail.smtp.auth=true -spring.mail.properties.mail.smtp.ssl.enable=true -spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory -spring.mail.properties.mail.smtp.socketFactory.fallback=false -spring.mail.properties.mail.smtp.socketFactory.port=465 -spring.mail.properties.mail.transport.protocol=smtp -spring.mail.properties.mail.debug=true - -# =================================================================== -# Server Configuration -# =================================================================== -server.port=8080 - -# =================================================================== -# Logging Configuration -# =================================================================== -logging.level.root=INFO -logging.level.com.dne.ems=DEBUG -logging.level.org.springframework.web=INFO -logging.level.org.hibernate=WARN - -# =================================================================== -# JWT Configuration -# =================================================================== +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.ssl.enable=true +spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory +spring.mail.properties.mail.smtp.socketFactory.fallback=false +spring.mail.properties.mail.smtp.socketFactory.port=465 +spring.mail.properties.mail.transport.protocol=smtp +spring.mail.properties.mail.debug=true + +# =================================================================== +# Server Configuration +# =================================================================== +server.port=8080 + +# =================================================================== +# Logging Configuration +# =================================================================== +logging.level.root=INFO +logging.level.com.dne.ems=DEBUG +logging.level.org.springframework.web=INFO +logging.level.org.hibernate=WARN + +# =================================================================== +# JWT Configuration +# =================================================================== jwt.secret=${JWT_SECRET:dev-change-me} -jwt.expiration=86400000 - -# =================================================================== -# Spring Task Configuration -# =================================================================== -spring.task.execution.pool.core-size=5 -spring.task.execution.pool.max-size=10 -spring.task.execution.pool.queue-capacity=25 +jwt.expiration=86400000 + +# =================================================================== +# Spring Task Configuration +# =================================================================== +spring.task.execution.pool.core-size=5 +spring.task.execution.pool.max-size=10 +spring.task.execution.pool.queue-capacity=25 spring.task.execution.thread-name-prefix=ems-async-