This commit is contained in:
2026-01-29 04:19:53 +08:00
parent d970068ebf
commit 5eac583d61
213 changed files with 68860 additions and 68794 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

File diff suppressed because it is too large Load Diff

192
README.md
View File

@@ -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 (公众监督员)**: 可以提交环境问题的反馈。
登录后,系统会根据您的角色,展示不同的菜单和界面主题。

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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`

File diff suppressed because it is too large Load Diff

View File

@@ -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
} ]

View File

@@ -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
} ]

View File

@@ -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
} ]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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" : "其他污染物"
} ]

View File

@@ -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" : [ ]
} ]

File diff suppressed because it is too large Load Diff

298
ems-backend/mvnw.cmd vendored
View File

@@ -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-<version>,maven-mvnd-<version>-<platform>}/<hash>
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-<version>,maven-mvnd-<version>-<platform>}/<hash>
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"

View File

@@ -1,141 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dne</groupId>
<artifactId>ems-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ems-backend</name>
<description>ems-backend</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Swagger UI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Google Guava for Caching -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.2-jre</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dne</groupId>
<artifactId>ems-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ems-backend</name>
<description>ems-backend</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Swagger UI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Google Guava for Caching -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.2-jre</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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系统主启动类
*
* <p>核心配置:
* <ul>
* <li>@SpringBootApplication - 启用Spring Boot自动配置排除数据库相关配置</li>
* <li>@ComponentScan - 扫描com.dne.ems包下的组件</li>
* <li>@EnableAsync - 启用异步处理</li>
* </ul>
*
* @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系统主启动类
*
* <p>核心配置:
* <ul>
* <li>@SpringBootApplication - 启用Spring Boot自动配置排除数据库相关配置</li>
* <li>@ComponentScan - 扫描com.dne.ems包下的组件</li>
* <li>@EnableAsync - 启用异步处理</li>
* </ul>
*
* @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);
}
}

View File

@@ -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
}
}

View File

@@ -1,37 +1,37 @@
package com.dne.ems.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
/**
* 文件存储配置属性类,用于管理文件上传存储的相关配置
*
* <p>该配置类通过Spring Boot的@ConfigurationProperties注解
* 自动绑定application配置文件中以"file"为前缀的属性。
* 主要用于指定上传文件的存储目录。
*
* <p>使用示例在application.yml中配置
* <pre>
* file:
* upload-dir: /path/to/uploads
* </pre>
*
* @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;
/**
* 文件存储配置属性类,用于管理文件上传存储的相关配置
*
* <p>该配置类通过Spring Boot的@ConfigurationProperties注解
* 自动绑定application配置文件中以"file"为前缀的属性。
* 主要用于指定上传文件的存储目录。
*
* <p>使用示例在application.yml中配置
* <pre>
* file:
* upload-dir: /path/to/uploads
* </pre>
*
* @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;
}

View File

@@ -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文档
*
* <p>该配置类使用SpringDoc OpenAPI实现提供以下功能
* <ul>
* <li>配置API文档的基本信息标题、版本等</li>
* <li>配置JWT Bearer Token认证方案</li>
* <li>为所有API端点添加安全要求</li>
* </ul>
*
* <p>通过该配置,可以在/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文档
*
* <p>该配置类使用SpringDoc OpenAPI实现提供以下功能
* <ul>
* <li>配置API文档的基本信息标题、版本等</li>
* <li>配置JWT Bearer Token认证方案</li>
* <li>为所有API端点添加安全要求</li>
* </ul>
*
* <p>通过该配置,可以在/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 {
// 配置类,无需实现内容
}

View File

@@ -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客户端功能
*
* <p>该配置类创建并配置RestTemplate Bean用于执行HTTP请求。
* RestTemplate是Spring提供的用于与RESTful服务交互的核心类
* 支持各种HTTP方法GET、POST、PUT、DELETE等
*
* <p>主要用途:
* <ul>
* <li>与外部API进行集成</li>
* <li>执行微服务间的通信</li>
* <li>获取外部数据源的信息</li>
* </ul>
*
* @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客户端功能
*
* <p>该配置类创建并配置RestTemplate Bean用于执行HTTP请求。
* RestTemplate是Spring提供的用于与RESTful服务交互的核心类
* 支持各种HTTP方法GET、POST、PUT、DELETE等
*
* <p>主要用途:
* <ul>
* <li>与外部API进行集成</li>
* <li>执行微服务间的通信</li>
* <li>获取外部数据源的信息</li>
* </ul>
*
* @author DNE开发团队
* @version 1.0
* @since 2024
*/
@Configuration
public class RestTemplateConfig {
/**
* 创建并配置 RestTemplate Bean
* @return 配置好的 RestTemplate 实例
*/
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -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客户端功能
*
* <p>该配置类创建并配置WebClient Bean用于执行响应式HTTP请求。
* WebClient是Spring WebFlux提供的非阻塞、响应式的HTTP客户端
* 支持异步请求处理和响应式流。
*
* <p>主要优势:
* <ul>
* <li>非阻塞I/O提高系统吞吐量</li>
* <li>支持响应式编程模型</li>
* <li>提供流式处理能力</li>
* <li>适用于高并发场景</li>
* </ul>
*
* @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客户端功能
*
* <p>该配置类创建并配置WebClient Bean用于执行响应式HTTP请求。
* WebClient是Spring WebFlux提供的非阻塞、响应式的HTTP客户端
* 支持异步请求处理和响应式流。
*
* <p>主要优势:
* <ul>
* <li>非阻塞I/O提高系统吞吐量</li>
* <li>支持响应式编程模型</li>
* <li>提供流式处理能力</li>
* <li>适用于高并发场景</li>
* </ul>
*
* @author DNE开发团队
* @version 1.0
* @since 2024
*/
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder().build();
}
}

View File

@@ -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<List<String>, String> {
private final ObjectMapper objectMapper;
@Override
public String convertToDatabaseColumn(List<String> 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<String> 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<List<String>, String> {
private final ObjectMapper objectMapper;
@Override
public String convertToDatabaseColumn(List<String> 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<String> 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();
}
}
}

View File

@@ -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<Void> signUp(@Valid @RequestBody SignUpRequest request) {
authService.registerUser(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
/**
* 用户登录认证接口
* @param request 包含用户凭证的登录请求体
* @return 包含JWT令牌的认证响应实体
*/
@PostMapping("/login")
public ResponseEntity<JwtAuthenticationResponse> signIn(@Valid @RequestBody LoginRequest request) {
return ResponseEntity.ok(authService.signIn(request));
}
/**
* 用户登出接口
* @return 成功则返回200 OK
*/
@PostMapping("/logout")
public ResponseEntity<Void> logout() {
authService.logout();
return ResponseEntity.ok().build();
}
/**
* 请求发送账号注册验证码到指定邮箱。
*
* @param email 接收验证码的邮箱地址
* @return 成功则返回200 OK
*/
@PostMapping("/send-verification-code")
public ResponseEntity<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> signUp(@Valid @RequestBody SignUpRequest request) {
authService.registerUser(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
/**
* 用户登录认证接口
* @param request 包含用户凭证的登录请求体
* @return 包含JWT令牌的认证响应实体
*/
@PostMapping("/login")
public ResponseEntity<JwtAuthenticationResponse> signIn(@Valid @RequestBody LoginRequest request) {
return ResponseEntity.ok(authService.signIn(request));
}
/**
* 用户登出接口
* @return 成功则返回200 OK
*/
@PostMapping("/logout")
public ResponseEntity<Void> logout() {
authService.logout();
return ResponseEntity.ok().build();
}
/**
* 请求发送账号注册验证码到指定邮箱。
*
* @param email 接收验证码的邮箱地址
* @return 成功则返回200 OK
*/
@PostMapping("/send-verification-code")
public ResponseEntity<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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
}

View File

@@ -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;
/**
* 仪表盘数据控制器,提供各类环境监测数据的统计和可视化接口
*
* <p>主要功能包括:
* <ul>
* <li>环境质量指标统计</li>
* <li>AQI数据分布和热力图</li>
* <li>污染物阈值管理</li>
* <li>任务完成情况统计</li>
* <li>各类趋势分析数据</li>
* </ul>
*
* @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<DashboardStatsDTO> 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<List<AqiDistributionDTO>> getAqiDistribution() {
List<AqiDistributionDTO> distribution = dashboardService.getAqiDistribution();
return ResponseEntity.ok(distribution);
}
/**
* 获取月度超标趋势数据
*
* @return 包含月度超标趋势数据的响应实体
* @see TrendDataPointDTO
*/
@GetMapping("/reports/monthly-exceedance-trend")
@PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')")
public ResponseEntity<List<TrendDataPointDTO>> getMonthlyExceedanceTrend() {
List<TrendDataPointDTO> trend = dashboardService.getMonthlyExceedanceTrend();
return ResponseEntity.ok(trend);
}
/**
* 获取网格覆盖情况数据
*
* @return 包含各城市网格覆盖情况的响应实体
* @see GridCoverageDTO
*/
@GetMapping("/reports/grid-coverage")
@PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')")
public ResponseEntity<List<GridCoverageDTO>> getGridCoverage() {
List<GridCoverageDTO> coverage = dashboardService.getGridCoverageByCity();
return ResponseEntity.ok(coverage);
}
/**
* 获取反馈热力图数据
*
* @return 包含热力图坐标点数据的响应实体
* @throws ResourceNotFoundException 如果未找到任何热力图数据
* @see HeatmapPointDTO
*/
@GetMapping("/map/heatmap")
@PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')")
public ResponseEntity<List<HeatmapPointDTO>> getHeatmapData() {
List<HeatmapPointDTO> 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<List<PollutionStatsDTO>> getPollutionStats() {
List<PollutionStatsDTO> stats = dashboardService.getPollutionStats();
return ResponseEntity.ok(stats);
}
/**
* 获取任务完成情况统计数据
*
* @return 包含任务完成率等统计数据的响应实体
* @see TaskStatsDTO
*/
@GetMapping("/reports/task-completion-stats")
@PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')")
public ResponseEntity<TaskStatsDTO> 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<List<AqiHeatmapPointDTO>> getAqiHeatmapData() {
List<AqiHeatmapPointDTO> 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<List<PollutantThresholdDTO>> getPollutantThresholds() {
log.info("接收到获取所有污染物阈值设置的请求");
List<PollutantThresholdDTO> 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<PollutantThresholdDTO> 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<PollutantThresholdDTO> 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<Map<String, List<TrendDataPointDTO>>> getPollutantMonthlyTrends() {
log.info("接收到获取每种污染物月度趋势数据的请求");
Map<String, List<TrendDataPointDTO>> 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;
/**
* 仪表盘数据控制器,提供各类环境监测数据的统计和可视化接口
*
* <p>主要功能包括:
* <ul>
* <li>环境质量指标统计</li>
* <li>AQI数据分布和热力图</li>
* <li>污染物阈值管理</li>
* <li>任务完成情况统计</li>
* <li>各类趋势分析数据</li>
* </ul>
*
* @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<DashboardStatsDTO> 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<List<AqiDistributionDTO>> getAqiDistribution() {
List<AqiDistributionDTO> distribution = dashboardService.getAqiDistribution();
return ResponseEntity.ok(distribution);
}
/**
* 获取月度超标趋势数据
*
* @return 包含月度超标趋势数据的响应实体
* @see TrendDataPointDTO
*/
@GetMapping("/reports/monthly-exceedance-trend")
@PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')")
public ResponseEntity<List<TrendDataPointDTO>> getMonthlyExceedanceTrend() {
List<TrendDataPointDTO> trend = dashboardService.getMonthlyExceedanceTrend();
return ResponseEntity.ok(trend);
}
/**
* 获取网格覆盖情况数据
*
* @return 包含各城市网格覆盖情况的响应实体
* @see GridCoverageDTO
*/
@GetMapping("/reports/grid-coverage")
@PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')")
public ResponseEntity<List<GridCoverageDTO>> getGridCoverage() {
List<GridCoverageDTO> coverage = dashboardService.getGridCoverageByCity();
return ResponseEntity.ok(coverage);
}
/**
* 获取反馈热力图数据
*
* @return 包含热力图坐标点数据的响应实体
* @throws ResourceNotFoundException 如果未找到任何热力图数据
* @see HeatmapPointDTO
*/
@GetMapping("/map/heatmap")
@PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')")
public ResponseEntity<List<HeatmapPointDTO>> getHeatmapData() {
List<HeatmapPointDTO> 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<List<PollutionStatsDTO>> getPollutionStats() {
List<PollutionStatsDTO> stats = dashboardService.getPollutionStats();
return ResponseEntity.ok(stats);
}
/**
* 获取任务完成情况统计数据
*
* @return 包含任务完成率等统计数据的响应实体
* @see TaskStatsDTO
*/
@GetMapping("/reports/task-completion-stats")
@PreAuthorize("hasAnyRole('DECISION_MAKER', 'ADMIN')")
public ResponseEntity<TaskStatsDTO> 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<List<AqiHeatmapPointDTO>> getAqiHeatmapData() {
List<AqiHeatmapPointDTO> 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<List<PollutantThresholdDTO>> getPollutantThresholds() {
log.info("接收到获取所有污染物阈值设置的请求");
List<PollutantThresholdDTO> 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<PollutantThresholdDTO> 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<PollutantThresholdDTO> 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<Map<String, List<TrendDataPointDTO>>> getPollutantMonthlyTrends() {
log.info("接收到获取每种污染物月度趋势数据的请求");
Map<String, List<TrendDataPointDTO>> trends = dashboardService.getPollutantMonthlyTrends();
log.info("返回 {} 种污染物的趋势数据", trends.size());
return ResponseEntity.ok(trends);
}
}

View File

@@ -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;
/**
* 公众反馈控制器,处理环境问题反馈的提交、查询和统计
*
* <p>主要功能包括:
* <ul>
* <li>公众环境问题反馈提交(支持认证用户和匿名用户)</li>
* <li>反馈信息查询与多条件过滤</li>
* <li>反馈数据统计分析</li>
* <li>反馈状态流转管理</li>
* </ul>
*
* <p>权限控制:
* <ul>
* <li>提交接口:所有认证用户</li>
* <li>查询接口:管理员、主管、决策者</li>
* </ul>
*
* @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格式仅用于测试)
*
* <p>此接口用于测试目的使用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<Feedback> 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);
}
/**
* 提交反馈(正式接口)
*
* <p>使用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<Feedback> 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);
}
/**
* 获取所有反馈(分页+多条件过滤)
*
* <p>支持以下过滤条件组合查询:
* <ul>
* <li>状态PENDING_REVIEW(待审核)/PROCESSED(已处理)/REJECTED(已拒绝)</li>
* <li>污染类型AIR/WATER/SOIL/NOISE等</li>
* <li>严重程度LOW/MEDIUM/HIGH/CRITICAL</li>
* <li>地理位置:城市/区县</li>
* <li>时间范围:开始日期至结束日期</li>
* <li>关键词:标题/描述模糊匹配</li>
* </ul>
*
* @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<Page<FeedbackResponseDTO>> 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<FeedbackResponseDTO> 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<FeedbackResponseDTO> 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<FeedbackStatsResponse> 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<FeedbackResponseDTO> 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;
/**
* 公众反馈控制器,处理环境问题反馈的提交、查询和统计
*
* <p>主要功能包括:
* <ul>
* <li>公众环境问题反馈提交(支持认证用户和匿名用户)</li>
* <li>反馈信息查询与多条件过滤</li>
* <li>反馈数据统计分析</li>
* <li>反馈状态流转管理</li>
* </ul>
*
* <p>权限控制:
* <ul>
* <li>提交接口:所有认证用户</li>
* <li>查询接口:管理员、主管、决策者</li>
* </ul>
*
* @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格式仅用于测试)
*
* <p>此接口用于测试目的使用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<Feedback> 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);
}
/**
* 提交反馈(正式接口)
*
* <p>使用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<Feedback> 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);
}
/**
* 获取所有反馈(分页+多条件过滤)
*
* <p>支持以下过滤条件组合查询:
* <ul>
* <li>状态PENDING_REVIEW(待审核)/PROCESSED(已处理)/REJECTED(已拒绝)</li>
* <li>污染类型AIR/WATER/SOIL/NOISE等</li>
* <li>严重程度LOW/MEDIUM/HIGH/CRITICAL</li>
* <li>地理位置:城市/区县</li>
* <li>时间范围:开始日期至结束日期</li>
* <li>关键词:标题/描述模糊匹配</li>
* </ul>
*
* @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<Page<FeedbackResponseDTO>> 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<FeedbackResponseDTO> 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<FeedbackResponseDTO> 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<FeedbackStatsResponse> 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<FeedbackResponseDTO> processFeedback(
@PathVariable Long id,
@Valid @RequestBody ProcessFeedbackRequest request) {
FeedbackResponseDTO updatedFeedback = feedbackService.processFeedback(id, request);
return ResponseEntity.ok(updatedFeedback);
}
}

View File

@@ -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;
/**
* 文件控制器,处理文件下载和预览请求
*
* <p>主要功能包括:
* <ul>
* <li>文件下载</li>
* <li>文件在线预览</li>
* </ul>
*
* @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<Resource> 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<Resource> 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;
/**
* 文件控制器,处理文件下载和预览请求
*
* <p>主要功能包括:
* <ul>
* <li>文件下载</li>
* <li>文件在线预览</li>
* </ul>
*
* @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<Resource> 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<Resource> 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);
}
}

View File

@@ -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;
/**
* 网格管理控制器,负责网格数据的查询、更新和网格员分配管理
*
* <p>主要功能包括:
* <ul>
* <li>网格数据查询与分页</li>
* <li>网格信息更新</li>
* <li>网格员分配与解除分配</li>
* <li>网格覆盖率统计</li>
* </ul>
*
* @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<List<Grid>> getGrids() {
List<Grid> 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<Grid> 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<List<GridCoverageDTO>> getGridCoverage() {
List<GridCoverageDTO> 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<String, Long> request) {
Long userId = request.get("userId");
if (userId == null) {
return ResponseEntity.badRequest().body("用户ID不能为空");
}
Optional<Grid> gridOpt = gridRepository.findById(gridId);
Optional<UserAccount> 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<Grid> gridOpt = gridRepository.findById(gridId);
if (gridOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
Grid grid = gridOpt.get();
// 使用新添加的方法查找指定网格坐标的网格员
List<UserAccount> 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<String, Long> 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<Grid> gridOpt = gridRepository.findByGridXAndGridY(gridX, gridY);
if (gridOpt.isEmpty()) {
logger.warn("分配网格员失败: 未找到网格, gridX={}, gridY={}", gridX, gridY);
// 尝试查询所有网格,看看是否有数据
List<Grid> 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<UserAccount> 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<UserAccount> 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<UserAccount> 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;
/**
* 网格管理控制器,负责网格数据的查询、更新和网格员分配管理
*
* <p>主要功能包括:
* <ul>
* <li>网格数据查询与分页</li>
* <li>网格信息更新</li>
* <li>网格员分配与解除分配</li>
* <li>网格覆盖率统计</li>
* </ul>
*
* @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<List<Grid>> getGrids() {
List<Grid> 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<Grid> 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<List<GridCoverageDTO>> getGridCoverage() {
List<GridCoverageDTO> 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<String, Long> request) {
Long userId = request.get("userId");
if (userId == null) {
return ResponseEntity.badRequest().body("用户ID不能为空");
}
Optional<Grid> gridOpt = gridRepository.findById(gridId);
Optional<UserAccount> 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<Grid> gridOpt = gridRepository.findById(gridId);
if (gridOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
Grid grid = gridOpt.get();
// 使用新添加的方法查找指定网格坐标的网格员
List<UserAccount> 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<String, Long> 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<Grid> gridOpt = gridRepository.findByGridXAndGridY(gridX, gridY);
if (gridOpt.isEmpty()) {
logger.warn("分配网格员失败: 未找到网格, gridX={}, gridY={}", gridX, gridY);
// 尝试查询所有网格,看看是否有数据
List<Grid> 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<UserAccount> 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<UserAccount> 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<UserAccount> 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());
}
}
}

View File

@@ -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;
/**
* 网格员任务控制器,处理网格员的任务管理操作
*
* <p>主要功能包括:
* <ul>
* <li>获取分配的任务列表</li>
* <li>接受任务</li>
* <li>提交任务完成情况</li>
* <li>查看任务详情</li>
* </ul>
*
* @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<Page<TaskSummaryDTO>> getMyTasks(
@AuthenticationPrincipal CustomUserDetails userDetails,
@Parameter(description = "Filter tasks by status") @RequestParam(required = false) TaskStatus status,
Pageable pageable
) {
Page<TaskSummaryDTO> 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<TaskSummaryDTO> 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<TaskSummaryDTO> submitTask(
@PathVariable Long taskId,
@RequestPart("comments") @Valid String comments,
@RequestPart(value = "files", required = false) List<MultipartFile> 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<TaskDetailDTO> 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;
/**
* 网格员任务控制器,处理网格员的任务管理操作
*
* <p>主要功能包括:
* <ul>
* <li>获取分配的任务列表</li>
* <li>接受任务</li>
* <li>提交任务完成情况</li>
* <li>查看任务详情</li>
* </ul>
*
* @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<Page<TaskSummaryDTO>> getMyTasks(
@AuthenticationPrincipal CustomUserDetails userDetails,
@Parameter(description = "Filter tasks by status") @RequestParam(required = false) TaskStatus status,
Pageable pageable
) {
Page<TaskSummaryDTO> 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<TaskSummaryDTO> 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<TaskSummaryDTO> submitTask(
@PathVariable Long taskId,
@RequestPart("comments") @Valid String comments,
@RequestPart(value = "files", required = false) List<MultipartFile> 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<TaskDetailDTO> getTaskDetails(
@PathVariable Long taskId,
@AuthenticationPrincipal CustomUserDetails userDetails) {
TaskDetailDTO taskDetails = gridWorkerTaskService.getTaskDetails(taskId, userDetails.getId());
return ResponseEntity.ok(taskDetails);
}
}

View File

@@ -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;
/**
* 地图控制器,处理地图网格数据的操作
*
* <p>主要功能包括:
* <ul>
* <li>获取完整地图网格数据</li>
* <li>创建或更新网格单元</li>
* <li>初始化地图</li>
* </ul>
*
* @author DNE开发团队
* @version 1.0
* @since 2024
*/
@RestController
@RequestMapping("/api/map")
@RequiredArgsConstructor
public class MapController {
private final MapGridRepository mapGridRepository;
/**
* 获取完整地图网格数据
*
* @return 包含所有地图网格数据的响应实体
*/
@GetMapping("/grid")
public ResponseEntity<List<MapGrid>> getFullMap() {
List<MapGrid> mapGrids = mapGridRepository.findAllByOrderByYAscXAsc();
return ResponseEntity.ok(mapGrids);
}
/**
* 创建或更新网格单元
*
* @param gridCellRequest 包含网格单元数据的请求体
* @return 包含保存后的网格单元的响应实体
* @throws IllegalArgumentException 如果坐标值为负数
*/
@PostMapping("/grid")
@Transactional
public ResponseEntity<MapGrid> 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<String> 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;
/**
* 地图控制器,处理地图网格数据的操作
*
* <p>主要功能包括:
* <ul>
* <li>获取完整地图网格数据</li>
* <li>创建或更新网格单元</li>
* <li>初始化地图</li>
* </ul>
*
* @author DNE开发团队
* @version 1.0
* @since 2024
*/
@RestController
@RequestMapping("/api/map")
@RequiredArgsConstructor
public class MapController {
private final MapGridRepository mapGridRepository;
/**
* 获取完整地图网格数据
*
* @return 包含所有地图网格数据的响应实体
*/
@GetMapping("/grid")
public ResponseEntity<List<MapGrid>> getFullMap() {
List<MapGrid> mapGrids = mapGridRepository.findAllByOrderByYAscXAsc();
return ResponseEntity.ok(mapGrids);
}
/**
* 创建或更新网格单元
*
* @param gridCellRequest 包含网格单元数据的请求体
* @return 包含保存后的网格单元的响应实体
* @throws IllegalArgumentException 如果坐标值为负数
*/
@PostMapping("/grid")
@Transactional
public ResponseEntity<MapGrid> 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<String> 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.");
}
}

View File

@@ -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<List<OperationLogDTO>> getMyOperationLogs(
@AuthenticationPrincipal CustomUserDetails userDetails) {
Long userId = userDetails.getUserAccount().getId();
List<OperationLogDTO> 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<List<OperationLogDTO>> 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<OperationLogDTO> 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<List<OperationLogDTO>> getMyOperationLogs(
@AuthenticationPrincipal CustomUserDetails userDetails) {
Long userId = userDetails.getUserAccount().getId();
List<OperationLogDTO> 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<List<OperationLogDTO>> 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<OperationLogDTO> logs = operationLogService.queryOperationLogs(userId, operationType, startTime, endTime);
return ResponseEntity.ok(logs);
}
}

View File

@@ -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*寻路算法功能
*
* <p>主要功能:
* <ul>
* <li>计算两点之间的最优路径</li>
* <li>考虑地图障碍物</li>
* </ul>
*
* @author DNE开发团队
* @version 1.0
* @since 2024
*/
@RestController
@RequestMapping("/api/pathfinding")
@RequiredArgsConstructor
public class PathfindingController {
private final AStarService aStarService;
/**
* 使用A*算法查找两点之间的路径
*
* <p>实现细节:
* <ol>
* <li>验证请求参数</li>
* <li>调用A*算法服务计算路径</li>
* <li>返回路径点列表</li>
* </ol>
*
* @param request 包含起点和终点坐标的请求体
* @return 包含路径点列表的响应实体,若无路径则返回空列表
* @throws IllegalArgumentException 如果请求参数无效
*/
@PostMapping("/find")
public ResponseEntity<List<Point>> 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<Point> 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*寻路算法功能
*
* <p>主要功能:
* <ul>
* <li>计算两点之间的最优路径</li>
* <li>考虑地图障碍物</li>
* </ul>
*
* @author DNE开发团队
* @version 1.0
* @since 2024
*/
@RestController
@RequestMapping("/api/pathfinding")
@RequiredArgsConstructor
public class PathfindingController {
private final AStarService aStarService;
/**
* 使用A*算法查找两点之间的路径
*
* <p>实现细节:
* <ol>
* <li>验证请求参数</li>
* <li>调用A*算法服务计算路径</li>
* <li>返回路径点列表</li>
* </ol>
*
* @param request 包含起点和终点坐标的请求体
* @return 包含路径点列表的响应实体,若无路径则返回空列表
* @throws IllegalArgumentException 如果请求参数无效
*/
@PostMapping("/find")
public ResponseEntity<List<Point>> 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<Point> path = aStarService.findPath(start, end);
return ResponseEntity.ok(path);
}
}

View File

@@ -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操作
*
* <p>主要功能包括:
* <ul>
* <li>用户账号创建</li>
* <li>用户信息查询与更新</li>
* <li>用户角色管理</li>
* <li>用户删除</li>
* </ul>
*
* @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<PageDTO<UserAccount>> 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<UserAccount> usersPage = personnelService.getUsers(role, name, pageable);
PageDTO<UserAccount> 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<UserAccount> 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<UserAccount> 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<UserAccount> 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操作
*
* <p>主要功能包括:
* <ul>
* <li>用户账号创建</li>
* <li>用户信息查询与更新</li>
* <li>用户角色管理</li>
* <li>用户删除</li>
* </ul>
*
* @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<PageDTO<UserAccount>> 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<UserAccount> usersPage = personnelService.getUsers(role, name, pageable);
PageDTO<UserAccount> 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<UserAccount> 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<UserAccount> 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<UserAccount> 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);
}
}

View File

@@ -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;
/**
* 用户个人资料控制器,处理认证用户相关数据
*
* <p>主要功能:
* <ul>
* <li>获取用户提交的反馈历史</li>
* <li>管理个人资料相关数据</li>
* </ul>
*
* @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;
/**
* 获取当前用户的反馈历史记录
*
* <p>实现细节:
* <ol>
* <li>从认证信息获取用户ID</li>
* <li>查询该用户提交的所有反馈</li>
* <li>返回分页结果</li>
* </ol>
*
* @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<List<UserFeedbackSummaryDTO>> getMyFeedbackHistory(
@AuthenticationPrincipal CustomUserDetails userDetails,
Pageable pageable) {
Page<UserFeedbackSummaryDTO> 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;
/**
* 用户个人资料控制器,处理认证用户相关数据
*
* <p>主要功能:
* <ul>
* <li>获取用户提交的反馈历史</li>
* <li>管理个人资料相关数据</li>
* </ul>
*
* @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;
/**
* 获取当前用户的反馈历史记录
*
* <p>实现细节:
* <ol>
* <li>从认证信息获取用户ID</li>
* <li>查询该用户提交的所有反馈</li>
* <li>返回分页结果</li>
* </ol>
*
* @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<List<UserFeedbackSummaryDTO>> getMyFeedbackHistory(
@AuthenticationPrincipal CustomUserDetails userDetails,
Pageable pageable) {
Page<UserFeedbackSummaryDTO> historyPage = userFeedbackService.getFeedbackHistoryByUserId(userDetails.getId(), pageable);
return ResponseEntity.ok(historyPage.getContent());
}
}

View File

@@ -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控制器处理无需认证的公共接口
*
* <p>主要功能:
* <ul>
* <li>接收公众反馈提交</li>
* <li>处理带图片上传的表单</li>
* </ul>
*
* @author DNE开发团队
* @version 1.0
* @since 2024
*/
@RestController
@RequestMapping("/api/public")
@RequiredArgsConstructor
public class PublicController {
private final FeedbackService feedbackService;
/**
* 提交公共反馈(支持图片上传)
*
* <p>实现细节:
* <ol>
* <li>验证反馈请求参数</li>
* <li>处理上传的图片文件(如果有)</li>
* <li>保存反馈到数据库</li>
* </ol>
*
* @param request 包含反馈信息的请求体(JSON格式)
* @param files 上传的图片文件数组(可选)
* @return 创建成功的反馈实体
* @throws InvalidRequestException 如果请求参数无效
* @throws FileUploadException 如果文件上传失败
*/
@PostMapping(value = "/feedback", consumes = "multipart/form-data")
public ResponseEntity<Feedback> 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控制器处理无需认证的公共接口
*
* <p>主要功能:
* <ul>
* <li>接收公众反馈提交</li>
* <li>处理带图片上传的表单</li>
* </ul>
*
* @author DNE开发团队
* @version 1.0
* @since 2024
*/
@RestController
@RequestMapping("/api/public")
@RequiredArgsConstructor
public class PublicController {
private final FeedbackService feedbackService;
/**
* 提交公共反馈(支持图片上传)
*
* <p>实现细节:
* <ol>
* <li>验证反馈请求参数</li>
* <li>处理上传的图片文件(如果有)</li>
* <li>保存反馈到数据库</li>
* </ol>
*
* @param request 包含反馈信息的请求体(JSON格式)
* @param files 上传的图片文件数组(可选)
* @return 创建成功的反馈实体
* @throws InvalidRequestException 如果请求参数无效
* @throws FileUploadException 如果文件上传失败
*/
@PostMapping(value = "/feedback", consumes = "multipart/form-data")
public ResponseEntity<Feedback> 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);
}
}

View File

@@ -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;
/**
* 主管控制器,处理反馈审核相关功能
*
* <p>主要职责:
* <ul>
* <li>获取待审核反馈列表</li>
* <li>审核通过/拒绝反馈</li>
* </ul>
*
* @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<List<Feedback>> getFeedbackForReview() {
List<Feedback> 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<Void> 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<Void> 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;
/**
* 主管控制器,处理反馈审核相关功能
*
* <p>主要职责:
* <ul>
* <li>获取待审核反馈列表</li>
* <li>审核通过/拒绝反馈</li>
* </ul>
*
* @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<List<Feedback>> getFeedbackForReview() {
List<Feedback> 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<Void> 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<Void> rejectFeedback(
@PathVariable Long feedbackId,
@RequestBody RejectFeedbackRequest request) {
supervisorService.rejectFeedback(feedbackId, request);
return ResponseEntity.ok().build();
}
}

View File

@@ -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;
/**
* 任务分配控制器,处理网格任务分配相关功能
*
* <p>主要职责:
* <ul>
* <li>获取未分配任务列表</li>
* <li>查询可用网格工作人员</li>
* <li>分配任务给工作人员</li>
* </ul>
*
* @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<List<Feedback>> getUnassignedFeedback() {
List<Feedback> feedbackList = taskAssignmentService.getUnassignedFeedback();
return ResponseEntity.ok(feedbackList);
}
/**
* 获取可用的网格工作人员列表
*
* @return 可用工作人员列表
* @throws AccessDeniedException 如果用户无权限
*/
@GetMapping("/grid-workers")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')")
public ResponseEntity<List<UserAccount>> getAvailableGridWorkers() {
log.info("API接收到获取可用网格员列表的请求");
try {
List<UserAccount> 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<Assignment> 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;
/**
* 任务分配控制器,处理网格任务分配相关功能
*
* <p>主要职责:
* <ul>
* <li>获取未分配任务列表</li>
* <li>查询可用网格工作人员</li>
* <li>分配任务给工作人员</li>
* </ul>
*
* @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<List<Feedback>> getUnassignedFeedback() {
List<Feedback> feedbackList = taskAssignmentService.getUnassignedFeedback();
return ResponseEntity.ok(feedbackList);
}
/**
* 获取可用的网格工作人员列表
*
* @return 可用工作人员列表
* @throws AccessDeniedException 如果用户无权限
*/
@GetMapping("/grid-workers")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')")
public ResponseEntity<List<UserAccount>> getAvailableGridWorkers() {
log.info("API接收到获取可用网格员列表的请求");
try {
List<UserAccount> 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<Assignment> 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) {}
}

View File

@@ -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;
/**
* 任务管理控制器,处理任务全生命周期管理
*
* <p>主要功能:
* <ul>
* <li>任务创建与分配</li>
* <li>任务状态管理</li>
* <li>任务审核与审批</li>
* <li>从反馈创建任务</li>
* </ul>
*
* @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<List<TaskSummaryDTO>> 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<TaskSummaryDTO> 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<TaskSummaryDTO> 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<TaskDetailDTO> 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<TaskDetailDTO> 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<TaskDetailDTO> cancelTask(@PathVariable Long taskId) {
TaskDetailDTO updatedTask = taskManagementService.cancelTask(taskId);
return ResponseEntity.ok(updatedTask);
}
/**
* 创建新任务
*
* @param request 包含任务信息的请求体
* @return 创建的任务详情
* @throws InvalidRequestException 如果请求参数无效
*/
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<TaskDetailDTO> 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<List<Feedback>> getPendingFeedback() {
List<Feedback> 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<TaskDetailDTO> 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<TaskDetailDTO> 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<TaskDetailDTO> 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;
/**
* 任务管理控制器,处理任务全生命周期管理
*
* <p>主要功能:
* <ul>
* <li>任务创建与分配</li>
* <li>任务状态管理</li>
* <li>任务审核与审批</li>
* <li>从反馈创建任务</li>
* </ul>
*
* @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<List<TaskSummaryDTO>> 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<TaskSummaryDTO> 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<TaskSummaryDTO> 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<TaskDetailDTO> 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<TaskDetailDTO> 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<TaskDetailDTO> cancelTask(@PathVariable Long taskId) {
TaskDetailDTO updatedTask = taskManagementService.cancelTask(taskId);
return ResponseEntity.ok(updatedTask);
}
/**
* 创建新任务
*
* @param request 包含任务信息的请求体
* @return 创建的任务详情
* @throws InvalidRequestException 如果请求参数无效
*/
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<TaskDetailDTO> 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<List<Feedback>> getPendingFeedback() {
List<Feedback> 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<TaskDetailDTO> 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<TaskDetailDTO> 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<TaskDetailDTO> rejectTask(
@PathVariable Long taskId,
@RequestBody @Valid TaskRejectionRequest request) {
TaskDetailDTO updatedTask = taskManagementService.rejectTask(taskId, request.getReason());
return ResponseEntity.ok(updatedTask);
}
}

View File

@@ -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
) {
}

View File

@@ -1,53 +1,53 @@
package com.dne.ems.dto;
import lombok.Data;
/**
* 空气质量指数分布数据传输对象
*
* <p>该类用于封装空气质量指数(AQI)的分布统计数据包括不同等级的AQI及其对应的数量。
* 主要用于仪表盘展示和数据分析报告。</p>
*
* @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;
/**
* 空气质量指数分布数据传输对象
*
* <p>该类用于封装空气质量指数(AQI)的分布统计数据包括不同等级的AQI及其对应的数量。
* 主要用于仪表盘展示和数据分析报告。</p>
*
* @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;
}
}

View File

@@ -1,34 +1,34 @@
package com.dne.ems.dto;
/**
* 空气质量指数热力图数据点传输对象
*
* <p>该类用于表示AQI热力图上的单个数据点包含网格坐标和该网格的平均AQI值。
* 主要用于生成热力图可视化展示。</p>
*
* @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;
/**
* 空气质量指数热力图数据点传输对象
*
* <p>该类用于表示AQI热力图上的单个数据点包含网格坐标和该网格的平均AQI值。
* 主要用于生成热力图可视化展示。</p>
*
* @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
);
}
}

View File

@@ -1,34 +1,34 @@
package com.dne.ems.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 任务受理人信息数据传输对象
*
* <p>该类用于封装任务受理人的基本信息包括ID、姓名和联系电话。
* 主要用于任务分配和展示受理人信息的场景。</p>
*
* @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;
/**
* 任务受理人信息数据传输对象
*
* <p>该类用于封装任务受理人的基本信息包括ID、姓名和联系电话。
* 主要用于任务分配和展示受理人信息的场景。</p>
*
* @version 1.0
* @since 2024
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AssigneeInfoDTO {
/**
* 受理人唯一标识符
*/
private Long id;
/**
* 受理人姓名
*/
private String name;
/**
* 受理人联系电话
*/
private String phone;
}

View File

@@ -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()
);
}
}

View File

@@ -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;
}

View File

@@ -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
) {
}

View File

@@ -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;
/**
* 错误响应数据传输对象
*
* <p>该类用于封装API错误响应的标准格式包含错误发生的时间戳、HTTP状态码、
* 错误类型、错误消息以及请求路径等信息。主要用于统一的异常处理和客户端错误提示。</p>
*
* @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;
/**
* 错误响应数据传输对象
*
* <p>该类用于封装API错误响应的标准格式包含错误发生的时间戳、HTTP状态码、
* 错误类型、错误消息以及请求路径等信息。主要用于统一的异常处理和客户端错误提示。</p>
*
* @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;
}

View File

@@ -1,51 +1,51 @@
/**
* 反馈数据传输对象
*
* <p>用于在服务层和表示层之间传递反馈信息,
* 包含反馈的基本信息和关联数据。
*
* @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;
/**
* 反馈数据传输对象
*
* <p>包含反馈的完整信息,用于展示和传输
*/
@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<String> attachmentUrls;
/**
* 反馈数据传输对象
*
* <p>用于在服务层和表示层之间传递反馈信息,
* 包含反馈的基本信息和关联数据。
*
* @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;
/**
* 反馈数据传输对象
*
* <p>包含反馈的完整信息,用于展示和传输
*/
@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<String> attachmentUrls;
}

View File

@@ -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;
/**
* 反馈响应数据传输对象
*
* <p>该类用于封装反馈信息的响应数据,包含反馈的详细信息、状态、位置信息、
* 提交者信息、关联任务以及附件信息等。主要用于前端展示和API响应。</p>
*
* @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<AttachmentDTO> 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;
/**
* 反馈响应数据传输对象
*
* <p>该类用于封装反馈信息的响应数据,包含反馈的详细信息、状态、位置信息、
* 提交者信息、关联任务以及附件信息等。主要用于前端展示和API响应。</p>
*
* @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<AttachmentDTO> 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;
}
}

View File

@@ -1,56 +1,56 @@
package com.dne.ems.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 反馈统计响应DTO
*
* <p>包含系统反馈数据的统计信息,用于仪表盘展示
*
* <p>统计指标说明:
* <ul>
* <li>total - 总反馈数</li>
* <li>pending - 待处理总数</li>
* <li>confirmed - 已确认数</li>
* <li>assigned - 已分配数</li>
* <li>inProgress - 处理中数</li>
* <li>resolved - 已解决数</li>
* <li>closed - 已关闭数</li>
* <li>rejected - 已拒绝数</li>
* <li>pendingReview - 待审核数</li>
* <li>pendingAssignment - 待分配数</li>
* <li>aiReviewing - AI审核中数</li>
* </ul>
*
* @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
*
* <p>包含系统反馈数据的统计信息,用于仪表盘展示
*
* <p>统计指标说明:
* <ul>
* <li>total - 总反馈数</li>
* <li>pending - 待处理总数</li>
* <li>confirmed - 已确认数</li>
* <li>assigned - 已分配数</li>
* <li>inProgress - 处理中数</li>
* <li>resolved - 已解决数</li>
* <li>closed - 已关闭数</li>
* <li>rejected - 已拒绝数</li>
* <li>pendingReview - 待审核数</li>
* <li>pendingAssignment - 待分配数</li>
* <li>aiReviewing - AI审核中数</li>
* </ul>
*
* @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;
}

View File

@@ -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;
/**
* 公众反馈提交请求数据传输对象
*
* <p>用于接收公众用户提交的环境问题反馈,包含问题描述、分类和位置信息
*
* <p>业务规则:
* <ul>
* <li>提交后状态自动设为PENDING_REVIEW(待审核)</li>
* <li>需经过主管审核后才能创建任务</li>
* <li>位置信息必须包含有效的网格坐标</li>
* </ul>
*
* @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
) {
/**
* 地理位置数据(嵌套记录)
*
* <p>用于精确定位反馈问题的发生位置
*
* <p>验证规则:
* <ul>
* <li>文字地址必须符合"省-市-区"格式</li>
* <li>网格坐标必须在有效范围内(1-1000)</li>
* <li>经纬度坐标可选,但提供时可提高定位精度</li>
* </ul>
*
* @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;
/**
* 公众反馈提交请求数据传输对象
*
* <p>用于接收公众用户提交的环境问题反馈,包含问题描述、分类和位置信息
*
* <p>业务规则:
* <ul>
* <li>提交后状态自动设为PENDING_REVIEW(待审核)</li>
* <li>需经过主管审核后才能创建任务</li>
* <li>位置信息必须包含有效的网格坐标</li>
* </ul>
*
* @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
) {
/**
* 地理位置数据(嵌套记录)
*
* <p>用于精确定位反馈问题的发生位置
*
* <p>验证规则:
* <ul>
* <li>文字地址必须符合"省-市-区"格式</li>
* <li>网格坐标必须在有效范围内(1-1000)</li>
* <li>经纬度坐标可选,但提供时可提高定位精度</li>
* </ul>
*
* @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
) {}
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -1,20 +1,20 @@
package com.dne.ems.dto;
/**
* 热力图数据点传输对象
*
* <p>该类用于表示热力图上的单个数据点,包含网格坐标和该点的强度值。
* 主要用于生成各类热力图可视化展示,如反馈密度、任务分布等。</p>
*
* @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;
/**
* 热力图数据点传输对象
*
* <p>该类用于表示热力图上的单个数据点,包含网格坐标和该点的强度值。
* 主要用于生成各类热力图可视化展示,如反馈密度、任务分布等。</p>
*
* @param gridX 网格X坐标
* @param gridY 网格Y坐标
* @param intensity 该点的强度值(如反馈事件数量、任务数量等)
* @version 1.0
* @since 2024
*/
public record HeatmapPointDTO(
Integer gridX,
Integer gridY,
long intensity
) {
}

View File

@@ -1,15 +1,15 @@
package com.dne.ems.dto;
/**
* JWT认证响应数据传输对象
*
* <p>该类用于封装认证成功后返回给客户端的JWT令牌信息。
* 主要用于用户登录或令牌刷新操作的响应数据。</p>
*
* @param accessToken JWT访问令牌用于后续请求的身份验证
* @version 1.0
* @since 2024
*/
public record JwtAuthenticationResponse(
String accessToken
package com.dne.ems.dto;
/**
* JWT认证响应数据传输对象
*
* <p>该类用于封装认证成功后返回给客户端的JWT令牌信息。
* 主要用于用户登录或令牌刷新操作的响应数据。</p>
*
* @param accessToken JWT访问令牌用于后续请求的身份验证
* @version 1.0
* @since 2024
*/
public record JwtAuthenticationResponse(
String accessToken
) {}

View File

@@ -1,34 +1,34 @@
package com.dne.ems.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
/**
* 位置更新请求数据传输对象
*
* <p>用于接收用户位置更新请求,包含经纬度坐标信息。
* 主要用于网格员上报当前位置或移动设备用户更新位置信息。</p>
*
* <p>验证规则:
* <ul>
* <li>纬度范围必须在-90到90度之间</li>
* <li>经度范围必须在-180到180度之间</li>
* </ul>
*
* @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;
/**
* 位置更新请求数据传输对象
*
* <p>用于接收用户位置更新请求,包含经纬度坐标信息。
* 主要用于网格员上报当前位置或移动设备用户更新位置信息。</p>
*
* <p>验证规则:
* <ul>
* <li>纬度范围必须在-90到90度之间</li>
* <li>经度范围必须在-180到180度之间</li>
* </ul>
*
* @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
) {}

View File

@@ -1,30 +1,30 @@
package com.dne.ems.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
/**
* 登录请求数据传输对象
*
* <p>该类用于封装用户登录请求的数据,包含邮箱和密码信息。
* 主要用于用户认证流程中的登录请求处理。</p>
*
* <p>验证规则:
* <ul>
* <li>邮箱不能为空且必须符合邮箱格式</li>
* <li>密码不能为空</li>
* </ul>
*
* @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;
/**
* 登录请求数据传输对象
*
* <p>该类用于封装用户登录请求的数据,包含邮箱和密码信息。
* 主要用于用户认证流程中的登录请求处理。</p>
*
* <p>验证规则:
* <ul>
* <li>邮箱不能为空且必须符合邮箱格式</li>
* <li>密码不能为空</li>
* </ul>
*
* @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
) {}

View File

@@ -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 -> "未知操作";
};
}
}

View File

@@ -1,48 +1,48 @@
package com.dne.ems.dto;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 分页数据传输对象
*
* <p>该类用于封装分页查询的结果数据,安全地向前端暴露分页信息,
* 避免了Spring Data的Page接口序列化问题。</p>
*
* @param <T> 分页内容的数据类型
* @version 1.0
* @since 2024
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<T> {
/**
* 当前页的内容列表
*/
private List<T> 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;
/**
* 分页数据传输对象
*
* <p>该类用于封装分页查询的结果数据,安全地向前端暴露分页信息,
* 避免了Spring Data的Page接口序列化问题。</p>
*
* @param <T> 分页内容的数据类型
* @version 1.0
* @since 2024
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<T> {
/**
* 当前页的内容列表
*/
private List<T> content;
/**
* 所有页面的元素总数
*/
private long totalElements;
/**
* 总页数
*/
private int totalPages;
/**
* 当前页码从0开始
*/
private int number;
/**
* 当前页面的元素数量
*/
private int size;
}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -1,30 +1,30 @@
package com.dne.ems.dto;
import lombok.Data;
/**
* A DTO for requesting a path from the A* service.
*
* <p>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}.</p>
*
* @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.
*
* <p>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}.</p>
*
* @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;
}

View File

@@ -1,15 +1,15 @@
package com.dne.ems.dto;
/**
* 二维坐标点数据传输对象
*
* <p>该类用于表示具有整数坐标的二维点,主要用于路径查找请求和在网格上表示位置。
* 作为简单的数据结构,用于各种空间计算和位置表示场景。</p>
*
* @param x X坐标值
* @param y Y坐标值
* @version 1.0
* @since 2024
*/
public record Point(int x, int y) {
package com.dne.ems.dto;
/**
* 二维坐标点数据传输对象
*
* <p>该类用于表示具有整数坐标的二维点,主要用于路径查找请求和在网格上表示位置。
* 作为简单的数据结构,用于各种空间计算和位置表示场景。</p>
*
* @param x X坐标值
* @param y Y坐标值
* @version 1.0
* @since 2024
*/
public record Point(int x, int y) {
}

View File

@@ -1,135 +1,135 @@
package com.dne.ems.dto;
import com.dne.ems.model.enums.PollutionType;
/**
* 污染物阈值数据传输对象
*
* <p>该类用于封装污染物的阈值相关信息,包括污染物类型、名称、阈值值、单位和描述等。
* 主要用于环境监测系统中污染物标准管理和阈值展示。</p>
*
* <p>提供了多种构造方法,支持通过污染物类型枚举或污染物名称字符串创建对象。
* 当使用名称字符串创建时会尝试将其转换为对应的PollutionType枚举值。</p>
*
* @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;
/**
* 污染物阈值数据传输对象
*
* <p>该类用于封装污染物的阈值相关信息,包括污染物类型、名称、阈值值、单位和描述等。
* 主要用于环境监测系统中污染物标准管理和阈值展示。</p>
*
* <p>提供了多种构造方法,支持通过污染物类型枚举或污染物名称字符串创建对象。
* 当使用名称字符串创建时会尝试将其转换为对应的PollutionType枚举值。</p>
*
* @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;
}
}

View File

@@ -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
) {}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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
) {}

View File

@@ -1,24 +1,24 @@
package com.dne.ems.dto;
import jakarta.validation.constraints.NotNull;
/**
* 任务审核请求DTO
*
* <p>用于管理员对提交的任务进行审核决策
*
* @param approved 是否通过审核(必填)
* @param comments 审核意见(可选)
*
* <p>业务规则:
* <ul>
* <li>只有状态为SUBMITTED的任务才能审核</li>
* <li>审核通过后任务状态变为COMPLETED</li>
* <li>审核不通过则返回ASSIGNED状态</li>
* </ul>
*/
public record TaskApprovalRequest(
@NotNull
Boolean approved,
String comments
package com.dne.ems.dto;
import jakarta.validation.constraints.NotNull;
/**
* 任务审核请求DTO
*
* <p>用于管理员对提交的任务进行审核决策
*
* @param approved 是否通过审核(必填)
* @param comments 审核意见(可选)
*
* <p>业务规则:
* <ul>
* <li>只有状态为SUBMITTED的任务才能审核</li>
* <li>审核通过后任务状态变为COMPLETED</li>
* <li>审核不通过则返回ASSIGNED状态</li>
* </ul>
*/
public record TaskApprovalRequest(
@NotNull
Boolean approved,
String comments
) {}

View File

@@ -1,25 +1,25 @@
package com.dne.ems.dto;
import jakarta.validation.constraints.NotNull;
/**
* 任务分配请求数据传输对象
*
* <p>用于主管将待分配任务指派给特定网格工作人员
*
* <p>业务规则:
* <ul>
* <li>只能分配给角色为GRID_WORKER的用户</li>
* <li>任务状态必须为PENDING_ASSIGNMENT(待分配)或ASSIGNED(已分配)</li>
* <li>分配后任务状态变为ASSIGNED</li>
* <li>会生成任务分配记录</li>
* </ul>
*
* @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;
/**
* 任务分配请求数据传输对象
*
* <p>用于主管将待分配任务指派给特定网格工作人员
*
* <p>业务规则:
* <ul>
* <li>只能分配给角色为GRID_WORKER的用户</li>
* <li>任务状态必须为PENDING_ASSIGNMENT(待分配)或ASSIGNED(已分配)</li>
* <li>分配后任务状态变为ASSIGNED</li>
* <li>会生成任务分配记录</li>
* </ul>
*
* @param assigneeId 网格工作人员ID(必填)
* @see com.dne.ems.model.enums.TaskStatus 任务状态枚举
* @see com.dne.ems.model.Assignment 任务分配记录实体
*/
public record TaskAssignmentRequest(
@NotNull
Long assigneeId
) {}

View File

@@ -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;
/**
* 任务创建请求数据传输对象
*
* <p>用于主管直接创建任务,包含任务基本属性和位置信息
*
* <p>业务规则:
* <ul>
* <li>创建后状态为PENDING_ASSIGNMENT(待分配)</li>
* <li>需指定有效的污染类型和严重级别</li>
* <li>位置信息必须包含有效的网格坐标</li>
* </ul>
*
* @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
) {
/**
* 位置数据(嵌套记录)
*
* <p>用于精确定位任务发生位置
*
* <p>验证规则:
* <ul>
* <li>文字地址必须符合"省-市-区"格式</li>
* <li>网格坐标必须在有效范围内(1-1000)</li>
* <li>经纬度坐标可选,但提供时可提高定位精度</li>
* </ul>
*
* @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;
/**
* 任务创建请求数据传输对象
*
* <p>用于主管直接创建任务,包含任务基本属性和位置信息
*
* <p>业务规则:
* <ul>
* <li>创建后状态为PENDING_ASSIGNMENT(待分配)</li>
* <li>需指定有效的污染类型和严重级别</li>
* <li>位置信息必须包含有效的网格坐标</li>
* </ul>
*
* @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
) {
/**
* 位置数据(嵌套记录)
*
* <p>用于精确定位任务发生位置
*
* <p>验证规则:
* <ul>
* <li>文字地址必须符合"省-市-区"格式</li>
* <li>网格坐标必须在有效范围内(1-1000)</li>
* <li>经纬度坐标可选,但提供时可提高定位精度</li>
* </ul>
*
* @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
) {}
}

View File

@@ -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;
/**
* 任务数据传输对象
*
* <p>该类用于封装任务的基本信息包含任务ID、标题、描述和状态等核心数据。
* 主要用于任务列表展示和简单数据传输场景。</p>
*
* @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;
/**
* 任务数据传输对象
*
* <p>该类用于封装任务的基本信息包含任务ID、标题、描述和状态等核心数据。
* 主要用于任务列表展示和简单数据传输场景。</p>
*
* @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;
}

View File

@@ -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
*
* <p>包含任务的完整详细信息,用于详情展示
*
* @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<TaskHistoryDTO> 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<String> 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<AttachmentDTO> 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
*
* <p>包含任务的完整详细信息,用于详情展示
*
* @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<TaskHistoryDTO> 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<String> 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<AttachmentDTO> attachments
) {}
}

View File

@@ -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;
/**
* 从反馈创建任务请求数据传输对象
*
* <p>用于主管将公众反馈转化为可分配的任务,包含任务基本属性和优先级设置
*
* <p>业务规则:
* <ul>
* <li>反馈状态必须为PENDING_REVIEW(待审核)</li>
* <li>创建后原反馈状态自动更新为PROCESSED(已处理)</li>
* <li>新任务初始状态为PENDING_ASSIGNMENT(待分配)</li>
* <li>任务标题需简明描述问题本质</li>
* <li>严重级别影响任务分配优先级和响应时限</li>
* </ul>
*
* @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 {
/**
* 任务标题
*
* <p>必填字段,用于简要描述任务内容
* <p>验证规则:
* <ul>
* <li>不能为空(@NotBlank)</li>
* <li>最大长度255字符(JPA约束)</li>
* <li>建议包含问题类型和位置关键词</li>
* </ul>
*/
@NotBlank(message = "任务标题不能为空")
private String title;
/**
* 严重级别
*
* <p>必填字段,用于确定任务处理优先级和响应时限
* <p>可选值:
* <ul>
* <li>LOW - 低优先级(72小时内处理)</li>
* <li>MEDIUM - 中优先级(48小时内处理)</li>
* <li>HIGH - 高优先级(24小时内处理)</li>
* <li>CRITICAL - 紧急(立即处理)</li>
* </ul>
* @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;
/**
* 从反馈创建任务请求数据传输对象
*
* <p>用于主管将公众反馈转化为可分配的任务,包含任务基本属性和优先级设置
*
* <p>业务规则:
* <ul>
* <li>反馈状态必须为PENDING_REVIEW(待审核)</li>
* <li>创建后原反馈状态自动更新为PROCESSED(已处理)</li>
* <li>新任务初始状态为PENDING_ASSIGNMENT(待分配)</li>
* <li>任务标题需简明描述问题本质</li>
* <li>严重级别影响任务分配优先级和响应时限</li>
* </ul>
*
* @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 {
/**
* 任务标题
*
* <p>必填字段,用于简要描述任务内容
* <p>验证规则:
* <ul>
* <li>不能为空(@NotBlank)</li>
* <li>最大长度255字符(JPA约束)</li>
* <li>建议包含问题类型和位置关键词</li>
* </ul>
*/
@NotBlank(message = "任务标题不能为空")
private String title;
/**
* 严重级别
*
* <p>必填字段,用于确定任务处理优先级和响应时限
* <p>可选值:
* <ul>
* <li>LOW - 低优先级(72小时内处理)</li>
* <li>MEDIUM - 中优先级(48小时内处理)</li>
* <li>HIGH - 高优先级(24小时内处理)</li>
* <li>CRITICAL - 紧急(立即处理)</li>
* </ul>
* @see com.dne.ems.model.enums.SeverityLevel 完整枚举定义
*/
@NotNull(message = "严重级别不能为空")
private SeverityLevel severityLevel;
}

View File

@@ -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) {}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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
) {}

View File

@@ -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
) {}

View File

@@ -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;
/**
* 任务摘要数据传输对象
*
* <p>该类用于封装任务的摘要信息,主要用于任务列表展示和简单数据传输场景。
* 包含任务的基本标识、状态、分配信息和位置等核心数据。</p>
*
* @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;
/**
* 任务摘要数据传输对象
*
* <p>该类用于封装任务的摘要信息,主要用于任务列表展示和简单数据传输场景。
* 包含任务的基本标识、状态、分配信息和位置等核心数据。</p>
*
* @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
) {}

View File

@@ -1,55 +1,55 @@
package com.dne.ems.dto;
/**
* 趋势数据点传输对象
*
* <p>该类用于表示时间序列趋势图中的单个数据点,主要用于环境数据趋势分析和可视化展示。
* 包含时间标识(通常为年月格式)和对应的数值(如超标次数)。</p>
*
* @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;
/**
* 趋势数据点传输对象
*
* <p>该类用于表示时间序列趋势图中的单个数据点,主要用于环境数据趋势分析和可视化展示。
* 包含时间标识(通常为年月格式)和对应的数值(如超标次数)。</p>
*
* @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;
}
}

View File

@@ -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
) {
}

View File

@@ -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
) {}

View File

@@ -1,55 +1,55 @@
package com.dne.ems.dto;
import com.dne.ems.model.UserAccount;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户信息数据传输对象
*
* <p>该类用于封装用户基本信息包含用户ID、姓名、电话和邮箱等核心数据。
* 主要用于在不暴露敏感信息的情况下,向前端传递用户信息。</p>
*
* <p>提供了从用户实体(UserAccount)转换为DTO的静态方法便于服务层使用。</p>
*
* @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;
/**
* 用户信息数据传输对象
*
* <p>该类用于封装用户基本信息包含用户ID、姓名、电话和邮箱等核心数据。
* 主要用于在不暴露敏感信息的情况下,向前端传递用户信息。</p>
*
* <p>提供了从用户实体(UserAccount)转换为DTO的静态方法便于服务层使用。</p>
*
* @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());
}
}

View File

@@ -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
) {}

View File

@@ -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;
}

View File

@@ -1,28 +1,28 @@
package com.dne.ems.dto;
import com.dne.ems.model.enums.Role;
import com.dne.ems.model.enums.UserStatus;
/**
* 用户摘要数据传输对象
*
* <p>该类用于封装用户的摘要信息,主要用于用户列表展示和简单数据传输场景。
* 包含用户的基本标识、个人信息、角色和状态等核心数据。</p>
*
* @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;
/**
* 用户摘要数据传输对象
*
* <p>该类用于封装用户的摘要信息,主要用于用户列表展示和简单数据传输场景。
* 包含用户的基本标识、个人信息、角色和状态等核心数据。</p>
*
* @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
) {}

View File

@@ -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<String> 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<String> skills,
Double currentLatitude,
Double currentLongitude
) {}

View File

@@ -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
*
* <p>该类封装了向火山引擎AI聊天服务发送请求所需的所有数据结构。
* 包括模型名称、消息列表和工具列表等核心组件。</p>
*
* @version 1.0
* @since 2025-06
*/
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class VolcanoChatRequest {
/**
* 要使用的AI模型名称
*/
private String model;
/**
* 对话消息列表,包含用户和系统的对话历史
*/
private List<Message> messages;
/**
* 可用工具列表定义AI可以调用的函数
*/
private List<Tool> tools;
/**
* 表示对话中的一条消息
* <p>包含角色信息和内容部分列表</p>
*/
@Data
@SuperBuilder
public static class Message {
private String role;
private List<ContentPart> content;
}
/**
* 内容部分接口,用于定义消息内容的不同类型
* <p>作为TextContentPart和ImageUrlContentPart的共同父接口</p>
*/
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<String> 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
*
* <p>该类封装了向火山引擎AI聊天服务发送请求所需的所有数据结构。
* 包括模型名称、消息列表和工具列表等核心组件。</p>
*
* @version 1.0
* @since 2025-06
*/
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class VolcanoChatRequest {
/**
* 要使用的AI模型名称
*/
private String model;
/**
* 对话消息列表,包含用户和系统的对话历史
*/
private List<Message> messages;
/**
* 可用工具列表定义AI可以调用的函数
*/
private List<Tool> tools;
/**
* 表示对话中的一条消息
* <p>包含角色信息和内容部分列表</p>
*/
@Data
@SuperBuilder
public static class Message {
private String role;
private List<ContentPart> content;
}
/**
* 内容部分接口,用于定义消息内容的不同类型
* <p>作为TextContentPart和ImageUrlContentPart的共同父接口</p>
*/
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<String> required;
}
}

View File

@@ -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
*
* <p>该类封装了从火山引擎AI聊天服务接收到的响应数据结构。
* 包含AI生成的回复内容和可能的工具调用信息。</p>
*
* @version 1.0
* @since 2025-06
*/
@Data
@NoArgsConstructor
public class VolcanoChatResponse {
private List<Choice> 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<ToolCall> 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
*
* <p>该类封装了从火山引擎AI聊天服务接收到的响应数据结构。
* 包含AI生成的回复内容和可能的工具调用信息。</p>
*
* @version 1.0
* @since 2025-06
*/
@Data
@NoArgsConstructor
public class VolcanoChatResponse {
private List<Choice> 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<ToolCall> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show More