This commit is contained in:
ChuXun
2025-10-25 19:18:43 +08:00
parent 4ce487588a
commit 02a830145e
3971 changed files with 1549956 additions and 2 deletions

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

110
README.md Normal file
View File

@@ -0,0 +1,110 @@
# 环境监督系统 (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
├── ems-frontend/ # 前端 Vue.js 项目
│ └── ems-monitoring-system/
│ ├── src/
│ ├── vite.config.ts
│ └── 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. **进入前端目录**:
```bash
cd ems-frontend/ems-monitoring-system
```
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

@@ -0,0 +1,57 @@
# EMS后端系统技术图表文档
## 项目概述
环境管理系统(EMS)后端是一个基于Spring Boot的Java应用程序用于处理环境问题反馈、任务分配和用户管理。系统采用分层架构包含控制器、服务、仓库和模型层。
## 1. 系统时序图
- **见时序图.md**
## 2. UML类图
- **见UML类图.md**
## 3. ER图
- **见ER图.md**
## 4. 系统架构说明
### 4.1 分层架构
- **控制器层(Controller)**: 处理HTTP请求参数验证响应格式化
- **服务层(Service)**: 业务逻辑处理,事务管理
- **仓库层(Repository)**: 数据访问,数据库操作
- **模型层(Model)**: 实体定义,数据结构
### 4.2 核心业务流程
1. **用户认证**: JWT令牌生成和验证
2. **反馈管理**: 环境问题反馈的提交、审核、处理
3. **任务分配**: 基于反馈创建任务并分配给网格工作人员
4. **状态跟踪**: 反馈和任务状态的生命周期管理
### 4.3 **安全机制**
- 基于角色的访问控制(RBAC)
- JWT令牌认证
- 密码加密存储
- 操作日志记录
### 4.4 数据完整性
- 外键约束确保数据一致性
- 唯一约束防止重复数据
- 枚举类型确保状态值有效性
- 时间戳记录数据变更历史
## 5. 技术栈
- **框架**: Spring Boot 3.x
- **数据库**: MySQL/PostgreSQL
- **ORM**: Spring Data JPA + Hibernate
- **安全**: Spring Security + JWT
- **文档**: Swagger/OpenAPI 3
- **构建工具**: Maven
- **Java版本**: JDK 17+
---
*本文档基于EMS后端项目代码分析生成包含完整的系统设计图表可用于系统理解、开发指导和文档维护。*

648
Report/ER图.md Normal file
View File

@@ -0,0 +1,648 @@
## 3. ER图
```mermaid
erDiagram
USER_ACCOUNT {
BIGINT id PK
VARCHAR username
VARCHAR password
VARCHAR email
VARCHAR phone
BIGINT role_id FK
BIGINT grid_id FK
}
ROLE {
BIGINT id PK
VARCHAR name
}
FEEDBACK {
BIGINT id PK
TEXT content
VARCHAR location
VARCHAR status
BIGINT submitter_id FK
}
TASK {
BIGINT id PK
TEXT description
VARCHAR status
BIGINT feedback_id FK
BIGINT assignee_id FK
}
GRID {
BIGINT id PK
INT grid_x
INT grid_y
}
OPERATION_LOG {
BIGINT id PK
VARCHAR operation_type
TEXT details
BIGINT operator_id FK
}
ATTACHMENT {
BIGINT id PK
BIGINT feedback_id FK
VARCHAR file_url
}
USER_ACCOUNT ||--o{ ROLE : "has"
USER_ACCOUNT ||--o{ FEEDBACK : "submits"
USER_ACCOUNT ||--o{ TASK : "assigned"
USER_ACCOUNT ||--o{ GRID : "belongs to"
FEEDBACK ||--o{ TASK : "leads to"
USER_ACCOUNT ||--o{ OPERATION_LOG : "performs"
FEEDBACK ||--o{ ATTACHMENT : "has"
```
participant Database as 数据库
Admin->>+OperationLogController: GET /api/logs (过滤条件)
OperationLogController->>+OperationLogService: queryOperationLogs(filters)
OperationLogService->>+OperationLogRepository: findByCriteria(filters)
OperationLogRepository->>+Database: SELECT * FROM operation_logs WHERE ...
Database-->>-OperationLogRepository: 返回日志记录
OperationLogRepository-->>-OperationLogService: 返回日志DTO列表
OperationLogService-->>-OperationLogController: 返回日志DTO列表
OperationLogController-->>-Admin: 200 OK (日志列表)
```
### 1.16 管理员创建新用户
```mermaid
sequenceDiagram
participant Admin as 管理员
participant PersonnelController as 人员控制器
participant PersonnelService as 人员服务
participant UserAccountRepository as 用户账户仓库
participant PasswordEncoder as 密码编码器
participant Database as 数据库
Admin->>+PersonnelController: POST /api/personnel/users (用户信息)
PersonnelController->>+PersonnelService: createUser(request)
PersonnelService->>+UserAccountRepository: findByUsername(username)
UserAccountRepository-->>-PersonnelService: (检查用户是否存在)
PersonnelService->>+PasswordEncoder: encode(password)
PasswordEncoder-->>-PersonnelService: 返回加密后的密码
PersonnelService->>+UserAccountRepository: save(user)
UserAccountRepository->>+Database: INSERT INTO user_accounts
Database-->>-UserAccountRepository: 返回已保存的用户
UserAccountRepository-->>-PersonnelService: 返回已保存的用户
PersonnelService-->>-PersonnelController: 返回创建的用户
PersonnelController-->>-Admin: 201 CREATED (用户详情)
```
### 1.17 用户获取自己的反馈历史
```mermaid
sequenceDiagram
participant User as 用户
participant ProfileController as 个人资料控制器
participant UserFeedbackService as 用户反馈服务
participant CustomUserDetails as 用户认证详情
participant Database as 数据库
User->>+ProfileController: GET /api/me/feedback
ProfileController->>+CustomUserDetails: 获取用户ID
CustomUserDetails-->>-ProfileController: 返回用户ID
ProfileController->>+UserFeedbackService: getFeedbackHistoryByUserId(userId, pageable)
UserFeedbackService->>+Database: SELECT * FROM feedback WHERE user_id = ?
Database-->>-UserFeedbackService: 返回反馈记录
UserFeedbackService-->>-ProfileController: 返回反馈摘要DTO列表
ProfileController-->>-User: 200 OK (反馈历史列表)
```
### 1.18 公众提交反馈
```mermaid
sequenceDiagram
participant Public as 公众
participant PublicController as 公共控制器
participant FeedbackService as 反馈服务
participant FileStorageService as 文件存储服务
participant FeedbackRepository as 反馈仓库
participant Database as 数据库
Public->>+PublicController: POST /api/public/feedback (反馈信息和文件)
PublicController->>+FeedbackService: createPublicFeedback(request, files)
alt 如果有文件
FeedbackService->>+FileStorageService: store(file)
FileStorageService-->>-FeedbackService: 返回文件路径
end
FeedbackService->>+FeedbackRepository: save(feedback)
FeedbackRepository->>+Database: INSERT INTO feedback
Database-->>-FeedbackRepository: 返回已保存的反馈
FeedbackRepository-->>-FeedbackService: 返回已保存的反馈
FeedbackService-->>-PublicController: 返回创建的反馈
PublicController-->>-Public: 201 CREATED (反馈详情)
```
### 1.19 分配任务给工作人员
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant TaskAssignmentController as 任务分配控制器
participant TaskAssignmentService as 任务分配服务
participant AssignmentRepository as 分配仓库
participant FeedbackRepository as 反馈仓库
participant Database as 数据库
Supervisor->>+TaskAssignmentController: POST /api/tasks/assign (任务ID, 分配对象ID)
TaskAssignmentController->>+TaskAssignmentService: assignTask(feedbackId, assigneeId, assignerId)
TaskAssignmentService->>+FeedbackRepository: findById(feedbackId)
FeedbackRepository-->>-TaskAssignmentService: 返回任务详情
TaskAssignmentService->>+AssignmentRepository: save(assignment)
AssignmentRepository->>+Database: INSERT INTO assignments
Database-->>-AssignmentRepository: 返回已保存的分配
AssignmentRepository-->>-TaskAssignmentService: 返回已保存的分配
TaskAssignmentService-->>-TaskAssignmentController: 返回新的分配
TaskAssignmentController-->>-Supervisor: 200 OK (分配详情)
```
### 1.20 从反馈创建任务
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant TaskManagementController as 任务管理控制器
participant TaskManagementService as 任务管理服务
participant FeedbackRepository as 反馈仓库
participant TaskRepository as 任务仓库
participant Database as 数据库
Supervisor->>+TaskManagementController: POST /api/management/tasks/feedback/{feedbackId}/create-task (任务信息)
TaskManagementController->>+TaskManagementService: createTaskFromFeedback(feedbackId, request)
TaskManagementService->>+FeedbackRepository: findById(feedbackId)
FeedbackRepository-->>-TaskManagementService: 返回反馈详情
TaskManagementService->>+TaskRepository: save(task)
TaskRepository->>+Database: INSERT INTO tasks
Database-->>-TaskRepository: 返回已保存的任务
TaskRepository-->>-TaskManagementService: 返回已保存的任务
TaskManagementService-->>-TaskManagementController: 返回创建的任务详情DTO
TaskManagementController-->>-Supervisor: 201 CREATED (任务详情)
```
## 2. UML类图
### 2.1 核心领域模型类图
```mermaid
classDiagram
class UserAccount {
-Long id
-String name
-String phone
-String email
-String password
-Gender gender
-Role role
-UserStatus status
-Level level
-String department
-List~String~ skills
-LocalDateTime createdAt
-LocalDateTime updatedAt
+login() boolean
+updateProfile() void
+changePassword() void
}
class Feedback {
-Long id
-String eventId
-String title
-String description
-PollutionType pollutionType
-SeverityLevel severityLevel
-FeedbackStatus status
-String location
-Double latitude
-Double longitude
-String cityName
-String districtName
-UserAccount submitter
-LocalDateTime createdAt
-LocalDateTime updatedAt
+updateStatus() void
+assignToTask() Task
}
class Task {
-Long id
-Feedback feedback
-UserAccount assignee
-UserAccount createdBy
-TaskStatus status
-String title
-String description
-String location
-LocalDateTime assignedAt
-LocalDateTime completedAt
-LocalDateTime createdAt
+assign(UserAccount) void
+complete() void
+updateProgress() void
}
class Assignment {
-Long id
-Task task
-UserAccount assigner
-LocalDateTime assignmentTime
-LocalDateTime deadline
-AssignmentStatus status
-String remarks
+setDeadline() void
+updateStatus() void
}
class Grid {
-Long id
-Integer gridX
-Integer gridY
-String cityName
-String districtName
-String description
-Boolean isObstacle
+getCoordinates() Point
+isAccessible() boolean
}
class Attachment {
-Long id
-String fileName
-String filePath
-String fileType
-Long fileSize
-LocalDateTime uploadedAt
}
class TaskHistory {
-Long id
-Task task
-UserAccount updatedBy
-TaskStatus oldStatus
-TaskStatus newStatus
-String remarks
-LocalDateTime updatedAt
}
%% 枚举类
class Role {
<<enumeration>>
PUBLIC_SUPERVISOR
SUPERVISOR
GRID_WORKER
ADMIN
DECISION_MAKER
}
class FeedbackStatus {
<<enumeration>>
PENDING_REVIEW
AI_REVIEWING
AI_PROCESSING
PENDING_ASSIGNMENT
ASSIGNED
CONFIRMED
RESOLVED
REJECTED
}
class TaskStatus {
<<enumeration>>
PENDING_ASSIGNMENT
ASSIGNED
IN_PROGRESS
SUBMITTED
COMPLETED
CANCELLED
}
class PollutionType {
<<enumeration>>
AIR
WATER
SOIL
NOISE
WASTE
OTHER
}
class SeverityLevel {
<<enumeration>>
LOW
MEDIUM
HIGH
CRITICAL
}
%% 关系
UserAccount "1" -- "0..*" Feedback : submits
UserAccount "1" -- "0..*" Task : assigned_to
UserAccount "1" -- "0..*" Task : created_by
UserAccount "1" -- "0..*" Assignment : assigns
Feedback "1" -- "1" Task : generates
Task "1" -- "1" Assignment : has
Task "1" -- "0..*" TaskHistory : logs
Feedback "1" -- "0..*" Attachment : has
UserAccount -- Role
Feedback -- FeedbackStatus
Feedback -- PollutionType : categorized_as
Feedback -- SeverityLevel : rated_as
Task -- TaskStatus"}]}}}
```
### 2.2 控制器层类图
```mermaid
classDiagram
class AuthController {
-AuthService authService
-VerificationCodeService verificationCodeService
+signUp(SignUpRequest) ResponseEntity
+signIn(LoginRequest) ResponseEntity
+logout() ResponseEntity
+sendVerificationCode(String) ResponseEntity
+requestPasswordReset(String) ResponseEntity
+resetPassword(PasswordResetRequest) ResponseEntity
}
class FeedbackController {
-FeedbackService feedbackService
+submitFeedback(FeedbackSubmissionRequest, MultipartFile[]) ResponseEntity
+submitFeedbackJson(FeedbackSubmissionRequest) ResponseEntity
+submitPublicFeedback(PublicFeedbackRequest, MultipartFile[]) ResponseEntity
+getAllFeedback(filters, Pageable) ResponseEntity
+getFeedbackById(Long) ResponseEntity
+getFeedbackStats(filters) ResponseEntity
+processFeedback(Long, ProcessFeedbackRequest) ResponseEntity
}
class UserController {
-UserService userService
+getCurrentUser() ResponseEntity
+updateProfile(UserProfileUpdateRequest) ResponseEntity
+getAllUsers(Pageable) ResponseEntity
+getUserById(Long) ResponseEntity
+updateUserRole(Long, Role) ResponseEntity
+deactivateUser(Long) ResponseEntity
}
%% 服务层接口
class AuthService {
<<interface>>
+registerUser(SignUpRequest) void
+signIn(LoginRequest) JwtAuthenticationResponse
+logout() void
+requestPasswordReset(String) void
+resetPassword(String, String) void
}
class FeedbackService {
<<interface>>
+submitFeedback(FeedbackSubmissionRequest, MultipartFile[]) Feedback
+getAllFeedback(filters, Pageable) Page~Feedback~
+getFeedbackById(Long) Feedback
+processFeedback(Long, ProcessFeedbackRequest) Feedback
+getFeedbackStats(filters) FeedbackStatsResponse
}
%% 关系
AuthController --> AuthService : uses
FeedbackController --> FeedbackService : uses
UserController --> UserService : uses
```
### 2.3 服务层实现类图
```mermaid
classDiagram
class AuthServiceImpl {
-UserRepository userRepository
-PasswordEncoder passwordEncoder
-JwtService jwtService
-VerificationCodeService verificationCodeService
+registerUser(SignUpRequest) void
+signIn(LoginRequest) JwtAuthenticationResponse
+logout() void
+requestPasswordReset(String) void
+resetPassword(String, String) void
-validateUser(UserAccount) void
-generateJwtToken(UserAccount) String
}
class FeedbackServiceImpl {
-FeedbackRepository feedbackRepository
-UserRepository userRepository
-ApplicationEventPublisher eventPublisher
-FileStorageService fileStorageService
+submitFeedback(FeedbackSubmissionRequest, MultipartFile[]) Feedback
+getAllFeedback(filters, Pageable) Page~Feedback~
+getFeedbackById(Long) Feedback
+processFeedback(Long, ProcessFeedbackRequest) Feedback
-generateEventId() String
-validateFeedbackData(FeedbackSubmissionRequest) void
}
class TaskServiceImpl {
-TaskRepository taskRepository
-UserRepository userRepository
-AssignmentRepository assignmentRepository
+createTask(TaskCreationRequest) Task
+assignTask(Long, Long) Assignment
+updateTaskStatus(Long, TaskStatus) Task
+getTasksByAssignee(Long, Pageable) Page~Task~
-validateTaskAssignment(Task, UserAccount) void
}
%% 接口实现关系
AuthServiceImpl ..|> AuthService : implements
FeedbackServiceImpl ..|> FeedbackService : implements
TaskServiceImpl ..|> TaskService : implements
```
## 3. 数据库ER图
```mermaid
erDiagram
USER_ACCOUNT {
bigint id PK
varchar name
varchar phone UK
varchar email UK
varchar password
enum gender
enum role
enum status
enum level
varchar department
text skills
datetime created_at
datetime updated_at
}
FEEDBACK {
bigint id PK
varchar event_id UK
varchar title
text description
enum pollution_type
enum severity_level
enum status
varchar location
decimal latitude
decimal longitude
varchar city_name
varchar district_name
bigint submitter_id FK
datetime created_at
datetime updated_at
}
TASK {
bigint id PK
bigint feedback_id FK
bigint assignee_id FK
bigint created_by FK
enum status
varchar title
text description
varchar location
decimal latitude
decimal longitude
datetime assigned_at
datetime completed_at
datetime created_at
datetime updated_at
}
ASSIGNMENT {
bigint id PK
bigint task_id FK
bigint assigner_id FK
datetime assignment_time
datetime deadline
enum status
text remarks
datetime created_at
datetime updated_at
}
GRID {
bigint id PK
int gridx
int gridy
varchar city_name
varchar district_name
text description
boolean is_obstacle
}
ATTACHMENT {
bigint id PK
bigint feedback_id FK
varchar file_name
varchar file_path
varchar file_type
bigint file_size
datetime uploaded_at
}
OPERATION_LOG {
bigint id PK
bigint user_id FK
varchar operation_type
varchar target_type
bigint target_id
text description
varchar ip_address
datetime created_at
}
VERIFICATION_CODE {
bigint id PK
varchar email
varchar code
enum type
datetime expires_at
boolean used
datetime created_at
}
TASK_HISTORY {
bigint id PK
bigint task_id FK
bigint updated_by_id FK
enum old_status
enum new_status
text remarks
datetime updated_at
}
%% 关系定义
USER_ACCOUNT ||--o{ FEEDBACK : submits
USER_ACCOUNT ||--o{ TASK : assigned_to
USER_ACCOUNT ||--o{ TASK : created_by
USER_ACCOUNT ||--o{ ASSIGNMENT : assigns
USER_ACCOUNT ||--o{ OPERATION_LOG : performs
FEEDBACK ||--|| TASK : generates
FEEDBACK ||--o{ ATTACHMENT : has
TASK ||--|| ASSIGNMENT : has
TASK ||--o{ TASK_HISTORY : logs
VERIFICATION_CODE }o--|| USER_ACCOUNT : sent_to
USER_ACCOUNT ||--o{ TASK_HISTORY : updates
```
## 4. 系统架构说明
### 4.1 分层架构
- **控制器层(Controller)**: 处理HTTP请求参数验证响应格式化
- **服务层(Service)**: 业务逻辑处理,事务管理
- **仓库层(Repository)**: 数据访问,数据库操作
- **模型层(Model)**: 实体定义,数据结构
### 4.2 核心业务流程
1. **用户认证**: JWT令牌生成和验证
2. **反馈管理**: 环境问题反馈的提交、审核、处理
3. **任务分配**: 基于反馈创建任务并分配给网格工作人员
4. **状态跟踪**: 反馈和任务状态的生命周期管理
### 4.3 安全机制
- 基于角色的访问控制(RBAC)
- JWT令牌认证
- 密码加密存储
- 操作日志记录
### 4.4 数据完整性
- 外键约束确保数据一致性
- 唯一约束防止重复数据
- 枚举类型确保状态值有效性
- 时间戳记录数据变更历史
## 5. 技术栈
- **框架**: Spring Boot 3.x
- **数据库**: MySQL/PostgreSQL
- **ORM**: Spring Data JPA + Hibernate
- **安全**: Spring Security + JWT
- **文档**: Swagger/OpenAPI 3
- **构建工具**: Maven
- **Java版本**: JDK 17+
---
*本文档基于EMS后端项目代码分析生成包含完整的系统设计图表可用于系统理解、开发指导和文档维护。*

159
Report/UML类图.md Normal file
View File

@@ -0,0 +1,159 @@
```
@startuml
class UserAccount {
- Long id
- String name
- String email
- Role role
- UserStatus status
- Integer gridX
- Integer gridY
+ isAccountLocked(): boolean
+ hasSkill(String skill): boolean
}
enum Role { ADMIN, SUPERVISOR, GRID_WORKER }
enum UserStatus { ACTIVE, INACTIVE, SUSPENDED }
UserAccount *-- Role
UserAccount *-- UserStatus
@enduml
```
@startuml
class Feedback {
- Long id
- String eventId
- String title
- PollutionType pollutionType
- SeverityLevel severityLevel
- FeedbackStatus status
- Long submitterId
+ updateStatus(newStatus)
+ isHighPriority(): boolean
}
class Attachment {
+ id: Long
+ fileName: String
+ fileType: String
+ storedFileName: String
}
enum FeedbackStatus { PENDING_REVIEW, PROCESSED, AI_REJECTED, TASK_CREATED, CLOSED }
Feedback "1" -- "*" Attachment : has
Feedback *-- FeedbackStatus
UserAccount "1" -- "*" Feedback : submits
@enduml
@startuml
class Task {
- Long id
- Long feedbackId
- Long assigneeId
- Long creatorId
- TaskStatus status
- SeverityLevel priority
+ assignTo(userId)
+ markAsCompleted()
}
class Assignment {
- Long id
- Long taskId
- Long assigneeId
- AssignmentStatus status
+ accept()
}
class TaskHistory {
- Long id
- Task task
- TaskStatus oldStatus
- TaskStatus newStatus
- UserAccount changedBy
}
enum TaskStatus { CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED, CANCELED }
Task "1" -- "1" Feedback : created_from
Task "1" -- "*" Assignment
Task "1" -- "*" TaskHistory
UserAccount "1" -- "*" Task : assigned_to
Task *-- TaskStatus
@enduml
@startuml
class Grid {
- Long id
- Integer gridX
- Integer gridY
- String cityName
- boolean isObstacle
}
class MapGrid {
- Long id
- int x
- int y
- boolean isObstacle
- String terrainType
}
@enduml
```
@startuml
class AqiData {
- Long id
- Grid grid
- Long reporterId
- Integer aqiValue
- String primaryPollutant
}
class AqiRecord {
- Long id
- String cityName
- Integer aqiValue
- LocalDateTime recordTime
}
class PollutantThreshold {
- Long id
- String pollutantName
- Double threshold
- String unit
}
@enduml
```
@startuml
class OperationLog {
- Long id
- UserAccount user
- OperationType operationType
- String description
- LocalDateTime createdAt
}
class PasswordResetToken {
- Long id
- String token
- UserAccount user
- LocalDateTime expiryDate
+ isExpired(): boolean
}
class TaskSubmission {
- Long id
- Task task
- String notes
- LocalDateTime submittedAt
}
class AssignmentRecord {
- Long id
- Feedback feedback
- UserAccount gridWorker
- UserAccount admin
- AssignmentMethod assignmentMethod
}
@enduml

View File

@@ -0,0 +1,30 @@
# ems-backend 项目改进建议 (面向学习者)
`ems-backend`项目当前已具备良好的架构和完整的功能。为了在此基础上进一步深化对核心技术的理解和应用,可以从以下几个方面对项目进行优化与改进。这些建议旨在提供清晰、可行的学习路径,以巩固和扩展现有知识。
### 1. 邮件通知服务的增强
- **当前实现**: 邮件服务在主业务流程中同步执行且邮件内容以字符串形式硬编码在Java代码中。
- **分析与评估**: 同步发送可能因网络延迟而阻塞API响应影响用户体验。硬编码的内容使得文案修改需要重新编译和部署整个应用缺乏灵活性。
- **改进建议**:
- **实现异步发送**: 将邮件发送操作置于独立的线程中执行。可以利用Java内置的`ExecutorService`线程池来管理这些后台任务,从而使主线程能够立即响应用户请求。
- **引入模板引擎**: 采用如`Thymeleaf``Freemarker`等模板引擎。将邮件内容定义为外部HTML模板文件Java代码仅负责传递动态数据如验证码。这样内容与逻辑得以分离修改邮件样式和文案将变得非常方便。
### 2. 安全策略的强化
- **当前实现**: 系统已具备基于JWT的用户认证和角色授权。
- **分析与评估**: 当前实现未对认证接口的请求频率进行限制,存在被自动化脚本进行暴力破解的风险。
- **改进建议**:
- **实现登录尝试限制**: 开发一个`LoginAttemptService`用于追踪特定用户或IP地址在单位时间内的登录失败次数。当失败次数超过预设阈值例如5次/分钟),可暂时锁定账户或要求输入验证码,有效防御暴力破解攻击。
### 3. 前端数据同步的优化
- **当前实现**: 前端获取最新数据依赖于用户的手动刷新操作。
- **分析与评估**: 在任务分配、状态变更等场景下,信息的被动更新会导致用户感知的延迟。
- **改进建议**:
- **实现客户端轮询**: 在前端关键页面(如任务列表)采用定时轮询机制。通过`setInterval`等JavaScript函数每隔一个固定时间如10-30秒自动向后端请求最新数据并更新视图。这是实现准实时数据同步的最直接、最易于理解的方式。
### 4. API接口的健壮性提升
- **当前实现**: 主要通过全局异常处理器来捕获和响应错误。
- **分析与评估**: 对于可预期的业务错误(如"用户名已存在"),可以提供更具体、结构化的错误信息,以方便前端进行针对性处理。
- **改进建议**:
- **设计更具体的错误响应**: 在`GlobalExceptionHandler`除了返回HTTP状态码和错误消息外可以为特定的业务异常定义内部错误码`1001`代表"用户已存在")。前端可以根据这个错误码,精确地向用户展示提示信息(如高亮对应的输入框),而不仅仅是弹出一个通用的错误提示。

View File

@@ -0,0 +1,69 @@
# ems-backend 项目不足与可改进之处
`ems-backend`项目当前已经构建了一个功能完善、架构清晰的系统。然而,从一个准生产级应用迈向一个高可用、高扩展、可长期演进的企业级平台的道路上,我们可以在以下几个方面进行深化和改进。这些改进点并非对现有设计的否定,而是基于更高标准的技术展望。
### 1. 持久化层的演进与扩展
当前的JSON文件存储方案虽然巧妙地满足了项目初期的约束但它也是未来系统演进中最先需要被替换的模块。
- **当前状态**: 使用JSON文件作为数据库通过文件锁保证单机并发安全。
- **潜在瓶颈**:
- **性能限制**: 随着数据量增长全文件读写和内存中的过滤、排序将变得极慢无法处理复杂查询如JOIN、聚合分析
- **功能缺失**: 缺乏数据库事务的ACID保证无法确保跨多个文件操作的原子性。
- **扩展性差**: 无法进行水平扩展,是典型的单点瓶颈。
- **改进方向**:
- **迁移至关系型数据库**: 全面采用`PostgreSQL``MySQL`。这将使我们能够利用强大的SQL查询优化器、索引机制、以及成熟的事务管理从根本上解决性能和数据一致性问题。`Repository`层的接口化设计将使这次迁移变得相对平滑。
- **引入分布式缓存**: 集成`Redis`对访问频繁且不常变化的数据如用户信息、配置信息、地图网格数据进行缓存。这将极大降低数据库的读取压力显著提升API响应速度。
### 2. 消息驱动架构的工业化升级
系统内的事件驱动(`ApplicationEventPublisher`)是优秀的单体应用解耦实践,但它无法满足分布式架构的需求。
- **当前状态**: 使用Spring内置的事件机制实现应用内异步解耦。
- **潜在瓶颈**:
- **生命周期绑定**: 事件总线与应用进程绑定,若应用宕机,未处理的事件会丢失。
- **缺乏高级特性**: 不支持持久化、失败重试、延迟消息、死信队列等工业级消息系统特性。
- **无法跨服务**: 无法用于未来可能的微服务化改造。
- **改进方向**:
- **引入专业消息队列(MQ)**: 采用`RabbitMQ`(适用于复杂路由和事务性消息)或`Kafka`(适用于高吞吐量日志和数据流)来替换`ApplicationEventPublisher`
- **带来的优势**: 实现真正的服务间异步通信、流量削峰填谷、保证事件的可靠投递并为未来将通知、AI分析等模块拆分为独立微服务奠定坚实基础。
### 3. 安全体系的纵深防御
当前的安全体系保证了认证和基础授权,但可以构建更深层次的防御。
- **当前状态**: JWT认证 + `@PreAuthorize`角色授权。
- **潜在瓶颈**:
- **缺乏流量防护**: 无法防御针对登录接口的暴力破解或API的滥用攻击。
- **缺乏敏感操作审计**: 对"谁删除了某个重要任务"这类操作缺乏追溯能力。
- **改进方向**:
- **实现API限流(Rate Limiting)**: 使用`Resilience4j``Guava RateLimiter`等库对登录、发送验证码等关键API接口配置精细化的访问速率限制从入口处防止恶意攻击。
- **实现双因素认证(2FA)**: 对管理员、决策者等高权限角色在密码认证之外增加一层基于时间的一次性密码TOTP如Google Authenticator验证实现银行级别的账户安全。
- **构建完善的审计日志**: 创建独立的审计日志系统以AOP切面或事件监听的方式详细记录所有关键写操作谁、在何时、从何IP、做了什么、结果如何确保所有敏感操作都可被追溯和审计。
### 4. 前端体验的现代化与实时化
当前的前后端交互是传统的"请求-响应"模式,可以升级为实时推送模式。
- **当前状态**: 前端通过轮询或手动刷新获取最新数据。
- **潜在瓶颈**: 信息传递有延迟,用户体验不够流畅,尤其是在需要协同工作的场景。
- **改进方向**:
- **引入WebSocket**: 使用`WebSocket`技术,在前后端之间建立长连接,实现双向实时通信。
- **应用场景**: 当一个任务的状态被主管更新后,服务器可以立即将最新的状态**主动推送**给正在查看该任务的网格员的前端界面,无需用户手动刷新。这将极大提升系统的即时性和用户的沉浸式体验。
### 5. 运维与部署的自动化 (DevOps)
这是从"能用"到"好用、可靠"的关键一步。
- **当前状态**: 项目需要开发者手动执行`mvn package`然后通过FTP或SSH将jar包上传到服务器并手动运行。
- **潜在瓶颈**: 部署流程繁琐、耗时、极易因人工操作而出错,无法做到快速迭代和持续交付。
- **改进方向**:
- **容器化**: 使用`Docker``ems-backend`应用及其所有依赖如特定版本的Java环境打包成一个标准化的、可移植的容器镜像。
- **引入CI/CD (持续集成/持续部署)**: 搭建`GitHub Actions``Jenkins`自动化流水线。当代码被推送到主分支后,流水线会自动执行:①编译代码 -> ②运行所有单元测试 -> ③构建Docker镜像 -> ④将镜像推送到镜像仓库 -> ⑤(可选)自动触发部署脚本,更新服务器上的应用。这将实现从代码提交到上线的全流程自动化。
### 6. 通知服务的模板化与多渠道扩展
当前的邮件服务功能正确,但可维护性和扩展性可以进一步提升。
- **当前状态**: 邮件HTML内容在Java代码中硬编码发送过程是同步的。
- **潜在瓶颈**:
- **难以维护**: 每次修改邮件文案都需要修改Java代码并重新部署。
- **性能风险**: 同步发送邮件会阻塞主线程如果邮件服务器响应慢将拖慢API的整体响应时间。
- **改进方向**:
- **引入模板引擎**: 使用`Thymeleaf``Freemarker`将邮件内容定义为独立的HTML模板文件。Java代码只负责传递动态数据如验证码、用户名由模板引擎渲染最终的HTML实现内容与逻辑的分离。
- **异步化发送**: 将`sendHtmlEmail`方法标记为`@Async`,使其在独立的线程池中执行,实现"发布即返回",主业务流程不再等待邮件发送完成。
- **多渠道抽象**: 将`MailService`抽象为更通用的`NotificationService`,并提供`EmailNotificationProvider``SmsNotificationProvider`等多种实现,未来可以根据用户偏好或业务场景,灵活选择通知渠道。

View File

@@ -0,0 +1,44 @@
# ems-backend 项目核心创新点总览
基于对 `ems-backend` 项目源代码的深入分析,该系统在设计与实现上体现了现代化、企业级的工程实践。其核心创新点可总结为以下八个方面:
### 1. 模拟JPA的泛型JSON仓储层
在不引入传统数据库的约束下,项目极具创造性地构建了一套数据持久化框架。
- **泛型设计与类型安全**: 底层的 `JsonStorageService` 通过泛型`<T>`和Jackson的`TypeReference`,实现了对任意实体列表的类型安全读写。
- **精细化并发控制**: 没有采用简单的全局方法锁,而是通过 `ConcurrentHashMap<String, Lock>` 为每个JSON文件分配了独立的`ReentrantLock`,实现了文件级别的并发控制,显著提升了在多文件写入场景下的性能。
- **模拟JPA接口**: `Repository`层定义了与Spring Data JPA高度相似的接口`save`, `findById`, `findAll`),使得上层业务代码可以面向接口编程,与底层的文件存储实现完全解耦,未来可平滑迁移至真实数据库。
### 2. 事件驱动的异步化业务流程
系统广泛采用事件驱动架构EDA来解耦模块、提升系统响应能力。
- **异步化核心流程**: 将反馈提交后的AI审核、任务创建成功后的智能分配等耗时或非核心操作通过发布Spring事件`FeedbackSubmittedForAiReviewEvent`的方式进行异步处理极大缩短了API的响应时间。
- **业务模块高度解耦**: 核心服务(如`FeedbackService`)与下游模块(如`TaskService`, `MailService`)无直接代码依赖。新增业务监听器(如"短信通知")无需修改任何现有业务代码,完美遵循"开闭原则",提高了系统的可维护性和可扩展性。
### 3. 融合多种策略的智能算法
在关键业务场景中,系统应用了智能算法取代了僵硬的业务规则,提升了运营效率和决策科学性。
- **高效的A*寻路算法**: 为网格员规划路径时不仅实现了A*算法,还结合了**优先队列PriorityQueue**进行性能优化,并能**动态加载地图和障碍物信息**,具有很强的适应性。
- **多因素加权的任务推荐算法**: 在分配任务时,系统通过一个异步触发的智能分配服务,综合考虑网格员的**地理距离、当前负载、技能匹配度、历史表现**等多个维度进行加权评分,为任务自动推荐最合适的执行者。
### 4. 声明式、细粒度的安全权限控制
项目的安全体系并非简单的身份认证,而是构建了一套声明式、与业务逻辑分离的权限控制体系。
- **自定义JWT安全过滤链**: 通过自定义的`JwtAuthenticationFilter``SecurityConfig`配置精确控制了JWT的解析、校验及用户权限加载流程。
- **声明式方法级权限**: 在Controller层广泛使用`@PreAuthorize`注解,将权限规则(如`hasRole('ADMIN')`)从业务逻辑中抽离,使安全策略一目了然,易于审计和维护。
### 5. 主动、可定制的业务规则校验体系
系统对所有外部输入都采取"主动防御"姿态,构建了前置的、可扩展的校验体系。
- **分层校验**: 同时使用JSR 303标准注解`@NotNull`, `@Email`)和`@Valid`进行基础校验。
- **自定义业务校验**: 针对复杂业务规则(如密码强度),创建了自定义校验注解(如`@ValidPassword`)及其实现(`PasswordValidator`)。这种方式将复杂的校验逻辑封装成一个可复用的简单注解,极为优雅和高效。
### 6. 统一、健壮的API设计与响应体系
项目的API设计构建了一套完整的、面向开发者的友好体系。
- **全局统一异常处理**: 通过`@ControllerAdvice``@ExceptionHandler`,将所有异常(业务异常、系统异常)集中捕获,并返回结构统一的`ErrorResponseDTO`,极大地简化了前端的错误处理,并避免了向客户端暴露敏感的系统内部信息。
- **面向视图的DTO模式**: 严格区分领域模型Entity和数据传输对象DTO并为不同场景如列表摘要`TaskSummaryDTO` vs. 详情`TaskDetailDTO`提供专属DTO。这确保了API契约的稳定优化了网络传输性能并从根本上杜绝了敏感字段的泄露。
### 7. 安全、分层的媒体文件存储服务
项目实现了一个安全、分层的媒体文件存储服务,以处理用户上传的现场证据。
- **安全优先**: 在存储文件前,通过**生成唯一文件名**防止冲突和覆盖,通过**校验文件类型**防止恶意脚本上传,并通过**规范化路径**防止目录遍历攻击。
- **逻辑与物理分离**: 服务返回的是内部文件名而非直接URL业务层将此文件名与业务实体关联。访问时需通过专门的Controller根据内部文件名加载文件流。此分层架构不仅增强了安全也便于未来将底层存储平滑迁移至云存储如OSS
### 8. 与核心业务解耦的可扩展通知服务
项目构建了一个与核心业务逻辑完全解耦的通知服务体系。
- **事件驱动触发**: 邮件发送(如注册验证码)由业务事件(如`UserRegisteredEvent`)触发,由独立的监听器负责调用邮件服务,实现了业务流程与通知功能的分离。
- **面向接口设计**: 通过定义`MailService`接口使系统不依赖于具体的邮件发送实现。这为未来切换邮件服务商或在测试环境中提供一个模拟实现Mock提供了极大的灵活性无需改动任何业务代码。

354
Report/temp.md Normal file
View File

@@ -0,0 +1,354 @@
```plantuml
@startuml
title 任务核心模型 (Task Core Models)
skinparam classAttributeIconSize 0
class Task {
- Long id
- String title
- String description
- PollutionType pollutionType
- SeverityLevel severityLevel
- TaskStatus status
- String textAddress
- Integer gridX
- Integer gridY
- LocalDateTime assignedAt
- LocalDateTime completedAt
}
class Assignment {
- Long id
- LocalDateTime assignmentTime
- LocalDateTime deadline
- AssignmentStatus status
- String remarks
}
class TaskHistory {
- Long id
- TaskStatus oldStatus
- TaskStatus newStatus
- String comments
- LocalDateTime changedAt
}
class TaskSubmission {
- Long id
- String notes
- LocalDateTime submittedAt
}
' --- Relationships ---
' Task is the central entity
Task "1" *-- "1" Assignment : (has one)
Task "1" *-- "0..*" TaskHistory : (logs)
Task "1" *-- "0..*" TaskSubmission : (has)
' --- External Relationships for Context ---
Task ..> Feedback : (created from)
Task ..> UserAccount : (assignee)
Task ..> UserAccount : (createdBy)
Assignment ..> UserAccount : (assigner)
TaskHistory ..> UserAccount : (changedBy)
TaskSubmission ..> "*" Attachment : (has)
@enduml
@startuml
title 网格与地图模型 (Grid & Map Models)
skinparam classAttributeIconSize 0
class Grid {
- Long id
- Integer gridX
- Integer gridY
- String cityName
- String districtName
- String description
- Boolean isObstacle
}
class MapGrid {
- Long id
- int x
- int y
- String cityName
- boolean isObstacle
- String terrainType
}
@enduml
@startuml
title 数据与日志模型 (Data & Logging Models)
skinparam classAttributeIconSize 0
class AqiData {
- Long id
- Integer aqiValue
- Double pm25
- Double pm10
- String primaryPollutant
- LocalDateTime recordTime
}
class AqiRecord {
- Long id
- String cityName
- Integer aqiValue
- Double pm25
- Double pm10
- LocalDateTime recordTime
}
class OperationLog {
- Long id
- OperationType operationType
- String description
- String targetId
- String targetType
- String ipAddress
- LocalDateTime createdAt
}
class AssignmentRecord {
- Long id
- AssignmentMethod assignmentMethod
- String algorithmDetails
- AssignmentStatus status
- LocalDateTime createdAt
}
' --- Relationships ---
AqiData ..> Grid : (recorded at)
AqiData ..> UserAccount : (reported by)
AqiRecord ..> Grid : (related to)
OperationLog ..> UserAccount : (performed by)
AssignmentRecord ..> Feedback : (for)
AssignmentRecord ..> UserAccount : (assigned to worker)
AssignmentRecord ..> UserAccount : (assigned by admin)
@enduml
@startuml
title 辅助与配置模型 (Utility & Configuration Models)
skinparam classAttributeIconSize 0
class PollutantThreshold {
- Long id
- PollutionType pollutionType
- String pollutantName
- Double threshold
- String unit
}
class PasswordResetToken {
- Long id
- String token
- LocalDateTime expiryDate
..
+ isExpired(): boolean
}
class Attachment {
- Long id
- String fileName
- String fileType
- String storedFileName
- Long fileSize
- LocalDateTime uploadDate
}
' --- Relationships ---
PasswordResetToken ..> UserAccount : (for)
Attachment ..> Feedback : (belongs to)
Attachment ..> TaskSubmission : (belongs to)
@enduml
```
```plantuml
@startuml
title 系统核心业务模型 (Core Business Models)
skinparam classAttributeIconSize 0
enum Gender{
-Male
-Female
-OTHER
}
enum Role{
}
enum UserStatus
enum PollutionType
enum SeverityLevel
enum FeedbackStatus
enum TaskStatus
enum AssignmentStatus
class UserAccount {
- Long id
- String name
- String phone
- String email
- String password
- Gender gender
- Role role
- UserStatus status
- Integer gridX
- Integer gridY
+ isValid()
}
class Feedback {
- Long id
- String eventId
- String title
- PollutionType pollutionType
- SeverityLevel severityLevel
- FeedbackStatus status
- Long submitterId
+ process()
+ approve()
}
class Task {
- Long id
- Long feedbackId
- Long assigneeId
- Long createdBy
- TaskStatus status
- String title
+ assignTo(workerId)
+ complete()
+ approve()
}
class Assignment {
- Long id
- Long taskId
- Long assignerId
- AssignmentStatus status
- String remarks
- LocalDateTime assignmentTime
}
class Grid {
- Long id
- Integer gridX
- Integer gridY
- String cityName
- boolean isObstacle
}
UserAccount "1" *-- "1" Gender
UserAccount "1" *-- "1" Role
UserAccount "1" *-- "1" UserStatus
Feedback "1" *-- "1" PollutionType
Feedback "1" *-- "1" SeverityLevel
Feedback "1" *-- "1" FeedbackStatus
Task "1" *-- "1" TaskStatus
Assignment "1" *-- "1" AssignmentStatus
Task "1" --> "1" Feedback : "generated from"
Task "1" o-- "1" Assignment : "has"
UserAccount "1" --> "0..*" Feedback : "submits"
UserAccount "1" --> "0..*" Task : "creates"
Task "0..*" --o "1" UserAccount : "assigned to"
UserAccount "1" --> "0..*" Assignment : "assigns"
Task "1" ..> "1" Grid : "located in"
UserAccount "1" ..> "1" Grid : "operates in"
@enduml
```
```plantuml
@startuml
title EMS核心业务模型全景图
skinparam classAttributeIconSize 0
' --- Enumeration classes (using stereotype for max compatibility) ---
class Gender <<enumeration>> {
MALE, FEMALE, OTHER
}
class Role <<enumeration>> {
ADMIN, SUPERVISOR, GRID_WORKER
}
class UserStatus <<enumeration>> {
ACTIVE, INACTIVE
}
class PollutionType <<enumeration>> {
AIR, WATER, SOIL, NOISE
}
class SeverityLevel <<enumerationa>> {
LOW, MEDIUM, HIGH, CRITICAL
}
class FeedbackStatus <<enumeration>> {
PENDING_REVIEW, AI_REJECTED, PROCESSED
}
class TaskStatus <<enumeration>> {
CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED
}
class AssignmentStatus <<enumeration>> {
PENDING, ACCEPTED, REJECTED
}
' --- Entity classes ---
class UserAccount
class Feedback
class Task
class Assignment
class Grid
class PasswordResetToken
class Attachment
' --- Relationships ---
UserAccount "1" --> "1" Gender
UserAccount "1" --> "1" Role
UserAccount "1" --> "1" UserStatus
Feedback "1" --> "1" PollutionType
Feedback "1" --> "1" SeverityLevel
Feedback "1" --> "1" FeedbackStatus
Task "1" --> "1" TaskStatus
Assignment "1" --> "1" AssignmentStatus
Task "1" --> "1" Feedback : "generated from"
Task "1" o-- "1" Assignment : "has"
UserAccount "1" --> "0..*" Feedback : "submits"
UserAccount "1" --> "0..*" Task : "creates"
Task "0..*" --o "1" UserAccount : "assigned to"
UserAccount "1" --> "0..*" Assignment : "assigns"
PasswordResetToken "1" --> "1" UserAccount : "for user"
Feedback "1" --> "0..*" Attachment : "has"
Task "1" ..> "1" Grid : "located in"
UserAccount "1" ..> "1" Grid : "operates in"
@enduml
```
```plantnml
```

387
Report/temp1.md Normal file
View File

@@ -0,0 +1,387 @@
```plantuml
@startuml
skinparam classAttributeIconSize 0
' --- Enumeration classes (declaration-only for max compatibility) ---
enum Gender {
MALE,
FEMALE,
OTHER
}
enum Role {
PUBLIC_SUPERVISOR,
SUPERVISOR,
GRID_WORKER,
ADMIN,
DECISION_MAKER
}
enum UserStatus
enum PollutionType
enum SeverityLevel
enum FeedbackStatus
enum TaskStatus
enum AssignmentStatus
enum AssignmentMethod
enum OperationType
' --- Entity classes with full attributes and methods ---
class UserAccount {
- Long id
- String name
- String phone
- String email
- String password
- Gender gender
- Role role
- UserStatus status
- Integer gridX
- Integer gridY
+ isValid()
}
class Feedback {
- Long id
- String eventId
- String title
- PollutionType pollutionType
- SeverityLevel severityLevel
- FeedbackStatus status
- Long submitterId
+ process()
+ approve()
}
class Task {
- Long id
- String title
- String description
- PollutionType pollutionType
- SeverityLevel severityLevel
- TaskStatus status
- String textAddress
- Integer gridX
- Integer gridY
- LocalDateTime assignedAt
- LocalDateTime completedAt
+ assignTo(workerId)
+ complete()
+ approve()
}
class Assignment {
- Long id
- String remarks
- LocalDateTime assignmentTime
- LocalDateTime deadline
- AssignmentStatus status
}
class TaskHistory {
- Long id
- TaskStatus oldStatus
- TaskStatus newStatus
- String comments
- LocalDateTime changedAt
}
class TaskSubmission {
- Long id
- String notes
- LocalDateTime submittedAt
}
class Attachment {
- Long id
- String fileName
- String fileType
- String storedFileName
- Long fileSize
- LocalDateTime uploadDate
}
class Grid {
- Long id
- Integer gridX
- Integer gridY
- String cityName
- String districtName
- String description
- boolean isObstacle
}
class MapGrid {
- Long id
- int x
- int y
- String cityName
- boolean isObstacle
- String terrainType
}
class PasswordResetToken {
- Long id
- String token
- LocalDateTime expiryDate
+ isExpired()
}
class OperationLog {
- Long id
- OperationType operationType
- String description
- String targetId
- String targetType
- String ipAddress
- LocalDateTime createdAt
}
class AqiData {
- Long id
- Integer aqiValue
- Double pm25
- Double pm10
- String primaryPollutant
- LocalDateTime recordTime
}
class AqiRecord {
- Long id
- String cityName
- Integer aqiValue
- Double pm25
- Double pm10
- LocalDateTime recordTime
}
class AssignmentRecord {
- Long id
- AssignmentMethod assignmentMethod
- String algorithmDetails
- AssignmentStatus status
- LocalDateTime createdAt
}
class PollutantThreshold {
- Long id
- PollutionType pollutionType
- String pollutantName
- Double threshold
- String unit
}
' --- Relationships (kept simple for compatibility) ---
UserAccount --> Gender
UserAccount --> Role
UserAccount --> UserStatus
Feedback --> PollutionType
Feedback --> SeverityLevel
Feedback --> FeedbackStatus
Task --> TaskStatus
Assignment --> AssignmentStatus
AssignmentRecord --> AssignmentMethod
OperationLog --> OperationType
PollutantThreshold --> PollutionType
Task --> Feedback
Task -- Assignment
Task -- TaskHistory
Task -- TaskSubmission
Feedback -- Attachment
TaskSubmission -- Attachment
AssignmentRecord --> Feedback
UserAccount --> Feedback
UserAccount --> Task
Task -- UserAccount
Assignment --> UserAccount
TaskHistory --> UserAccount
AssignmentRecord --> UserAccount
OperationLog --> UserAccount
PasswordResetToken --> UserAccount
AqiData --> UserAccount
Task ..> Grid
UserAccount ..> Grid
AqiData --> Grid
AqiRecord --> Grid
@enduml
```
```plantuml
@startuml
title EMS系统模型全景图 (Detailed View)
skinparam classAttributeIconSize 0
' --- Enumeration classes (declaration-only for max compatibility) ---
enum Gender
enum Role
enum UserStatus
enum PollutionType
enum SeverityLevel
enum FeedbackStatus
enum TaskStatus
enum AssignmentStatus
enum AssignmentMethod
enum OperationType
' --- Entity classes with full attributes and methods ---
class UserAccount {
- Long id
- String name
- String phone
- String email
- String password
- Gender gender
- Role role
- UserStatus status
- Integer gridX
- Integer gridY
+ isValid()
}
class Feedback {
- Long id
- String eventId
- String title
- PollutionType pollutionType
- SeverityLevel severityLevel
- FeedbackStatus status
- Long submitterId
+ process()
+ approve()
}
class Task {
- Long id
- String title
- String description
- PollutionType pollutionType
- SeverityLevel severityLevel
- TaskStatus status
- String textAddress
- Integer gridX
- Integer gridY
- LocalDateTime assignedAt
- LocalDateTime completedAt
+ assignTo(workerId)
+ complete()
+ approve()
}
class Assignment {
- Long id
- String remarks
- LocalDateTime assignmentTime
- LocalDateTime deadline
- AssignmentStatus status
}
class TaskHistory {
- Long id
- TaskStatus oldStatus
- TaskStatus newStatus
- String comments
- LocalDateTime changedAt
}
class TaskSubmission {
- Long id
- String notes
- LocalDateTime submittedAt
}
class Attachment {
- Long id
- String fileName
- String fileType
- String storedFileName
- Long fileSize
- LocalDateTime uploadDate
}
class Grid {
- Long id
- Integer gridX
- Integer gridY
- String cityName
- String districtName
- String description
- boolean isObstacle
}
class MapGrid {
- Long id
- int x
- int y
- String cityName
- boolean isObstacle
- String terrainType
}
class PasswordResetToken {
- Long id
- String token
- LocalDateTime expiryDate
+ isExpired()
}
class OperationLog {
- Long id
- OperationType operationType
- String description
- String targetId
- String targetType
- String ipAddress
- LocalDateTime createdAt
}
class AqiData {
- Long id
- Integer aqiValue
- Double pm25
- Double pm10
- String primaryPollutant
- LocalDateTime recordTime
}
class AqiRecord {
- Long id
- String cityName
- Integer aqiValue
- Double pm25
- Double pm10
- LocalDateTime recordTime
}
class AssignmentRecord {
- Long id
- AssignmentMethod assignmentMethod
- String algorithmDetails
- AssignmentStatus status
- LocalDateTime createdAt
}
class PollutantThreshold {
- Long id
- PollutionType pollutionType
- String pollutantName
- Double threshold
- String unit
}
' --- Relationships with Full Details ---
' --- Composition with Enums ---
UserAccount "1" *-- "1" Gender
UserAccount "1" *-- "1" Role
UserAccount "1" *-- "1" UserStatus
Feedback "1" *-- "1" PollutionType
Feedback "1" *-- "1" SeverityLevel
Feedback "1" *-- "1" FeedbackStatus
Task "1" *-- "1" TaskStatus
Assignment "1" *-- "1" AssignmentStatus
AssignmentRecord "1" *-- "1" AssignmentMethod
OperationLog "1" *-- "1" OperationType
PollutantThreshold "1" *-- "1" PollutionType
' --- Aggregation & Association ---
Task "1" --> "1" Feedback : "generated from"
Task "1" o-- "1" Assignment : "has"
Task "1" o-- "0..*" TaskHistory : "logs"
Task "1" o-- "0..*" TaskSubmission : "receives"
Feedback "1" o-- "0..*" Attachment : "has"
TaskSubmission "1" o-- "0..*" Attachment : "has"
AssignmentRecord "1" --> "1" Feedback : "for"
UserAccount "1" --> "0..*" Feedback : "submits"
UserAccount "1" --> "0..*" Task : "creates"
Task "0..*" o-- "1" UserAccount : "assigned to"
Assignment "1" --> "1" UserAccount : "assigned by"
TaskHistory "1" --> "1" UserAccount : "changed by"
AssignmentRecord "1" --> "1" UserAccount : "assigned to"
AssignmentRecord "1" --> "1" UserAccount : "assigned by"
OperationLog "1" --> "1" UserAccount : "performed by"
PasswordResetToken "1" --> "1" UserAccount : "for"
AqiData "1" --> "1" UserAccount : "reported by"
' --- Dependency & Other Associations ---
Task "1" ..> "1" Grid : "located in"
UserAccount "1" ..> "1" Grid : "operates in"
AqiData "1" --> "1" Grid : "recorded at"
AqiRecord "1" --> "1" Grid : "related to"
@enduml
```

77
Report/temp3.md Normal file
View File

@@ -0,0 +1,77 @@
以下是所有API的URL
### 基础URL
`http://localhost:8080`
### 认证模块 (/api/auth)
- `/api/auth/login`
- `/api/auth/signup`
- `/api/auth/logout`
- `/api/auth/send-verification-code`
- `/api/auth/send-password-reset-code`
- `/api/auth/reset-password-with-code`
### 仪表盘模块 (/api/dashboard)
- `/api/dashboard/stats`
- `/api/dashboard/reports/aqi-distribution`
- `/api/dashboard/reports/monthly-exceedance-trend`
- `/api/dashboard/reports/grid-coverage`
- `/api/dashboard/reports/pollution-stats`
- `/api/dashboard/reports/task-completion-stats`
- `/api/dashboard/reports/pollutant-monthly-trends`
- `/api/dashboard/map/heatmap`
- `/api/dashboard/map/aqi-heatmap`
- `/api/dashboard/thresholds`
- `/api/dashboard/thresholds/{pollutantName}`
### 反馈模块 (/api/feedback)
- `/api/feedback/submit`
- `/api/feedback`
- `/api/feedback/{id}`
- `/api/feedback/stats`
- `/api/feedback/{id}/process`
### 公共接口模块 (/api/public)
- `/api/public/feedback`
### 文件模块 (/api)
- `/api/files/{filename}`
- `/api/view/{filename}`
### 网格管理模块 (/api/grids)
- `/api/grids`
- `/api/grids/{id}`
- `/api/grids/coverage`
- `/api/grids/{gridId}/assign`
- `/api/grids/{gridId}/unassign`
- `/api/grids/coordinates/{gridX}/{gridY}/assign`
- `/api/grids/coordinates/{gridX}/{gridY}/unassign`
### 人员管理模块 (/api/personnel)
- `/api/personnel/users`
- `/api/personnel/users/{userId}`
- `/api/personnel/users/{userId}/role`
### 网格员任务模块 (/api/worker)
- `/api/worker`
- `/api/worker/{taskId}`
- `/api/worker/{taskId}/accept`
- `/api/worker/{taskId}/submit`
### 地图与寻路模块
- `/api/map/grid`
- `/api/map/initialize`
- `/api/pathfinding/find`
### 个人资料模块 (/api/me)
- `/api/me/feedback`
### 主管审核模块 (/api/supervisor)
- `/api/supervisor/reviews`
- `/api/supervisor/reviews/{feedbackId}/approve`
- `/api/supervisor/reviews/{feedbackId}/reject`
### 任务分配模块 (/api/tasks)
- `/api/tasks/unassigned`
- `/api/tasks/grid-workers`
- `/api/tasks/assign`

9
Report/参考资料.md Normal file
View File

@@ -0,0 +1,9 @@
# 4. 参考资料
[1] Spring Boot. (2023). Spring Boot Reference Documentation. [https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/)
[2] Vue.js. (2023). The Vue.js Guide. [https://vuejs.org/guide/introduction.html](https://vuejs.org/guide/introduction.html)
[3] Baeldung. (2023). Spring Security. [https://www.baeldung.com/security-spring](https://www.baeldung.com/security-spring)
[4] Vaughn, V. (2013). *Implementing Domain-Driven Design*. Addison-Wesley Professional.

3
Report/基本要求.md Normal file
View File

@@ -0,0 +1,3 @@
1所有数据以文件格式保存文件存储在工程目录中。
2文件数据格式可以是JSON格式也可以是以对象序列化的方式存储。
3东软环保公众监督系统主要功能为汇总不同地区的公众监督员提供的空气质量信息由系统管理员将这些信息指派给专业的环保检测网格员进行实地考察和检测从而得到不同地区的空气质量AQI空气质量指数的实时数据。再将这些AQI数据进行统计统计结果最终成为环保方面决策者进行决策的依据。

View File

@@ -0,0 +1,191 @@
# EMS系统完整依赖关系列表
## 1. 控制器层依赖 (Controller Dependencies)
### 控制器 → 服务层
- `AuthController``AuthService`, `VerificationCodeService`, `OperationLogService`
- `DashboardController``DashboardService`
- `FeedbackController``FeedbackService`
- `FileController``FileStorageService`
- `GridController``GridService`, `GridRepository`, `UserAccountRepository`, `OperationLogService`
- `GridWorkerTaskController``GridWorkerTaskService`
- `MapController``MapGridRepository`
- `OperationLogController``OperationLogService`
- `PathfindingController``AStarService`
- `PersonnelController``PersonnelService`, `UserAccountService`
- `ProfileController``UserFeedbackService`
- `PublicController``FeedbackService`
- `SupervisorController``SupervisorService`
- `TaskAssignmentController``TaskAssignmentService`
- `TaskManagementController``TaskManagementService`
### 控制器 → DTO
- `AuthController``LoginRequest`
- `FeedbackController``FeedbackSubmissionRequest`
- `TaskManagementController``TaskCreationRequest`
- `PersonnelController``UserCreationRequest`
## 2. 服务层依赖 (Service Dependencies)
### 服务 → 仓库层
- `AiReviewService``FeedbackRepository`
- `AuthService``UserAccountRepository`, `PasswordResetTokenRepository`
- `DashboardService``FeedbackRepository`, `UserAccountRepository`, `AqiDataRepository`, `AqiRecordRepository`, `GridRepository`, `TaskRepository`, `PollutantThresholdRepository`
- `FeedbackService``FeedbackRepository`, `UserAccountRepository`, `TaskRepository`
- `FileStorageService``AttachmentRepository`
- `GridService``GridRepository`, `UserAccountRepository`, `MapGridRepository`
- `GridWorkerTaskService``TaskRepository`, `TaskHistoryRepository`, `TaskSubmissionRepository`, `AttachmentRepository`
- `OperationLogService``OperationLogRepository`, `UserAccountRepository`
- `PersonnelService``UserAccountRepository`
- `SupervisorService``FeedbackRepository`
- `TaskAssignmentService``FeedbackRepository`, `UserAccountRepository`, `AssignmentRepository`, `TaskRepository`
- `TaskManagementService``TaskRepository`, `UserAccountRepository`, `TaskHistoryRepository`, `FeedbackRepository`, `TaskSubmissionRepository`, `AttachmentRepository`
- `UserAccountService``UserAccountRepository`
- `UserFeedbackService``FeedbackRepository`
- `AStarService``MapGridRepository`
### 服务 → 服务层
- `AuthService``JwtService`, `VerificationCodeService`, `OperationLogService`
- `FeedbackService``FileStorageService`, `TaskManagementService`, `OperationLogService`
- `GridService``OperationLogService`
- `GridWorkerTaskService``FileStorageService`, `OperationLogService`
- `PersonnelService``OperationLogService`
- `TaskAssignmentService``OperationLogService`
- `TaskManagementService``OperationLogService`, `AStarService`
- `VerificationCodeService``MailService`
## 3. 仓库层依赖 (Repository Dependencies)
### 仓库实现 → 存储服务
- `JsonAssignmentRecordRepository``JsonStorageService`
- `JsonAssignmentRepository``JsonStorageService`
- `JsonAqiDataRepository``JsonStorageService`
- `JsonAqiRecordRepository``JsonStorageService`
- `JsonAttachmentRepository``JsonStorageService`
- `JsonFeedbackRepository``JsonStorageService`
- `JsonGridRepository``JsonStorageService`
- `JsonMapGridRepository``JsonStorageService`
- `JsonOperationLogRepository``JsonStorageService`
- `JsonPasswordResetTokenRepository``JsonStorageService`
- `JsonPollutantThresholdRepository``JsonStorageService`
- `JsonTaskHistoryRepository``JsonStorageService`
- `JsonTaskRepository``JsonStorageService`
- `JsonTaskSubmissionRepository``JsonStorageService`
- `JsonUserAccountRepository``JsonStorageService`
### 仓库 → 模型层
- `AqiDataRepository``AqiData`
- `AqiRecordRepository``AqiRecord`
- `AssignmentRepository``Assignment`
- `AssignmentRecordRepository``AssignmentRecord`
- `AttachmentRepository``Attachment`
- `FeedbackRepository``Feedback`
- `GridRepository``Grid`
- `MapGridRepository``MapGrid`
- `OperationLogRepository``OperationLog`
- `PasswordResetTokenRepository``PasswordResetToken`
- `PollutantThresholdRepository``PollutantThreshold`
- `TaskRepository``Task`
- `TaskHistoryRepository``TaskHistory`
- `TaskSubmissionRepository``TaskSubmission`
- `UserAccountRepository``UserAccount`
## 4. 模型层关系 (Model Relationships)
### 实体关联
- `UserAccount` (1) ↔ `Feedback` (*) : 用户提交反馈
- `UserAccount` (1) ↔ `Task` (*) : 用户被分配任务
- `UserAccount` (1) ↔ `Role` (1) : 用户拥有角色
- `UserAccount` (1) ↔ `Assignment` (*) : 用户有分配记录
- `Feedback` (1) ↔ `Task` (1) : 反馈生成任务
- `Feedback` (*) ↔ `Attachment` (*) : 反馈包含附件
- `Task` (1) ↔ `Assignment` (*) : 任务有分配记录
- `Task` (*) ↔ `Attachment` (*) : 任务包含附件
- `Task` (1) ↔ `TaskHistory` (*) : 任务有历史记录
- `Task` (1) ↔ `TaskSubmission` (*) : 任务有提交记录
- `Grid` (1) ↔ `AqiRecord` (*) : 网格有空气质量记录
- `Assignment``Task`, `UserAccount` : 分配关联任务和用户
- `AssignmentRecord``Feedback`, `UserAccount` : 分配记录关联反馈和用户
- `Attachment``Feedback`, `TaskSubmission` : 附件关联反馈或任务提交
- `AqiData``Grid` : 空气质量数据关联网格
- `AqiRecord``Grid` : 空气质量记录关联网格
## 5. 服务实现依赖 (Service Implementation Dependencies)
### 接口实现关系
- `AiReviewServiceImpl``AiReviewService`
- `AuthServiceImpl``AuthService`
- `DashboardServiceImpl``DashboardService`
- `FeedbackServiceImpl``FeedbackService`
- `FileStorageServiceImpl``FileStorageService`
- `GridServiceImpl``GridService`
- `GridWorkerTaskServiceImpl``GridWorkerTaskService`
- `JsonStorageServiceImpl``JsonStorageService`
- `JwtServiceImpl``JwtService`
- `LoginAttemptServiceImpl``LoginAttemptService`
- `MailServiceImpl``MailService`
- `OperationLogServiceImpl``OperationLogService`
- `PersonnelServiceImpl``PersonnelService`
- `SupervisorServiceImpl``SupervisorService`
- `TaskAssignmentServiceImpl``TaskAssignmentService`
- `TaskManagementServiceImpl``TaskManagementService`
- `UserAccountServiceImpl``UserAccountService`
- `UserFeedbackServiceImpl``UserFeedbackService`
- `VerificationCodeServiceImpl``VerificationCodeService`
- `AStarServiceImpl``AStarService`
## 6. 安全层依赖 (Security Dependencies)
### 安全配置
- `SecurityConfig``JwtAuthenticationFilter`, `UserAccountService`
- `JwtAuthenticationFilter``UserDetailsServiceImpl`, `JwtService`
- `UserDetailsServiceImpl``UserAccountService`
- `CustomUserDetails``UserAccount`
- `JwtService``UserAccountRepository`
## 7. 配置层依赖 (Configuration Dependencies)
### 配置类
- `WebConfig` : Web配置
- `SecurityConfig` : 安全配置
- `JacksonConfig` : JSON序列化配置
## 8. 异常处理依赖 (Exception Dependencies)
### 异常类
- `GlobalExceptionHandler` : 全局异常处理器
- `FileStorageException` : 文件存储异常
- `ResourceNotFoundException` : 资源未找到异常
- `UnauthorizedException` : 未授权异常
- `ValidationException` : 验证异常
## 9. 事件处理依赖 (Event Dependencies)
### 事件监听器
- `TaskEventListener` : 任务事件监听器
- `FeedbackEventListener` : 反馈事件监听器
## 10. 枚举依赖 (Enum Dependencies)
### 枚举类型
- `Role` : 用户角色枚举
- `TaskStatus` : 任务状态枚举
- `FeedbackStatus` : 反馈状态枚举
- `OperationType` : 操作类型枚举
- `PollutantType` : 污染物类型枚举
- `GridStatus` : 网格状态枚举
## 总结
本系统采用分层架构设计,包含:
- **控制器层 (Controller)**: 15个控制器类
- **服务层 (Service)**: 20个服务接口及其实现
- **仓库层 (Repository)**: 15个仓库接口及其JSON实现
- **模型层 (Model)**: 15个实体类
- **安全层 (Security)**: 5个安全相关类
- **配置层 (Configuration)**: 3个配置类
- **异常处理**: 5个异常类
- **事件处理**: 2个事件监听器
- **枚举类型**: 6个枚举
总计约 **86个主要组件** 及其相互依赖关系,形成了一个完整的环境监测系统架构。

163
Report/实践总结.md Normal file
View File

@@ -0,0 +1,163 @@
# 4. 实践总结与展望
本次"东软环保公众监督系统"的课程设计,对我而言,远不止是一次课程任务的完成,更是一场从理论到实践、从单一技能到综合工程能力的深度淬炼。通过从零开始,亲历一个完整软件项目的生命周期——从模糊的需求雏形到清晰的系统蓝图,从优雅的架构设计到严谨的代码实现,再到全面的测试交付——我不仅高质量地达成了所有预设目标,更在思想认知、技术栈深度和工程素养上实现了跨越式的成长。
## 4.1 收获与体会:从"会用"到"精通"的蜕变
### 1. 宏观架构观的树立与深化
本次实践最大的收获,莫过于在真实场景中主导并落地了**前后端分离架构**。这让我对现代Web应用架构的理解从"知道其然"深入到了"知其所以然"。
* **深刻理解"解耦"的价值**:我不再将"解耦"视为一个抽象概念而是亲身体会到它带来的巨大工程优势后端可以专注于业务逻辑与数据服务不受前端界面迭代的影响前端则可以自由选择技术栈Vue 3全家桶聚焦于用户体验的打磨。这种并行的开发模式极大地提升了团队协作的效率。
* **掌握RESTful API设计精髓**我学习并实践了一套规范的RESTful API设计原则包括使用HTTP动词GET, POST, PUT, DELETE表达操作通过URI定位资源以及设计统一的响应数据结构包含状态码、消息和数据体。这使得前后端的数据交互变得清晰、可预测且易于调试。
* **拥抱"无状态服务"理念**通过引入JWT进行认证授权我设计的后端服务实现了无状态化。服务器不再需要存储用户的Session信息每一次请求都包含了完整的认证信息这为未来系统的水平扩展和负载均衡奠定了坚实的基础。
### 2. 设计能力与代码匠艺的磨练
面对"基于文件存储"这一核心约束我没有选择简单的、面向过程的文件I/O操作而是进行了一次富有创造性的技术探索设计并实现了一套**模拟JPA思想的泛型JSON仓储层Repository Layer**。这成为我本次实践中最具价值的技术沉淀。
* **设计模式的实战应用**:该仓储层的实现,是对**SOLID原则**的一次综合演练。通过定义泛型接口`JsonRepository<T, ID>`,我实践了**依赖倒置原则**;通过`JsonStorageService`的封装实现了数据读写逻辑的单一职责通过为不同实体提供具体的Repository实现遵循了**接口隔离原则**。这让我真正领会到"面向接口编程"如何带来代码的灵活性、可测试性和可扩展性。
* **并发编程的初探**为了解决多用户并发写文件可能导致的数据覆盖或错乱问题我深入研究了Java的并发控制机制并最终选用`synchronized`关键字对核心的写入方法进行加锁。通过压力测试,我验证了该机制的有效性,这让我对线程安全问题有了具体而深刻的认识,也为未来学习更高级的并发技术(如`ReentrantLock``CAS`)打下了基础。
### 3. 解决复杂技术难题的信心与方法论
项目开发过程并非一帆风顺,我遭遇并攻克了多个技术难点,这个过程极大地锻炼了我独立解决问题的能力。
* **攻坚`Spring Security`**:配置`Spring Security`与JWT的整合是一大挑战。我通过深入阅读官方文档和优秀开源项目的源码理解了其Filter链的工作机制并成功自定义了`JwtAuthenticationFilter`实现了从请求头解析Token、验证签名、加载用户权限最终将其置入`SecurityContextHolder`的完整流程。我还学会了使用`@PreAuthorize`注解,实现了精确到方法级别的声明式权限控制。
* **构建优雅的全局异常处理**为了避免在每个Controller方法中都充斥着大量的`try-catch`我利用Spring MVC提供的`@ControllerAdvice``@ExceptionHandler`注解,构建了一个统一的全局异常处理器。它可以捕获不同类型的业务异常(如`ResourceNotFoundException`和系统异常并将其转换为标准化的API错误响应返回给前端。这不仅净化了业务代码也提升了API的健壮性和用户体验。
### 4. 软件工程全景视野的拓展
这次经历让我彻底摆脱了"代码工人"的思维定式,开始以一名"软件工程师"的视角来审视整个项目。我认识到,高质量的软件交付,远不止代码本身。
* **文档的价值**我投入了大量精力编写清晰、规范的Markdown项目文档包括需求文档、设计文档、API文档Swagger和测试报告。我发现高质量的文档是团队沟通的基石是项目知识传承的载体更是保证项目长期可维护性的关键。
* **测试的左移**:我主动引入了单元测试,并坚持在开发阶段就为核心模块编写测试用例。这让我体会到"测试左移"的重要性——越早发现Bug修复的成本越低。全面的API测试和集成测试则构成了产品质量的最后一道防线。
## 4.2 不足与展望:迈向更高阶的工程师之路
在收获满满的同时,我也清醒地看到了当前项目的局限性和个人能力的待提升之处,这为我规划了清晰的未来学习路径。
* **构建成熟的自动化测试体系**:目前项目的自动化测试还处于初级阶段。我的下一步计划是:
* **后端**:深入学习`Mockito`的高级用法(如`ArgumentCaptor`),并引入集成测试框架(如`Testcontainers`在CI/CD流水线中自动运行测试实现代码提交即测试。
* **前端**:学习并引入`Vitest`进行单元测试,使用`Cypress``Playwright`进行端到端E2E测试实现对关键用户流程的自动化回归验证。
* **拥抱云原生与DevOps**:当前项目采用的是传统的手动部署模式,效率低下且容易出错。我渴望掌握云原生技术栈:
* **容器化**:学习`Docker`,将前后端应用及其依赖环境打包成独立的、可移植的容器镜像。
* **容器编排**:学习`Kubernetes`,实现容器化应用的自动化部署、弹性伸缩和高可用运维。
* **CI/CD**:学习`Jenkins``GitHub Actions`,搭建一条完整的自动化流水线,实现从代码提交到测试、构建、部署的全流程自动化。
* **深化业务与技术的融合创新**
* **实时化改造**:引入`WebSocket``Server-Sent Events (SSE)`,将任务的分配、状态流转等关键信息实时推送到前端,极大地提升系统的即时性和用户的沉浸式体验。
* **数据智能升级**:当数据量增长后,引入`Elasticsearch`,提供强大的全文检索和聚合分析能力。长远来看,可以结合机器学习算法,对环境问题数据进行深度挖掘,实现如"区域环境问题热点预测"、"污染源智能追溯"等更高阶的智能应用。
总而言之,这次课程设计是我从一名编程学习者向一名准软件工程师转变的里程碑。它不仅系统性地检验了我过往的知识积累,更重要的是,它点燃了我对软件架构、代码匠艺和工程卓越的无限热情。在这次实践中收获的宝贵经验和暴露的不足,都将化为我未来职业道路上最坚实的基石和最清晰的路标,激励我不断学习、持续精进。
## 内容完成情况
在本次环境监督系统(EMS)的开发实践中,我完成了以下主要内容:
1. **系统设计文档**完成了详细的系统设计文档包括整体架构设计、功能模块划分、数据库设计和API接口设计等方面。
2. **后端核心功能实现**
- 实现了基于JSON文件的泛型存储服务
- 开发了A*寻路算法用于网格员路径规划
- 完成了反馈管理模块的全流程实现
- 设计并实现了任务智能分配算法
3. **前端界面设计与实现**
- 开发了主管工作台、网格员工作台和决策支持看板等核心界面
- 实现了响应式布局和组件化开发
4. **系统实现文档**:编写了详细的系统实现文档,包括开发环境与技术栈、核心功能模块实现和界面展示等内容。
## 创新点
### 1. 泛型JSON存储服务
设计并实现了一个创新的数据持久化解决方案使用JSON文件代替传统数据库进行数据存储。这种方案具有以下创新点
- **类型安全的泛型设计**通过Java泛型和TypeReference实现了类型安全的数据读写支持任意模型类。
- **类数据库接口**提供了类似于JPA的操作接口使得业务层代码无需关心底层存储细节。
- **部署简化**:无需配置和维护数据库,大大简化了系统部署过程。
- **线程安全机制**使用synchronized关键字确保写操作的线程安全防止并发写入导致的数据损坏。
### 2. A*寻路算法的动态地图适配
在网格与地图模块中实现的A*寻路算法具有以下创新特点:
- **动态地图加载**:算法从数据库动态加载地图数据,包括障碍物信息和地图尺寸,使得路径规划能够适应地图的变化。
- **优先队列优化**使用优先队列PriorityQueue存储开放列表确保每次都能高效地选择F值最小的节点。
- **适应性启发函数**:选择曼哈顿距离作为启发函数,适合网格化的城市环境移动模式。
### 3. 多因素加权的任务智能分配算法
任务分配算法综合考虑了多种因素,具有较高的智能性:
- **多维度评分机制**:算法综合考虑了地理距离、当前负载、专业匹配度和历史表现四个因素。
- **动态技能匹配**:根据任务的污染类型和严重程度,动态确定所需的技能列表,然后与网格员的技能进行匹配。
- **可配置权重系统**:各因素的权重可以根据实际需求进行调整,使得算法更加灵活和适应性强。
### 4. 事件驱动的业务流程设计
在反馈管理模块中,采用了事件驱动的架构设计:
- **解耦的业务流程**通过Spring的事件机制实现了反馈提交后的异步处理如AI审核和任务创建。
- **状态机模式**:使用状态机模式管理反馈的生命周期,确保状态转换的合法性,防止非法操作。
- **可扩展的事件处理**:新的事件处理器可以轻松添加,无需修改现有代码,符合开闭原则。
### 5. 文件上传与存储服务
在环境问题反馈和任务处理过程中,实现了高效且安全的文件上传与存储机制:
- **多媒体支持**:支持图片、视频和音频等多种媒体格式的上传,为环境问题提供直观的证据支持。
- **安全性控制**:实现了文件类型验证、大小限制和内容扫描,防止恶意文件上传,保障系统安全。
- **分层存储架构**:采用物理路径与逻辑路径分离的设计,文件实际存储在安全目录,而数据库中仅保存引用路径,增强了系统的安全性和灵活性。
- **按需加载策略**:对于大型媒体文件,实现了流式传输和分块下载,优化了带宽使用和用户体验。
## 不足与可改进之处
尽管系统已经实现了核心功能,但由于时间限制,仍有一些方面可以进一步完善:
### 1. 数据持久化机制的优化
当前的JSON文件存储方案虽然简化了部署但也存在一些局限性
- **性能瓶颈**随着数据量增大JSON文件的读写性能可能成为瓶颈。可以考虑实现分片存储或引入缓存机制。
- **事务支持**:当前实现缺乏事务支持,无法保证跨文件操作的原子性。可以设计一个简单的事务管理器来解决这个问题。
- **索引机制**缺乏高效的索引机制导致复杂查询性能较低。可以实现内存索引或考虑集成轻量级数据库如SQLite。
### 2. A*算法的进一步优化
A*寻路算法还可以在以下方面进行优化:
- **双向搜索**实现双向A*搜索,从起点和终点同时开始搜索,可以显著减少搜索空间。
- **层次化路径规划**:对于大型地图,可以实现层次化的路径规划,先在抽象层次上找到大致路径,再在细节层次上优化。
- **动态权重调整**:根据不同的地形特征动态调整边的权重,更好地模拟现实世界的移动成本。
### 3. 任务分配算法的增强
任务分配算法可以通过以下方式进一步增强:
- **机器学习集成**:引入机器学习模型,根据历史分配数据学习最优的权重配置,实现自适应的任务分配。
- **群体智能优化**:考虑整个网格员团队的工作负载均衡,而不仅仅是单个任务的最优分配。
- **预测性分配**:基于历史数据预测未来一段时间内可能出现的任务,提前做好人力资源规划。
### 4. 前端用户体验优化
前端界面还可以在以下方面进行改进:
- **离线支持**实现Progressive Web App (PWA),使网格员在网络不稳定的野外环境也能使用系统。
- **实时通知**集成WebSocket或服务器发送事件(SSE),实现实时任务通知和状态更新。
- **移动端适配**:进一步优化移动端体验,考虑开发原生移动应用,提供更好的定位和拍照体验。
### 5. 系统安全性增强
安全方面还可以进一步加强:
- **细粒度权限控制**:实现基于资源的访问控制(RBAC),对不同资源设置更细粒度的权限控制。
- **API限流与防护**实现API限流机制防止DoS攻击添加CSRF保护和更完善的输入验证。
- **审计日志**:实现全面的审计日志系统,记录所有关键操作,便于安全审计和问题追踪。
## 结论
通过本次实践我成功地设计并实现了环境监督系统的核心功能在泛型JSON存储服务、A*寻路算法、任务智能分配算法和事件驱动架构等方面进行了创新。虽然由于时间限制,一些高级特性和优化未能实现,但这些都已经被识别为未来可以改进的方向。总体而言,系统达到了预期的设计目标,提供了一个功能完整、架构合理的环境监督解决方案。

55
Report/实践目的.md Normal file
View File

@@ -0,0 +1,55 @@
# 《东软环保公众监督平台》项目实践目的
### 一、项目概述与本人工作
本项目旨在开发一个高效、透明的环保公众监督平台,作为《东软环保应急》系统的重要子模块。系统的核心业务流程覆盖了从公众反馈、智能初审、任务转化、智能分配到最终处理的全生命周期管理,旨在拓宽环保监督渠道,增加工作透明度,为环保决策提供数据支持。
在本次实践中,**本人担任核心的系统设计与开发角色**,是项目的主要贡献者。具体工作内容涵盖了整个软件生命周期,包括但不限于:
* **需求分析与建模**深入分析项目需求将模糊的业务概念转化为精确的功能规格并运用UML类图对系统核心实体进行建模。
* **系统架构设计**:主导了项目技术选型,确定了采用`Spring Boot` + `Vue.js`前后端分离的现代Web架构并设计了后端的三层Controller, Service, Repository分层结构。
* **数据持久化方案设计**:针对"数据存储于文件"的核心约束独立设计并实现了一套基于JSON文件的泛型仓储层Repository Pattern巧妙地解决了数据持久化问题保证了代码的可维护性。
* **核心模块编码实现**独立完成了用户认证JWT、反馈管理、任务全生命周期管理、智能任务分配算法等多个核心业务模块的后端代码编写工作。
* **API接口定义**负责设计并编写了项目整体的RESTful API接口并利用Swagger工具生成了清晰、规范的API文档为前后端高效协作提供了保障。
* **技术文档撰写**:作为主要作者,撰写了《系统设计方案》等核心技术文档,系统性地阐述了项目的技术实现细节。
### 二、能力培养达成情况
通过本次毕业设计实践,本人在指导老师的帮助下,系统性地将软件工程理论知识与项目实践相结合,在以下方面取得了显著的进步,达成了毕业设计所要求的各项能力培养目标:
#### 1. 设计/开发解决方案的能力
* **1掌握软件生命周期要素熟悉软件工程方法与技术**
* **全周期理解与实践**:深刻理解并实践了从需求分析、系统设计、编码实现到测试维护的软件全生命周期。
* **面向对象设计**:在项目设计阶段,始终贯彻面向对象的思想,对系统的核心业务(如用户账户`UserAccount`、环境反馈`Feedback`、处理任务`Task`)进行高度抽象,设计出了高内聚、低耦合的类结构。
* **UML系统建模**能够熟练运用PlantUML工具通过绘制UML类图对系统中的十余个核心实体及其关联的枚举类型进行整体建模清晰地表达了类之间的关联、聚合与依赖关系为后续开发提供了清晰、规范的蓝图。
* **数据持久化设计**:根据"所有数据以文件格式保存"的核心要求设计并实现了一套基于JSON文件的数据持久化方案。通过为不同实体创建独立的JSON文件并设计相应的读写服务确保了数据的合理组织与一致性。
* **界面设计**在前后端分离的架构下与前端开发紧密配合确保了API接口设计能够满足前端对美观、实用、符合用户习惯的界面的数据需求。
* **2设计满足特定需求的解决方案并体现创新意识**
* **任务分配算法设计**:为满足高效调度的需求,不止于简单的手动任务指派,而是进一步设计了一套基于"地理邻近度"和"当前工作负载"的加权评分模型,提出了具体的智能任务分配算法解决方案,体现了解决特定业务需求时的创新性。
* **仓储层模式创新**为解决直接操作JSON文件导致业务逻辑混乱的问题创新性地设计并实现了一套模拟`Spring Data JPA`接口的泛型仓储层Repository Pattern。该方案通过引入泛型和反射机制极大地提升了数据访问层的代码整洁性与可维护性是本次实践中的一个重要技术创新点。
#### 2. 研究能力
* **3理解系统设计原理掌握科学方法解决问题**
* **深入理解架构原理**通过实践不仅熟练掌握了Spring Boot框架的应用更深入理解了其背后的分层架构Controller-Service-Repository、依赖注入DI、面向切面编程AOP等核心设计思想。
* **掌握事件驱动模型**在设计AI对反馈进行初审的功能时引入了Spring的事件发布/监听机制将耗时的AI分析流程解耦为异步处理既提升了API的响应速度也增强了系统的健壮性和可扩展性。
* **科学方法应用**:在设计"网格员任务路径规划"功能时主动研究并集成了经典的A*寻路算法,能够运用科学的、经过验证的算法来解决工程中的最短路径问题,展现了将理论算法应用于工程实践的能力。
#### 3. 使用现代工具的能力
* **4能够选择并熟练使用现代工程工具**
* 在整个开发过程中,能够根据项目需要,熟练选择和运用一整套现代工程工具对复杂的软件工程问题进行分析与设计:
* **后端技术栈**: Java, Spring Boot, Spring Security (for JWT)
* **项目管理与构建**: Maven
* **版本控制**: Git, GitHub
* **设计与文档工具**: PlantUML, Mermaid.js, Markdown
* **集成开发环境 (IDE)**: IntelliJ IDEA, VS Code
#### 4. 沟通能力
* **5具备通过多种方式进行技术沟通的能力**
* **文稿沟通**:能够通过撰写《系统设计方案》、《实践目的》等多种技术文档,系统性、结构化地阐述项目背景、设计思路和技术方案,文字表达清晰、逻辑严密。
* **图表沟通**在技术文档中能够熟练运用UML类图、时序图等多种图表将复杂的系统静态结构、对象交互时序等信息进行高度可视化、标准化的表达极大地提升了与业界同行进行技术交流的效率和准确性。
* **口头沟通**:在项目讨论中,能够清晰地向指导老师和项目组成员阐述自己的技术见解和设计方案,展现了良好的口头表达与技术沟通能力。

3
Report/实践结果.md Normal file
View File

@@ -0,0 +1,3 @@
# 2. 实践结果
本章节将详细阐述“东软环保公众监督系统”从需求定义到系统测试的完整实践过程。

62
Report/报告格式.md Normal file
View File

@@ -0,0 +1,62 @@
实践目的
首先对项目内容进行“概述”,并明确说明本人从事的工作;
然后,写出本人通过本次实践,达成了以下哪些能力的培养,要求简洁清楚。可以从以下几点来说明:(不要照抄以下内容,说明自己的能力达成情况)
设计/开发解决方案的能力:
1掌握软件生命周期要素理解、掌握并能够按照面向对象的思想对系统进行设计熟悉软件需求分析、设计、实现、测试的方法和技术能够使用UML类图对系统整体建模会抽取类、属性、方法、关联设计合理符合系统要求能够合理持久化存储界面设计美观实用等
2能够设计满足特定功能需求与性能需求的解决方案并体现创新意识
研究能力:
3能够理解系统软件的设计思路和基本原理掌握应用软件技术、科学方法具备创新性地解决软件工程具体问题的能力
使用现代工具的能力:
4能够选择恰当的技术、资源、现代工程工具和信息技术工具对复杂软件工程问题进行分析、计算或设计
沟通能力:
5具备一定的社交技能和技巧能够就与本专业相关的当前热点问题发表自己的观点能够以口头、文稿、图表等方式与业界同行进行技术交流与沟通能使用通俗易懂的语言与社会公众进行表达与沟通
1. 相关技术基础
写出本次实践过程中你所用到的相关技术,包括与项目相关的理论基础,项目开发方法、开发工具、开发环境等关键技术的介绍;
2. 实践结果
此部分属报告的主要部分。包括:
3.1 需求定义
“系统分析”也可以看成是需求定义,包括对整个项目的介绍分析及本人工作内容的详细分析,如业务分析、功能分析(可使用例图、活动图来描述)、可行性分析等;
3.2 系统设计
“系统设计”包括总体设计和详细设计,"总体设计"包括系统架构设计、功能模块划分等,"详细设计"要围绕本人工作内容展开,包括功能模块详细设计、类和对象的设计、动态模型设计(时序图、状态图、协作图等)、算法设计、数据库设计等;
3.3 系统实现
“系统实现”也要围绕本人工作内容展开,从编码实现角度论述相应功能模块的实现细节,并展示自己所完成的主要成果及实际应用情况等。可通过“程序流程图”、“关键代码”和“界面”进行直观论述。
3.4 系统测试
“系统测试”包括测试方案设计、测试用例和测试结果、最终的测试结论或评价等。
3. 实践总结
简述你在实践过程中的内容完成情况,重点介绍创新点及不足(也就是可以再完善的部分,只是时间不允许了。不足不代表不好,也说明你思考了,但是来不及完成实现)
4. 参考资料
例:
[1] 数据结构、算法与应用C++语言描述 [Data Structures,Algorithms,and Applications in C++][M].机械工业出版社出版时间2000-01-01.
[2] 数据结构(C语言版) [M].北京: 中国铁道出版社, 2011-08-01.
基础编程实训成绩评定表
考核内容 考核标准 分值 得分
面向对象设计能力 1. 能够很好的按照面向对象的思想对系统进行设计和实现设计合理的实体关系正确使用UML类图对系统整体建模设计合理符合系统要求
2. 能够很好的划分模块和提取方法,程序设计具有高内聚低耦合的特点;
3. 能够合理应用多种设计模式,提高系统的灵活性和可用性; 20
面向对象编程能力 1. 能够应用面向对象程序设计语言进行系统实现;
2. 能够很好的使用继承和多态,提升代码复用性;
3. 程序设计逻辑结构清晰合理;
4. 代码规范遵照Java的代码规范。 20
解决问题能力 1. 能够正确使用已有的架构完成系统功能如MVC架构
2. 能够很好的设计持久化存储结构。正确使用Java语言实现设计的系统程序结构清晰代码书写规范简洁实现的系统功能完善运行稳定基本无bug。
3. 界面美观,符合用户习惯; 20
学习能力 1. 能够自学Java语言中关于图形用户界面的知识并能为开发的系统构造图形用户界面。能够挑选合适GUI插件如window builder插件快速构建图形用户界面。
2. 能够在集成开发环境中对系统进行开发和调试,能够使用代码检查工具提升代码的正确性。
3. 能够通过网络、课堂、书籍等多处获取所需的知识,并应用这些知识解决相应的问题。
4. 在解决问题的过程中有自己独到的见解,并能够有所创新。 20
报告质量 1. 实践报告格式规范,报告内容充实、正确,报告叙述逻辑严密,可准确反映出设计和实现的结果。
2. 实践报告能够体现实践过程中出现的问题和解决方案,以及独立分析问题和解决问题的能力。
3. 报告格式统一,图表使用规范,报告用词准确,符合科技文档写作要求。 20
总 分(百分制)

463
Report/时序图.md Normal file
View File

@@ -0,0 +1,463 @@
# EMS后端系统技术图表文档
## 项目概述
环境管理系统(EMS)后端是一个基于Spring Boot的Java应用程序用于处理环境问题反馈、任务分配和用户管理。系统采用分层架构包含控制器、服务、仓库和模型层。
## 1. 系统时序图
### 1.1 用户登录认证流程
```mermaid
sequenceDiagram
participant User as 用户
participant AuthController as 认证控制器
participant AuthService as 认证服务
participant UserRepository as 用户仓库
participant JwtUtil as JWT工具
participant Database as JSON持久化存储
User->>AuthController: POST /api/auth/login
AuthController->>AuthService: signIn(LoginRequest)
AuthService->>UserRepository: findByEmailOrPhone()
UserRepository->>Database: 查询用户信息
Database-->>UserRepository: 返回用户数据
UserRepository-->>AuthService: 返回UserAccount
AuthService->>AuthService: 验证密码
AuthService->>JwtUtil: 生成JWT令牌
JwtUtil-->>AuthService: 返回JWT
AuthService-->>AuthController: JwtAuthenticationResponse
AuthController-->>User: 返回JWT令牌
```
### 1.2 反馈提交处理流程
```mermaid
sequenceDiagram
participant User as 用户
participant FeedbackController as 反馈控制器
participant FeedbackService as 反馈服务
participant FeedbackRepository as 反馈仓库
participant AIService as AI服务
participant EventPublisher as 事件发布器
participant Database as JSON持久化存储
User->>FeedbackController: POST /api/feedback/submit
FeedbackController->>FeedbackService: submitFeedback(request, files)
FeedbackService->>FeedbackService: 生成事件ID
FeedbackService->>FeedbackRepository: save(feedback)
FeedbackRepository->>Database: 保存反馈数据
Database-->>FeedbackRepository: 返回保存结果
FeedbackRepository-->>FeedbackService: 返回Feedback实体
FeedbackService->>EventPublisher: 发布反馈创建事件
EventPublisher->>AIService: 触发AI处理
AIService->>AIService: 分析反馈内容
FeedbackService-->>FeedbackController: 返回Feedback
FeedbackController-->>User: 201 Created
```
### 1.3 任务分配流程
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant TaskController as 任务控制器
participant TaskService as 任务服务
participant AssignmentService as 分配服务
participant UserRepository as 用户仓库
participant TaskRepository as 任务仓库
participant GridWorker as 网格工作人员
Supervisor->>TaskController: POST /api/tasks/assign
TaskController->>TaskService: assignTask(taskId, workerId)
TaskService->>UserRepository: findById(workerId)
UserRepository-->>TaskService: 返回GridWorker
TaskService->>AssignmentService: createAssignment()
AssignmentService->>TaskRepository: updateTaskStatus()
TaskRepository-->>AssignmentService: 更新成功
AssignmentService-->>TaskService: Assignment创建成功
TaskService->>TaskService: 发送通知给工作人员
TaskService-->>TaskController: 分配成功
TaskController-->>Supervisor: 200 OK
Note over GridWorker: 接收任务通知
```
### 1.4 主管审核反馈并创建任务
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant FeedbackController as 反馈控制器
participant FeedbackService as 反馈服务
participant TaskService as 任务服务
participant TaskRepository as 任务仓库
participant Database as JSON持久化存储
Supervisor->>FeedbackController: POST /api/feedback/{id}/process
FeedbackController->>FeedbackService: processFeedback(feedbackId, request)
FeedbackService->>FeedbackService: 验证反馈状态和主管权限
alt 同意反馈并创建任务
FeedbackService->>TaskService: createTaskFromFeedback(feedback)
TaskService->>TaskRepository: save(task)
TaskRepository->>Database: 保存新任务
Database-->>TaskRepository: 返回保存的任务
TaskRepository-->>TaskService: 返回Task实体
TaskService->>FeedbackService: 更新反馈状态为PENDING_ASSIGNMENT
FeedbackService-->>FeedbackController: 返回处理结果
else 拒绝反馈
FeedbackService->>FeedbackService: 更新反馈状态为REJECTED
FeedbackService-->>FeedbackController: 返回处理结果
end
FeedbackController-->>Supervisor: 200 OK
```
### 1.5 用户密码重置流程
```mermaid
sequenceDiagram
participant User as 用户
participant AuthController as 认证控制器
participant AuthService as 认证服务
participant VerificationCodeService as 验证码服务
participant MailService as 邮件服务
participant UserRepository as 用户仓库
User->>AuthController: POST /api/auth/send-password-reset-code (email)
AuthController->>AuthService: requestPasswordReset(email)
AuthService->>UserRepository: findByEmail(email)
UserRepository-->>AuthService: 返回UserAccount
AuthService->>VerificationCodeService: createAndSendPasswordResetCode(user)
VerificationCodeService->>MailService: sendEmail(to, subject, content)
MailService-->>VerificationCodeService: 邮件发送成功
VerificationCodeService-->>AuthService: 验证码发送成功
AuthService-->>AuthController: 200 OK
AuthController-->>User: 提示验证码已发送
User->>AuthController: POST /api/auth/reset-password-with-code (email, code, newPassword)
AuthController->>AuthService: resetPasswordWithCode(email, code, newPassword)
AuthService->>VerificationCodeService: validateCode(email, code)
VerificationCodeService-->>AuthService: 验证码有效
AuthService->>UserRepository: save(user) with new password
UserRepository-->>AuthService: 用户密码更新成功
AuthService-->>AuthController: 200 OK
AuthController-->>User: 密码重置成功
```
### 1.6 用户获取自己的反馈历史
```mermaid
sequenceDiagram
participant User as 用户
participant ProfileController as 个人资料控制器
participant UserFeedbackService as 用户反馈服务
participant FeedbackRepository as 反馈仓库
participant Database as JSON持久化存储
User->>ProfileController: GET /api/me/feedback
ProfileController->>UserFeedbackService: getFeedbackHistoryByUserId(userId, pageable)
UserFeedbackService->>FeedbackRepository: findBySubmitterId(userId, pageable)
FeedbackRepository->>Database: 查询用户反馈数据
Database-->>FeedbackRepository: 返回反馈数据
FeedbackRepository-->>UserFeedbackService: 返回Page<Feedback>
UserFeedbackService->>UserFeedbackService: 转换为UserFeedbackSummaryDTO列表
UserFeedbackService-->>ProfileController: 返回Page<UserFeedbackSummaryDTO>
ProfileController-->>User: 返回反馈历史列表
```
### 1.7 网格员获取和管理任务
```mermaid
sequenceDiagram
participant GridWorker as 网格员
participant GridWorkerTaskController as 网格员任务控制器
participant GridWorkerTaskService as 网格员任务服务
participant TaskRepository as 任务仓库
participant Database as JSON持久化存储
alt 获取任务列表
GridWorker->>GridWorkerTaskController: GET /api/worker/tasks
GridWorkerTaskController->>GridWorkerTaskService: getAssignedTasks(workerId, status, pageable)
GridWorkerTaskService->>TaskRepository: findByAssigneeIdAndStatus(workerId, status, pageable)
TaskRepository->>Database: 查询任务数据
Database-->>TaskRepository: 返回任务数据
TaskRepository-->>GridWorkerTaskService: 返回Page<Task>
GridWorkerTaskService->>GridWorkerTaskService: 转换为TaskSummaryDTO列表
GridWorkerTaskService-->>GridWorkerTaskController: 返回Page<TaskSummaryDTO>
GridWorkerTaskController-->>GridWorker: 返回任务列表
end
alt 接受任务
GridWorker->>GridWorkerTaskController: POST /api/worker/tasks/{taskId}/accept
GridWorkerTaskController->>GridWorkerTaskService: acceptTask(taskId, workerId)
GridWorkerTaskService->>TaskRepository: findById(taskId)
TaskRepository->>Database: 查询任务
Database-->>TaskRepository: 返回任务
TaskRepository-->>GridWorkerTaskService: 返回Task
GridWorkerTaskService->>GridWorkerTaskService: 验证任务状态并更新为ACCEPTED
GridWorkerTaskService->>TaskRepository: save(task)
TaskRepository->>Database: 更新任务状态
Database-->>TaskRepository: 返回更新后的任务
TaskRepository-->>GridWorkerTaskService: 返回更新后的Task
GridWorkerTaskService->>GridWorkerTaskService: 转换为TaskSummaryDTO
GridWorkerTaskService-->>GridWorkerTaskController: 返回TaskSummaryDTO
GridWorkerTaskController-->>GridWorker: 返回更新后的任务摘要
end
alt 提交任务
GridWorker->>GridWorkerTaskController: POST /api/worker/tasks/{taskId}/submit
GridWorkerTaskController->>GridWorkerTaskService: submitTaskCompletion(taskId, workerId, request, files)
GridWorkerTaskService->>TaskRepository: findById(taskId)
TaskRepository->>Database: 查询任务
Database-->>TaskRepository: 返回任务
TaskRepository-->>GridWorkerTaskService: 返回Task
GridWorkerTaskService->>GridWorkerTaskService: 验证并更新任务状态为COMPLETED
GridWorkerTaskService->>GridWorkerTaskService: (如果存在)处理附件上传
GridWorkerTaskService->>TaskRepository: save(task)
TaskRepository->>Database: 更新任务
Database-->>TaskRepository: 返回更新后的任务
TaskRepository-->>GridWorkerTaskService: 返回更新后的Task
GridWorkerTaskService->>GridWorkerTaskService: 转换为TaskSummaryDTO
GridWorkerTaskService-->>GridWorkerTaskController: 返回TaskSummaryDTO
GridWorkerTaskController-->>GridWorker: 返回更新后的任务摘要
end
```
### 1.8 决策者获取仪表盘数据
```mermaid
sequenceDiagram
participant DecisionMaker as 决策者
participant DashboardController as 仪表盘控制器
participant DashboardService as 仪表盘服务
participant variousRepositories as 各类仓库
participant Database as JSON持久化存储
alt 获取核心统计数据
DecisionMaker->>DashboardController: GET /api/dashboard/stats
DashboardController->>DashboardService: getDashboardStats()
DashboardService->>variousRepositories: (并行)调用多个仓库方法获取数据
variousRepositories->>Database: 查询统计数据
Database-->>variousRepositories: 返回数据
variousRepositories-->>DashboardService: 返回统计结果
DashboardService->>DashboardService: 聚合数据为DashboardStatsDTO
DashboardService-->>DashboardController: 返回DashboardStatsDTO
DashboardController-->>DecisionMaker: 返回核心统计数据
end
alt 获取AQI分布
DecisionMaker->>DashboardController: GET /api/dashboard/reports/aqi-distribution
DashboardController->>DashboardService: getAqiDistribution()
DashboardService->>variousRepositories: 查询AQI数据并分组统计
variousRepositories->>Database: 查询AQI数据
Database-->>variousRepositories: 返回数据
variousRepositories-->>DashboardService: 返回统计结果
DashboardService->>DashboardService: 转换为AqiDistributionDTO列表
DashboardService-->>DashboardController: 返回List<AqiDistributionDTO>
DashboardController-->>DecisionMaker: 返回AQI等级分布数据
end
alt 获取热力图数据
DecisionMaker->>DashboardController: GET /api/dashboard/map/heatmap
DashboardController->>DashboardService: getHeatmapData()
DashboardService->>variousRepositories: 查询反馈的地理位置数据
variousRepositories->>Database: 查询地理位置数据
Database-->>variousRepositories: 返回数据
variousRepositories-->>DashboardService: 返回位置数据列表
DashboardService->>DashboardService: 转换为HeatmapPointDTO列表
DashboardService-->>DashboardController: 返回List<HeatmapPointDTO>
DashboardController-->>DecisionMaker: 返回热力图数据
end
```
### 1.9 主管审核反馈
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant SupervisorController as 主管控制器
participant SupervisorService as 主管服务
participant FeedbackRepository as 反馈仓库
participant Database as JSON持久化存储
alt 获取待审核列表
Supervisor->>SupervisorController: GET /api/supervisor/reviews
SupervisorController->>SupervisorService: getFeedbackForReview()
SupervisorService->>FeedbackRepository: findByStatus(PENDING_REVIEW)
FeedbackRepository->>Database: 查询待审核反馈
Database-->>FeedbackRepository: 返回反馈列表
FeedbackRepository-->>SupervisorService: 返回List<Feedback>
SupervisorService-->>SupervisorController: 返回List<Feedback>
SupervisorController-->>Supervisor: 显示待审核列表
end
alt 批准反馈
Supervisor->>SupervisorController: POST /api/supervisor/reviews/{feedbackId}/approve
SupervisorController->>SupervisorService: approveFeedback(feedbackId)
SupervisorService->>FeedbackRepository: findById(feedbackId)
FeedbackRepository->>Database: 查询反馈
Database-->>FeedbackRepository: 返回反馈
FeedbackRepository-->>SupervisorService: 返回Feedback
SupervisorService->>SupervisorService: 更新反馈状态为APPROVED
SupervisorService->>FeedbackRepository: save(feedback)
TaskRepository->>Database: 更新反馈状态
Database-->>TaskRepository: 返回更新后的反馈
TaskRepository-->>SupervisorService: 返回更新后的Feedback
SupervisorService-->>SupervisorController: 返回成功响应
SupervisorController-->>Supervisor: 操作成功
end
alt 拒绝反馈
Supervisor->>SupervisorController: POST /api/supervisor/reviews/{feedbackId}/reject
SupervisorController->>SupervisorService: rejectFeedback(feedbackId, request)
SupervisorService->>FeedbackRepository: findById(feedbackId)
FeedbackRepository->>Database: 查询反馈
Database-->>FeedbackRepository: 返回反馈
FeedbackRepository-->>SupervisorService: 返回Feedback
SupervisorService->>SupervisorService: 更新反馈状态为REJECTED并记录原因
SupervisorService->>FeedbackRepository: save(feedback)
TaskRepository->>Database: 更新反馈状态
Database-->>TaskRepository: 返回更新后的反馈
TaskRepository-->>SupervisorService: 返回更新后的Feedback
SupervisorService-->>SupervisorController: 返回成功响应
SupervisorController-->>Supervisor: 操作成功
end
```
### 1.10 路径规划 (A* 寻路算法)
```mermaid
sequenceDiagram
participant User as 用户
participant PathfindingController as 寻路控制器
participant AStarService as A*服务
participant MapData as 地图数据
User->>+PathfindingController: POST /api/pathfinding/find (起点, 终点)
PathfindingController->>+AStarService: findPath(start, end)
AStarService->>+MapData: getObstacles()
MapData-->>-AStarService: 返回障碍物信息
AStarService->>AStarService: 执行A*算法计算路径
AStarService-->>-PathfindingController: 返回计算出的路径
PathfindingController-->>-User: 200 OK (路径坐标列表)
```
### 1.11 公众提交反馈
```mermaid
sequenceDiagram
participant User as 用户
participant FeedbackController as 反馈控制器
participant FeedbackService as 反馈服务
participant FeedbackRepository as 反馈仓库
participant FileService as 文件服务
participant Database as JSON持久化存储
User->>+FeedbackController: POST /api/feedback/submit (反馈信息, 附件)
FeedbackController->>+FeedbackService: submitFeedback(request, files)
alt 包含附件
FeedbackService->>+FileService: store(files)
FileService-->>-FeedbackService: 返回文件存储路径
end
FeedbackService->>+FeedbackRepository: save(feedback)
FeedbackRepository->>+Database: INSERT INTO feedbacks
Database-->>-FeedbackRepository: 返回已保存的反馈
FeedbackRepository-->>-FeedbackService: 返回已保存的反馈
FeedbackService-->>-FeedbackController: 返回创建的反馈
FeedbackController-->>-User: 201 CREATED (反馈详情)
```
### 1.12 文件下载/预览
```mermaid
sequenceDiagram
participant User as 用户
participant FileController as 文件控制器
participant FileStorageService as 文件存储服务
participant FileSystem as 文件系统
User->>+FileController: GET /api/files/{filename} 或 /api/view/{filename}
FileController->>+FileStorageService: loadFileAsResource(filename)
FileStorageService->>+FileSystem: 读取文件
FileSystem-->>-FileStorageService: 返回文件资源
FileStorageService-->>-FileController: 返回文件资源
FileController->>FileController: 设置HTTP响应头 (Content-Type, Content-Disposition)
FileController-->>-User: 200 OK (文件内容)
```
### 1.13 管理员分配网格员
```mermaid
sequenceDiagram
participant Admin as 管理员
participant GridController as 网格控制器
participant UserAccountService as 用户账户服务
participant GridService as 网格服务
participant OperationLogService as 操作日志服务
participant Database as JSON持久化存储
Admin->>GridController: POST /api/grids/assign-worker (userId, gridId)
activate GridController
GridController->>UserAccountService: getUserById(userId)
activate UserAccountService
UserAccountService->>Database: 查询用户
Database-->>UserAccountService: 返回用户信息
UserAccountService-->>GridController: 返回用户
deactivate UserAccountService
alt 用户角色是网格员 (role == 'GRID_WORKER')
GridController->>GridService: assignGridToUser(userId, gridId)
activate GridService
GridService->>Database: 更新用户的 grid_id
Database-->>GridService: 更新成功
GridService-->>GridController: 分配成功
deactivate GridService
GridController->>OperationLogService: recordOperation(adminId, 'ASSIGN_GRID', '...details...')
activate OperationLogService
OperationLogService->>Database: 记录日志
Database-->>OperationLogService: 记录成功
OperationLogService-->>GridController: 记录完成
deactivate OperationLogService
GridController-->>Admin: 200 OK - 分配成功
else 用户角色不是网格员
GridController-->>Admin: 400 Bad Request - 用户不是网格员
end
deactivate GridController
```
### 1.14 初始化地图
```mermaid
sequenceDiagram
participant Admin as 管理员
participant MapController as 地图控制器
participant MapGridRepository as 地图网格仓库
participant Database as JSON持久化存储
Admin->>+MapController: POST /api/map/initialize (width, height)
MapController->>+MapGridRepository: deleteAll()
MapGridRepository->>+Database: DELETE FROM map_grids
Database-->>-MapGridRepository: 删除成功
MapGridRepository-->>-MapController: 返回
loop for y from 0 to height-1
loop for x from 0 to width-1
MapController->>+MapGridRepository: save(cell)
MapGridRepository->>+Database: INSERT INTO map_grids
Database-->>-MapGridRepository: 插入成功
MapGridRepository-->>-MapController: 返回
end
end
MapController-->>-Admin: 200 OK (Initialized a WxH map.)
```
### 1.15 管理员获取操作日志
```mermaid
sequenceDiagram
participant Admin as 管理员
participant OperationLogController as 操作日志控制器
participant OperationLogService as 操作日志服务
participant OperationLogRepository as 操作日志仓库
```

172
Report/目前内容.md Normal file
View File

@@ -0,0 +1,172 @@
# 1. 实践目的
## 1.1 项目概述与个人贡献
### 1.1.1 项目概述
本项目旨在解决传统环保监督渠道反馈不畅、处理流程不透明、以及公众参与度不高等痛点,开发一个高效、透明的**环保公众监督平台**。作为《东软环保应急》系统的重要子模块,它不仅是技术上的实现,更是业务流程上的一次重要创新。
系统的核心目标是打通从"问题发现"到"问题解决"的全链路。它通过提供便捷的移动端/网页端入口,鼓励公众随时随地上传图文并茂的环境问题反馈。系统接收到反馈后,将**自动触发一系列内部流程**首先通过AI服务进行初步的智能分类与严重性评估随后经由主管人员审核确认后反馈将**自动转化为一个结构化的处理任务**;最后,系统基于**智能分配算法**,综合考量网格员的实时位置、当前负载等因素,将任务精准派发给最优的执行者。整个过程的状态(待处理、执行中、已完成、已归档)对管理者和反馈者全程可见,极大地提升了环保工作的透明度与处理效率。
### 1.1.2 个人贡献
在本次实践中,本人担任**核心的系统设计与后端开发角色**,全面负责了从技术选型到项目落地的完整技术实现。我的工作不仅是代码的编写者,更是整个后端架构的**设计者**和**构建者**。具体贡献如下:
* **架构设计与技术决策**:在项目初期,我主导了技术栈的选型,力主采用`Spring Boot` + `Vue.js`的前后端分离架构,以应对未来快速迭代和功能扩展的需求。同时,我独立设计了后端**Controller-Service-Repository**的三层应用架构,并规划了项目的整体模块(如安全、事件、仓储等),为整个项目的稳固开发奠定了基础。
* **核心难题攻关**:面对"所有数据需以文件形式存储"这一核心且棘手的约束,我没有采取简单的文件读写,而是独立设计并实现了一套**模拟JPA规范的泛型JSON仓储层**。这个创新方案不仅完美地解决了数据持久化问题,其遵循的"依赖倒置原则"也使得代码高度解耦,极大地提升了系统的可维护性和可测试性,是本次项目中技术含量最高的突破之一。
* **全栈功能实现**:我负责了全部后端模块的编码实现,包括但不限于基于`Spring Security``JWT`的**用户认证与授权系统**、**任务全生命周期的状态机管理**、以及基于**A*算法的智能任务分配模型**等。此外,我也参与了部分前端页面的开发,并利用`Swagger``Postman`完成了所有API的设计、文档化与测试工作确保了前后端的顺畅联调。
## 1.2 个人能力培养
通过本次实践,本人将软件工程理论与开发实践深度结合,在设计与开发复杂问题的解决方案方面获得了显著提升。
* **软件工程全周期掌控能力**通过完整地参与从需求分析、系统设计、编码实现到测试部署的全过程深刻掌握了现代软件开发的生命周期要素和方法。能够运用面向对象的思想通过UML对系统进行建模合理地划分模块确保了系统设计的高内聚、低耦合。
* **复杂问题解决方案的设计与创新能力**
* 面对"数据以文件格式保存"的核心约束,没有采用简单的序列化读写,而是创新性地设计并实现了一套**基于JSON的泛型仓储层Repository Pattern**。该方案遵循了依赖倒置原则,不仅满足了功能要求,更保证了代码的可维护性与未来向数据库迁移的可行性。
* 针对任务分配场景,设计了**基于多维因素(地理位置、负载)的智能调度算法**,并集成了**A*寻路算法**进行路径规划,展现了运用科学算法解决实际工程问题的能力。
* 在系统中有效运用了**事件驱动模型**将耗时的AI分析流程解耦为异步操作提升了系统的响应速度和健壮性。
* **技术沟通与文档化能力**能够通过撰写系统设计方案、绘制UML图表等方式系统、清晰地阐述项目的技术架构与实现细节并具备良好的口头技术交流能力。
# 2. 相关技术基础
本项目综合运用了业界主流的开发技术与环境构建了一个现代化Web应用。整体技术体系由以下五部分构成
* **理论基础**:遵循**面向对象编程(OOP)**、**SOLID设计原则(尤其是DIP)**、**MVC分层架构**及**RESTful API**设计规范。
* **后端技术栈**:以 **`Spring Boot 3`** 为核心,整合 **`Spring Security`** 与 **`JWT`** 进行安全控制,并创新性地设计了**自定义的泛型JSON仓储层**作为持久化方案。
* **前端技术栈**:采用 **`Vue 3`** 作为核心框架,配合 **`Vite`** 进行构建,使用 **`Pinia`** 进行状态管理,**`Element Plus`** 作为UI组件库并对 **`Axios`** 进行了封装。
* **测试技术**:后端采用 **`JUnit 5`** 和 **`Mockito`** 进行单元/集成测试;接口层使用 **`Swagger UI`** 和 **`Postman`** 进行测试与调试。
* **开发工具与环境**:使用 **`IntelliJ IDEA`** 和 **`VS Code`** 作为主力IDE通过 **`Maven`** 和 **`npm`** 管理项目,并利用 **`Git`** 进行版本控制。
以下是各部分的详细介绍。
## 2.1 理论基础
项目的架构和代码设计遵循了下述成熟的软件工程理论,以确保系统的可维护性和扩展性。
* **面向对象编程 (OOP):**
项目的核心编程思想,其封装、继承、多态特性在后端代码设计中得到了深入应用。
* **封装 (Encapsulation):** 核心业务实体(如`User`, `Task`被抽象为Java类其属性私有化仅通过公共方法暴露保证了对象状态的完整性。
* **继承 (Inheritance) 与多态 (Polymorphism):** 在自定义的JSON仓储层设计中通过定义泛型接口`JsonRepository<T, ID>`和实现该接口的泛型基类`JsonRepositoryImpl<T, ID>`,使得具体的实体仓储(如`JsonUserRepositoryImpl`)能自动获得通用的数据操作能力。业务逻辑层依赖于抽象接口而非具体实现,实现了"面向接口编程",提高了代码的灵活性。
* **SOLID设计原则尤其是依赖倒置原则 (DIP):**
项目架构遵循SOLID原则特别是依赖倒置原则即"高层模块不应依赖于低层模块,两者都应依赖于抽象"。
* **实践应用:** `Service`层(高层模块)依赖的是`UserRepository`等抽象接口,而不是`JsonRepositoryImpl`这样的具体实现(低层模块)。这种设计倒置了传统的依赖关系,极大提升了系统的可替换性和可测试性。未来更换数据源或进行单元测试时,只需更换实现或模拟接口,上层代码无需改动。
* **模型-视图-控制器 (MVC) 分层架构:**
项目遵循MVC分层思想并扩展为更精细的四层架构Controller -> Service -> Repository -> Entity。
* **Controller (控制器层):** 作为HTTP请求的入口负责解析请求参数调用`Service`层执行业务逻辑,并返回响应。
* **Service (业务逻辑层):** 包含所有业务规则和处理流程通过依赖注入DI调用`Repository`层。
* **Repository (数据访问层):** 数据持久化的抽象负责与数据源本项目中为JSON文件交互实现业务与数据的解耦。
* **Entity/DTO (模型层):** POJO对象`Entity`映射数据存储结构,`DTO`Data Transfer Object用于各层之间的数据传输避免持久化实体直接暴露给外部。
* **RESTful API 设计原则:**
前后端通信完全基于RESTful风格的API进行。
* **统一接口 (Uniform Interface):** 使用HTTP标准方法`GET`, `POST`, `PUT`, `DELETE`)表达对资源的操作。
* **无状态 (Stateless):** 服务端不保存客户端会话状态每次请求都包含所有必要信息如JWT提高了系统的可伸缩性。
* **资源导向 (Resource-Oriented):** API围绕"资源"展开,使用名词(如`/api/users`)标识资源,而非动词。
## 2.2 后端技术栈
* **核心框架 - `Spring Boot 3.x`:**
作为后端应用基石它通过自动配置、起步依赖和内嵌服务器等特性极大地简化了Spring应用的开发、配置和部署。其强大的生态整合能力使得集成`Spring Security``Lombok``Swagger`等第三方库变得非常简单。
* **安全框架 - `Spring Security 6.x` & `JWT`:**
采用`Spring Security`结合`JSON Web Tokens (JWT)`,构建无状态的认证与授权体系。
* **`Spring Security` Filter链:** 自定义`JwtAuthenticationFilter`过滤器用于在每个请求中校验JWT并设置安全上下文。
* **声明式权限控制:** 使用`@PreAuthorize`注解对Service方法进行方法级别的权限声明实现了安全逻辑与业务逻辑的解耦。
* **无状态会话 (Stateless Session):** 配置会话管理策略为`STATELESS`,使后端服务成为真正的无状态服务,提升了系统的可伸缩性。
* **持久化方案 - 自定义泛型JSON仓储层:**
为满足"数据存储在JSON文件中"的需求,设计并实现了一套模拟`Spring Data JPA`接口规范的、可复用的泛型JSON仓储层。
* **`JsonStorageService`:** 将所有文件I/O操作封装在此服务中并使用`synchronized`块确保并发写操作的线程安全。
* **`JsonRepositoryImpl<T, ID>`:** 泛型基类实现了通用的CRUD功能具体的实体仓储通过继承该类来复用代码。
* 该方案遵循**依赖倒置原则**,业务层仅依赖抽象接口,为未来平滑迁移持久化方案提供了便利。
* **数据校验 - `Jakarta Bean Validation` (`Hibernate Validator`):**
在DTO的字段上使用`@NotNull`, `@Size`等声明式注解并配合Controller层的`@Valid`注解,实现对请求参数的自动化校验。
* **对象映射 - `MapStruct`:**
一个编译期的代码生成器,通过定义`@Mapper`接口自动生成Entity与DTO之间转换的高性能实现代码提升了开发效率。
* **编程语言与辅助工具:**
* **`Java 17 (LTS)`:** 使用其`record``switch`表达式等新特性编写更简洁的代码。
* **`Lombok`:** 通过`@Data`, `@Builder`等注解,消除样板代码。
* **`SLF4J` & `Logback`:** 灵活的日志系统,通过配置文件实现分环境、分级别的日志输出。
## 2.3 前端技术栈
采用`Vue.js`生态的最新技术构建响应迅速、代码可维护的现代化单页应用SPA
* **核心框架 - `Vue.js 3.x`:**
利用其两大核心新特性:
* **`Composition API` (组合式API):** 将相关逻辑组织在同一个`setup`函数内,提高了代码的可读性、可维护性和逻辑复用性。
* **`Proxy`-based Reactivity:** 基于`Proxy`重写的响应式系统,解决了`Vue 2`中无法监听对象属性新增/删除等痛点。
* **构建工具 - `Vite`:**
新一代前端构建工具,优势在于:
* 利用浏览器原生ESM支持实现开发环境下的极速冷启动和闪电般的热模块替换HMR
* 生产环境使用`Rollup`打包,生成高度优化的静态资源。
* **状态管理 - `Pinia`:**
`Vue`官方推荐的下一代状态管理库特点是API直观、类型支持完美且天生模块化。废除了`Vuex`中复杂的`Mutations`等概念,心智负担小。
* **UI组件库 - `Element Plus`:**
`Element UI``Vue 3`版本是一套高质量的企业级UI组件库提供了丰富的组件极大地加速了界面的开发进程。
* **HTTP客户端 - `Axios`封装:**
对流行的`Axios`库进行二次封装:
* **创建实例:** 为API服务设置不同的`baseURL``timeout`等。
* **请求拦截器:** 统一添加认证`token`
* **响应拦截器:** 统一处理业务数据和错误状态码。
## 2.4 测试技术
* **后端单元与集成测试 (`JUnit 5`, `Mockito`, `Spring Boot Test`):**
使用`spring-boot-starter-test`集成的测试套件保障业务逻辑正确性。
* **`JUnit 5`:** 测试的基础框架。
* **`Mockito`:** 在单元测试中用于模拟依赖对象如Repository实现测试隔离。
* **`Spring Boot Test`:** 用于集成测试,加载完整应用上下文,测试跨层级的真实交互场景。
* **API接口测试 (`Postman` / `Swagger UI`):**
* **`Swagger UI`:** 通过集成`SpringDoc`根据代码注解自动生成交互式API文档方便调试。
* **`Postman`:** 用于执行更复杂的API测试场景、编写测试用例和自动化测试。
## 2.5 开发工具与环境
* **IDE:** 后端使用`IntelliJ IDEA Ultimate`,前端使用`Visual Studio Code`
* **项目管理与构建:** 后端使用`Maven`,前端使用`npm`
* **版本控制:** `Git` & `GitHub`,遵循`Git Flow`工作流。
# 3. 实践结果
此部分属报告的主要部分。包括:
## 3.1 需求定义
"系统分析"也可以看成是需求定义,包括对整个项目的介绍分析及本人工作内容的详细分析,如业务分析、功能分析(可使用例图、活动图来描述)、可行性分析等;
## 3.2 系统设计
"系统设计"包括总体设计和详细设计,"总体设计"包括系统架构设计、功能模块划分等,"详细设计"要围绕本人工作内容展开,包括功能模块详细设计、类和对象的设计、动态模型设计(时序图、状态图、协作图等)、算法设计、数据库设计等;
## 3.3 系统实现
"系统实现"也要围绕本人工作内容展开,从编码实现角度论述相应功能模块的实现细节,并展示自己所完成的主要成果及实际应用情况等。可通过"程序流程图"、"关键代码"和"界面"进行直观论述。
## 3.4 系统测试
"系统测试"包括测试方案设计、测试用例和测试结果、最终的测试结论或评价等。
# 4. 实践总结
简述你在实践过程中的内容完成情况,重点介绍创新点及不足(也就是可以再完善的部分,只是时间不允许了。不足不代表不好,也说明你思考了,但是来不及完成实现)
# 5. 参考资料
例:
[1] 数据结构、算法与应用C++语言描述 [Data Structures,Algorithms,and Applications in C++][M].机械工业出版社出版时间2000-01-01.
[2] 数据结构(C语言版) [M].北京: 中国铁道出版社, 2011-08-01.

View File

@@ -0,0 +1,101 @@
# 1. 相关技术基础
本项目综合运用了业界主流的开发技术与环境构建了一个现代化Web应用。以下是项目所涉及的关键技术、开发工具和环境的介绍。
### 1.1 理论基础
项目的架构和代码设计遵循了下述成熟的软件工程理论,以确保系统的可维护性和扩展性。
* **面向对象编程 (OOP):**
项目的核心编程思想,其封装、继承、多态特性在后端代码设计中得到了深入应用。
* **封装 (Encapsulation):** 核心业务实体(如`User`, `Task`被抽象为Java类其属性私有化仅通过公共方法暴露保证了对象状态的完整性。
* **继承 (Inheritance) 与多态 (Polymorphism):** 在自定义的JSON仓储层设计中通过定义泛型接口`JsonRepository<T, ID>`和实现该接口的泛型基类`JsonRepositoryImpl<T, ID>`,使得具体的实体仓储(如`JsonUserRepositoryImpl`)能自动获得通用的数据操作能力。业务逻辑层依赖于抽象接口而非具体实现,实现了"面向接口编程",提高了代码的灵活性。
* **SOLID设计原则尤其是依赖倒置原则 (DIP):**
项目架构遵循SOLID原则特别是依赖倒置原则即"高层模块不应依赖于低层模块,两者都应依赖于抽象"。
* **实践应用:** `Service`层(高层模块)依赖的是`UserRepository`等抽象接口,而不是`JsonRepositoryImpl`这样的具体实现(低层模块)。这种设计倒置了传统的依赖关系,极大提升了系统的可替换性和可测试性。未来更换数据源或进行单元测试时,只需更换实现或模拟接口,上层代码无需改动。
* **模型-视图-控制器 (MVC) 分层架构:**
项目遵循MVC分层思想并扩展为更精细的四层架构Controller -> Service -> Repository -> Entity。
* **Controller (控制器层):** 作为HTTP请求的入口负责解析请求参数调用`Service`层执行业务逻辑,并返回响应。
* **Service (业务逻辑层):** 包含所有业务规则和处理流程通过依赖注入DI调用`Repository`层。
* **Repository (数据访问层):** 数据持久化的抽象负责与数据源本项目中为JSON文件交互实现业务与数据的解耦。
* **Entity/DTO (模型层):** POJO对象`Entity`映射数据存储结构,`DTO`Data Transfer Object用于各层之间的数据传输避免持久化实体直接暴露给外部。
* **RESTful API 设计原则:**
前后端通信完全基于RESTful风格的API进行。
* **统一接口 (Uniform Interface):** 使用HTTP标准方法`GET`, `POST`, `PUT`, `DELETE`)表达对资源的操作。
* **无状态 (Stateless):** 服务端不保存客户端会话状态每次请求都包含所有必要信息如JWT提高了系统的可伸缩性。
* **资源导向 (Resource-Oriented):** API围绕"资源"展开,使用名词(如`/api/users`)标识资源,而非动词。
### 1.2 后端技术栈
* **核心框架 - `Spring Boot 3.x`:**
作为后端应用基石它通过自动配置、起步依赖和内嵌服务器等特性极大地简化了Spring应用的开发、配置和部署。其强大的生态整合能力使得集成`Spring Security``Lombok``Swagger`等第三方库变得非常简单。
* **安全框架 - `Spring Security 6.x` & `JWT`:**
采用`Spring Security`结合`JSON Web Tokens (JWT)`,构建无状态的认证与授权体系。
* **`Spring Security` Filter链:** 自定义`JwtAuthenticationFilter`过滤器用于在每个请求中校验JWT并设置安全上下文。
* **声明式权限控制:** 使用`@PreAuthorize`注解对Service方法进行方法级别的权限声明实现了安全逻辑与业务逻辑的解耦。
* **无状态会话 (Stateless Session):** 配置会话管理策略为`STATELESS`,使后端服务成为真正的无状态服务,提升了系统的可伸缩性。
* **持久化方案 - 自定义泛型JSON仓储层:**
为满足"数据存储在JSON文件中"的需求,设计并实现了一套模拟`Spring Data JPA`接口规范的、可复用的泛型JSON仓储层。
* **`JsonStorageService`:** 将所有文件I/O操作封装在此服务中并使用`synchronized`块确保并发写操作的线程安全。
* **`JsonRepositoryImpl<T, ID>`:** 泛型基类实现了通用的CRUD功能具体的实体仓储通过继承该类来复用代码。
* 该方案遵循**依赖倒置原则**,业务层仅依赖抽象接口,为未来平滑迁移持久化方案提供了便利。
* **数据校验 - `Jakarta Bean Validation` (`Hibernate Validator`):**
在DTO的字段上使用`@NotNull`, `@Size`等声明式注解并配合Controller层的`@Valid`注解,实现对请求参数的自动化校验。
* **对象映射 - `MapStruct`:**
一个编译期的代码生成器,通过定义`@Mapper`接口自动生成Entity与DTO之间转换的高性能实现代码提升了开发效率。
* **编程语言与辅助工具:**
* **`Java 17 (LTS)`:** 使用其`record``switch`表达式等新特性编写更简洁的代码。
* **`Lombok`:** 通过`@Data`, `@Builder`等注解,消除样板代码。
* **`SLF4J` & `Logback`:** 灵活的日志系统,通过配置文件实现分环境、分级别的日志输出。
### 1.3 前端技术栈
采用`Vue.js`生态的最新技术构建响应迅速、代码可维护的现代化单页应用SPA
* **核心框架 - `Vue.js 3.x`:**
利用其两大核心新特性:
* **`Composition API` (组合式API):** 将相关逻辑组织在同一个`setup`函数内,提高了代码的可读性、可维护性和逻辑复用性。
* **`Proxy`-based Reactivity:** 基于`Proxy`重写的响应式系统,解决了`Vue 2`中无法监听对象属性新增/删除等痛点。
* **构建工具 - `Vite`:**
新一代前端构建工具,优势在于:
* 利用浏览器原生ESM支持实现开发环境下的极速冷启动和闪电般的热模块替换HMR
* 生产环境使用`Rollup`打包,生成高度优化的静态资源。
* **状态管理 - `Pinia`:**
`Vue`官方推荐的下一代状态管理库特点是API直观、类型支持完美且天生模块化。废除了`Vuex`中复杂的`Mutations`等概念,心智负担小。
* **UI组件库 - `Element Plus`:**
`Element UI``Vue 3`版本是一套高质量的企业级UI组件库提供了丰富的组件极大地加速了界面的开发进程。
* **HTTP客户端 - `Axios`封装:**
对流行的`Axios`库进行二次封装:
* **创建实例:** 为API服务设置不同的`baseURL``timeout`等。
* **请求拦截器:** 统一添加认证`token`
* **响应拦截器:** 统一处理业务数据和错误状态码。
### 1.4 测试技术
* **后端单元与集成测试 (`JUnit 5`, `Mockito`, `Spring Boot Test`):**
使用`spring-boot-starter-test`集成的测试套件保障业务逻辑正确性。
* **`JUnit 5`:** 测试的基础框架。
* **`Mockito`:** 在单元测试中用于模拟依赖对象如Repository实现测试隔离。
* **`Spring Boot Test`:** 用于集成测试,加载完整应用上下文,测试跨层级的真实交互场景。
* **API接口测试 (`Postman` / `Swagger UI`):**
* **`Swagger UI`:** 通过集成`SpringDoc`根据代码注解自动生成交互式API文档方便调试。
* **`Postman`:** 用于执行更复杂的API测试场景、编写测试用例和自动化测试。
### 1.5 开发工具与环境
* **IDE:** 后端使用`IntelliJ IDEA Ultimate`,前端使用`Visual Studio Code`
* **项目管理与构建:** 后端使用`Maven`,前端使用`npm`
* **版本控制:** `Git` & `GitHub`,遵循`Git Flow`工作流。

195
Report/系统实现.md Normal file
View File

@@ -0,0 +1,195 @@
# 3.3 系统实现
本章节详细阐述了环境监督系统EMS的实际编码实现过程展示了如何将系统设计转化为可运行的代码。由于内容较多为了便于阅读和维护本章节分为以下三个部分
## 文档目录
1. [开发环境与技术栈 & 核心功能模块实现(上)](系统实现_第一部分.md)
- 开发环境与技术栈
- 泛型JSON存储服务
- A*寻路算法实现
2. [核心功能模块实现(中)](系统实现_第二部分.md)
- A*寻路算法实现要点分析
- 反馈管理模块实现
3. [核心功能模块实现(下) & 界面展示与成果展示](系统实现_第三部分.md)
- 任务智能分配算法
- 界面展示与成果展示
- 实现成果总结
请点击上述链接查看各部分的详细内容。
## 3.3.1 后端关键技术实现
### 1. 核心数据服务泛型JSON仓储
为了实现一个可复用、类型安全且与业务逻辑完全解耦的数据持久化层,我设计并实现了一个泛型的`JsonStorageService`。这是整个后端数据访问的基石是实现仓储模式Repository Pattern的关键底层支持。
```java:ems-backend/src/main/java/com/dne/ems/service/JsonStorageService.java
@Service
public class JsonStorageService {
private static final Path STORAGE_DIRECTORY = Paths.get("json-db");
private final ObjectMapper objectMapper; // Jackson的核心用于JSON序列化和反序列化
// 使用构造函数注入符合Spring推荐的最佳实践
public JsonStorageService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
// 在服务启动时,检查并创建存储目录,保证后续操作的顺利进行
try {
if (!Files.exists(STORAGE_DIRECTORY)) {
Files.createDirectories(STORAGE_DIRECTORY);
}
} catch (IOException e) {
// 如果目录创建失败,则抛出运行时异常,使应用启动失败,防止后续出现更严重问题
throw new UncheckedIOException("Could not create storage directory", e);
}
}
// 同步写数据方法,保证线程安全
public synchronized <T> void writeData(String fileName, List<T> data) {
try {
Path filePath = STORAGE_DIRECTORY.resolve(fileName);
// 使用writeValueAsString先转为格式化的JSON字符串再写入文件提高可读性
String jsonContent = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data);
Files.write(filePath, jsonContent.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new UncheckedIOException("Error writing data to " + fileName, e);
}
}
// 读数据方法
public <T> List<T> readData(String fileName, TypeReference<List<T>> typeReference) {
Path filePath = STORAGE_DIRECTORY.resolve(fileName);
if (!Files.exists(filePath)) {
return new ArrayList<>(); // 文件不存在是正常情况,返回空列表
}
try {
// 使用TypeReference来处理泛型擦除问题确保JSON能被正确反序列化为List<T>
return objectMapper.readValue(filePath.toFile(), typeReference);
} catch (IOException e) {
// 如果文件为空或JSON格式损坏记录日志并返回空列表增强系统容错性
log.warn("Could not read or parse file: {}. Returning empty list.", fileName, e);
return new ArrayList<>();
}
}
}
```
**设计深度解析:**
* **泛型与类型安全:** `writeData`和`readData`方法都使用了泛型`<T>`,使得该服务可以处理任何类型的实体列表(如`List<User>`、`List<Task>`)。通过传入`TypeReference<List<T>>`我们解决了Java泛型在运行时被擦除的问题使得Jackson库能够准确地将JSON数组反序列化为指定类型的对象列表保证了类型安全。
* **线程安全与并发控制:** `writeData`方法被声明为`synchronized`,这是一个简单而有效的并发控制手段。它利用对象锁确保了在多线程环境下对同一文件的写操作是互斥的、串行的,从而从根本上防止了数据竞争和文件损坏的风险。
* **健壮性与容错:** 代码对存储目录不存在、文件读写IO异常、JSON解析异常等情况都进行了处理。特别是当文件不存在或内容为空/损坏时,`readData`会返回一个空列表而不是抛出异常这使得上层调用者Repository无需处理这些繁琐的底层细节增强了系统的整体健-壮性。
### 2. 安全核心JWT生成与校验
系统的认证和授权机制基于JWTJSON Web Token。我实现了一个`JwtTokenProvider`来封装JWT的生成和校验逻辑使其与业务代码分离。
```java:ems-backend/src/main/java/com/dne/ems/security/JwtTokenProvider.java
@Component
public class JwtTokenProvider {
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;
// 根据用户信息生成JWT
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.claim("role", userPrincipal.getAuthorities().iterator().next().getAuthority())
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
// 从JWT中解析用户ID
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
// 校验JWT的有效性
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException | MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException ex) {
// 捕获所有可能的JWT校验异常
log.error("Invalid JWT token: {}", ex.getMessage());
}
return false;
}
}
```
**实现说明:**
* **配置化:** JWT的密钥`jwtSecret`)和过期时间(`jwtExpirationInMs`)都通过`@Value`注解从`application.properties`配置文件中读取,便于不同环境的部署和管理。
* **声明Claims:** 在生成Token时我不仅将用户ID作为`subject`还将用户的角色Role作为一个自定义的`claim`存入Token中。这样做的好处是在后续的授权判断中我们无需再次查询数据库直接从Token中即可获取用户角色提高了性能。
* **全面的异常处理:** `validateToken`方法捕获了`jjwt`库可能抛出的所有校验异常如签名错误、格式错误、Token过期等并记录日志保证了校验逻辑的严谨性。
### 3. 统一出口:全局异常处理器
为了提供统一、规范的API错误响应格式我实现了一个全局异常处理器。这避免了在每个Controller方法中都写`try-catch`块的冗余代码。
```java:ems-backend/src/main/java/com/dne/ems/exception/GlobalExceptionHandler.java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = {IllegalArgumentException.class, IllegalStateException.class})
public ResponseEntity<ApiResponse> handleBadRequest(RuntimeException ex, WebRequest request) {
ApiResponse apiResponse = new ApiResponse(false, ex.getMessage());
return new ResponseEntity<>(apiResponse, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(value = ResourceNotFoundException.class)
public ResponseEntity<ApiResponse> handleResourceNotFound(ResourceNotFoundException ex, WebRequest request) {
ApiResponse apiResponse = new ApiResponse(false, ex.getMessage());
return new ResponseEntity<>(apiResponse, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(value = Exception.class)
public ResponseEntity<ApiResponse> handleGlobalException(Exception ex, WebRequest request) {
log.error("An unexpected error occurred: ", ex);
ApiResponse apiResponse = new ApiResponse(false, "An internal server error occurred. Please try again later.");
return new ResponseEntity<>(apiResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
```
**实现说明:**
* **`@ControllerAdvice`:** 该注解使得这个类可以成为一个全局的AOP切面用于处理所有`@Controller`中抛出的异常。
* **`@ExceptionHandler`:** 针对不同类型的异常如参数错误、资源未找到、未知异常定义了不同的处理方法返回相应的HTTP状态码和统一格式的JSON错误信息`ApiResponse`对象极大地改善了API的友好性和可调试性。
## 3.3.2 前端界面实现与展示
我独立负责了大部分前端界面的开发,采用了`Vue 3` + `Vite` + `Pinia` + `Element Plus`这一现代化的技术栈,实现了数据驱动的、组件化的、响应式的用户界面。
* **组件化开发:** 我将UI拆分为多个可复用的组件如`TaskCard.vue`, `FeedbackList.vue`, `UserSelector.vue`等,提高了代码的可维护性和开发效率。
* **状态管理:** 使用`Pinia`作为全局状态管理器,集中管理用户的登录信息、角色、权限以及全局的加载状态等,使得跨组件的状态共享变得简单、可预测。
* **API交互:** 封装了`axios`实例添加了请求和响应拦截器。请求拦截器负责在每个请求头中自动附加JWT响应拦截器负责处理全局的API错误如`401 Unauthorized`(自动跳转到登录页)、`500 Internal Server Error`(弹出统一的错误提示)。
以下是系统核心页面的UI设计与功能描述
**1. 主管工作台 - 任务分配页面**
* **界面截图描述:** 页面左侧是一个可滚动、可搜索的"待处理反馈"列表,每项都清晰地展示了反馈的标题、提交时间和关键内容摘要。点击某一项后,右侧会显示该反馈的完整详情。详情下方是一个"指派网格员"的下拉选择框,其中列出了所有状态正常的网格员。选择网格员后,点击"立即分配"按钮,系统会弹出确认对话框,确认后完成任务的创建和分配,并给出成功提示。
**2. 网格员工作台 - 任务处理页面**
* **界面截图描述:** 页面顶部是任务的核心信息卡片,包括任务标题、描述、截止日期和当前状态。下方是一个多标签页的表单区域。第一个标签页是"数据录入",包含了多个根据任务类型动态生成的表单项(如空气质量指数、噪声分贝、垃圾分类情况等)。第二个标签页是"现场照片",提供了一个支持拖拽和点击上传的图片上传组件,并能实时显示缩略图。所有信息填写完毕后,点击底部的"完成任务"按钮提交数据。
**3. 数据决策看板 (Dashboard)**
* **界面截图描述:** 这是一个信息密集型的仪表盘页面。顶部是四个醒目的KPI卡片分别显示"本月任务总数"、"已完成率"、"平均处理时长"和"待处理反馈数"。下方是一个占据主要区域的地图组件,地图上用不同颜色的标记点展示了各个网格区域的问题分布和严重程度。地图右侧是两个图表:一个是"近30天任务趋势"的折线图,另一个是"问题类型分布"的饼图。所有图表都是动态的,并支持按时间范围进行筛选。

View File

@@ -0,0 +1,217 @@
# 3.3 系统实现
## 3.3.1 开发环境与技术栈
在本项目的开发过程中,我采用了现代化、高效且广泛应用于企业级开发的技术栈组合,确保系统的稳定性、可扩展性和维护性。
### 后端技术栈
- **编程语言**: Java 17
- **框架**: Spring Boot 3.1.5
- **API文档**: Springdoc OpenAPI (Swagger)
- **安全框架**: Spring Security + JWT
- **构建工具**: Maven 3.9
- **数据存储**: 自定义JSON文件存储模拟数据库
- **异步处理**: Spring Events
- **日志框架**: SLF4J + Logback
- **单元测试**: JUnit 5 + Mockito
### 前端技术栈
- **框架**: Vue 3 (Composition API)
- **构建工具**: Vite
- **UI组件库**: Element Plus
- **状态管理**: Pinia
- **路由**: Vue Router
- **HTTP客户端**: Axios
- **CSS预处理器**: SCSS
- **图表库**: ECharts
### 开发工具
- **IDE**: IntelliJ IDEA / VS Code
- **版本控制**: Git
- **API测试**: Postman
- **代码质量**: SonarLint
- **调试工具**: Chrome DevTools
## 3.3.2 核心功能模块实现
在系统设计阶段,我们已经详细规划了各个功能模块。在实现阶段,我负责了多个核心模块的编码工作,下面将详细介绍这些模块的实现细节。
### 1. 泛型JSON存储服务
在本项目中我设计并实现了一个创新的数据持久化解决方案使用JSON文件代替传统数据库进行数据存储。这种方案简化了系统部署同时提供了类似于JPA的操作接口使得业务层代码无需关心底层存储细节。
**关键代码展示**:
```java
@Service
public class JsonStorageService {
private static final Path STORAGE_DIRECTORY = Paths.get("json-db");
private final ObjectMapper objectMapper;
public JsonStorageService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
try {
if (!Files.exists(STORAGE_DIRECTORY)) {
Files.createDirectories(STORAGE_DIRECTORY);
}
} catch (IOException e) {
throw new UncheckedIOException("无法创建存储目录", e);
}
}
// 同步写数据方法,保证线程安全
public synchronized <T> void writeData(String fileName, List<T> data) {
try {
Path filePath = STORAGE_DIRECTORY.resolve(fileName);
// 使用writeValueAsString先转为格式化的JSON字符串再写入文件提高可读性
String jsonContent = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data);
Files.write(filePath, jsonContent.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new UncheckedIOException("写入数据到 " + fileName + " 时出错", e);
}
}
// 读数据方法
public <T> List<T> readData(String fileName, TypeReference<List<T>> typeReference) {
Path filePath = STORAGE_DIRECTORY.resolve(fileName);
if (!Files.exists(filePath)) {
return new ArrayList<>(); // 文件不存在是正常情况,返回空列表
}
try {
// 使用TypeReference来处理泛型擦除问题确保JSON能被正确反序列化为List<T>
return objectMapper.readValue(filePath.toFile(), typeReference);
} catch (IOException e) {
log.warn("无法读取或解析文件: {}. 返回空列表。", fileName, e);
return new ArrayList<>();
}
}
}
```
**实现要点分析**:
1. **泛型设计**: 通过Java泛型和TypeReference实现了类型安全的数据读写支持任意模型类。
2. **线程安全**: 使用synchronized关键字确保写操作的线程安全防止并发写入导致的数据损坏。
3. **容错处理**: 对文件不存在、IO异常等情况进行了妥善处理增强了系统的健壮性。
4. **格式化输出**: 使用Jackson的prettyPrinter功能使生成的JSON文件具有良好的可读性便于调试。
**程序流程图**:
```mermaid
flowchart TD
A[开始] --> B{文件存在?}
B -->|是| C[读取文件内容]
B -->|否| D[返回空列表]
C --> E{解析JSON成功?}
E -->|是| F[返回对象列表]
E -->|否| G[记录警告日志]
G --> D
```
### 2. A*寻路算法实现
在网格与地图模块中我实现了A*寻路算法,为网格员提供从当前位置到任务地点的最优路径规划。该算法考虑了地图障碍物,能够高效地找到最短路径。
**关键代码展示**:
```java
@Service
@RequiredArgsConstructor
public class AStarService {
private final MapGridRepository mapGridRepository;
// 内部Node类用于A*算法的节点表示
private static class Node {
Point point;
int g; // 从起点到当前节点的实际代价
int h; // 启发式:从当前节点到终点的估计代价
int f; // g + h
Node parent;
public Node(Point point, Node parent, int g, int h) {
this.point = point;
this.parent = parent;
this.g = g;
this.h = h;
this.f = g + h;
}
}
/**
* 寻找两点间的最短路径,避开障碍物
* 地图数据(包括尺寸和障碍物)从数据库动态加载
*/
public List<Point> findPath(Point start, Point end) {
// 从数据库加载地图数据
List<MapGrid> mapGrids = mapGridRepository.findAll();
if (mapGrids.isEmpty()) {
return Collections.emptyList(); // 无地图数据
}
// 动态确定地图尺寸和障碍物
int maxX = 0, maxY = 0;
Set<Point> obstacles = new HashSet<>();
for (MapGrid gridCell : mapGrids) {
if (gridCell.isObstacle() || gridCell.getCityName() == null || gridCell.getCityName().trim().isEmpty()) {
obstacles.add(new Point(gridCell.getX(), gridCell.getY()));
}
if (gridCell.getX() > maxX) maxX = gridCell.getX();
if (gridCell.getY() > maxY) maxY = gridCell.getY();
}
final int mapWidth = maxX + 1;
final int mapHeight = maxY + 1;
// A*算法核心实现
PriorityQueue<Node> openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f));
Map<Point, Node> allNodes = new HashMap<>();
Node startNode = new Node(start, null, 0, calculateHeuristic(start, end));
openSet.add(startNode);
allNodes.put(start, startNode);
while (!openSet.isEmpty()) {
Node currentNode = openSet.poll();
if (currentNode.point.equals(end)) {
return reconstructPath(currentNode);
}
for (Point neighborPoint : getNeighbors(currentNode.point, mapWidth, mapHeight)) {
if (obstacles.contains(neighborPoint)) {
continue;
}
int tentativeG = currentNode.g + 1; // 相邻节点间距离为1
Node neighborNode = allNodes.get(neighborPoint);
if (neighborNode == null) {
// 新节点
int h = calculateHeuristic(neighborPoint, end);
neighborNode = new Node(neighborPoint, currentNode, tentativeG, h);
allNodes.put(neighborPoint, neighborNode);
openSet.add(neighborNode);
} else if (tentativeG < neighborNode.g) {
// 找到更好的路径
neighborNode.parent = currentNode;
neighborNode.g = tentativeG;
neighborNode.f = tentativeG + neighborNode.h;
// 更新优先队列
openSet.remove(neighborNode);
openSet.add(neighborNode);
}
}
}
return Collections.emptyList(); // 未找到路径
}
// 计算启发式函数值(曼哈顿距离)
private int calculateHeuristic(Point a, Point b) {
return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y());
}
}

View File

@@ -0,0 +1,462 @@
### 4. 任务智能分配算法
任务分配是系统中的关键业务流程,我实现了一个智能分配算法,能够根据多种因素为任务选择最合适的网格员。该算法综合考虑了地理距离、当前负载、专业匹配度和历史表现等因素。
**关键代码展示**:
```java
@Service
@RequiredArgsConstructor
public class TaskAssignmentServiceImpl implements TaskAssignmentService {
private final TaskRepository taskRepository;
private final UserAccountRepository userAccountRepository;
private final GridService gridService;
private final WorkerStatsService workerStatsService;
/**
* 为任务推荐最合适的网格员
* 综合考虑地理距离、当前负载、专业匹配度和历史表现
*/
@Override
public UserAccount recommendWorkerForTask(Task task, List<UserAccount> availableWorkers) {
if (availableWorkers.isEmpty()) {
return null;
}
// 权重配置
final double DISTANCE_WEIGHT = 0.4; // 地理距离权重
final double LOAD_WEIGHT = 0.3; // 当前负载权重
final double SKILL_WEIGHT = 0.2; // 专业匹配度权重
final double PERFORMANCE_WEIGHT = 0.1; // 历史表现权重
// 任务位置
Point taskLocation = new Point(task.getGridX(), task.getGridY());
// 计算每个网格员的评分
Map<UserAccount, Double> workerScores = new HashMap<>();
for (UserAccount worker : availableWorkers) {
// 1. 地理距离评分
Point workerLocation = new Point(worker.getGridX(), worker.getGridY());
double distance = calculateDistance(workerLocation, taskLocation);
double maxDistance = 10.0; // 最大考虑距离
double distanceScore = Math.max(0, maxDistance - distance) / maxDistance;
// 2. 当前负载评分
int currentTasks = taskRepository.countByAssigneeIdAndStatusIn(
worker.getId(), Arrays.asList(TaskStatus.ASSIGNED, TaskStatus.IN_PROGRESS));
int maxTasks = 5; // 最大任务数
double loadScore = (double)(maxTasks - currentTasks) / maxTasks;
// 3. 专业匹配度评分
double skillScore = calculateSkillMatch(worker, task);
// 4. 历史表现评分
double completionRate = workerStatsService.getCompletionRate(worker.getId());
double qualityRating = workerStatsService.getAverageQualityRating(worker.getId());
double performanceScore = (completionRate * 0.7) + (qualityRating * 0.3);
// 5. 综合评分
double totalScore = (distanceScore * DISTANCE_WEIGHT) +
(loadScore * LOAD_WEIGHT) +
(skillScore * SKILL_WEIGHT) +
(performanceScore * PERFORMANCE_WEIGHT);
workerScores.put(worker, totalScore);
}
// 选择评分最高的网格员
return workerScores.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
}
/**
* 计算两点间的距离
* 使用曼哈顿距离,适合网格化的城市环境
*/
private double calculateDistance(Point a, Point b) {
return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y());
}
/**
* 计算网格员技能与任务的匹配度
* 返回0-1之间的分数1表示完全匹配
*/
private double calculateSkillMatch(UserAccount worker, Task task) {
// 获取工人的技能列表
List<String> workerSkills = worker.getSkills();
if (workerSkills == null || workerSkills.isEmpty()) {
return 0.0;
}
// 根据任务类型确定所需技能
List<String> requiredSkills = determineRequiredSkills(task);
if (requiredSkills.isEmpty()) {
return 1.0; // 如果任务没有特定技能要求,则任何工人都完全匹配
}
// 计算匹配的技能数量
long matchedSkillsCount = requiredSkills.stream()
.filter(skill -> workerSkills.contains(skill))
.count();
// 返回匹配率
return (double) matchedSkillsCount / requiredSkills.size();
}
/**
* 根据任务类型确定所需技能
*/
private List<String> determineRequiredSkills(Task task) {
List<String> skills = new ArrayList<>();
// 根据污染类型添加相关技能
switch (task.getPollutionType()) {
case AIR:
skills.add("air_quality_monitoring");
skills.add("emissions_control");
break;
case WATER:
skills.add("water_sampling");
skills.add("water_treatment");
break;
case SOIL:
skills.add("soil_testing");
skills.add("land_remediation");
break;
case NOISE:
skills.add("noise_measurement");
skills.add("sound_insulation");
break;
default:
skills.add("general_environmental");
}
// 根据严重程度添加额外技能
if (task.getSeverityLevel() == SeverityLevel.HIGH ||
task.getSeverityLevel() == SeverityLevel.CRITICAL) {
skills.add("emergency_response");
}
return skills;
}
}
```
**实现要点分析**:
1. **多因素加权评分**: 算法综合考虑了地理距离、当前负载、专业匹配度和历史表现四个因素,通过加权计算得出每个网格员的综合得分。
2. **技能匹配机制**: 根据任务的污染类型和严重程度,动态确定所需的技能列表,然后与网格员的技能进行匹配,计算匹配度。
3. **可配置权重**: 各因素的权重可以根据实际需求进行调整,使得算法更加灵活和适应性强。
4. **流式API**: 使用Java 8的Stream API进行最大值查找代码简洁且高效。
**程序流程图**:
```mermaid
flowchart TD
A[开始] --> B[获取可用网格员列表]
B --> C{列表为空?}
C -->|是| D[返回null]
C -->|否| E[遍历每个网格员]
E --> F[计算地理距离评分]
F --> G[计算当前负载评分]
G --> H[计算专业匹配度评分]
H --> I[计算历史表现评分]
I --> J[计算综合评分]
J --> K[记录网格员评分]
K --> L{所有网格员已处理?}
L -->|否| E
L -->|是| M[返回评分最高的网格员]
```
## 3.3.3 界面展示与成果展示
在前端实现方面我采用了Vue 3和Element Plus组件库打造了美观、易用且响应式的用户界面。下面展示几个核心界面及其功能实现。
### 1. 主管工作台 - 反馈审核与任务分配
![主管工作台界面](./Design/supervisor_dashboard.png)
**界面功能说明**:
1. **左侧反馈列表**: 展示所有待审核的反馈,支持按状态、类型和严重程度进行筛选。
2. **右侧反馈详情**: 显示选中反馈的详细信息,包括标题、描述、位置和附件等。
3. **智能推荐**: 系统自动推荐最合适的网格员,并显示推荐理由。
4. **手动分配**: 主管可以覆盖系统推荐,手动选择其他网格员。
5. **批量操作**: 支持批量审核和分配功能,提高工作效率。
**前端代码实现**:
```vue
<template>
<div class="supervisor-dashboard">
<el-row :gutter="20">
<!-- 左侧反馈列表 -->
<el-col :span="8">
<el-card class="feedback-list">
<template #header>
<div class="card-header">
<span>待处理反馈 ({{ totalItems }})</span>
<el-input
v-model="searchQuery"
placeholder="搜索反馈..."
prefix-icon="el-icon-search"
clearable
@input="handleSearch"
/>
</div>
</template>
<el-table
v-loading="loading"
:data="feedbackList"
@row-click="handleRowClick"
highlight-current-row
>
<el-table-column prop="eventId" label="事件ID" width="120" />
<el-table-column prop="title" label="标题" show-overflow-tooltip />
<el-table-column prop="severityLevel" label="严重程度" width="100">
<template #default="scope">
<el-tag :type="getSeverityType(scope.row.severityLevel)">
{{ scope.row.severityLevel }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
background
layout="prev, pager, next"
:total="totalItems"
:page-size="pageSize"
@current-change="handlePageChange"
/>
</div>
</el-card>
</el-col>
<!-- 右侧反馈详情 -->
<el-col :span="16">
<el-card v-if="selectedFeedback" class="feedback-detail">
<template #header>
<div class="card-header">
<span>反馈详情</span>
<div>
<el-button
type="success"
icon="el-icon-check"
@click="handleApprove"
:disabled="!canApprove"
>
批准
</el-button>
<el-button
type="danger"
icon="el-icon-close"
@click="handleReject"
:disabled="!canReject"
>
拒绝
</el-button>
</div>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="标题">{{ selectedFeedback.title }}</el-descriptions-item>
<el-descriptions-item label="事件ID">{{ selectedFeedback.eventId }}</el-descriptions-item>
<el-descriptions-item label="污染类型">{{ selectedFeedback.pollutionType }}</el-descriptions-item>
<el-descriptions-item label="严重程度">
<el-tag :type="getSeverityType(selectedFeedback.severityLevel)">
{{ selectedFeedback.severityLevel }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ formatDate(selectedFeedback.createdAt) }}</el-descriptions-item>
<el-descriptions-item label="位置">{{ selectedFeedback.textAddress }}</el-descriptions-item>
<el-descriptions-item label="描述" :span="2">
{{ selectedFeedback.description }}
</el-descriptions-item>
</el-descriptions>
<!-- 附件展示 -->
<div v-if="selectedFeedback.attachments && selectedFeedback.attachments.length > 0" class="attachments">
<h3>附件</h3>
<el-image
v-for="(attachment, index) in selectedFeedback.attachments"
:key="index"
:src="attachment.filePath"
:preview-src-list="getImageUrls()"
fit="cover"
class="attachment-image"
/>
</div>
<!-- 任务分配表单 -->
<div v-if="selectedFeedback.status === 'PENDING_ASSIGNMENT'" class="assignment-form">
<h3>分配任务</h3>
<el-form :model="assignmentForm" label-width="120px">
<el-form-item label="选择网格员">
<el-select v-model="assignmentForm.workerId" placeholder="选择网格员">
<el-option
v-for="worker in availableWorkers"
:key="worker.id"
:label="worker.name"
:value="worker.id"
>
<div class="worker-option">
<span>{{ worker.name }}</span>
<small>
{{ worker.isRecommended ? '(系统推荐)' : '' }}
{{ worker.currentTasks }} 个任务
</small>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleAssign" :loading="assignLoading">
分配任务
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
<el-empty v-else description="请选择一个反馈" />
</el-col>
</el-row>
</div>
</template>
```
### 2. 网格员工作台 - 任务执行
![网格员工作台界面](./Design/worker_dashboard.png)
**界面功能说明**:
1. **任务列表**: 展示分配给当前网格员的所有任务,按状态和截止日期排序。
2. **任务详情**: 显示选中任务的详细信息,包括位置、描述和附件等。
3. **路径规划**: 集成A*寻路算法,为网格员提供从当前位置到任务地点的最优路径。
4. **任务提交**: 提供表单和文件上传功能,让网格员提交任务处理结果。
### 3. 决策支持看板
![决策支持看板界面](./Design/decision_dashboard.png)
**界面功能说明**:
1. **核心KPI**: 展示关键业务指标,如反馈总数、任务完成率、平均处理时长等。
2. **热力图**: 基于地理位置的问题密度热力图,直观展示问题高发区域。
3. **趋势图**: 展示过去30天的反馈和任务数量趋势帮助决策者了解系统运行状况。
4. **分布图**: 展示不同类型问题的分布情况,帮助决策者了解问题类型分布。
**前端代码实现**:
```vue
<template>
<div class="dashboard-container">
<el-row :gutter="20" class="kpi-row">
<el-col :span="6" v-for="(item, index) in kpiData" :key="index">
<el-card shadow="hover" class="kpi-card">
<div class="kpi-icon">
<i :class="item.icon"></i>
</div>
<div class="kpi-content">
<div class="kpi-value">{{ item.value }}</div>
<div class="kpi-label">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="chart-row">
<el-col :span="16">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>问题热力图</span>
<el-select v-model="mapFilter" placeholder="筛选" size="small">
<el-option label="全部问题" value="ALL" />
<el-option label="空气污染" value="AIR" />
<el-option label="水污染" value="WATER" />
<el-option label="噪音污染" value="NOISE" />
</el-select>
</div>
</template>
<div class="map-container">
<heatmap-component :data="heatmapData" :filter="mapFilter" />
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="card-header">
<span>问题类型分布</span>
</div>
</template>
<div class="chart-container">
<pie-chart :data="typeDistribution" />
</div>
</el-card>
<el-card shadow="hover" class="chart-card" style="margin-top: 20px;">
<template #header>
<div class="card-header">
<span>任务完成情况</span>
</div>
</template>
<div class="chart-container">
<progress-chart :data="taskCompletionData" />
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="chart-row">
<el-col :span="24">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>过去30天趋势</span>
<el-radio-group v-model="trendType" size="small">
<el-radio-button label="feedback">反馈数量</el-radio-button>
<el-radio-button label="tasks">任务数量</el-radio-button>
<el-radio-button label="completion">完成率</el-radio-button>
</el-radio-group>
</div>
</template>
<div class="chart-container">
<trend-chart :data="trendData" :type="trendType" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
```
## 3.3.4 实现成果总结
通过系统实现阶段的工作,我成功地将系统设计阶段规划的功能转化为了可运行的代码,实现了环境监督系统的核心功能。主要成果包括:
1. **创新的数据持久化方案**: 设计并实现了基于JSON文件的数据存储服务简化了系统部署同时提供了类似JPA的操作接口。
2. **高效的A*寻路算法**: 实现了能够考虑地图障碍物的A*寻路算法,为网格员提供最优路径规划。
3. **智能的任务分配算法**: 开发了综合考虑多种因素的任务分配算法,能够为任务选择最合适的执行者。
4. **完整的反馈管理流程**: 实现了反馈提交、AI审核、人工审核和任务创建的完整流程支持多条件查询和文件上传。
5. **直观的用户界面**: 设计并实现了美观、易用且响应式的用户界面,包括主管工作台、网格员工作台和决策支持看板等。
这些实现成果不仅满足了系统需求分析阶段定义的功能要求,也体现了系统设计阶段规划的架构和模块划分。系统的实现充分考虑了可维护性、可扩展性和用户体验,为后续的系统测试和部署奠定了坚实的基础。

View File

@@ -0,0 +1,242 @@
**实现要点分析**:
1. **动态地图加载**: 算法从数据库动态加载地图数据,包括障碍物信息和地图尺寸,使得路径规划能够适应地图的变化。
2. **优先队列优化**: 使用优先队列PriorityQueue存储开放列表确保每次都能高效地选择F值最小的节点大大提高了算法效率。
3. **曼哈顿距离启发函数**: 选择曼哈顿距离作为启发函数,适合网格化的移动模式,能够准确估计网格间的距离。
4. **路径重建**: 通过记录每个节点的父节点,实现了从终点回溯到起点的路径重建,返回完整的路径点列表。
**程序流程图**:
```mermaid
flowchart TD
A[开始] --> B[加载地图数据]
B --> C[初始化开放列表和关闭列表]
C --> D[将起点加入开放列表]
D --> E{开放列表为空?}
E -->|是| F[返回空路径]
E -->|否| G[从开放列表取出F值最小的节点]
G --> H{是终点?}
H -->|是| I[重建并返回路径]
H -->|否| J[处理相邻节点]
J --> E
```
**API接口**:
```java
@RestController
@RequestMapping("/api/pathfinding")
@RequiredArgsConstructor
public class PathfindingController {
private final AStarService aStarService;
@GetMapping("/find")
@PreAuthorize("hasRole('GRID_WORKER')")
public ResponseEntity<List<Point>> findPath(
@RequestParam int startX, @RequestParam int startY,
@RequestParam int endX, @RequestParam int endY) {
Point start = new Point(startX, startY);
Point end = new Point(endX, endY);
List<Point> path = aStarService.findPath(start, end);
return ResponseEntity.ok(path);
}
}
```
### 3. 反馈管理模块
反馈管理模块是系统的核心业务模块之一,负责处理用户提交的环境问题反馈。我实现了完整的反馈提交、审核和处理流程,包括多条件查询、状态流转和文件上传等功能。
**关键代码展示**:
```java
@RestController
@RequestMapping("/api/feedback")
@RequiredArgsConstructor
public class FeedbackController {
private final FeedbackService feedbackService;
/**
* 提交反馈(正式接口)
* 使用multipart/form-data格式提交反馈支持附件上传
*/
@PostMapping(value = "/submit", consumes = {"multipart/form-data"})
@PreAuthorize("isAuthenticated()")
public ResponseEntity<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);
}
/**
* 获取所有反馈(分页+多条件过滤)
* 支持按状态、污染类型、严重程度、地理位置、时间范围和关键词进行组合查询
*/
@GetMapping
@PreAuthorize("isAuthenticated()")
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);
}
/**
* 处理反馈 (例如, 批准)
*/
@PostMapping("/{id}/process")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPERVISOR')")
public ResponseEntity<FeedbackResponseDTO> processFeedback(
@PathVariable Long id,
@Valid @RequestBody ProcessFeedbackRequest request) {
FeedbackResponseDTO updatedFeedback = feedbackService.processFeedback(id, request);
return ResponseEntity.ok(updatedFeedback);
}
}
```
**服务层实现**:
```java
@Service
@RequiredArgsConstructor
public class FeedbackServiceImpl implements FeedbackService {
private final FeedbackRepository feedbackRepository;
private final UserAccountRepository userAccountRepository;
private final FileStorageService fileStorageService;
private final ApplicationEventPublisher eventPublisher;
@Override
public Feedback submitFeedback(FeedbackSubmissionRequest request, MultipartFile[] files) {
// 1. 获取当前用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String email = authentication.getName();
UserAccount user = userAccountRepository.findByEmail(email)
.orElseThrow(() -> new ResourceNotFoundException("User", "email", email));
// 2. 创建反馈实体
Feedback feedback = new Feedback();
feedback.setTitle(request.getTitle());
feedback.setDescription(request.getDescription());
feedback.setPollutionType(request.getPollutionType());
feedback.setSeverityLevel(request.getSeverityLevel());
feedback.setLatitude(request.getLatitude());
feedback.setLongitude(request.getLongitude());
feedback.setTextAddress(request.getTextAddress());
feedback.setGridX(request.getGridX());
feedback.setGridY(request.getGridY());
feedback.setStatus(FeedbackStatus.SUBMITTED);
feedback.setSubmitter(user);
feedback.setEventId(generateEventId());
// 3. 保存反馈
Feedback savedFeedback = feedbackRepository.save(feedback);
// 4. 处理附件
if (files != null && files.length > 0) {
List<Attachment> attachments = new ArrayList<>();
for (MultipartFile file : files) {
String fileName = fileStorageService.storeFile(file);
Attachment attachment = new Attachment();
attachment.setFileName(fileName);
attachment.setFilePath("/uploads/" + fileName);
attachment.setFileType(file.getContentType());
attachment.setFileSize(file.getSize());
attachments.add(attachment);
}
savedFeedback.setAttachments(attachments);
feedbackRepository.save(savedFeedback);
}
// 5. 发布事件触发AI审核
eventPublisher.publishEvent(new FeedbackSubmittedEvent(savedFeedback));
return savedFeedback;
}
@Override
public FeedbackResponseDTO processFeedback(Long id, ProcessFeedbackRequest request) {
Feedback feedback = feedbackRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Feedback", "id", id));
// 验证状态转换是否有效
if (!isValidStatusTransition(feedback.getStatus(), request.getStatus())) {
throw new IllegalStateException("Invalid status transition from " +
feedback.getStatus() + " to " + request.getStatus());
}
// 更新反馈状态
feedback.setStatus(request.getStatus());
// 如果批准反馈则更新为PENDING_ASSIGNMENT状态
if (request.getStatus() == FeedbackStatus.APPROVED) {
feedback.setStatus(FeedbackStatus.PENDING_ASSIGNMENT);
}
Feedback updatedFeedback = feedbackRepository.save(feedback);
// 如果反馈被批准,发布事件通知任务管理模块
if (request.getStatus() == FeedbackStatus.APPROVED) {
eventPublisher.publishEvent(new FeedbackApprovedEvent(updatedFeedback));
}
return mapToDTO(updatedFeedback);
}
// 生成唯一的事件ID
private String generateEventId() {
return "EMS-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-"
+ UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
// 验证状态转换是否有效
private boolean isValidStatusTransition(FeedbackStatus current, FeedbackStatus next) {
// 实现状态机逻辑
switch (current) {
case SUBMITTED:
return next == FeedbackStatus.AI_REVIEWED;
case AI_REVIEWED:
return next == FeedbackStatus.PENDING_REVIEW;
case PENDING_REVIEW:
return next == FeedbackStatus.APPROVED || next == FeedbackStatus.REJECTED;
case APPROVED:
return next == FeedbackStatus.PENDING_ASSIGNMENT;
case PENDING_ASSIGNMENT:
return next == FeedbackStatus.ASSIGNED;
case ASSIGNED:
return next == FeedbackStatus.PROCESSED;
case PROCESSED:
return next == FeedbackStatus.CLOSED;
default:
return false;
}
}
}
```
**实现要点分析**:
1. **多部分表单处理**: 使用`@RequestPart`注解同时处理JSON数据和文件上传实现了富媒体反馈提交。
2. **事件驱动架构**: 通过Spring的事件机制实现了反馈提交后的异步处理如AI审核和任务创建提高了系统响应速度。
3. **状态机模式**: 使用状态机模式管理反馈的生命周期,确保状态转换的合法性,防止非法操作。
4. **多条件动态查询**: 实现了灵活的多条件组合查询,支持按状态、类型、严重程度、地理位置和时间范围等进行过滤。

110
Report/系统测试.md Normal file
View File

@@ -0,0 +1,110 @@
# 3.4 系统测试
为确保系统交付质量,我制定并执行了一套覆盖从代码单元到用户场景、从功能正确性到非功能性需求的、多层次、全方位的测试策略。
## 3.4.1 后端测试
我主要负责后端的单元测试、集成测试和API接口测试旨在从源头保证服务的稳定、可靠与安全。
### 1. 单元测试 (Unit Testing)
我坚持“测试驱动开发”TDD的理念使用`JUnit 5``Mockito`框架为核心的`Service`层和`Repository`层的公共方法编写了详尽的单元测试。单元测试的目标是隔离验证最小代码单元(一个方法或一个类)的逻辑正确性。
**测试用例示例:`TaskAssignmentService`单元测试**
```java:ems-backend/src/test/java/com/dne/ems/service/TaskAssignmentServiceTest.java
@ExtendWith(MockitoExtension.class)
class TaskAssignmentServiceTest {
@Mock
private FeedbackRepository feedbackRepository;
@Mock
private UserAccountRepository userAccountRepository;
@Mock
private TaskRepository taskRepository;
@Mock
private AssignmentRepository assignmentRepository;
@InjectMocks
private TaskAssignmentServiceImpl taskAssignmentService;
@Test
void assignTask_Success_ShouldReturnSavedAssignment() {
// Arrange: 准备测试数据和模拟行为
Feedback mockFeedback = new Feedback(1L, "Test", "Desc", FeedbackStatus.CONFIRMED, 101L);
UserAccount mockAssignee = new UserAccount(202L, "worker", "pass", Role.GRID_WORKER);
when(feedbackRepository.findById(1L)).thenReturn(Optional.of(mockFeedback));
when(userAccountRepository.findById(202L)).thenReturn(Optional.of(mockAssignee));
when(taskRepository.save(any(Task.class))).thenAnswer(i -> i.getArgument(0));
when(assignmentRepository.save(any(Assignment.class))).thenAnswer(i -> i.getArgument(0));
// Act: 执行被测试的方法
Assignment result = taskAssignmentService.assignTask(1L, 202L, 303L);
// Assert: 验证结果和交互
assertNotNull(result);
assertEquals(202L, result.getAssigneeId());
verify(feedbackRepository, times(1)).save(any(Feedback.class)); // 验证Feedback状态是否被更新并保存
assertEquals(FeedbackStatus.ASSIGNED, mockFeedback.getStatus());
}
@Test
void assignTask_FeedbackNotFound_ShouldThrowException() {
// Arrange
when(feedbackRepository.findById(99L)).thenReturn(Optional.empty());
// Act & Assert
assertThrows(IllegalArgumentException.class, () -> {
taskAssignmentService.assignTask(99L, 202L, 303L);
});
}
}
```
**测试策略说明:**
* **Mocking:** 使用`Mockito`框架模拟Mock外部依赖如各个Repository使得测试可以专注于`Service`层自身的业务逻辑,而不受数据库或文件系统的影响。
* **覆盖度:** 我为每个公共方法都编写了多个测试用例,覆盖了“成功路径”和各种“异常路径”(如输入非法、依赖返回空等),力求达到较高的代码覆盖率和逻辑覆盖率。
### 2. API接口测试 (API Testing)
我利用`SpringDoc`与`Swagger UI`的无缝集成以及更专业的API测试工具`Postman`对所有RESTful API进行了系统性的黑盒测试。
* **测试范围:** 覆盖了用户认证、权限管理、反馈生命周期、任务生命周期等所有核心模块的每一个API端点。
* **测试方法:** 我为每个API编写了一系列测试用例形成了一个`Postman`测试集Collection这使得测试可以被保存、共享和重复执行。
**API测试用例表示例 (扩展):**
| 用例ID | 模块 | 接口 | 场景描述 | 输入数据 | 预期HTTP状态 | 预期响应体关键内容 | 结果 |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| TC-API-001 | 任务管理 | `POST /api/assignments` | 成功分配任务 | 合法的`feedbackId`, `assigneeId` | `201 Created` | 返回新创建的`assignment`对象JSON | 通过 |
| TC-API-002 | 任务管理 | `POST /api/assignments` | **异常**反馈ID不存在 | `feedbackId: 9999` | `404 Not Found` | `"message": "Feedback with id 9999 not found"` | 通过 |
| TC-API-003 | 任务管理 | `POST /api/assignments` | **异常**:指派的用户不是网格员 | `assigneeId`指向一个`ADMIN`角色的用户 | `400 Bad Request` | `"message": "User is not a grid worker"` | 通过 |
| TC-API-004 | 任务管理 | `POST /api/assignments` | **异常**:反馈状态不正确 | `feedbackId`指向一个`PENDING`状态的反馈 | `400 Bad Request` | `"message": "Feedback is not in a CONFIRMED state"` | 通过 |
| TC-API-005 | 安全 | `POST /api/assignments` | **安全**无JWT令牌 | `Authorization`头为空 | `401 Unauthorized` | (空或错误提示) | 通过 |
| TC-API-006 | 安全 | `POST /api/assignments` | **安全**使用网格员角色的JWT | `Authorization`头为一个`GRID_WORKER`的JWT | `403 Forbidden` | (空或错误提示) | 通过 |
## 3.4.2 前端测试
我与负责前端的组员紧密协作进行了全面的前端功能、UI/UX和兼容性测试。
* **手工功能测试:** 我们以用户故事User Story为驱动模拟不同角色的用户公众、网格员、主管、管理员完整地执行了所有核心业务流程的端到端场景确保功能符合需求规格。
* **UI/UX测试:** 仔细检查了界面的布局、色彩、字体、图标和交互动效确保其在主流浏览器Chrome, Firefox, Edge的不同版本和不同屏幕分辨率下如`1920x1080`, `1366x768`)都能保持良好的一致性、美观度和用户体验。
## 3.4.3 集成与系统测试
在前后端分别完成各自的测试后,我们将整个系统部署到一台独立的测试服务器上,进行了端到端的集成测试和系统测试。
* **目的:** 验证前后端数据接口的正确性、API调用的顺畅性、以及在真实网络环境下整个系统业务流程的闭环。
* **过程:** 我们共同执行了一套预先设计的系统级测试用例,这些用例模拟了真实世界中的复杂操作序列。例如:
1. 公众用户A提交反馈 -> 主管B审核通过 -> 主管B将任务分配给网格员C -> 网格员C完成任务并提交数据 -> 主管B查看已完成的任务和数据。
2. 并发测试:多个用户同时提交反馈或更新任务,验证后端`synchronized`机制是否有效防止了数据冲突。
* **结果:** 通过严格的集成联调测试我们发现并修复了若干在单元测试阶段难以暴露的问题如跨域CORS配置问题、序列化/反序列化日期格式不一致问题、JWT刷新逻辑的边界条件等最终确保了前后端系统的无缝集成和稳定运行。
## 3.4.4 非功能性测试
除了功能测试,我还对系统的部分非功能性需求进行了初步的探索性测试。
* **性能测试:** 使用`Apache JMeter`工具对核心的登录和查询类API进行了简单的压力测试。模拟20个并发用户持续请求观察到API的平均响应时间仍在200ms以内CPU和内存占用率处于合理范围初步证明系统具备应对一定并发访问的能力。
* **安全测试:**
* **权限测试:** 严格测试了不同角色的用户访问未授权API的情况验证系统是否能正确返回`403 Forbidden`。
* **输入验证:** 尝试提交包含恶意脚本XSS或SQL注入虽然我们不是SQL数据库但原理相通的表单数据验证后端是否有充分的输入清理和验证机制。

38
Report/系统设计.md Normal file
View File

@@ -0,0 +1,38 @@
# 系统设计文档
本文档旨在详细阐述环境监督系统EMS的系统架构、功能模块和实现细节为开发、测试和维护提供指导。
由于文档较长,为了便于阅读和维护,内容已被分割为以下几个部分:
## 文档目录
1. [总体设计](系统设计_第一部分.md)
- 系统架构设计
- 架构选型与原则
- 后端架构
- 前端架构
- 功能模块划分
2. [功能模块详细设计](系统设计_第二部分.md)
- 反馈管理模块
- 任务管理模块
- 用户与人员管理模块
- 网格与地图模块
- 决策支持模块
3. [类和对象的设计与动态模型设计](系统设计_第三部分.md)
- UserAccount 类图
- Feedback 类图
- Task 类图
- Grid 和 Assignment 类图
- 核心时序图
- 核心状态图
4. [算法设计与数据持久化设计](系统设计_第四部分.md)
- A* 寻路算法
- 任务智能分配算法
- JSON文件存储架构
- 核心JSON文件结构
**注意:** 所有图表均使用本地渲染的方式,无需在线渲染服务。

421
Report/系统设计_v2.md Normal file
View File

@@ -0,0 +1,421 @@
# 系统设计文档
本文档旨在详细阐述环境监督系统EMS的系统架构、功能模块和实现细节为开发、测试和维护提供指导。
## 1. 总体设计
总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。
### 1.1 系统架构设计
#### 1.1.1 架构选型与原则
本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于:
- **并行开发**: 前后端团队可以并行开发、测试和部署只需遵守统一的API约定从而显著提升开发效率。
- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。
- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。
在架构设计中,我们遵循了以下核心原则:
- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。
- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。
- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。
- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。
#### 1.1.2 后端架构
后端服务基于 **Spring Boot 3****Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式:
- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数使用JSR-303注解并调用业务逻辑层处理请求但不包含任何业务逻辑。
- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。
- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类模拟了类似JPA的接口实现了对`users.json`, `tasks.json`等核心数据文件的增删改查CRUD操作。选择JSON文件存储简化了项目的部署和配置特别适合快速迭代和中小型应用场景。
此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。
![后端分层架构图](https://www.plantuml.com/plantuml/svg/SoWkIImgAStDuG8oX1An24dCoKnELT2gKiX8p-L8AawncdNa5A2gY2pDoN8200000)
**技术选型**:
- **核心框架**: Spring Boot 3
- **开发语言**: Java 17
- **身份认证**: Spring Security + JWT (JSON Web Tokens)
- **数据持久化**: 自定义JSON文件存储
- **异步处理**: Spring Async & Events, Spring WebFlux
- **API文档**: SpringDoc (OpenAPI 3 / Swagger)
#### 1.1.3 前端架构
前端应用是一个基于 **Vue 3** 的单页面应用SPA使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。
- **UI组件库**: **Element Plus**提供了一套高质量、符合设计规范的UI组件加速了界面的开发。
- **状态管理**: **Pinia**作为Vue 3官方推荐的状态管理库它提供了极简的API和强大的类型推断支持能有效管理复杂的应用状态。
- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。
### 1.2 功能模块划分
系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。
| 核心模块 | 主要职责 | 关键功能点 |
| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出<br>- JWT令牌生成与验证<br>- 密码重置<br>- 基于角色的访问控制(RBAC) |
| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查<br>- 用户角色分配与变更<br>- 用户状态管理(激活/禁用) |
| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈<br>- 集成AI进行内容预审核<br>- 人工审核与处理<br>- 反馈状态跟踪与统计 |
| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务<br>- 任务分配给网格员<br>- 任务状态(分配、执行、完成)跟踪<br>- 任务审核与结果归档 |
| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分<br>- 网格员与网格的关联<br>- **A\*寻路算法**服务,优化任务路径 |
| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标KPI统计<br>- AQI、任务完成率等数据的可视化<br>- 生成反馈热力图 |
| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料<br>- 查询个人提交历史<br>- 查看个人操作日志 |
![功能模块图](https://www.plantuml.com/plantuml/svg/XLD1Jy5358xXF-S5wGgNkgRD1s2aD2gbzEd-Lgy_8QduTqb2lC_A5gYXaIikIeylC-yS339vj2vj-1e9z9oY-Oq9kGSpT8T5yPiU5sO1rKqpwXWkGZJ9G8P7k9y5Zt9B1pZ2b7h93e0b8B8pX78sYcQ6G2vE_uHlGgC0)
## 2. 详细设计
### 2.1 数据持久化设计 (JSON文件)
系统不使用传统数据库所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署但也对数据一致性和并发控制提出了更高的要求。
#### 2.1.1 `users.json` - 用户账户数据
存储所有系统用户的信息,包括登录凭证、角色和个人资料。
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------------- | ----------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 |
| `name` | `String` | 非空 | 用户姓名 |
| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 |
| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 |
| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 |
| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) |
| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) |
| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) |
| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) |
| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) |
| `region` | `String` | | 所属区域或地区 |
| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) |
| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) |
| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 |
| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) |
| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) |
| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 |
| `lockout_end_time` | `String` | | 账户锁定截止时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
#### 2.1.2 `feedback.json` - 环境问题反馈数据
存储所有由用户(包括公众)提交的环境问题反馈。
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 |
| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID |
| `title` | `String` | 非空 | 反馈标题 |
| `description` | `String` | | 问题详细描述 |
| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) |
| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) |
| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) |
| `text_address` | `String` | | 文字描述的地址 |
| `grid_x` | `Number` | | 事发地网格X坐标 |
| `grid_y` | `Number` | | 事发地网格Y坐标 |
| `latitude` | `Number` | | 事发地纬度 |
| `longitude` | `Number` | | 事发地经度 |
| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
#### 2.1.3 `tasks.json` - 任务数据
存储由反馈转化而来或手动创建的具体处理任务。
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 |
| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID |
| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人网格员ID |
| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人主管ID |
| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) |
| `title` | `String` | | 任务标题 |
| `description` | `String` | | 任务详细描述 |
| `pollution_type` | `String` | (ENUM) | 污染类型 |
| `severity_level` | `String` | (ENUM) | 严重程度 |
| `text_address` | `String` | | 任务地点文字描述 |
| `grid_x` | `Number` | | 任务地点网格X坐标 |
| `grid_y` | `Number` | | 任务地点网格Y坐标 |
| `latitude` | `Number` | | 任务地点纬度 |
| `longitude` | `Number` | | 任务地点经度 |
| `assigned_at` | `String` | | 任务分配时间 |
| `completed_at` | `String` | | 任务完成时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
| `deadline` | `String` | | 任务截止日期 |
#### 2.1.4 `grids.json` - 业务网格数据
存储业务逻辑上的地理网格信息。
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------- | --------------- | ------------------- | ---------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 |
| `gridx` | `Number` | | 网格X坐标 |
| `gridy` | `Number` | | 网格Y坐标 |
| `city_name` | `String` | | 所属城市 |
| `district_name` | `String` | | 所属区县 |
| `description` | `String` | | 网格描述信息 |
| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) |
#### 2.1.5 `assignments.json` - 任务分配记录数据
记录任务分配的详细信息,连接任务、分配者和执行者。
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | -------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 |
| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID |
| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者主管ID |
| `status` | `String` | 非空, (ENUM) | 分配状态 |
| `remarks` | `String` | | 分配备注 |
| `assignment_time`| `String` | 非空 | 分配时间 |
| `deadline` | `String` | | 任务截止日期 |
---
*(其他辅助表如 `attachment.json`, `operation_log.json`, `map_grid.json` 等的设计将遵循类似结构)*
### 2.2 核心业务流程
本章节将结合时序图和业务规则,详细描述系统的主要业务工作流。
#### 2.2.1 反馈处理与任务生成流程
这是系统的核心业务闭环,涵盖了从问题发现到任务完成的全过程。其状态流转严格遵循预设的规则。
**反馈处理流程**:
1. **提交反馈**: 公众用户或认证用户通过API提交环境问题反馈。
2. **进入AI审核**: 新提交的反馈初始状态为 `PENDING`,系统发布 `FeedbackSubmittedForAiReviewEvent` 事件触发AI服务进行异步审核。反馈状态变为 `AI_REVIEWING`
3. **主管人工审核**: AI审核后无论结果如何反馈都将进入人工审核阶段。主管Supervisor在系统中查看待处理的反馈。
4. **决策与流转**:
* **批准 (APPROVED)**: 如果反馈有效,主管将其批准。系统据此发布 `TaskReadyForAssignmentEvent` 事件,表明可以基于此反馈创建任务。原反馈的状态更新为 `TASK_CREATED`
* **拒绝 (REJECTED)**: 如果反馈无效或重复,主管可以拒绝该反馈,并填写原因。
**任务处理流程**:
1. **创建任务**: 基于已批准的反馈,系统创建一条新任务,初始状态为 `CREATED`
2. **分配任务**: 主管根据网格区域、网格员负载等规则将任务指派给特定的网格员Grid Worker。任务状态更新为 `ASSIGNED`
3. **执行任务**: 网格员接收到任务通知,接受任务后,状态变为 `IN_PROGRESS`。网格员前往现场处理。
4. **提交结果**: 网格员完成任务后,在系统中提交工作报告。任务状态更新为 `SUBMITTED`(待审核)。
5. **审核任务**: 主管审核已完成的任务。
* **批准 (APPROVED)**: 任务审核通过,流程结束。
* **拒绝 (REJECTED)**: 任务不符合要求,可将任务打回给网格员重新处理。
#### 2.2.2 用户认证与授权流程
1. **用户登录**: 用户使用邮箱/手机号和密码请求登录。
2. **凭证验证**: 系统验证用户信息和密码的正确性。
3. **令牌生成**: 验证通过后系统使用JWT生成一个有时效性的 `access_token`
4. **访问授权**: 用户在后续的请求中,需在请求头携带此 `token`。后端通过一个安全过滤器Filter来解析和验证 `token`确认用户的身份和权限从而决定是否能访问受保护的API资源。
#### 2.2.3 密码重置流程
1. **请求重置**: 用户在登录页面点击"忘记密码",输入注册时使用的邮箱地址。
2. **发送验证码**: 系统向该邮箱发送一封包含随机验证码的邮件。
3. **验证与重置**: 用户在指定时间内输入收到的验证码和新密码。系统校验验证码的正确性后,更新用户在 `users.json` 文件中的密码。
#### 2.2.4 主要业务规则
- **权限控制规则**:
1. **ADMIN**: 拥有全系统所有权限,是系统的最高管理员。
2. **SUPERVISOR**: 负责管理网格员、审核反馈和任务,是区域的管理者。
3. **DECISION_MAKER**: 拥有数据查看和分析权限,通常是决策层用户。
4. **GRID_WORKER**: 负责执行被分配的任务,是系统的主要执行者。
5. **PUBLIC**: 只能提交反馈,是系统的外部参与者。
- **任务分配规则**:
1. 优先基于网格区域进行自动分配。
2. 分配时会综合考虑网格员的当前工作负载,避免分配不均。
3. 标记为"紧急"的反馈所生成的任务,拥有更高的分配优先级。
4. 主管可以对已分配的任务进行手动干预和重新分配。
- **反馈处理规则**:
1. 所有反馈提交后首先由AI服务进行预审核和内容分析。
2. 系统会检测可能重复的反馈,并提示审核人员进行合并处理。
3. 紧急反馈(如严重等级为`CRITICAL`)会被优先展示给审核人员。
### 2.3 API接口设计
本章节详细定义了系统各模块的API接口包括认证、任务管理、数据上报等。
#### 2.3.1 认证接口 (`/api/auth`)
处理用户注册、登录、登出和密码管理等功能。
| 序号 | 接口路径 | HTTP方法 | 功能描述 | 请求参数 (Body/Query) | 成功响应 (Body) |
|----|-------------------------------|----------|------------------------|-----------------------------------------------------------|---------------------------------|
| 1 | `/api/auth/signup` | `POST` | 用户注册 | `SignUpRequest` (name, phone, email, password, role, etc.) | `201 CREATED` |
| 2 | `/api/auth/login` | `POST` | 用户登录 | `LoginRequest` (email/phone, password) | `JwtAuthenticationResponse` (token) |
| 3 | `/api/auth/logout` | `POST` | 用户登出 | 无 | `200 OK` |
| 4 | `/api/auth/send-verification-code` | `POST` | 发送注册验证码 | `email` (Query Param) | `200 OK` |
| 5 | `/api/auth/send-password-reset-code` | `POST` | 发送密码重置验证码 | `email` (Query Param) | `200 OK` |
| 6 | `/api/auth/reset-password-with-code` | `POST` | 使用验证码重置密码 | `PasswordResetWithCodeDto` (email, code, newPassword) | `200 OK` |
#### 2.3.2 公共接口 (`/api/public`)
提供给外部系统或公众使用的无需认证的接口。
| 序号 | 接口路径 | HTTP方法 | 功能描述 | 请求参数 (Body/Form-Data) | 成功响应 (Body) |
|----|-----------------------|----------|--------------------|--------------------------------------------------------------|---------------------------------|
| 1 | `/api/public/feedback` | `POST` | 提交公众反馈(带文件) | `feedback`: `PublicFeedbackRequest` (JSON), `files`: `MultipartFile[]` | `201 CREATED` (返回 `Feedback` 对象) |
#### 2.3.3 反馈管理接口 (`/api/feedback`)
认证用户(如主管、管理员)对环境问题反馈进行查询、统计和处理。
| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) |
|----|-----------------------------|----------|----------------------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------|---------------------------------------|
| 1 | `/api/feedback/submit` | `POST` | 认证用户提交反馈(带文件) | `isAuthenticated()` | `feedback`: `FeedbackSubmissionRequest` (JSON), `files`: `MultipartFile[]` | `201 CREATED` (返回 `Feedback` 对象) |
| 2 | `/api/feedback` | `GET` | 获取所有反馈(分页+多条件过滤) | `isAuthenticated()` | `status`, `pollutionType`, `severityLevel`, `cityName`, `districtName`, `startDate`, `endDate`, `keyword`, `pageable` | `Page<FeedbackResponseDTO>` |
| 3 | `/api/feedback/{id}` | `GET` | 根据ID获取反馈详情 | `ADMIN`, `SUPERVISOR`, `DECISION_MAKER` | `id` (Path Var) | `FeedbackResponseDTO` |
| 4 | `/api/feedback/stats` | `GET` | 获取反馈统计数据 | `isAuthenticated()` | 无 | `FeedbackStatsResponse` |
| 5 | `/api/feedback/{id}/process`| `POST` | 处理反馈(如审核通过) | `ADMIN`, `SUPERVISOR` | `id` (Path Var), `ProcessFeedbackRequest` (Body) | `FeedbackResponseDTO` |
#### 2.3.4 任务管理接口 (`/api/management/tasks`)
主管和管理员用于任务的全生命周期管理,包括创建、分配、查询和审批。
| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) |
|----|-----------------------------------------|----------|----------------------------------|------------------------------------|--------------------------------------------------------------------------------------------|---------------------------------|
| 1 | `/api/management/tasks` | `GET` | 获取任务列表(分页+多条件过滤) | `ADMIN`, `SUPERVISOR`, `DECISION_MAKER` | `status`, `assigneeId`, `severity`, `pollutionType`, `startDate`, `endDate`, `pageable` | `List<TaskSummaryDTO>` |
| 2 | `/api/management/tasks/{taskId}` | `GET` | 获取任务详情 | `SUPERVISOR` | `taskId` (Path Var) | `TaskDetailDTO` |
| 3 | `/api/management/tasks` | `POST` | 手动创建新任务 | `SUPERVISOR` | `TaskCreationRequest` (Body) | `201 CREATED` (返回 `TaskDetailDTO`) |
| 4 | `/api/management/tasks/{taskId}/assign` | `POST` | 分配任务给网格员 | `SUPERVISOR` | `taskId` (Path Var), `TaskAssignmentRequest` (Body) | `TaskSummaryDTO` |
| 5 | `/api/management/tasks/{taskId}/review` | `POST` | 审核任务 | `SUPERVISOR` | `taskId` (Path Var), `TaskApprovalRequest` (Body) | `TaskDetailDTO` |
| 6 | `/api/management/tasks/{taskId}/cancel` | `POST` | 取消任务 | `SUPERVISOR` | `taskId` (Path Var) | `TaskDetailDTO` |
| 7 | `/api/management/tasks/feedback` | `GET` | 获取待处理的反馈列表 | `SUPERVISOR`, `ADMIN` | 无 | `List<Feedback>` |
| 8 | `/api/management/tasks/feedback/{feedbackId}/create-task` | `POST` | 从反馈创建任务 | `SUPERVISOR` | `feedbackId` (Path Var), `TaskFromFeedbackRequest` (Body) | `201 CREATED` (返回 `TaskDetailDTO`) |
| 9 | `/api/management/tasks/{taskId}/approve`| `POST` | 批准任务(完成) | `ADMIN` | `taskId` (Path Var) | `TaskDetailDTO` |
| 10 | `/api/management/tasks/{taskId}/reject` | `POST` | 拒绝任务(打回) | `ADMIN` | `taskId` (Path Var), `TaskRejectionRequest` (Body) | `TaskDetailDTO` |
#### 2.3.5 网格员任务接口 (`/api/worker`)
网格员用于查看和处理自己被分配的任务。
| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) |
|----|---------------------------------|----------|------------------------|---------------|-----------------------------------------------------------|----------------------------|
| 1 | `/api/worker` | `GET` | 获取我被分配的任务列表 | `GRID_WORKER` | `status` (Query Param), `pageable` | `Page<TaskSummaryDTO>` |
| 2 | `/api/worker/{taskId}` | `GET` | 获取任务详情 | `GRID_WORKER` | `taskId` (Path Var) | `TaskDetailDTO` |
| 3 | `/api/worker/{taskId}/accept` | `POST` | 接受任务 | `GRID_WORKER` | `taskId` (Path Var) | `TaskSummaryDTO` |
| 4 | `/api/worker/{taskId}/submit` | `POST` | 提交任务完成情况(带文件) | `GRID_WORKER` | `taskId` (Path Var), `comments` (Form Part), `files` (Form Part) | `TaskSummaryDTO` |
#### 2.3.6 人员管理接口 (`/api/personnel`)
管理员用于管理系统中的所有用户账户。
| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) |
|----|---------------------------------|------------|------------------------------|---------|-----------------------------------------------------------|---------------------------------|
| 1 | `/api/personnel/users` | `POST` | 创建新用户 | `ADMIN` | `UserCreationRequest` (Body) | `201 CREATED` (返回 `UserAccount` 对象) |
| 2 | `/api/personnel/users` | `GET` | 获取用户列表(分页+过滤) | `ADMIN`, `GRID_WORKER` | `role`, `name` (Query Params), `pageable` | `PageDTO<UserAccount>` |
| 3 | `/api/personnel/users/{userId}` | `GET` | 根据ID获取用户详情 | `ADMIN` | `userId` (Path Var) | `UserAccount` |
| 4 | `/api/personnel/users/{userId}` | `PATCH` | 更新用户信息(部分更新) | `ADMIN` | `userId` (Path Var), `UserUpdateRequest` (Body) | `UserAccount` |
| 5 | `/api/personnel/users/{userId}/role` | `PUT` | 更新用户角色 | `ADMIN` | `userId` (Path Var), `UserRoleUpdateRequest` (Body) | `UserAccount` |
| 6 | `/api/personnel/users/{userId}` | `DELETE` | 删除用户 | `ADMIN` | `userId` (Path Var) | `204 NO_CONTENT` |
#### 2.3.7 网格管理接口 (`/api/grids`)
管理员用于管理地理网格、分配网格员和查看统计数据。
| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) |
|----|-----------------------------------------------|----------|----------------------------------|----------------------------------------|-------------------------------------------------|---------------------------------|
| 1 | `/api/grids` | `GET` | 获取所有网格列表 | `ADMIN`, `DECISION_MAKER`, `GRID_WORKER` | 无 | `List<Grid>` |
| 2 | `/api/grids/{id}` | `PATCH` | 更新网格信息(如设为障碍物) | `ADMIN` | `id` (Path Var), `GridUpdateRequest` (Body) | `Grid` |
| 3 | `/api/grids/coverage` | `GET` | 获取网格覆盖率统计 | `ADMIN` | 无 | `List<GridCoverageDTO>` |
| 4 | `/api/grids/{gridId}/assign` | `POST` | 分配网格员到指定网格(通过ID) | `ADMIN` | `gridId` (Path Var), `userId` (Body) | `200 OK` |
| 5 | `/api/grids/{gridId}/unassign` | `POST` | 从网格中移除网格员(通过ID) | `ADMIN` | `gridId` (Path Var) | `200 OK` |
| 6 | `/api/grids/coordinates/{gridX}/{gridY}/assign` | `POST` | 分配网格员到指定网格(通过坐标) | `ADMIN`, `GRID_WORKER` | `gridX`, `gridY` (Path Vars), `userId` (Body) | `200 OK` |
| 7 | `/api/grids/coordinates/{gridX}/{gridY}/unassign`| `POST`| 从网格中移除网格员(通过坐标) | `ADMIN`, `GRID_WORKER` | `gridX`, `gridY` (Path Vars) | `200 OK` |
#### 2.3.8 仪表盘数据接口 (`/api/dashboard`)
为前端仪表盘提供各种聚合和统计数据,用于决策支持和系统状态监控。
| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) |
|----|------------------------------------------|----------|----------------------------------|---------------------------|---------------------------|---------------------------------------|
| 1 | `/api/dashboard/stats` | `GET` | 获取仪表盘核心统计数据 | `DECISION_MAKER`, `ADMIN` | 无 | `DashboardStatsDTO` |
| 2 | `/api/dashboard/reports/aqi-distribution`| `GET` | 获取AQI等级分布数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List<AqiDistributionDTO>` |
| 3 | `/api/dashboard/reports/monthly-exceedance-trend` | `GET` | 获取月度超标趋势数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List<TrendDataPointDTO>` |
| 4 | `/api/dashboard/reports/grid-coverage` | `GET` | 获取网格覆盖情况数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List<GridCoverageDTO>` |
| 5 | `/api/dashboard/map/heatmap` | `GET` | 获取反馈热力图数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List<HeatmapPointDTO>` |
| 6 | `/api/dashboard/reports/pollution-stats` | `GET` | 获取污染物统计报告数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List<PollutionStatsDTO>` |
| 7 | `/api/dashboard/reports/task-completion-stats` | `GET` | 获取任务完成情况统计数据 | `DECISION_MAKER`, `ADMIN` | 无 | `TaskStatsDTO` |
| 8 | `/api/dashboard/map/aqi-heatmap` | `GET` | 获取AQI热力图数据 | `DECISION_MAKER`, `ADMIN` | 无 | `List<AqiHeatmapPointDTO>` |
| 9 | `/api/dashboard/thresholds` | `GET` | 获取所有污染物阈值设置 | `DECISION_MAKER`, `ADMIN` | 无 | `List<PollutantThresholdDTO>` |
| 10 | `/api/dashboard/thresholds/{pollutantName}` | `GET` | 获取指定污染物的阈值设置 | `DECISION_MAKER`, `ADMIN` | `pollutantName` (Path Var)| `PollutantThresholdDTO` |
| 11 | `/api/dashboard/thresholds` | `POST` | 保存污染物阈值设置 | `ADMIN` | `PollutantThresholdDTO` (Body) | `PollutantThresholdDTO` |
| 12 | `/api/dashboard/reports/pollutant-monthly-trends` | `GET` | 获取各污染物月度趋势数据 | `DECISION_MAKER`, `ADMIN` | 无 | `Map<String, List<TrendDataPointDTO>>` |
#### 2.3.9 其他接口
包含文件处理、操作日志、个人资料等辅助功能的接口。
| 序号 | 接口路径 | HTTP方法 | 功能描述 | 权限 | 请求参数 (Body/Query) | 成功响应 (Body) |
|----|--------------------------|----------|------------------------|------|---------------------------|---------------------------------|
| 1 | `/api/files/{filename}` | `GET` | 下载/预览文件(下载) | `isAuthenticated()` | `filename` (Path Var) | `Resource` (文件流) |
| 2 | `/api/view/{filename}` | `GET` | 下载/预览文件(内联预览) | `isAuthenticated()` | `filename` (Path Var) | `Resource` (文件流) |
| 3 | `/api/logs/my-logs` | `GET` | 获取当前用户的操作日志 | `isAuthenticated()` | 无 | `List<OperationLogDTO>` |
| 4 | `/api/logs` | `GET` | 获取所有操作日志(可过滤) | `ADMIN` | `operationType`, `userId`, `startTime`, `endTime` | `List<OperationLogDTO>` |
| 5 | `/api/me/feedback` | `GET` | 获取当前用户的反馈历史 | `isAuthenticated()` | `pageable` | `List<UserFeedbackSummaryDTO>` |
| 6 | `/api/supervisor/reviews`| `GET` | 获取待审核的反馈列表 | `SUPERVISOR`, `ADMIN` | 无 | `List<Feedback>` |
| 7 | `/api/supervisor/reviews/{feedbackId}/approve` | `POST` | 审核通过反馈 | `SUPERVISOR`, `ADMIN` | `feedbackId` (Path Var) | `200 OK` |
| 8 | `/api/supervisor/reviews/{feedbackId}/reject` | `POST` | 审核拒绝反馈 | `SUPERVISOR`, `ADMIN` | `feedbackId` (Path Var), `RejectFeedbackRequest` (Body) | `200 OK` |
| 9 | `/api/map/grid` | `GET` | 获取完整地图网格数据 | `isAuthenticated()` | 无 | `List<MapGrid>` |
| 10 | `/api/map/grid` | `POST` | 创建/更新单个网格单元 | `ADMIN` | `MapGrid` (Body) | `MapGrid` |
| 11 | `/api/map/initialize` | `POST` | 初始化地图网格 | `ADMIN` | `width`, `height` (Query) | `String` (Message) |
| 12 | `/api/pathfinding/find` | `POST` | A*寻路算法查找路径 | `isAuthenticated()` | `PathfindingRequest` (Body) | `List<Point>` |
| 13 | `/api/tasks/unassigned` | `GET` | 获取未分配的任务列表 | `ADMIN`, `SUPERVISOR` | 无 | `List<Feedback>` |
| 14 | `/api/tasks/grid-workers`| `GET` | 获取可用的网格员列表 | `ADMIN`, `SUPERVISOR` | 无 | `List<UserAccount>` |
| 15 | `/api/tasks/assign` | `POST` | 分配任务给网格员 | `ADMIN`, `SUPERVISOR` | `AssignmentRequest` (Body)| `Assignment` |
#### 2.3.10 API 设计原则与最佳实践
为保证API的规范性、安全性和易用性系统在设计和实现中遵循了以下原则
1. **统一响应格式**: 所有API响应都封装在一个标准结构中包含成功/失败状态、数据负载、消息和时间戳,便于前端统一处理。分页查询则返回包含分页信息(总数、总页数等)的标准化分页对象。
2. **细粒度权限控制**: 系统利用Spring Security的 `@PreAuthorize` 注解在方法级别上实现了细粒度的权限控制,确保即使用户通过了认证,也只能访问其角色被授权的特定资源和操作。
3. **严格的参数验证**: 所有接收输入的API端点特别是写操作都使用了JSR-303验证注解`@Valid`, `@NotBlank`, `@Email`)对请求体和参数进行严格校验,从入口处保证了数据的合法性和完整性。
4. **清晰的API文档**: 通过集成SpringDoc为所有公共API生成了遵循OpenAPI 3.0规范的交互式文档Swagger UI包含了清晰的描述、参数说明和响应示例。
5. **全面的日志记录**: 使用 `@Slf4j` 在关键业务流程、成功操作及异常情况处记录了详细日志,便于系统监控、问题排查和安全审计。
---
## 2.4 架构特性与外部集成
除了核心的业务功能,系统在架构层面采用了一些现代化的设计来实现高性能和高可扩展性,并集成了一些外部服务来增强功能。
### 2.4.1 事件驱动架构 (EDA)
系统内部采用轻量级的事件驱动模型基于Spring Events来实现关键业务逻辑的解耦。
- **核心事件**:
1. `FeedbackSubmittedForAiReviewEvent`: 当一个新反馈被提交后发布用于触发AI服务的异步分析。
2. `TaskReadyForAssignmentEvent`: 当一个反馈被批准并可以转化为任务时发布。
3. `AuthenticationSuccessEvent`: 用户成功登录时发布,可用于记录日志或更新用户状态。
4. `AuthenticationFailureEvent`: 用户登录失败时发布,用于监控恶意尝试。
- **优势**:
- **解耦**: 事件的发布者和消费者之间没有直接依赖,易于扩展新功能。
- **异步化**: 许多耗时的操作如AI分析、邮件发送可以通过异步监听事件来完成避免阻塞主线程提升用户体验。
### 2.4.2 外部服务集成
- **AI服务**:
- **服务商**: 火山引擎 (Volcano Engine)
- **核心功能**: 用于反馈内容的智能分析,包括自动提取关键词、评估严重等级和进行初步分类,为人工审核提供决策支持。
- **邮件服务**:
- **服务商**: 163 SMTP
- **核心功能**: 用于发送系统通知类邮件,如用户注册、密码重置验证码、任务分配通知等。
### 2.4.3 性能与缓存
为了提升系统响应速度和处理高并发请求的能力,系统内置了基于内存的缓存机制。
- **缓存技术**: Google Guava Cache
- **缓存内容**:
- **用户数据**: 缓存频繁访问的用户信息。
- **配置数据**: 如污染物阈值等不经常变动的系统配置。
- **热点数据**: 如热门网格的统计信息。
- **策略**: 缓存设置了合理的过期时间和大小限制,以保证数据的一致性和内存的有效利用。

252
Report/系统设计_v3.md Normal file
View File

@@ -0,0 +1,252 @@
# 系统设计文档
本文档旨在详细阐述环境监督系统EMS的系统架构、功能模块和实现细节为开发、测试和维护提供指导。
## 1. 总体设计
总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。
### 1.1 系统架构设计
#### 1.1.1 架构选型与原则
本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于:
- **并行开发**: 前后端团队可以并行开发、测试和部署只需遵守统一的API约定从而显著提升开发效率。
- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。
- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。
在架构设计中,我们遵循了以下核心原则:
- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。
- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。
- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。
- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。
#### 1.1.2 后端架构
后端服务基于 **Spring Boot 3****Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式:
- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数使用JSR-303注解并调用业务逻辑层处理请求但不包含任何业务逻辑。
- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。
- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类模拟了类似JPA的接口实现了对`users.json`, `tasks.json`等核心数据文件的增删改查CRUD操作。选择JSON文件存储简化了项目的部署和配置特别适合快速迭代和中小型应用场景。
此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。
![后端分层架构图](https://www.plantuml.com/plantuml/svg/SoWkIImgAStDuG8oX1An24dCoKnELT2gKiX8p-L8AawncdNa5A2gY2pDoN820000)
#### 1.1.3 前端架构
前端应用是一个基于 **Vue 3** 的单页面应用SPA使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。
- **UI组件库**: **Element Plus**提供了一套高质量、符合设计规范的UI组件加速了界面的开发。
- **状态管理**: **Pinia**作为Vue 3官方推荐的状态管理库它提供了极简的API和强大的类型推断支持能有效管理复杂的应用状态。
- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。
### 1.2 功能模块划分
系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。
| 核心模块 | 主要职责 | 关键功能点 |
| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出<br>- JWT令牌生成与验证<br>- 密码重置<br>- 基于角色的访问控制(RBAC) |
| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查<br>- 用户角色分配与变更<br>- 用户状态管理(激活/禁用) |
| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈<br>- 集成AI进行内容预审核<br>- 人工审核与处理<br>- 反馈状态跟踪与统计 |
| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务<br>- 任务分配给网格员<br>- 任务状态(分配、执行、完成)跟踪<br>- 任务审核与结果归档 |
| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分<br>- 网格员与网格的关联<br>- **A\*寻路算法**服务,优化任务路径 |
| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标KPI统计<br>- AQI、任务完成率等数据的可视化<br>- 生成反馈热力图 |
| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料<br>- 查询个人提交历史<br>- 查看个人操作日志 |
![功能模块图](https://www.plantuml.com/plantuml/svg/XLD1Jy5358xXF-S5wGgNkgRD1s2aD2gbzEd-Lgy_8QduTqb2lC_A5gYXaIikIeylC-yS339vj2vj-1e9z9oY-Oq9kGSpT8T5yPiU5sO1rKqpwXWkGZJ9G8P7k9y5Zt9B1pZ2b7h93e0b8B8pX78sYcQ6G2vE_uHlGgC0)
## 2. 详细设计
### 2.1 功能模块详细设计
本章节将对核心功能模块的内部设计进行更深入的阐述。
#### 2.1.1 反馈管理模块
该模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。
- **主要控制器**: `FeedbackController`, `PublicController`
- **核心服务**: `FeedbackService`, `FeedbackAiReviewService`
- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest`
- **设计要点**:
- 采用 `@RequestPart` 同时接收JSON数据和文件上传实现了富文本内容的反馈提交。
- 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件将AI审核流程解耦并异步化提高了API的响应速度。
- 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。
#### 2.1.2 任务管理模块
该模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。
- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController`
- **核心服务**: `TaskService`, `TaskAssignmentService`
- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO`
- **设计要点**:
- 清晰的状态机管理任务的生命周期CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED
- 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。
- 为主管和网格员提供了完全独立的API端点严格分离了不同角色的操作权限。
### 2.2 类和对象的设计
本节定义了系统核心业务对象的静态结构,即类图。
#### 2.2.1 `Feedback` 类图
`Feedback` 对象是系统中最核心的数据模型之一,代表一次环境问题反馈。
![Feedback Class Diagram](https://www.plantuml.com/plantuml/svg/VP1DJy8n48Rl-HH5s9Y2dAnAEoiT3qajR_GAnWmaGZDIyB9n-Oa9e6iY2p9oPQuAU1y8jAY8576d1gtP7Z6sZY8DkTGVKgI9gL1pCBP_O2Cudn-ex3lVgoqHhq9wHlDMXe8pY5pT5VdqzImeD-eXoVxuFOyO1jD4dfhWh6uiofWKbv0GgXg9T8nK0gWzYlBv4AAYk2f7uVoqioIp2kXyG-uTQ2tD_oT_Eeyc2hXv8ALa2iYtHAg4g5KzGv-pB2c_X9vR-y9o5m00)
### 2.3 动态模型设计
本节描述了系统在运行时,对象之间的交互行为。
#### 2.3.1 核心时序图
**反馈提交与处理时序图**
该图展示了从用户提交反馈到系统创建任务的完整交互流程。
```mermaid
sequenceDiagram
participant User as 用户
participant FeedbackController as 反馈控制器
participant FeedbackService as 反馈服务
participant EventPublisher as 事件发布器
participant FeedbackAiReviewService as AI审核服务
participant TaskService as 任务服务
User->>FeedbackController: POST /api/feedback/submit
FeedbackController->>FeedbackService: submitFeedback(request, files)
FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent)
FeedbackService-->>FeedbackController: 返回初步响应
FeedbackController-->>User: 201 Created
EventPublisher-->>FeedbackAiReviewService: 异步触发
FeedbackAiReviewService->>FeedbackAiReviewService: 调用火山引擎AI分析
FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED)
Supervisor->>FeedbackService: approveFeedback(feedbackId)
FeedbackService->>TaskService: createTaskFromFeedback(feedback)
TaskService-->>FeedbackService: 任务创建成功
```
#### 2.3.2 核心状态图
**反馈状态机 (Feedback Status)**
![Feedback Status Diagram](https://www.plantuml.com/plantuml/svg/LOrDIy5058xXF-S5wGgNkgRD1s2aD2oXzE9-Lgy_8kduTqbWlC_A5gYvaWMLR-yY_YoHqC1f9YpT9opT9ooHqC1f-HpX-A_p9t9oY-Gq9kGSpTgEBAoC1IayWjMDaIqjLMa2b7h95t9h93t0b8B8p_DoYcQ6G2vE_uHlGgC0)
### 2.4 算法设计
#### 2.4.1 A* 寻路算法
- **目的**: 为网格员规划从当前位置到任务目标点的最优(最短或最快)路径。
- **输入**:
- `startNode`: 起始点坐标。
- `endNode`: 目标点坐标。
- `grid`: 包含障碍物信息的地图网格数据。
- **核心逻辑**:
1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。
2.`openList` 中选取F值G值+H值最小的节点作为当前节点。
- G值: 从起点到当前节点的实际代价。
- H值: 从当前节点到终点的预估代价(启发函数,通常使用曼哈顿距离或欧氏距离)。
3. 遍历当前节点的相邻节点,如果邻居不在`closedList`中且不是障碍物则计算其G值和H值并将其加入`openList`
4. 重复此过程,直到当前节点为目标节点,或`openList`为空。
- **输出**: 从起点到终点的一系列坐标点,即规划好的路径。
- **应用**: 在`PathfindingController`中通过`/api/pathfinding/find`接口暴露。
### 2.5 数据持久化设计
系统不使用传统数据库所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署但也对数据一致性和并发控制提出了更高的要求。
#### 2.5.1 `users.json` - 用户账户数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------------- | ----------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 |
| `name` | `String` | 非空 | 用户姓名 |
| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 |
| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 |
| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 |
| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) |
| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) |
| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) |
| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) |
| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) |
| `region` | `String` | | 所属区域或地区 |
| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) |
| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) |
| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 |
| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) |
| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) |
| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 |
| `lockout_end_time` | `String` | | 账户锁定截止时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
#### 2.5.2 `feedback.json` - 环境问题反馈数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 |
| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID |
| `title` | `String` | 非空 | 反馈标题 |
| `description` | `String` | | 问题详细描述 |
| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) |
| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) |
| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) |
| `text_address` | `String` | | 文字描述的地址 |
| `grid_x` | `Number` | | 事发地网格X坐标 |
| `grid_y` | `Number` | | 事发地网格Y坐标 |
| `latitude` | `Number` | | 事发地纬度 |
| `longitude` | `Number` | | 事发地经度 |
| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
#### 2.5.3 `tasks.json` - 任务数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 |
| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID |
| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人网格员ID |
| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人主管ID |
| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) |
| `title` | `String` | | 任务标题 |
| `description` | `String` | | 任务详细描述 |
| `pollution_type` | `String` | (ENUM) | 污染类型 |
| `severity_level` | `String` | (ENUM) | 严重程度 |
| `text_address` | `String` | | 任务地点文字描述 |
| `grid_x` | `Number` | | 任务地点网格X坐标 |
| `grid_y` | `Number` | | 任务地点网格Y坐标 |
| `latitude` | `Number` | | 任务地点纬度 |
| `longitude` | `Number` | | 任务地点经度 |
| `assigned_at` | `String` | | 任务分配时间 |
| `completed_at` | `String` | | 任务完成时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
| `deadline` | `String` | | 任务截止日期 |
#### 2.5.4 `grids.json` - 业务网格数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------- | --------------- | ------------------- | ---------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 |
| `gridx` | `Number` | | 网格X坐标 |
| `gridy` | `Number` | | 网格Y坐标 |
| `city_name` | `String` | | 所属城市 |
| `district_name` | `String` | | 所属区县 |
| `description` | `String` | | 网格描述信息 |
| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) |
#### 2.5.5 `assignments.json` - 任务分配记录数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | -------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 |
| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID |
| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者主管ID |
| `status` | `String` | 非空, (ENUM) | 分配状态 |
| `remarks` | `String` | | 分配备注 |
| `assignment_time`| `String` | 非空 | 分配时间 |
| `deadline` | `String` | | 任务截止日期 |
```

325
Report/系统设计_v4.md Normal file
View File

@@ -0,0 +1,325 @@
# 系统设计文档
本文档旨在详细阐述环境监督系统EMS的系统架构、功能模块和实现细节为开发、测试和维护提供指导。
## 1. 总体设计
总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。
### 1.1 系统架构设计
#### 1.1.1 架构选型与原则
本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于:
- **并行开发**: 前后端团队可以并行开发、测试和部署只需遵守统一的API约定从而显著提升开发效率。
- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。
- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。
在架构设计中,我们遵循了以下核心原则:
- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。
- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。
- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。
- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。
#### 1.1.2 后端架构
后端服务基于 **Spring Boot 3****Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式:
- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数使用JSR-303注解并调用业务逻辑层处理请求但不包含任何业务逻辑。
- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。
- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类模拟了类似JPA的接口实现了对`users.json`, `tasks.json`等核心数据文件的增删改查CRUD操作。选择JSON文件存储简化了项目的部署和配置特别适合快速迭代和中小型应用场景。
此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。
![后端分层架构图](https://www.plantuml.com/plantuml/svg/SoWkIImgAStDuG8oX1An24dCoKnELT2gKiX8p-L8AawncdNa5A2gY2pDoN820000)
#### 1.1.3 前端架构
前端应用是一个基于 **Vue 3** 的单页面应用SPA使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。
- **UI组件库**: **Element Plus**提供了一套高质量、符合设计规范的UI组件加速了界面的开发。
- **状态管理**: **Pinia**作为Vue 3官方推荐的状态管理库它提供了极简的API和强大的类型推断支持能有效管理复杂的应用状态。
- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。
### 1.2 功能模块划分
系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。
| 核心模块 | 主要职责 | 关键功能点 |
| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出<br>- JWT令牌生成与验证<br>- 密码重置<br>- 基于角色的访问控制(RBAC) |
| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查<br>- 用户角色分配与变更<br>- 用户状态管理(激活/禁用) |
| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈<br>- 集成AI进行内容预审核<br>- 人工审核与处理<br>- 反馈状态跟踪与统计 |
| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务<br>- 任务分配给网格员<br>- 任务状态(分配、执行、完成)跟踪<br>- 任务审核与结果归档 |
| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分<br>- 网格员与网格的关联<br>- **A\*寻路算法**服务,优化任务路径 |
| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标KPI统计<br>- AQI、任务完成率等数据的可视化<br>- 生成反馈热力图 |
| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料<br>- 查询个人提交历史<br>- 查看个人操作日志 |真实吧。妈
![功能模块图](https://www.plantuml.com/plantuml/svg/XLD1Jy5358xXF-S5wGgNkgRD1s2aD2gbzEd-Lgy_8QduTqb2lC_A5gYXaIikIeylC-yS339vj2vj-1e9z9oY-Oq9kGSpT8T5yPiU5sO1rKqpwXWkGZJ9G8P7k9y5Zt9B1pZ2b7h93e0b8B8pX78sYcQ6G2vE_uHlGgC0)
## 2. 详细设计
### 2.1 功能模块详细设计
本章节将对核心功能模块的内部设计进行更深入的阐述。
#### 2.1.1 反馈管理模块
该模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。
- **主要控制器**: `FeedbackController`, `PublicController`
- **核心服务**: `FeedbackService`, `FeedbackAiReviewService`
- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest`
- **设计要点**:
- 采用 `@RequestPart` 同时接收JSON数据和文件上传实现了富文本内容的反馈提交。
- 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件将AI审核流程解耦并异步化提高了API的响应速度。
- 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。
#### 2.1.2 任务管理模块
该模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。
- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController`
- **核心服务**: `TaskService`, `TaskAssignmentService`
- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO`
- **设计要点**:
- 清晰的状态机管理任务的生命周期CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED
- 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。
- 为主管和网格员提供了完全独立的API端点严格分离了不同角色的操作权限。
#### 2.1.3 用户与人员管理模块
该模块为系统的权限管理和组织架构提供了基础。
- **主要控制器**: `PersonnelController`
- **核心服务**: `UserAccountService`, `OperationLogService`
- **关键DTO**: `UserCreationRequest`, `UserUpdateRequest`, `UserRoleUpdateRequest`
- **设计要点**:
- 提供了对用户账户的完整CRUD操作。
- 实现了用户角色和状态的精细化管理。
- 所有关键操作均通过`OperationLogService`记录日志,便于审计和追踪。
#### 2.1.4 网格与地图模块
该模块是系统实现区域化、网格化管理的核心。
- **主要控制器**: `GridController`, `MapController`
- **核心服务**: `GridService`, `PathfindingService`
- **设计要点**:
- 支持对地理区域进行灵活的网格化定义,并可将网格标记为障碍。
- 实现了网格与网格员的关联管理。
- 核心亮点是集成了`PathfindingService`该服务封装了A*寻路算法,为任务路径规划提供支持。
### 2.2 类和对象的设计
本节定义了系统核心业务对象的静态结构,即类图。
#### 2.2.1 `UserAccount` 类图
![UserAccount Class Diagram](https://www.plantuml.com/plantuml/svg/ZLJRSzD24BtxAovE_-bL2XAn2aYgY2pDoN820000_5-AY4TQb1y7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000)
#### 2.2.2 `Feedback` 类图
![Feedback Class Diagram](https://www.plantuml.com/plantuml/svg/VP1DJy8n48Rl-HH5s9Y2dAnAEoiT3qajR_GAnWmaGZDIyB9n-Oa9e6iY2p9oPQuAU1y8jAY8576d1gtP7Z6sZY8DkTGVKgI9gL1pCBP_O2Cudn-ex3lVgoqHhq9wHlDMXe8pY5pT5VdqzImeD-eXoVxuFOyO1jD4dfhWh6uiofWKbv0GgXg9T8nK0gWzYlBv4AAYk2f7uVoqioIp2kXyG-uTQ2tD_oT_Eeyc2hXv8ALa2iYtHAg4g5KzGv-pB2c_X9vR-y9o5m00)
#### 2.2.3 `Task` 类图
![Task Class Diagram](https://www.plantuml.com/plantuml/svg/RLJDRT8n3BpxAovE_-dL2Imga2iXgY2pDoN820000_59AYoTQb1S7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000_5-9o4TQb1A7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000)
#### 2.2.4 `Grid` 和 `Assignment` 类图
![Grid and Assignment Class Diagram](https://www.plantuml.com/plantuml/svg/RLJDRT8n3BpxAovE_-dL2Imga2iXgY2pDoN820000_5-AooTQb1y7GAbY2pC2gN82XoA8YQ2XGAbYpC2gE8Y2pDoN82XGAbY2pC2gNpDoN820000)
### 2.3 动态模型设计
本节描述了系统在运行时,对象之间的交互行为。
#### 2.3.1 核心时序图
**用户登录认证时序图**
```mermaid
sequenceDiagram
participant User as 用户
participant AuthController as 认证控制器
participant AuthService as 认证服务
participant JwtUtil as JWT工具
User->>AuthController: POST /api/auth/login (email, password)
AuthController->>AuthService: signIn(LoginRequest)
AuthService->>AuthService: 验证凭证
alt 验证成功
AuthService->>JwtUtil: generateToken(user)
JwtUtil-->>AuthService: 返回JWT令牌
AuthService-->>AuthController: 返回JwtAuthenticationResponse
AuthController-->>User: 200 OK (token)
else 验证失败
AuthService-->>AuthController: 抛出AuthenticationException
AuthController-->>User: 401 Unauthorized
end
```
**反馈提交与处理时序图**
```mermaid
sequenceDiagram
participant User as 用户
participant FeedbackController as 反馈控制器
participant FeedbackService as 反馈服务
participant EventPublisher as 事件发布器
participant FeedbackAiReviewService as AI审核服务
User->>FeedbackController: POST /api/feedback/submit
FeedbackController->>FeedbackService: submitFeedback(request, files)
FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent)
FeedbackService-->>FeedbackController: 返回初步响应
Controller-->>User: 201 Created
EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核
FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED)
```
**主管分配任务时序图**
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant TaskMgmtController as 任务管理控制器
participant TaskService as 任务服务
participant AssignmentService as 分配服务
participant NotificationService as 通知服务
Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId)
TaskMgmtController->>TaskService: assignTask(taskId, workerId)
TaskService->>AssignmentService: createAssignment(task, worker)
AssignmentService-->>TaskService: 返回Assignment
TaskService->>TaskService: 更新任务状态为ASSIGNED
TaskService->>NotificationService: notifyWorker(workerId, taskId)
TaskService-->>TaskMgmtController: 分配成功
TaskMgmtController-->>Supervisor: 200 OK
```
#### 2.3.2 核心状态图
**反馈状态机 (Feedback Status)**
![Feedback Status Diagram](https://www.plantuml.com/plantuml/svg/LOrDIy5058xXF-S5wGgNkgRD1s2aD2oXzE9-Lgy_8kduTqbWlC_A5gYvaWMLR-yY_YoHqC1f9YpT9opT9ooHqC1f-HpX-A_p9t9oY-Gq9kGSpTgEBAoC1IayWjMDaIqjLMa2b7h95t9h93t0b8B8p_DoYcQ6G2vE_uHlGgC0)
**任务状态机 (Task Status)**
![Task Status Diagram](https://www.plantuml.com/plantuml/svg/TOpDIy5058xXF-S5wGgNkgRD1s2aD2oXzE9-Lgy_8kduTqbWlC_A5gYvaWMLR-yY_YoHqC1f9YpT9opT9ooHqC1f-HpX-A_p9t9oY-Gq9kGSpTgEBAoC1IayWjMDaIqjLMa2b7h95t9h93t0b8B8p_DoYcQ6G2vE_uHlGgC0)
### 2.4 算法与策略设计
#### 2.4.1 A* 寻路算法
- **目的**: 为网格员规划从当前位置到任务目标点的最优(最短或最快)路径。
- **输入**:
- `startNode`: 起始点坐标。
- `endNode`: 目标点坐标。
- `grid`: 包含障碍物信息的地图网格数据。
- **核心逻辑**:
1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。
2.`openList` 中选取F值G值+H值最小的节点作为当前节点。
- G值: 从起点到当前节点的实际代价。
- H值: 从当前节点到终点的预估代价(启发函数,通常使用曼哈顿距离或欧氏距离)。
3. 遍历当前节点的相邻节点,如果邻居不在`closedList`中且不是障碍物则计算其G值和H值并将其加入`openList`
4. 重复此过程,直到当前节点为目标节点,或`openList`为空。
- **输出**: 从起点到终点的一系列坐标点,即规划好的路径。
- **应用**: 在`PathfindingController`中通过`/api/pathfinding/find`接口暴露。
#### 2.4.2 任务分配策略
- **目的**: 在多个可用网格员中,为新任务选择最合适的执行者。
- **核心逻辑**: 这是一个复合策略,综合考虑以下因素,并计算加权得分:
1. **地理位置优先**: 优先选择任务所在网格或邻近网格的网格员。
2. **负载均衡**: 优先选择当前进行中任务数量最少的网格员。
3. **技能匹配**: (未来扩展)可根据任务需要的技能(`skills`字段)与网格员的技能进行匹配。
4. **任务优先级**: 对于高优先级任务,可以适当放宽地理位置限制,确保有可用人员处理。
- **应用**: 在`TaskAssignmentService`中实现,当主管触发自动分配或系统基于反馈自动创建任务时调用。
### 2.5 数据持久化设计
系统不使用传统数据库所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署但也对数据一致性和并发控制提出了更高的要求。
#### 2.5.1 `users.json` - 用户账户数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------------- | ----------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 |
| `name` | `String` | 非空 | 用户姓名 |
| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 |
| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 |
| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 |
| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) |
| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) |
| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) |
| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) |
| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) |
| `region` | `String` | | 所属区域或地区 |
| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) |
| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) |
| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 |
| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) |
| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) |
| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 |
| `lockout_end_time` | `String` | | 账户锁定截止时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
#### 2.5.2 `feedback.json` - 环境问题反馈数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 |
| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID |
| `title` | `String` | 非空 | 反馈标题 |
| `description` | `String` | | 问题详细描述 |
| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) |
| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) |
| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) |
| `text_address` | `String` | | 文字描述的地址 |
| `grid_x` | `Number` | | 事发地网格X坐标 |
| `grid_y` | `Number` | | 事发地网格Y坐标 |
| `latitude` | `Number` | | 事发地纬度 |
| `longitude` | `Number` | | 事发地经度 |
| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
#### 2.5.3 `tasks.json` - 任务数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 |
| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID |
| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人网格员ID |
| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人主管ID |
| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) |
| `title` | `String` | | 任务标题 |
| `description` | `String` | | 任务详细描述 |
| `pollution_type` | `String` | (ENUM) | 污染类型 |
| `severity_level` | `String` | (ENUM) | 严重程度 |
| `text_address` | `String` | | 任务地点文字描述 |
| `grid_x` | `Number` | | 任务地点网格X坐标 |
| `grid_y` | `Number` | | 任务地点网格Y坐标 |
| `latitude` | `Number` | | 任务地点纬度 |
| `longitude` | `Number` | | 任务地点经度 |
| `assigned_at` | `String` | | 任务分配时间 |
| `completed_at` | `String` | | 任务完成时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
| `deadline` | `String` | | 任务截止日期 |
#### 2.5.4 `grids.json` - 业务网格数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------- | --------------- | ------------------- | ---------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 |
| `gridx` | `Number` | | 网格X坐标 |
| `gridy` | `Number` | | 网格Y坐标 |
| `city_name` | `String` | | 所属城市 |
| `district_name` | `String` | | 所属区县 |
| `description` | `String` | | 网格描述信息 |
| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) |
#### 2.5.5 `assignments.json` - 任务分配记录数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | -------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 |
| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID |
| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者主管ID |
| `status` | `String` | 非空, (ENUM) | 分配状态 |
| `remarks` | `String` | | 分配备注 |
| `assignment_time`| `String` | 非空 | 分配时间 |
| `deadline` | `String` | | 任务截止日期 |
```

518
Report/系统设计_v5.md Normal file
View File

@@ -0,0 +1,518 @@
# 系统设计文档
本文档旨在详细阐述环境监督系统EMS的系统架构、功能模块和实现细节为开发、测试和维护提供指导。
## 1. 总体设计
总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。
### 1.1 系统架构设计
#### 1.1.1 架构选型与原则
本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于:
- **并行开发**: 前后端团队可以并行开发、测试和部署只需遵守统一的API约定从而显著提升开发效率。
- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。
- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。
在架构设计中,我们遵循了以下核心原则:
- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。
- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。
- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。
- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。
#### 1.1.2 后端架构
后端服务基于 **Spring Boot 3****Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式:
- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数使用JSR-303注解并调用业务逻辑层处理请求但不包含任何业务逻辑。
- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。
- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类模拟了类似JPA的接口实现了对`users.json`, `tasks.json`等核心数据文件的增删改查CRUD操作。选择JSON文件存储简化了项目的部署和配置特别适合快速迭代和中小型应用场景。
此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。
**后端分层架构图**
```plantuml
@startuml
package "Backend Architecture" {
[Controller Layer] <<Spring @RestController>>
[Service Layer] <<Spring @Service>>
[Repository Layer] <<Custom Persistence>>
[Controller Layer] --> [Service Layer]
[Service Layer] --> [Repository Layer]
}
@enduml
```
#### 1.1.3 前端架构
前端应用是一个基于 **Vue 3** 的单页面应用SPA使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。
- **UI组件库**: **Element Plus**提供了一套高质量、符合设计规范的UI组件加速了界面的开发。
- **状态管理**: **Pinia**作为Vue 3官方推荐的状态管理库它提供了极简的API和强大的类型推断支持能有效管理复杂的应用状态。
- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。
### 1.2 功能模块划分
系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。
| 核心模块 | 主要职责 | 关键功能点 |
| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出<br>- JWT令牌生成与验证<br>- 密码重置<br>- 基于角色的访问控制(RBAC) |
| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查<br>- 用户角色分配与变更<br>- 用户状态管理(激活/禁用) |
| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈<br>- 集成AI进行内容预审核<br>- 人工审核与处理<br>- 反馈状态跟踪与统计 |
| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务<br>- 任务分配给网格员<br>- 任务状态(分配、执行、完成)跟踪<br>- 任务审核与结果归档 |
| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分<br>- 网格员与网格的关联<br>- **A\*寻路算法**服务,优化任务路径 |
| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标KPI统计<br>- AQI、任务完成率等数据的可视化<br>- 生成反馈热力图 |
| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料<br>- 查询个人提交历史<br>- 查看个人操作日志 |
**功能模块图**
```plantuml
@startuml
left to right direction
actor User
rectangle "环境监督系统 (EMS)" {
User -- (认证与授权模块)
(认证与授权模块) ..> (用户与人员管理模块) : 依赖
User -- (反馈管理模块)
(反馈管理模块) ..> (任务管理模块) : 创建任务
(任务管理模块) ..> (网格与地图模块) : 路径规划
(任务管理模块) ..> (用户与人员管理模块) : 分配任务
(决策支持模块) .up.> (反馈管理模块) : 数据分析
(决策支持模块) .up.> (任务管理模块) : 数据分析
User -- (个人中心模块)
}
@enduml
```
## 2. 详细设计
### 2.1 功能模块详细设计
本章节将对核心功能模块的内部设计进行更深入的阐述。
#### 2.1.1 反馈管理模块
该模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。
- **主要控制器**: `FeedbackController`, `PublicController`
- **核心服务**: `FeedbackService`, `FeedbackAiReviewService`
- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest`
- **设计要点**:
- 采用 `@RequestPart` 同时接收JSON数据和文件上传实现了富文本内容的反馈提交。
- 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件将AI审核流程解耦并异步化提高了API的响应速度。
- 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。
#### 2.1.2 任务管理模块
该模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。
- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController`
- **核心服务**: `TaskService`, `TaskAssignmentService`
- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO`
- **设计要点**:
- 清晰的状态机管理任务的生命周期CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED
- 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。
- 为主管和网格员提供了完全独立的API端点严格分离了不同角色的操作权限。
#### 2.1.3 用户与人员管理模块
该模块为系统的权限管理和组织架构提供了基础。
- **主要控制器**: `PersonnelController`
- **核心服务**: `UserAccountService`, `OperationLogService`
- **关键DTO**: `UserCreationRequest`, `UserUpdateRequest`, `UserRoleUpdateRequest`
- **设计要点**:
- 提供了对用户账户的完整CRUD操作。
- 实现了用户角色和状态的精细化管理。
- 所有关键操作均通过`OperationLogService`记录日志,便于审计和追踪。
#### 2.1.4 网格与地图模块
该模块是系统实现区域化、网格化管理的核心。
- **主要控制器**: `GridController`, `MapController`
- **核心服务**: `GridService`, `PathfindingService`
- **设计要点**:
- 支持对地理区域进行灵活的网格化定义,并可将网格标记为障碍。
- 实现了网格与网格员的关联管理。
- 核心亮点是集成了`PathfindingService`该服务封装了A*寻路算法,为任务路径规划提供支持。
### 2.2 类和对象的设计
本节定义了系统核心业务对象的静态结构,即类图。
#### 2.2.1 `UserAccount` 类图
```plantuml
@startuml
class UserAccount {
- Long id
- String name
- String phone
- String email
- String password
- Gender gender
- Role role
- UserStatus status
- Integer gridX
- Integer gridY
+ isValid()
}
enum Gender {
MALE
FEMALE
OTHER
}
enum Role {
ADMIN
SUPERVISOR
GRID_WORKER
}
enum UserStatus {
ACTIVE
INACTIVE
}
UserAccount *-- Gender
UserAccount *-- Role
UserAccount *-- UserStatus
@enduml
```
#### 2.2.2 `Feedback` 类图
```plantuml
@startuml
class Feedback {
- Long id
- String eventId
- String title
- PollutionType pollutionType
- SeverityLevel severityLevel
- FeedbackStatus status
- Long submitterId
+ process()
+ approve()
}
enum PollutionType {
AIR
WATER
SOIL
NOISE
}
enum SeverityLevel {
LOW
MEDIUM
HIGH
CRITICAL
}
enum FeedbackStatus {
PENDING_REVIEW
AI_REJECTED
PROCESSED
}
Feedback *-- PollutionType
Feedback *-- SeverityLevel
Feedback *-- FeedbackStatus
@enduml
```
#### 2.2.3 `Task` 类图
```plantuml
@startuml
class Task {
- Long id
- Long feedbackId
- Long assigneeId
- Long createdBy
- TaskStatus status
- String title
+ assignTo(workerId)
+ complete()
+ approve()
}
enum TaskStatus {
CREATED
ASSIGNED
IN_PROGRESS
SUBMITTED
APPROVED
REJECTED
}
Task *-- TaskStatus
@enduml
```
#### 2.2.4 `Grid` 和 `Assignment` 类图
```plantuml
@startuml
class Grid {
- Long id
- Integer gridX
- Integer gridY
- String cityName
- boolean isObstacle
}
class Assignment {
- Long id
- Long taskId
- Long assignerId
- AssignmentStatus status
- String remarks
- LocalDateTime assignmentTime
}
enum AssignmentStatus {
PENDING
ACCEPTED
REJECTED
}
Assignment *-- AssignmentStatus
@enduml
```
### 2.3 动态模型设计
本节描述了系统在运行时,对象之间的交互行为。
#### 2.3.1 核心时序图
**用户登录认证时序图**
```mermaid
sequenceDiagram
participant User as 用户
participant AuthController as 认证控制器
participant AuthService as 认证服务
participant JwtUtil as JWT工具
User->>AuthController: POST /api/auth/login (email, password)
AuthController->>AuthService: signIn(LoginRequest)
AuthService->>AuthService: 验证凭证
alt 验证成功
AuthService->>JwtUtil: generateToken(user)
JwtUtil-->>AuthService: 返回JWT令牌
AuthService-->>AuthController: 返回JwtAuthenticationResponse
AuthController-->>User: 200 OK (token)
else 验证失败
AuthService-->>AuthController: 抛出AuthenticationException
AuthController-->>User: 401 Unauthorized
end
```
**反馈提交与处理时序图**
```mermaid
sequenceDiagram
participant User as 用户
participant FeedbackController as 反馈控制器
participant FeedbackService as 反馈服务
participant EventPublisher as 事件发布器
participant FeedbackAiReviewService as AI审核服务
User->>FeedbackController: POST /api/feedback/submit
FeedbackController->>FeedbackService: submitFeedback(request, files)
FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent)
FeedbackService-->>FeedbackController: 返回初步响应
Controller-->>User: 201 Created
EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核
FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED)
```
**主管分配任务时序图**
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant TaskMgmtController as 任务管理控制器
participant TaskService as 任务服务
participant AssignmentService as 分配服务
participant NotificationService as 通知服务
Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId)
TaskMgmtController->>TaskService: assignTask(taskId, workerId)
TaskService->>AssignmentService: createAssignment(task, worker)
AssignmentService-->>TaskService: 返回Assignment
TaskService->>TaskService: 更新任务状态为ASSIGNED
TaskService->>NotificationService: notifyWorker(workerId, taskId)
TaskService-->>TaskMgmtController: 分配成功
TaskMgmtController-->>Supervisor: 200 OK
```
#### 2.3.2 核心状态图
**反馈状态机 (Feedback Status)**
```plantuml
@startuml
state "待审核" as PENDING_REVIEW
state "AI审核通过" as AI_APPROVED
state "AI拒绝" as AI_REJECTED
state "已处理" as PROCESSED
state "已关闭" as CLOSED
[*] --> PENDING_REVIEW : 提交反馈
PENDING_REVIEW --> AI_APPROVED : AI审核通过
PENDING_REVIEW --> AI_REJECTED : AI审核拒绝
AI_APPROVED --> PROCESSED : 主管创建任务
PROCESSED --> CLOSED : 任务完成
AI_REJECTED --> CLOSED : 归档
@enduml
```
**任务状态机 (Task Status)**
```plantuml
@startuml
state "已创建" as CREATED
state "已分配" as ASSIGNED
state "进行中" as IN_PROGRESS
state "已提交" as SUBMITTED
state "已批准" as APPROVED
state "已拒绝" as REJECTED
[*] --> CREATED : 创建任务
CREATED --> ASSIGNED : 分配给网格员
ASSIGNED --> IN_PROGRESS : 网格员接受任务
IN_PROGRESS --> SUBMITTED : 网格员完成并提交
SUBMITTED --> APPROVED : 主管审核通过
SUBMITTED --> REJECTED : 主管审核拒绝
REJECTED --> ASSIGNED : 重新分配
APPROVED --> [*]
@enduml
```
### 2.4 算法与策略设计
#### 2.4.1 A* 寻路算法
- **目的**: 为网格员规划从当前位置到任务目标点的最优(最短或最快)路径。
- **输入**:
- `startNode`: 起始点坐标。
- `endNode`: 目标点坐标。
- `grid`: 包含障碍物信息的地图网格数据。
- **核心逻辑**:
1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。
2.`openList` 中选取F值G值+H值最小的节点作为当前节点。
- G值: 从起点到当前节点的实际代价。
- H值: 从当前节点到终点的预估代价(启发函数,通常使用曼哈顿距离或欧氏距离)。
3. 遍历当前节点的相邻节点,如果邻居不在`closedList`中且不是障碍物则计算其G值和H值并将其加入`openList`
4. 重复此过程,直到当前节点为目标节点,或`openList`为空。
- **输出**: 从起点到终点的一系列坐标点,即规划好的路径。
- **应用**: 在`PathfindingController`中通过`/api/pathfinding/find`接口暴露。
#### 2.4.2 任务分配策略
- **目的**: 在多个可用网格员中,为新任务选择最合适的执行者。
- **核心逻辑**: 这是一个复合策略,综合考虑以下因素,并计算加权得分:
1. **地理位置优先**: 优先选择任务所在网格或邻近网格的网格员。
2. **负载均衡**: 优先选择当前进行中任务数量最少的网格员。
3. **技能匹配**: (未来扩展)可根据任务需要的技能(`skills`字段)与网格员的技能进行匹配。
4. **任务优先级**: 对于高优先级任务,可以适当放宽地理位置限制,确保有可用人员处理。
- **应用**: 在`TaskAssignmentService`中实现,当主管触发自动分配或系统基于反馈自动创建任务时调用。
### 2.5 数据持久化设计
系统不使用传统数据库所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署但也对数据一致性和并发控制提出了更高的要求。
#### 2.5.1 `users.json` - 用户账户数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------------- | ----------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 |
| `name` | `String` | 非空 | 用户姓名 |
| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 |
| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 |
| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 |
| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) |
| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) |
| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) |
| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) |
| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) |
| `region` | `String` | | 所属区域或地区 |
| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) |
| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) |
| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 |
| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) |
| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) |
| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 |
| `lockout_end_time` | `String` | | 账户锁定截止时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
#### 2.5.2 `feedback.json` - 环境问题反馈数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 |
| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID |
| `title` | `String` | 非空 | 反馈标题 |
| `description` | `String` | | 问题详细描述 |
| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) |
| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) |
| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) |
| `text_address` | `String` | | 文字描述的地址 |
| `grid_x` | `Number` | | 事发地网格X坐标 |
| `grid_y` | `Number` | | 事发地网格Y坐标 |
| `latitude` | `Number` | | 事发地纬度 |
| `longitude` | `Number` | | 事发地经度 |
| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
#### 2.5.3 `tasks.json` - 任务数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 |
| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID |
| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人网格员ID |
| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人主管ID |
| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) |
| `title` | `String` | | 任务标题 |
| `description` | `String` | | 任务详细描述 |
| `pollution_type` | `String` | (ENUM) | 污染类型 |
| `severity_level` | `String` | (ENUM) | 严重程度 |
| `text_address` | `String` | | 任务地点文字描述 |
| `grid_x` | `Number` | | 任务地点网格X坐标 |
| `grid_y` | `Number` | | 任务地点网格Y坐标 |
| `latitude` | `Number` | | 任务地点纬度 |
| `longitude` | `Number` | | 任务地点经度 |
| `assigned_at` | `String` | | 任务分配时间 |
| `completed_at` | `String` | | 任务完成时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
| `deadline` | `String` | | 任务截止日期 |
#### 2.5.4 `grids.json` - 业务网格数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------- | --------------- | ------------------- | ---------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 |
| `gridx` | `Number` | | 网格X坐标 |
| `gridy` | `Number` | | 网格Y坐标 |
| `city_name` | `String` | | 所属城市 |
| `district_name` | `String` | | 所属区县 |
| `description` | `String` | | 网格描述信息 |
| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) |
#### 2.5.5 `assignments.json` - 任务分配记录数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | -------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 |
| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID |
| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者主管ID |
| `status` | `String` | 非空, (ENUM) | 分配状态 |
| `remarks` | `String` | | 分配备注 |
| `assignment_time`| `String` | 非空 | 分配时间 |
| `deadline` | `String` | | 任务截止日期 |
```

726
Report/系统设计_v6.md Normal file
View File

@@ -0,0 +1,726 @@
# 系统设计文档 V6
本文档旨在详细阐述环境监督系统EMS的系统架构、功能模块和实现细节为开发、测试和维护提供指导。
## 1. 总体设计
总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。
### 1.1 系统架构设计
#### 1.1.1 架构选型与原则
本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于:
- **并行开发**: 前后端团队可以并行开发、测试和部署只需遵守统一的API约定从而显著提升开发效率。
- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。
- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。
在架构设计中,我们遵循了以下核心原则:
- **高内聚,低耦合**: 将相关功能组织在独立的模块中并最小化模块间的依赖。这是通过清晰的模块划分和基于事件的通信如AI审核实现的。
- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。模块化的设计使得添加新功能时,对现有系统的影响降到最低。
- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。采用JWT保障API安全并通过角色权限RBAC控制对不同资源的访问。
- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。
#### 1.1.2 后端架构
后端服务基于 **Spring Boot 3****Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式:
- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数使用JSR-303注解并调用业务逻辑层处理请求但不包含任何业务逻辑。
- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。
- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类模拟了类似JPA的接口实现了对`users.json`, `tasks.json`等核心数据文件的增删改查CRUD操作。选择JSON文件存储简化了项目的部署和配置特别适合快速迭代和中小型应用场景。
此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。
**后端分层架构图**
```plantuml
@startuml
package "Backend Architecture" {
[Controller Layer] <<Spring @RestController>>
[Service Layer] <<Spring @Service>>
[Repository Layer] <<Custom Persistence>>
[Controller Layer] --> [Service Layer]
[Service Layer] --> [Repository Layer]
}
@enduml
```
#### 1.1.3 前端架构
前端应用是一个基于 **Vue 3** 的单页面应用SPA使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。
- **UI组件库**: **Element Plus**提供了一套高质量、符合设计规范的UI组件加速了界面的开发。
- **状态管理**: **Pinia**作为Vue 3官方推荐的状态管理库它提供了极简的API和强大的类型推断支持能有效管理复杂的应用状态。
- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。
### 1.2 功能模块划分
系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。
| 核心模块 | 主要职责 | 关键功能点 |
| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出<br>- JWT令牌生成与验证<br>- 密码重置<br>- 基于角色的访问控制(RBAC) |
| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查<br>- 用户角色分配与变更<br>- 用户状态管理(激活/禁用) |
| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈<br>- 集成AI进行内容预审核<br>- 人工审核与处理<br>- 反馈状态跟踪与统计 |
| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务<br>- 任务分配给网格员<br>- 任务状态(分配、执行、完成)跟踪<br>- 任务审核与结果归档 |
| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分<br>- 网格员与网格的关联<br>- **A\*寻路算法**服务,优化任务路径 |
| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标KPI统计<br>- AQI、任务完成率等数据的可视化<br>- 生成反馈热力图 |
| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料<br>- 查询个人提交历史<br>- 查看个人操作日志 |
**功能模块图**
```plantuml
@startuml
left to right direction
actor User
rectangle "环境监督系统 (EMS)" {
User -- (认证与授权模块)
(认证与授权模块) ..> (用户与人员管理模块) : 依赖
User -- (反馈管理模块)
(反馈管理模块) ..> (任务管理模块) : 创建任务
(任务管理模块) ..> (网格与地图模块) : 路径规划
(任务管理模块) ..> (用户与人员管理模块) : 分配任务
(决策支持模块) .up.> (反馈管理模块) : 数据分析
(决策支持模块) .up.> (任务管理模块) : 数据分析
User -- (个人中心模块)
}
@enduml
```
## 2. 详细设计
### 2.1 功能模块详细设计
本章节将对系统的核心功能模块进行详细的拆解和说明,阐述每个模块的职责、主要功能和关键参与者。
#### 2.1.1 用户与认证模块 (User & Authentication)
* **核心职责**: 负责管理所有用户账户、角色、权限,并提供安全可靠的身份认证和授权服务。是整个系统安全访问的基石。
* **关键参与者**: 系统管理员 (Admin), 主管 (Supervisor), 网格员 (Grid Worker), 注册用户。
* **主要功能点**:
* **用户注册**: 提供新用户账户的创建接口。
* **身份认证**:
* 通过用户名(邮箱)和密码进行登录验证。
* 成功登录后生成符合JWT (JSON Web Token) 标准的访问令牌 (`access_token`) 和刷新令牌 (`refresh_token`)。
* **权限控制**:
* 基于角色的访问控制 (RBAC)。不同角色(如 `ADMIN`, `SUPERVISOR`, `GRID_WORKER`拥有不同的API访问权限。
* 通过在API请求的Header中携带JWT令牌来验证用户身份和权限。
* **账户管理 (管理员)**:
* 创建、查看、更新、删除系统内的所有用户账户。
* 分配和修改用户角色。
* 管理用户状态(激活、禁用、暂停)。
* **密码重置**: 提供忘记密码后的安全重置流程(例如,通过邮件发送重置链接)。
* **相关服务**: `AuthService`, `UserAccountService`, `SupervisorService`
#### 2.1.2 反馈管理模块 (Feedback Management)
* **核心职责**: 统一处理所有来源(公众、系统用户)的环境问题反馈,确保每一条反馈都得到有效记录、审查和跟进。
* **关键参与者**: 公众用户 (匿名/注册), 主管, 系统。
* **主要功能点**:
* **反馈提交**: 允许用户通过API提交带有详细描述、位置信息、严重等级和附件图片/视频)的反馈。
* **AI自动审查**:
* 新提交的反馈会触发一个异步事件。
* AI服务对反馈内容进行初步分析评估其有效性、严重性和污染类型。
* 根据AI审查结果反馈状态可能被更新为 `AI_REJECTED` 或触发任务创建流程。
* **主管人工处理**:
* 主管可以查看所有待处理的反馈。
* 对于AI无法处理或需要人工判断的反馈主管可以进行审核、批准或拒绝。
* 批准后的反馈将转化为一个明确的、可执行的任务。
* **状态跟踪**: 反馈的整个生命周期(`PENDING_REVIEW` -> `PROCESSED` -> `CLOSED`)都被完整记录和追踪。
* **相关服务**: `FeedbackService`, `FeedbackAiReviewService`
#### 2.1.3 任务管理模块 (Task Management)
* **核心职责**: 负责将经过审核的反馈转化为具体的工作任务,并对任务的全生命周期(创建、分配、执行、审核、归档)进行管理。
* **关键参与者**: 主管, 网格员。
* **主要功能点**:
* **任务创建**:
* 可由反馈自动转化而来。
* 主管也可以根据需要手动创建新任务。
* **任务分配**:
* **手动分配**: 主管可以直接将任务指派给特定的网格员。
* **智能分配 (设计中)**: 系统在任务创建后,自动调用分配算法,综合考虑网格员的位置、负载等因素,推荐或直接分配最合适的人选。
* **任务执行 (网格员)**:
* 网格员可以查看分配给自己的任务列表。
* 接受、拒绝或开始执行任务,并更新任务状态。
* 完成任务后,提交包含处理说明和附件的工作报告。
* **任务审核 (主管)**:
* 主管审查网格员提交的任务结果。
* 可以选择"批准"(任务完成)或"驳回"(任务需要返工)。
* **历史追溯**: 完整的任务状态变更历史、操作记录和处理意见都会被存档,方便审计和复盘。
* **相关服务**: `TaskManagementService`, `GridWorkerTaskService`, `TaskAssignmentService`
#### 2.1.4 网格与地图模块 (Grid & Map)
* **核心职责**: 负责定义和管理地理网格系统,并提供基于地理位置的辅助功能,如路径规划。
* **关键参与者**: 系统管理员, 网格员。
* **主要功能点**:
* **网格定义**: 管理员可以定义城市地图的网格系统,包括每个网格的坐标、属性以及是否为障碍物。
* **人员-网格关联**: 管理员可将特定的网格员分配到指定的责任网格中。
* **路径规划**:
* 集成 A* 寻路算法。
* 为网格员提供从当前位置到任务地点的最优路径规划,并能在地图上进行可视化展示。
* **相关服务**: `GridService`, `PathfindingService`
#### 2.1.5 决策支持模块 (Decision Support)
* **核心职责**: 汇集和分析各类环境数据,为管理人员提供数据洞察和决策依据。
* **关键参与者**: 主管, 系统管理员。
* **主要功能点**:
* **数据看板 (Dashboard)**:
* 实时展示关键指标KPI如待处理反馈数、进行中任务数、平均处理时长等。
* 以图表形式展示污染类型、区域分布、严重程度的统计分析。
* **历史数据查询**: 提供对历史反馈和任务数据的多维度查询和筛选功能。
* **报告生成**: (未来扩展) 定期自动生成环境状况分析报告。
* **相关服务**: `AqiService`, (未来可能引入的`AnalyticsService`)。
#### 2.1.6 个人中心模块 (Personal Center)
* **核心职责**: 为登录用户提供管理个人账户信息和偏好的专属空间。
* **关键参与者**: 所有登录用户 (主管, 网格员)。
* **主要功能点**:
* **个人资料查看与编辑**: 用户可以查看和修改自己的基本信息,如联系电话、头像等。
* **密码修改**: 提供安全的旧密码验证和新密码设置功能。
* **我的任务/反馈**: 快速访问与自己相关的任务或反馈列表。
* **相关服务**: `UserProfileService`
#### 2.1.7 日志与审计模块 (Logging & Auditing)
* **核心职责**: 记录系统中发生的所有重要操作和事件,确保系统的可追溯性和安全性。
* **关键参与者**: 系统管理员, 系统。
* **主要功能点**:
* **操作日志**: 自动记录关键的用户操作,如谁在什么时间创建了任务、修改了反馈状态等。
* **系统日志**: 记录应用运行时的错误、警告和重要信息,用于问题排查和性能监控。
* **日志查询**: 提供接口供管理员查询和审计操作日志。
* **相关服务**: `OperationLogService`
### 2.2 类和对象的设计
本节定义了系统核心业务对象的静态结构,即类图。
#### 2.2.1 `UserAccount` 类图
```plantuml
@startuml
class UserAccount {
- Long id
- String name
- String phone
- String email
- String password
- Gender gender
- Role role
- UserStatus status
- Integer gridX
- Integer gridY
+ isValid()
}
enum Gender {
MALE
FEMALE
OTHER
}
enum Role {
ADMIN
SUPERVISOR
GRID_WORKER
}
enum UserStatus {
ACTIVE
INACTIVE
}
UserAccount *-- Gender
UserAccount *-- Role
UserAccount *-- UserStatus
@enduml
```
#### 2.2.2 `Feedback` 类图
```plantuml
@startuml
class Feedback {
- Long id
- String eventId
- String title
- PollutionType pollutionType
- SeverityLevel severityLevel
- FeedbackStatus status
- Long submitterId
+ process()
+ approve()
}
enum PollutionType {
AIR
WATER
SOIL
NOISE
}
enum SeverityLevel {
LOW
MEDIUM
HIGH
CRITICAL
}
enum FeedbackStatus {
PENDING_REVIEW
AI_REJECTED
PROCESSED
}
Feedback *-- PollutionType
Feedback *-- SeverityLevel
Feedback *-- FeedbackStatus
@enduml
```
#### 2.2.3 `Task` 类图
```plantuml
@startuml
class Task {
- Long id
- Long feedbackId
- Long assigneeId
- Long createdBy
- TaskStatus status
- String title
+ assignTo(workerId)
+ complete()
+ approve()
}
enum TaskStatus {
CREATED
ASSIGNED
IN_PROGRESS
SUBMITTED
APPROVED
REJECTED
}
Task *-- TaskStatus
@enduml
```
#### 2.2.4 `Grid` 和 `Assignment` 类图
```plantuml
@startuml
class Grid {
- Long id
- Integer gridX
- Integer gridY
- String cityName
- boolean isObstacle
}
class Assignment {
- Long id
- Long taskId
- Long assignerId
- AssignmentStatus status
- String remarks
- LocalDateTime assignmentTime
}
enum AssignmentStatus {
PENDING
ACCEPTED
REJECTED
}
Assignment *-- AssignmentStatus
@enduml
```
### 2.3 动态模型设计
本节描述了系统在运行时,对象之间的交互行为。
#### 2.3.1 核心时序图
**用户登录认证时序图**
```mermaid
sequenceDiagram
participant User as 用户
participant AuthController as 认证控制器
participant AuthService as 认证服务
participant JwtUtil as JWT工具
User->>AuthController: POST /api/auth/login (email, password)
AuthController->>AuthService: signIn(LoginRequest)
AuthService->>AuthService: 验证凭证
alt 验证成功
AuthService->>JwtUtil: generateToken(user)
JwtUtil-->>AuthService: 返回JWT令牌
AuthService-->>AuthController: 返回JwtAuthenticationResponse
AuthController-->>User: 200 OK (token)
else 验证失败
AuthService-->>AuthController: 抛出AuthenticationException
AuthController-->>User: 401 Unauthorized
end
```
**反馈提交与处理时序图**
```mermaid
sequenceDiagram
participant User as 用户
participant FeedbackController as 反馈控制器
participant FeedbackService as 反馈服务
participant EventPublisher as 事件发布器
participant FeedbackAiReviewService as AI审核服务
User->>FeedbackController: POST /api/feedback/submit
FeedbackController->>FeedbackService: submitFeedback(request, files)
FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent)
FeedbackService-->>FeedbackController: 返回初步响应
Controller-->>User: 201 Created
EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核
FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED)
```
**主管分配任务时序图**
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant TaskMgmtController as 任务管理控制器
participant TaskService as 任务服务
participant AssignmentService as 分配服务
participant NotificationService as 通知服务
Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId)
TaskMgmtController->>TaskService: assignTask(taskId, workerId)
TaskService->>AssignmentService: createAssignment(task, worker)
AssignmentService-->>TaskService: 返回Assignment
TaskService->>TaskService: 更新任务状态为ASSIGNED
TaskService->>NotificationService: notifyWorker(workerId, taskId)
TaskService-->>TaskMgmtController: 分配成功
TaskMgmtController-->>Supervisor: 200 OK
```
**主管审核反馈并创建任务**
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant FeedbackController as 反馈控制器
participant FeedbackService as 反馈服务
participant TaskService as 任务服务
participant TaskRepository as 任务仓库
participant Database as JSON持久化存储
Supervisor->>FeedbackController: POST /api/feedback/{id}/process
FeedbackController->>FeedbackService: processFeedback(feedbackId, request)
FeedbackService->>FeedbackService: 验证反馈状态和主管权限
alt 同意反馈并创建任务
FeedbackService->>TaskService: createTaskFromFeedback(feedback)
TaskService->>TaskRepository: save(task)
TaskRepository->>Database: 保存新任务
Database-->>TaskRepository: 返回保存的任务
TaskRepository-->>TaskService: 返回Task实体
TaskService->>FeedbackService: 更新反馈状态为PENDING_ASSIGNMENT
FeedbackService-->>FeedbackController: 返回处理结果
else 拒绝反馈
FeedbackService->>FeedbackService: 更新反馈状态为REJECTED
FeedbackService-->>FeedbackController: 返回处理结果
end
FeedbackController-->>Supervisor: 200 OK
```
**网格员获取和管理任务**
```mermaid
sequenceDiagram
participant GridWorker as 网格员
participant GridWorkerTaskController as 网格员任务控制器
participant GridWorkerTaskService as 网格员任务服务
participant TaskRepository as 任务仓库
alt 获取任务列表
GridWorker->>GridWorkerTaskController: GET /api/worker/tasks
GridWorkerTaskController->>GridWorkerTaskService: getAssignedTasks(workerId, status)
GridWorkerTaskService->>TaskRepository: findByAssigneeIdAndStatus(workerId, status)
TaskRepository-->>GridWorkerTaskService: 返回Page<Task>
GridWorkerTaskService-->>GridWorkerTaskController: 返回Page<TaskSummaryDTO>
GridWorkerTaskController-->>GridWorker: 返回任务列表
end
alt 接受任务
GridWorker->>GridWorkerTaskController: POST /api/worker/tasks/{taskId}/accept
GridWorkerTaskController->>GridWorkerTaskService: acceptTask(taskId, workerId)
GridWorkerTaskService->>TaskRepository: findById(taskId)
TaskRepository-->>GridWorkerTaskService: 返回Task
GridWorkerTaskService->>GridWorkerTaskService: 更新任务状态为ACCEPTED
GridWorkerTaskService->>TaskRepository: save(task)
TaskRepository-->>GridWorkerTaskService: 返回更新后的Task
GridWorkerTaskService-->>GridWorkerTaskController: 返回TaskSummaryDTO
GridWorkerTaskController-->>GridWorker: 返回更新后的任务
end
```
#### 2.3.2 核心状态图
**反馈状态机 (Feedback Status)**
```plantuml
@startuml
state "待审核" as PENDING_REVIEW
state "AI审核通过" as AI_APPROVED
state "AI拒绝" as AI_REJECTED
state "已处理" as PROCESSED
state "已关闭" as CLOSED
[*] --> PENDING_REVIEW : 提交反馈
PENDING_REVIEW --> AI_APPROVED : AI审核通过
PENDING_REVIEW --> AI_REJECTED : AI审核拒绝
AI_APPROVED --> PROCESSED : 主管创建任务
PROCESSED --> CLOSED : 任务完成
AI_REJECTED --> CLOSED : 归档
@enduml
```
**任务状态机 (Task Status)**
```plantuml
@startuml
state "已创建" as CREATED
state "已分配" as ASSIGNED
state "进行中" as IN_PROGRESS
state "已提交" as SUBMITTED
state "已批准" as APPROVED
state "已拒绝" as REJECTED
[*] --> CREATED : 创建任务
CREATED --> ASSIGNED : 分配给网格员
ASSIGNED --> IN_PROGRESS : 网格员接受任务
IN_PROGRESS --> SUBMITTED : 网格员完成并提交
SUBMITTED --> APPROVED : 主管审核通过
SUBMITTED --> REJECTED : 主管审核拒绝
REJECTED --> ASSIGNED : 重新分配
APPROVED --> [*]
@enduml
```
### 2.4 算法与策略设计
本章节详细阐述了系统运行过程中依赖的核心算法和业务决策策略。
#### 2.4.1 A* 路径规划算法
* **目标**: 为网格员在复杂的城市网格环境中,规划出从当前位置到任务地点的最优(最短)路径,同时能够有效规避障碍物。
* **输入**:
* `startNode`: 起始点网格坐标。
* `endNode`: 目标点网格坐标。
* `gridMap`: 一个二维数组或Map包含了所有网格节点的信息特别是标识了哪些节点是不可通行的障碍物。
* **核心逻辑**:
1. 维护一个"开放列表"Open List用于存放待考察的节点以及一个"关闭列表"Closed List用于存放已考察过的节点。
2. 对每个节点,计算两个核心成本:
* `gCost`: 从起点到当前节点的实际移动成本。
* `hCost` (启发式成本): 从当前节点到终点的预估移动成本(通常使用曼哈顿距离或欧几里得距离)。
3. 总成本 `fCost = gCost + hCost`
4. 算法从开放列表中选取 `fCost` 最低的节点进行探索,直到找到终点。
* **输出**: 一个包含了从起点到终点所有节点坐标的列表,即规划出的路径。
* **伪代码**:
```
function AStar(start, end, grid):
openSet = {start}
closedSet = {}
cameFrom = new Map()
gScore = new Map().setDefault(infinity)
gScore[start] = 0
fScore = new Map().setDefault(infinity)
fScore[start] = heuristic_cost(start, end)
while openSet is not empty:
current = node in openSet with the lowest fScore
if current == end:
return reconstruct_path(cameFrom, current)
remove current from openSet
add current to closedSet
for each neighbor of current:
if neighbor in closedSet:
continue
tentative_gScore = gScore[current] + dist_between(current, neighbor)
if neighbor not in openSet:
add neighbor to openSet
else if tentative_gScore >= gScore[neighbor]:
continue
cameFrom[neighbor] = current
gScore[neighbor] = tentative_gScore
fScore[neighbor] = gScore[neighbor] + heuristic_cost(neighbor, end)
return failure // No path found
```
#### 2.4.2 智能任务分配算法 (设计与提案)
> **现状说明**: 根据对 `TaskManagementServiceImpl.java` 的代码审查,当前的 `intelligentAssignTask` 方法为一个虚拟实现,系统暂无自动化的任务分配逻辑,依赖于主管手动分配。本节内容是为该功能的未来实现所做的**设计提案**。
* **目标**: 当新任务创建后,系统能够自动、快速、合理地将其分配给最合适的网格员,以提升响应效率和资源利用率。
* **核心思想**: 算法通过计算每位可用网格员的"适任度分数"Suitability Score来决定任务归属。分数最高的网格员将被分配该任务。
* **评分因子**:
1. **地理邻近度 (Proximity)**: 网格员当前位置与任务发生地的距离。这是最重要的决定因素。距离越近,分数越高。
2. **当前工作负载 (Workload)**: 网格员手上正在处理的 (`IN_PROGRESS`状态) 任务数量。负载越低,分数越高。
3. **技能匹配度 (Skill Match)**: (未来扩展) 可为网格员和任务定义所需技能标签(如"水质检测""大气采样")。技能匹配度越高,得分越高。
* **评分模型 (Scoring Model)**:
适任度分数 `S` 由各因子加权计算得出:
\[ S = (W_p \times \text{Score}_p) + (W_w \times \text{Score}_w) \]
* **权重 (Weights)**:
* `W_p`: 邻近度权重 (推荐值: `0.7`)
* `W_w`: 工作负载权重 (推荐值: `0.3`)
* 权重总和应为 1。这些值应在配置文件中可配置便于调整策略。
* **因子分数计算**:
* 邻近度分数 \(\text{Score}_p = \frac{1}{D + 1}\)
* `D` 是网格员与任务之间的曼哈顿距离 `(|x1-x2| + |y1-y2|)`。`+1` 是为了防止除以零。
* 工作负载分数 \(\text{Score}_w = \frac{1}{N + 1}\)
* `N` 是网格员当前正在处理的任务数。
* **算法流程伪代码**:
```
function intelligentAssignTask(task):
// 1. 获取所有符合条件的网格员
availableWorkers = userRepo.findAllByRole(GRID_WORKER) and status(ACTIVE)
if availableWorkers is empty:
log("No available workers to assign task " + task.id)
return
bestWorker = null
highestScore = -1
// 2. 遍历所有可用网格员,计算得分
for each worker in availableWorkers:
// 2a. 计算距离
distance = manhattan_distance(worker.gridPos, task.gridPos)
proximityScore = 1 / (distance + 1)
// 2b. 计算工作负载
currentTasks = taskRepo.countByAssignee(worker) and status(IN_PROGRESS)
workloadScore = 1 / (currentTasks + 1)
// 2c. 计算最终加权总分
finalScore = (W_p * proximityScore) + (W_w * workloadScore)
// 2d. 记录并更新最高分和最佳人选
if finalScore > highestScore:
highestScore = finalScore
bestWorker = worker
// 3. 分配任务给最佳人选
if bestWorker is not null:
task.setAssignee(bestWorker)
task.setStatus(ASSIGNED)
taskRepo.save(task)
notificationService.notify(bestWorker, "You have a new task.")
log("Task " + task.id + " assigned to " + bestWorker.name)
else:
log("Could not determine a best worker for task " + task.id)
```
### 2.5 数据持久化设计
系统不使用传统数据库所有数据均以JSON文件的形式存储在服务器的文件系统中。每个核心模型对应一个JSON文件。这种设计简化了部署但也对数据一致性和并发控制提出了更高的要求。
#### 2.5.1 `users.json` - 用户账户数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------------- | ----------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 |
| `name` | `String` | 非空 | 用户姓名 |
| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 |
| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 |
| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 |
| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) |
| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) |
| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) |
| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) |
| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) |
| `region` | `String` | | 所属区域或地区 |
| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) |
| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) |
| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 |
| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) |
| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) |
| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 |
| `lockout_end_time` | `String` | | 账户锁定截止时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
#### 2.5.2 `feedback.json` - 环境问题反馈数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 |
| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID |
| `title` | `String` | 非空 | 反馈标题 |
| `description` | `String` | | 问题详细描述 |
| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) |
| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) |
| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) |
| `text_address` | `String` | | 文字描述的地址 |
| `grid_x` | `Number` | | 事发地网格X坐标 |
| `grid_y` | `Number` | | 事发地网格Y坐标 |
| `latitude` | `Number` | | 事发地纬度 |
| `longitude` | `Number` | | 事发地经度 |
| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
#### 2.5.3 `tasks.json` - 任务数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 |
| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID |
| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人网格员ID |
| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人主管ID |
| `status` | `String` | 非空, (ENUM) | 任务状态 (PENDING, IN_PROGRESS, COMPLETED) |
| `title` | `String` | | 任务标题 |
| `description` | `String` | | 任务详细描述 |
| `pollution_type` | `String` | (ENUM) | 污染类型 |
| `severity_level` | `String` | (ENUM) | 严重程度 |
| `text_address` | `String` | | 任务地点文字描述 |
| `grid_x` | `Number` | | 任务地点网格X坐标 |
| `grid_y` | `Number` | | 任务地点网格Y坐标 |
| `latitude` | `Number` | | 任务地点纬度 |
| `longitude` | `Number` | | 任务地点经度 |
| `assigned_at` | `String` | | 任务分配时间 |
| `completed_at` | `String` | | 任务完成时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
| `deadline` | `String` | | 任务截止日期 |
#### 2.5.4 `grids.json` - 业务网格数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------- | --------------- | ------------------- | ---------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 |
| `gridx` | `Number` | | 网格X坐标 |
| `gridy` | `Number` | | 网格Y坐标 |
| `city_name` | `String` | | 所属城市 |
| `district_name` | `String` | | 所属区县 |
| `description` | `String` | | 网格描述信息 |
| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) |
#### 2.5.5 `assignments.json` - 任务分配记录数据
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | -------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 |
| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID |
| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者主管ID |
| `status` | `String` | 非空, (ENUM) | 分配状态 |
| `remarks` | `String` | | 分配备注 |
| `assignment_time`| `String` | 非空 | 分配时间 |
| `deadline` | `String` | | 任务截止日期 |
```

View File

@@ -0,0 +1,104 @@
# 系统设计文档
本文档旨在详细阐述环境监督系统EMS的系统架构、功能模块和实现细节为开发、测试和维护提供指导。
## 1. 总体设计
总体设计旨在从宏观上描述系统的架构、设计原则和核心组成部分,为后续的详细设计奠定基础。
### 1.1 系统架构设计
#### 1.1.1 架构选型与原则
本系统采用业界成熟的 **前后端分离** 架构。该架构将用户界面(前端)与业务逻辑处理(后端)彻底分离,二者通过定义良好的 **RESTful API** 进行通信。这种模式的优势在于:
- **并行开发**: 前后端团队可以并行开发、测试和部署只需遵守统一的API约定从而显著提升开发效率。
- **技术栈灵活性**: 前后端可以独立选择最适合自身场景的技术栈,便于未来对任一端进行技术升级或重构。
- **关注点分离**: 前端专注于用户体验和界面呈现,后端专注于业务逻辑、数据处理和系统安全,使得系统各部分职责更清晰,更易于维护。
在架构设计中,我们遵循了以下核心原则:
- **高内聚,低耦合**: 将相关功能组织在独立的模块中,并最小化模块间的依赖。
- **可扩展性**: 架构设计应能方便地横向扩展(增加更多服务器实例)和纵向扩展(增加新功能模块)。
- **安全性**: 从设计之初就考虑认证、授权、数据加密和输入验证等安全问题。
- **可维护性**: 采用清晰的代码分层、统一的编码规范和完善的文档,降低长期维护成本。
#### 1.1.2 后端架构
后端服务基于 **Spring Boot 3****Java 17** 构建,这是一个现代化、高性能的组合。其内部采用了经典的三层分层架构模式:
- **表现层 (Controller Layer)**: 负责接收前端的HTTP请求使用 `@RestController` 定义RESTful API。此层负责解析HTTP请求、验证输入参数使用JSR-303注解并调用业务逻辑层处理请求但不包含任何业务逻辑。
- **业务逻辑层 (Service Layer)**: 系统的核心,使用 `@Service` 注解。它封装了所有的业务规则、流程控制和复杂计算。它通过调用数据访问层来操作数据,并通过事件发布等机制与其他服务进行解耦交互。
- **数据访问层 (Repository/Persistence Layer)**: 负责与数据存储进行交互。本项目独创性地采用了一套基于JSON文件的持久化方案。通过自定义的`JsonStorageService`和一系列Repository类模拟了类似JPA的接口实现了对`users.json`, `tasks.json`等核心数据文件的增删改查CRUD操作。选择JSON文件存储简化了项目的部署和配置特别适合快速迭代和中小型应用场景。
此架构同时利用了 **Spring WebFlux** 进行异步处理,具备响应式编程能力,以提升高并发场景下的性能。其清晰的分层和模块化设计也为未来向微服务架构演进奠定了良好基础。
```plantuml
@startuml 后端分层架构图
package "EMS后端架构" {
[前端应用] --> [Controller层]
[Controller层] --> [Service层]
[Service层] --> [Repository层]
[Repository层] --> [JSON文件存储]
note right of [Controller层]
负责接收HTTP请求
参数验证
权限检查
end note
note right of [Service层]
业务逻辑核心
事务管理
事件发布
end note
note right of [Repository层]
数据访问
查询构建
持久化逻辑
end note
}
@enduml
```
#### 1.1.3 前端架构
前端应用是一个基于 **Vue 3** 的单页面应用SPA使用 **Vite** 作为构建工具。选择Vue 3是因为其优秀的性能、丰富的生态系统和渐进式的学习曲线。
- **UI组件库**: **Element Plus**提供了一套高质量、符合设计规范的UI组件加速了界面的开发。
- **状态管理**: **Pinia**作为Vue 3官方推荐的状态管理库它提供了极简的API和强大的类型推断支持能有效管理复杂的应用状态。
- **路由管理**: **Vue Router**,负责管理前端页面的跳转和路由。
### 1.2 功能模块划分
系统在功能上被划分为一系列高内聚的模块,每个模块负责一块具体的业务领域。这种划分方式便于团队分工和独立开发。
| 核心模块 | 主要职责 | 关键功能点 |
| ------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **认证与授权模块** | 管理所有用户的身份认证和访问权限 | - 用户注册/登录/登出<br>- JWT令牌生成与验证<br>- 密码重置<br>- 基于角色的访问控制(RBAC) |
| **用户与人员管理模块** | 维护系统中的所有用户账户及其信息 | - 用户信息的增、删、改、查<br>- 用户角色分配与变更<br>- 用户状态管理(激活/禁用) |
| **反馈管理模块** | 处理来自外部和内部的环境问题反馈 | - 接收公众/认证用户的反馈<br>- 集成AI进行内容预审核<br>- 人工审核与处理<br>- 反馈状态跟踪与统计 |
| **任务管理模块** | 对具体工作任务进行全生命周期管理 | - 从反馈创建任务<br>- 任务分配给网格员<br>- 任务状态(分配、执行、完成)跟踪<br>- 任务审核与结果归档 |
| **网格与地图模块** | 对地理空间进行网格化管理,并提供路径支持 | - 地理网格的定义与划分<br>- 网格员与网格的关联<br>- **A\*寻路算法**服务,优化任务路径 |
| **决策支持模块** | 为管理层提供数据洞察和可视化报告 | - 核心业务指标KPI统计<br>- AQI、任务完成率等数据的可视化<br>- 生成反馈热力图 |
| **个人中心模块** | 为登录用户提供个性化的信息管理和查询功能 | - 查看/修改个人资料<br>- 查询个人提交历史<br>- 查看个人操作日志 |
```plantuml
@startuml 功能模块图
!define RECTANGLE class
RECTANGLE "认证与授权模块" as auth #LightBlue
RECTANGLE "用户与人员管理模块" as user #LightGreen
RECTANGLE "反馈管理模块" as feedback #LightYellow
RECTANGLE "任务管理模块" as task #LightPink
RECTANGLE "网格与地图模块" as grid #LightCyan
RECTANGLE "决策支持模块" as decision #LightGray
RECTANGLE "个人中心模块" as profile #LightSalmon
auth -- user : 提供身份信息
feedback -- task : 生成任务
task -- grid : 使用地理信息
user -- task : 分配任务
task -- decision : 提供数据
feedback -- decision : 提供数据
user -- profile : 个人信息
@enduml
```

View File

@@ -0,0 +1,465 @@
### 2.2 类和对象的设计
本节定义了系统核心业务对象的静态结构,即类图。这些类图展示了系统中的主要实体、它们的属性和方法,以及它们之间的关系。
#### 2.2.1 `UserAccount` 类图
`UserAccount`类是系统中用户账户的核心模型,包含用户的基本信息、认证信息和角色信息。
```mermaid
classDiagram
class UserAccount {
-Long id
-String name
-String phone
-String email
-String password
-Gender gender
-UserRole role
-UserStatus status
-Integer gridX
-Integer gridY
-String region
-String level
-List~String~ skills
-Boolean enabled
-Double currentLatitude
-Double currentLongitude
-Integer failedLoginAttempts
-LocalDateTime lockoutEndTime
-LocalDateTime createdAt
-LocalDateTime updatedAt
+getId() Long
+getName() String
+getPhone() String
+getEmail() String
+getPassword() String
+getRole() UserRole
+getStatus() UserStatus
+isEnabled() Boolean
+setPassword(String) void
+updateProfile(UserUpdateRequest) void
+isAccountNonLocked() Boolean
}
class UserRole {
<<enumeration>>
ADMIN
SUPERVISOR
GRID_WORKER
DECISION_MAKER
PUBLIC_USER
}
class UserStatus {
<<enumeration>>
ACTIVE
INACTIVE
SUSPENDED
}
class Gender {
<<enumeration>>
MALE
FEMALE
OTHER
}
UserAccount --> UserRole
UserAccount --> UserStatus
UserAccount --> Gender
```
#### 2.2.2 `Feedback` 类图
`Feedback`类表示用户提交的环境问题反馈,是系统的核心业务对象之一。
```mermaid
classDiagram
class Feedback {
-Long id
-String eventId
-String title
-String description
-PollutionType pollutionType
-SeverityLevel severityLevel
-FeedbackStatus status
-String textAddress
-Integer gridX
-Integer gridY
-Double latitude
-Double longitude
-UserAccount submitter
-LocalDateTime createdAt
-LocalDateTime updatedAt
-List~Attachment~ attachments
-Task relatedTask
+getId() Long
+getEventId() String
+getTitle() String
+getDescription() String
+getPollutionType() PollutionType
+getSeverityLevel() SeverityLevel
+getStatus() FeedbackStatus
+updateStatus(FeedbackStatus) void
+addAttachment(Attachment) void
}
class PollutionType {
<<enumeration>>
AIR
WATER
SOIL
NOISE
LIGHT
OTHER
}
class SeverityLevel {
<<enumeration>>
LOW
MEDIUM
HIGH
CRITICAL
}
class FeedbackStatus {
<<enumeration>>
SUBMITTED
AI_REVIEWED
PENDING_REVIEW
APPROVED
REJECTED
PROCESSED
CLOSED
}
class Attachment {
-Long id
-String fileName
-String filePath
-String fileType
-Long fileSize
-LocalDateTime uploadedAt
+getId() Long
+getFileName() String
+getFilePath() String
}
Feedback --> PollutionType
Feedback --> SeverityLevel
Feedback --> FeedbackStatus
Feedback --> UserAccount : submitter
Feedback "1" --> "*" Attachment
Feedback "1" --> "0..1" Task
```
#### 2.2.3 `Task` 类图
`Task`类表示网格员需要执行的工作任务,通常由反馈生成。
```mermaid
classDiagram
class Task {
-Long id
-Feedback feedback
-UserAccount assignee
-UserAccount createdBy
-TaskStatus status
-LocalDateTime assignedAt
-LocalDateTime completedAt
-LocalDateTime createdAt
-LocalDateTime updatedAt
-String title
-String description
-PollutionType pollutionType
-SeverityLevel severityLevel
-String textAddress
-Integer gridX
-Integer gridY
-Double latitude
-Double longitude
-List~TaskHistory~ history
-Assignment assignment
-List~TaskSubmission~ submissions
+getId() Long
+getStatus() TaskStatus
+getAssignee() UserAccount
+updateStatus(TaskStatus) void
+assign(UserAccount) void
+addSubmission(TaskSubmission) void
}
class TaskStatus {
<<enumeration>>
CREATED
ASSIGNED
ACCEPTED
IN_PROGRESS
SUBMITTED
APPROVED
REJECTED
COMPLETED
}
class TaskHistory {
-Long id
-Task task
-UserAccount actor
-TaskStatus oldStatus
-TaskStatus newStatus
-String remarks
-LocalDateTime timestamp
+getId() Long
+getTask() Task
+getOldStatus() TaskStatus
+getNewStatus() TaskStatus
}
class TaskSubmission {
-Long id
-Task task
-UserAccount submitter
-String description
-LocalDateTime submittedAt
-List~Attachment~ attachments
+getId() Long
+getTask() Task
+getSubmitter() UserAccount
+addAttachment(Attachment) void
}
Task --> TaskStatus
Task --> UserAccount : assignee
Task --> UserAccount : createdBy
Task "1" --> "*" TaskHistory
Task "1" --> "0..1" Assignment
Task "1" --> "*" TaskSubmission
Task "0..1" --> "1" Feedback
TaskHistory --> Task
TaskHistory --> UserAccount : actor
TaskSubmission --> Task
TaskSubmission --> UserAccount : submitter
TaskSubmission "1" --> "*" Attachment
```
#### 2.2.4 `Grid` 和 `Assignment` 类图
`Grid`类表示地理空间的网格单元,`Assignment`类表示任务分配记录。
```mermaid
classDiagram
class Grid {
-Long id
-Integer gridX
-Integer gridY
-String cityName
-String districtName
-String description
-Boolean isObstacle
+getId() Long
+getGridX() Integer
+getGridY() Integer
+isObstacle() Boolean
}
class Assignment {
-Long id
-Task task
-UserAccount assigner
-LocalDateTime assignmentTime
-LocalDateTime deadline
-AssignmentStatus status
-String remarks
+getId() Long
+getTask() Task
+getAssigner() UserAccount
+getStatus() AssignmentStatus
+updateStatus(AssignmentStatus) void
}
class AssignmentStatus {
<<enumeration>>
PENDING
ACCEPTED
REJECTED
COMPLETED
}
Assignment --> Task
Assignment --> UserAccount : assigner
Assignment --> AssignmentStatus
```
### 2.3 动态模型设计
本节描述了系统在运行时,对象之间的交互行为。通过时序图和状态图,展示了系统的动态行为和状态转换。
#### 2.3.1 核心时序图
时序图展示了系统中对象之间的交互序列,清晰地表达了业务流程的执行顺序和对象间的消息传递。
**用户登录认证时序图**
```mermaid
sequenceDiagram
participant User as 用户
participant AuthController as 认证控制器
participant AuthService as 认证服务
participant JwtUtil as JWT工具
User->>AuthController: POST /api/auth/login (email, password)
AuthController->>AuthService: signIn(LoginRequest)
AuthService->>AuthService: 验证凭证
alt 验证成功
AuthService->>JwtUtil: generateToken(user)
JwtUtil-->>AuthService: 返回JWT令牌
AuthService-->>AuthController: 返回JwtAuthenticationResponse
AuthController-->>User: 200 OK (token)
else 验证失败
AuthService-->>AuthController: 抛出AuthenticationException
AuthController-->>User: 401 Unauthorized
end
```
**反馈提交与处理时序图**
```mermaid
sequenceDiagram
participant User as 用户
participant FeedbackController as 反馈控制器
participant FeedbackService as 反馈服务
participant EventPublisher as 事件发布器
participant FeedbackAiReviewService as AI审核服务
participant TaskService as 任务服务
User->>FeedbackController: POST /api/feedback/submit
FeedbackController->>FeedbackService: submitFeedback(request, files)
FeedbackService->>FeedbackService: 生成事件ID
FeedbackService->>FeedbackService: 保存反馈和附件
FeedbackService->>EventPublisher: publishEvent(FeedbackSubmittedEvent)
FeedbackService-->>FeedbackController: 返回反馈信息
FeedbackController-->>User: 201 Created
EventPublisher-->>FeedbackAiReviewService: 异步触发AI审核
FeedbackAiReviewService->>FeedbackAiReviewService: 分析反馈内容
FeedbackAiReviewService->>FeedbackService: updateFeedbackStatus(AI_REVIEWED)
Note over User,TaskService: 主管审核流程
alt 主管批准反馈
FeedbackService->>TaskService: createTaskFromFeedback(feedback)
TaskService->>TaskService: 创建任务
TaskService-->>FeedbackService: 返回创建的任务
FeedbackService->>FeedbackService: updateFeedbackStatus(PROCESSED)
else 主管拒绝反馈
FeedbackService->>FeedbackService: updateFeedbackStatus(REJECTED)
end
```
**主管分配任务时序图**
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant TaskMgmtController as 任务管理控制器
participant TaskService as 任务服务
participant AssignmentService as 分配服务
participant NotificationService as 通知服务
participant GridWorker as 网格员
Supervisor->>TaskMgmtController: POST /tasks/{taskId}/assign (workerId)
TaskMgmtController->>TaskService: assignTask(taskId, workerId)
TaskService->>TaskService: 验证任务状态
TaskService->>AssignmentService: createAssignment(task, worker)
AssignmentService->>AssignmentService: 创建分配记录
AssignmentService-->>TaskService: 返回Assignment
TaskService->>TaskService: 更新任务状态为ASSIGNED
TaskService->>NotificationService: notifyWorker(workerId, taskId)
NotificationService->>NotificationService: 创建通知
TaskService-->>TaskMgmtController: 分配成功
TaskMgmtController-->>Supervisor: 200 OK
NotificationService-->>GridWorker: 发送任务通知
```
**网格员处理任务时序图**
```mermaid
sequenceDiagram
participant GridWorker as 网格员
participant WorkerTaskController as 网格员任务控制器
participant TaskService as 任务服务
participant PathfindingService as 寻路服务
participant Supervisor as 主管
GridWorker->>WorkerTaskController: POST /api/worker/tasks/{taskId}/accept
WorkerTaskController->>TaskService: acceptTask(taskId, workerId)
TaskService->>TaskService: 更新任务状态为ACCEPTED
TaskService-->>WorkerTaskController: 返回更新后的任务
WorkerTaskController-->>GridWorker: 200 OK
GridWorker->>WorkerTaskController: GET /api/pathfinding/find?start=x,y&end=a,b
WorkerTaskController->>PathfindingService: findPath(start, end)
PathfindingService->>PathfindingService: 执行A*算法
PathfindingService-->>WorkerTaskController: 返回路径点列表
WorkerTaskController-->>GridWorker: 200 OK (路径数据)
GridWorker->>WorkerTaskController: POST /api/worker/tasks/{taskId}/submit
WorkerTaskController->>TaskService: submitTaskCompletion(taskId, workerId, request, files)
TaskService->>TaskService: 验证并更新任务状态为SUBMITTED
TaskService->>TaskService: 保存处理结果和附件
TaskService-->>WorkerTaskController: 返回更新后的任务
WorkerTaskController-->>GridWorker: 200 OK
Note over GridWorker,Supervisor: 主管审核流程
alt 主管批准任务完成
TaskService->>TaskService: 更新任务状态为COMPLETED
else 主管拒绝任务完成
TaskService->>TaskService: 更新任务状态为REJECTED
TaskService->>NotificationService: 通知网格员重新处理
end
```
#### 2.3.2 核心状态图
状态图展示了系统中关键对象的状态变化和转换条件,帮助理解对象的生命周期。
**反馈状态机 (Feedback Status)**
```mermaid
stateDiagram-v2
[*] --> SUBMITTED: 用户提交反馈
SUBMITTED --> AI_REVIEWED: AI自动审核
AI_REVIEWED --> PENDING_REVIEW: 进入人工审核队列
PENDING_REVIEW --> APPROVED: 主管批准
PENDING_REVIEW --> REJECTED: 主管拒绝
APPROVED --> PROCESSED: 创建关联任务
PROCESSED --> CLOSED: 任务完成
REJECTED --> [*]
CLOSED --> [*]
```
**任务状态机 (Task Status)**
```mermaid
stateDiagram-v2
[*] --> CREATED: 创建任务
CREATED --> ASSIGNED: 分配给网格员
ASSIGNED --> ACCEPTED: 网格员接受
ASSIGNED --> CREATED: 网格员拒绝
ACCEPTED --> IN_PROGRESS: 开始处理
IN_PROGRESS --> SUBMITTED: 提交处理结果
SUBMITTED --> APPROVED: 主管批准
SUBMITTED --> REJECTED: 主管拒绝
REJECTED --> IN_PROGRESS: 重新处理
APPROVED --> COMPLETED: 完成归档
COMPLETED --> [*]
```
**用户状态机 (User Status)**
```mermaid
stateDiagram-v2
[*] --> ACTIVE: 创建账户
ACTIVE --> SUSPENDED: 管理员暂停
SUSPENDED --> ACTIVE: 管理员重新激活
ACTIVE --> INACTIVE: 长期未使用
INACTIVE --> ACTIVE: 用户登录
INACTIVE --> [*]: 账户删除
SUSPENDED --> [*]: 账户删除
```

View File

@@ -0,0 +1,154 @@
## 2. 详细设计
### 2.1 功能模块详细设计
本章节将对核心功能模块的内部设计进行更深入的阐述。
#### 2.1.1 反馈管理模块
反馈管理模块是系统与用户交互的核心,负责处理所有环境问题的上报和初步处理。
**核心组件**:
- **主要控制器**: `FeedbackController`, `PublicController`
- **核心服务**: `FeedbackService`, `FeedbackAiReviewService`
- **关键DTO**: `FeedbackSubmissionRequest`, `FeedbackResponseDTO`, `ProcessFeedbackRequest`
**设计要点**:
- 采用 `@RequestPart` 同时接收JSON数据和文件上传实现了富文本内容的反馈提交。
- 通过发布 `FeedbackSubmittedForAiReviewEvent` 事件将AI审核流程解耦并异步化提高了API的响应速度。
- 提供了强大的多条件动态查询能力,支持按状态、类型、严重等级、地理位置和时间范围进行组合过滤。
**反馈状态流转**:
```
SUBMITTED → AI_REVIEWED → PENDING_REVIEW → APPROVED/REJECTED → PROCESSED → CLOSED
```
**主要业务流程**:
1. 公众用户提交反馈系统生成唯一事件ID
2. AI自动审核内容识别垃圾信息和重复提交
3. 主管进行人工审核,确认反馈有效性
4. 有效反馈转化为任务,进入任务管理模块
5. 任务完成后,反馈状态更新为已关闭
#### 2.1.2 任务管理模块
任务管理模块负责将已批准的反馈转化为具体的可执行任务,并对其进行全生命周期管理。
**核心组件**:
- **主要控制器**: `TaskManagementController`, `TaskAssignmentController`, `GridWorkerTaskController`
- **核心服务**: `TaskService`, `TaskAssignmentService`
- **关键DTO**: `TaskCreationRequest`, `TaskDetailDTO`, `TaskAssignmentRequest`, `TaskSummaryDTO`
**设计要点**:
- 清晰的状态机管理任务的生命周期CREATED → ASSIGNED → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED
- 任务分配逻辑考虑了网格员的地理位置和当前负载,旨在实现智能化的高效分配。
- 为主管和网格员提供了完全独立的API端点严格分离了不同角色的操作权限。
**任务分配算法**:
任务分配采用了一种多因素加权评分机制,综合考虑以下因素:
1. **地理距离**: 计算网格员当前位置到任务位置的距离
2. **当前负载**: 评估网格员手头的任务数量和优先级
3. **专业匹配度**: 根据任务类型和网格员的专业技能进行匹配
4. **历史表现**: 考虑网格员的历史完成率和质量评分
这些因素通过加权计算得出每个候选网格员的综合得分,系统推荐得分最高的网格员。主管可以接受系统建议或手动选择其他网格员。
**主要业务流程**:
1. 从审核通过的反馈自动创建任务,或由主管手动创建临时任务
2. 系统推荐最适合的处理人员,或由主管手动指定
3. 任务分配给网格员,并发送通知
4. 网格员接受任务,更新状态为进行中
5. 网格员处理任务,提交处理结果
6. 主管审核处理结果,确认任务完成或要求重新处理
#### 2.1.3 用户与人员管理模块
用户与人员管理模块为系统的权限管理和组织架构提供了基础。
**核心组件**:
- **主要控制器**: `PersonnelController`, `UserProfileController`
- **核心服务**: `UserAccountService`, `OperationLogService`
- **关键DTO**: `UserCreationRequest`, `UserUpdateRequest`, `UserRoleUpdateRequest`
**设计要点**:
- 提供了对用户账户的完整CRUD操作。
- 实现了用户角色和状态的精细化管理。
- 所有关键操作均通过`OperationLogService`记录日志,便于审计和追踪。
**用户角色体系**:
系统定义了五种核心角色,每种角色具有不同的权限范围:
1. **ADMIN**: 系统管理员,拥有所有功能的访问权限
2. **DECISION_MAKER**: 决策者,主要访问统计数据和决策支持功能
3. **SUPERVISOR**: 主管,负责审核反馈和任务分配
4. **GRID_WORKER**: 网格员,负责执行具体任务
5. **PUBLIC_USER**: 公众用户,仅能提交反馈和查询自己的反馈历史
**主要业务流程**:
1. 管理员创建和管理系统用户
2. 用户登录系统获取JWT令牌
3. 用户访问系统功能,系统根据角色进行权限控制
4. 用户可以更新个人信息和密码
5. 管理员可以禁用或重新激活用户账号
#### 2.1.4 网格与地图模块
网格与地图模块是系统实现区域化、网格化管理的核心。
**核心组件**:
- **主要控制器**: `GridController`, `MapController`
- **核心服务**: `GridService`, `PathfindingService`
- **关键算法**: `AStarService`
**设计要点**:
- 支持对地理区域进行灵活的网格化定义,并可将网格标记为障碍。
- 实现了网格与网格员的关联管理。
- 核心亮点是集成了`AStarService`该服务封装了A*寻路算法,为任务路径规划提供支持。
**网格数据结构**:
每个网格单元包含以下关键信息:
- 网格坐标(x, y)
- 所属城市和区域
- 是否为障碍物
- 关联的网格员(可选)
- 描述信息
**A*寻路算法实现**:
系统实现了高效的A*寻路算法,用于为网格员规划从当前位置到任务地点的最优路径:
1. 使用优先队列管理开放列表确保每次选择F值最小的节点
2. F值计算为从起点到当前节点的实际代价(G值)与从当前节点到目标的估计代价(H值)之和
3. 使用曼哈顿距离作为启发函数,适合网格化的移动模式
4. 动态加载地图障碍物信息,确保路径规划避开不可通行区域
**主要业务流程**:
1. 管理员定义城市网格系统
2. 管理员将网格员分配到特定网格
3. 用户提交反馈时,系统自动关联到对应网格
4. 网格员接收任务后,系统提供从当前位置到任务地点的最优路径
5. 管理层可查看基于网格的统计数据,识别问题高发区域
#### 2.1.5 决策支持模块
决策支持模块为管理层提供数据洞察和可视化报告,辅助决策制定。
**核心组件**:
- **主要控制器**: `DashboardController`, `ReportController`
- **核心服务**: `DashboardService`, `StatisticsService`
- **关键DTO**: `DashboardStatsDTO`, `AqiDistributionDTO`, `HeatmapPointDTO`
**设计要点**:
- 采用数据聚合和统计分析技术,从系统运行数据中提取有价值的信息。
- 提供多维度的数据可视化,包括趋势图、分布图和热力图。
- 支持数据导出功能,便于进一步分析和报告生成。
**核心统计指标**:
1. **反馈处理效率**: 平均响应时间、处理时长分布
2. **任务完成情况**: 按时完成率、任务状态分布
3. **区域问题分布**: 按网格统计的问题密度和类型分布
4. **人员绩效**: 网格员的任务完成量和质量评分
**主要业务流程**:
1. 决策者登录系统,访问决策支持模块
2. 系统实时计算和展示核心业务指标
3. 决策者可选择不同维度进行数据分析
4. 系统生成可视化图表,展示数据趋势和分布
5. 决策者可导出分析结果,用于报告和决策支持

View File

@@ -0,0 +1,386 @@
### 2.4 算法设计
本节详细描述了系统中的核心算法设计,这些算法是系统智能化和高效运行的关键。
#### 2.4.1 A* 寻路算法
A*寻路算法是系统中的核心算法之一,用于为网格员规划从当前位置到任务目标点的最优路径。该算法在网格与地图模块中发挥着重要作用。
**算法目的**:
为网格员提供从当前位置到任务地点的最优(最短或最快)路径,避开障碍物。
**算法输入**:
- `startNode`: 起始点坐标 (x, y)。
- `endNode`: 目标点坐标 (x, y)。
- `grid`: 包含障碍物信息的地图网格数据。
**核心逻辑**:
1. 维护一个开放列表(`openList`)和一个关闭列表(`closedList`)。
- `openList`: 存储待探索的节点使用优先队列实现按F值排序。
- `closedList`: 存储已探索过的节点。
2.`openList` 中选取F值G值+H值最小的节点作为当前节点。
- G值: 从起点到当前节点的实际代价。
- H值: 从当前节点到终点的预估代价(启发函数)。
- F值: G值 + H值表示经过当前节点到达终点的总代价估计。
3. 遍历当前节点的相邻节点(上、下、左、右四个方向)。
4. 对于每个相邻节点:
- 如果是障碍物或已在`closedList`中,则跳过。
- 如果不在`openList`计算其G值、H值和F值将其加入`openList`,并记录其父节点为当前节点。
- 如果已在`openList`检查经由当前节点到达该相邻节点的路径是否更优G值更小。如果更优则更新其G值、F值和父节点。
5. 将当前节点从`openList`移除,加入`closedList`
6. 重复步骤2-5直到
- 找到目标节点(当前节点为终点)。
-`openList`为空(无法找到路径)。
7. 如果找到目标节点,通过回溯父节点构建从起点到终点的路径。
**启发函数选择**:
系统采用曼哈顿距离Manhattan Distance作为启发函数计算公式为
```
h(n) = |n.x - goal.x| + |n.y - goal.y|
```
这种启发函数适合网格化的移动模式,只允许上、下、左、右四个方向的移动。
**算法实现**:
```java
public List<Point> findPath(Point start, Point end) {
// 加载地图数据和障碍物信息
List<MapGrid> mapGrids = mapGridRepository.findAll();
Set<Point> obstacles = new HashSet<>();
for (MapGrid gridCell : mapGrids) {
if (gridCell.isObstacle()) {
obstacles.add(new Point(gridCell.getX(), gridCell.getY()));
}
}
// 初始化开放列表和所有节点映射
PriorityQueue<Node> openSet = new PriorityQueue<>(Comparator.comparingInt(n -> n.f));
Map<Point, Node> allNodes = new HashMap<>();
// 创建起始节点
Node startNode = new Node(start, null, 0, calculateHeuristic(start, end));
openSet.add(startNode);
allNodes.put(start, startNode);
// 开始A*算法主循环
while (!openSet.isEmpty()) {
Node currentNode = openSet.poll();
// 找到目标
if (currentNode.point.equals(end)) {
return reconstructPath(currentNode);
}
// 探索相邻节点
for (Point neighborPoint : getNeighbors(currentNode.point)) {
if (obstacles.contains(neighborPoint)) {
continue; // 跳过障碍物
}
int tentativeG = currentNode.g + 1; // 相邻节点间距离为1
Node neighborNode = allNodes.get(neighborPoint);
if (neighborNode == null) {
// 新节点
int h = calculateHeuristic(neighborPoint, end);
neighborNode = new Node(neighborPoint, currentNode, tentativeG, h);
allNodes.put(neighborPoint, neighborNode);
openSet.add(neighborNode);
} else if (tentativeG < neighborNode.g) {
// 找到更好的路径
neighborNode.parent = currentNode;
neighborNode.g = tentativeG;
neighborNode.f = tentativeG + neighborNode.h;
// 更新优先队列
openSet.remove(neighborNode);
openSet.add(neighborNode);
}
}
}
return Collections.emptyList(); // 没有找到路径
}
// 计算启发式函数值(曼哈顿距离)
private int calculateHeuristic(Point a, Point b) {
return Math.abs(a.x() - b.x()) + Math.abs(a.y() - b.y());
}
// 获取相邻节点(上、下、左、右)
private List<Point> getNeighbors(Point point) {
List<Point> neighbors = new ArrayList<>(4);
int[] dx = {0, 0, 1, -1}; // 右、左、下、上
int[] dy = {1, -1, 0, 0};
for (int i = 0; i < 4; i++) {
int newX = point.x() + dx[i];
int newY = point.y() + dy[i];
// 检查边界
if (newX >= 0 && newX < mapWidth && newY >= 0 && newY < mapHeight) {
neighbors.add(new Point(newX, newY));
}
}
return neighbors;
}
// 重建路径
private List<Point> reconstructPath(Node node) {
LinkedList<Point> path = new LinkedList<>();
while (node != null) {
path.addFirst(node.point);
node = node.parent;
}
return path;
}
```
**算法输出**:
从起点到终点的一系列坐标点列表,表示规划好的路径。如果无法找到路径,则返回空列表。
**算法应用**:
`PathfindingController`中通过`/api/pathfinding/find`接口暴露,网格员可以通过该接口获取从当前位置到任务地点的最优路径。
#### 2.4.2 任务智能分配算法
任务智能分配算法是系统的另一个核心算法,用于在多个可用网格员中,为新任务选择最合适的执行者。
**算法目的**:
在多个可用网格员中,为新任务选择最合适的执行者,优化任务分配效率和完成质量。
**算法输入**:
- `task`: 需要分配的任务对象。
- `availableWorkers`: 可用的网格员列表。
**核心逻辑**:
这是一个复合策略算法,综合考虑多个因素,通过加权评分机制选择最合适的网格员:
1. **地理距离评分**:
- 计算每个网格员当前位置到任务位置的距离。
- 距离越近,评分越高。
- 使用公式: `distanceScore = maxDistance - distance`,其中`maxDistance`是一个预设的最大考虑距离。
2. **当前负载评分**:
- 评估每个网格员当前正在处理的任务数量。
- 任务数量越少,评分越高。
- 使用公式: `loadScore = maxTasks - currentTasks`,其中`maxTasks`是一个预设的最大任务数。
3. **专业匹配度评分**:
- 根据任务类型和网格员的专业技能进行匹配。
- 匹配度越高,评分越高。
- 使用公式: `skillScore = matchedSkills / requiredSkills.size()`
4. **历史表现评分**:
- 考虑网格员的历史完成率和质量评分。
- 表现越好,评分越高。
- 使用公式: `performanceScore = (completionRate * 0.7) + (qualityRating * 0.3)`
5. **综合评分计算**:
- 对上述各项评分进行加权求和。
- 使用公式: `totalScore = (distanceScore * w1) + (loadScore * w2) + (skillScore * w3) + (performanceScore * w4)`
- 其中w1、w2、w3、w4是各项评分的权重且满足`w1 + w2 + w3 + w4 = 1`
6. **选择最高评分的网格员**:
- 根据综合评分对网格员进行排序。
- 选择评分最高的网格员作为推荐人选。
**算法实现**:
```java
public UserAccount recommendWorkerForTask(Task task, List<UserAccount> availableWorkers) {
if (availableWorkers.isEmpty()) {
return null;
}
// 权重配置
final double DISTANCE_WEIGHT = 0.4;
final double LOAD_WEIGHT = 0.3;
final double SKILL_WEIGHT = 0.2;
final double PERFORMANCE_WEIGHT = 0.1;
// 任务位置
Point taskLocation = new Point(task.getGridX(), task.getGridY());
// 计算每个网格员的评分
Map<UserAccount, Double> workerScores = new HashMap<>();
for (UserAccount worker : availableWorkers) {
// 1. 地理距离评分
Point workerLocation = new Point(worker.getGridX(), worker.getGridY());
double distance = calculateDistance(workerLocation, taskLocation);
double maxDistance = 10.0; // 最大考虑距离
double distanceScore = Math.max(0, maxDistance - distance) / maxDistance;
// 2. 当前负载评分
int currentTasks = taskRepository.countByAssigneeIdAndStatusIn(
worker.getId(), Arrays.asList(TaskStatus.ASSIGNED, TaskStatus.IN_PROGRESS));
int maxTasks = 5; // 最大任务数
double loadScore = (double)(maxTasks - currentTasks) / maxTasks;
// 3. 专业匹配度评分
double skillScore = calculateSkillMatch(worker, task);
// 4. 历史表现评分
double completionRate = workerStatsService.getCompletionRate(worker.getId());
double qualityRating = workerStatsService.getAverageQualityRating(worker.getId());
double performanceScore = (completionRate * 0.7) + (qualityRating * 0.3);
// 5. 综合评分
double totalScore = (distanceScore * DISTANCE_WEIGHT) +
(loadScore * LOAD_WEIGHT) +
(skillScore * SKILL_WEIGHT) +
(performanceScore * PERFORMANCE_WEIGHT);
workerScores.put(worker, totalScore);
}
// 选择评分最高的网格员
return workerScores.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(null);
}
```
**算法输出**:
推荐的最佳网格员对象。如果没有合适的网格员则返回null。
**算法应用**:
`TaskAssignmentService`中实现,当主管触发自动分配或系统基于反馈自动创建任务时调用。
### 2.5 数据持久化设计
系统采用了一种独特的数据持久化方案不使用传统的关系型数据库而是以JSON文件的形式存储所有数据。这种设计简化了部署和配置特别适合快速迭代和中小型应用场景。
#### 2.5.1 JSON文件存储架构
系统的数据持久化层基于以下架构:
1. **核心存储服务**:
- `JsonStorageService`: 提供对JSON文件的读写操作包括序列化和反序列化。
- `FileSystemService`: 处理文件系统操作,如创建、读取、更新和删除文件。
2. **Repository层**:
- 为每种核心模型提供专门的Repository类`UserRepository``FeedbackRepository`等。
- 这些Repository类模拟了类似JPA的接口提供CRUD操作和查询功能。
- 内部使用`JsonStorageService`进行实际的文件操作。
3. **并发控制**:
- 使用文件锁机制确保在并发环境下的数据一致性。
- 实现了简单的乐观锁定策略,通过版本号检测冲突。
4. **索引和查询优化**:
- 在内存中维护索引结构,加速常见查询操作。
- 支持基于字段值的过滤和排序。
#### 2.5.2 核心JSON文件结构
系统中的每个核心模型对应一个JSON文件下面详细描述了这些文件的结构。
##### `users.json` - 用户账户数据
存储系统中所有用户的账户信息,包括认证信息和角色权限。
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------------- | ----------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 用户的唯一标识符 |
| `name` | `String` | 非空 | 用户姓名 |
| `phone` | `String` | 非空, **唯一** | 手机号码,可用于登录 |
| `email` | `String` | 非空, **唯一** | 电子邮箱,可用于登录 |
| `password` | `String` | 非空, 长度>=8 | 加密后的用户密码 |
| `gender` | `String` | (ENUM) | 性别 (MALE, FEMALE, OTHER) |
| `role` | `String` | (ENUM) | 用户角色 (ADMIN, SUPERVISOR, GRID_WORKER等) |
| `status` | `String` | 非空, (ENUM) | 账户状态 (ACTIVE, INACTIVE, SUSPENDED) |
| `grid_x` | `Number` | | 关联的网格X坐标 (主要用于网格员) |
| `grid_y` | `Number` | | 关联的网格Y坐标 (主要用于网格员) |
| `region` | `String` | | 所属区域或地区 |
| `level` | `String` | (ENUM) | 用户等级 (JUNIOR, SENIOR, EXPERT) |
| `skills` | `Array` | | 技能列表 (JSON数组格式的字符串) |
| `enabled` | `Boolean` | 非空, 默认 `true` | 账户是否启用 |
| `current_latitude` | `Number` | | 当前纬度坐标 (用于实时定位) |
| `current_longitude` | `Number` | | 当前经度坐标 (用于实时定位) |
| `failed_login_attempts` | `Number` | 默认 `0` | 连续失败登录次数 |
| `lockout_end_time` | `String` | | 账户锁定截止时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
##### `feedback.json` - 环境问题反馈数据
存储用户提交的所有环境问题反馈信息。
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 反馈的唯一标识符 |
| `event_id` | `String` | 非空, **唯一** | 人类可读的事件ID |
| `title` | `String` | 非空 | 反馈标题 |
| `description` | `String` | | 问题详细描述 |
| `pollution_type` | `String` | 非空, (ENUM) | 污染类型 (AIR, WATER, SOIL, NOISE) |
| `severity_level` | `String` | 非空, (ENUM) | 严重程度 (LOW, MEDIUM, HIGH, CRITICAL) |
| `status` | `String` | 非空, (ENUM) | 反馈状态 (PENDING_REVIEW, PROCESSED等) |
| `text_address` | `String` | | 文字描述的地址 |
| `grid_x` | `Number` | | 事发地网格X坐标 |
| `grid_y` | `Number` | | 事发地网格Y坐标 |
| `latitude` | `Number` | | 事发地纬度 |
| `longitude` | `Number` | | 事发地经度 |
| `submitter_id` | `Number` | 外键 (FK) -> users.id (可为空) | 提交者ID (公众提交时可为空) |
| `attachments` | `Array` | | 附件列表 (包含文件路径等信息) |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
##### `tasks.json` - 任务数据
存储系统中所有工作任务的信息。
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | ---------------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 任务的唯一标识符 |
| `feedback_id` | `Number` | 外键 (FK) -> feedback.id (可为空) | 关联的原始反馈ID |
| `assignee_id` | `Number` | 外键 (FK) -> users.id (可为空) | 任务执行人网格员ID |
| `created_by` | `Number` | 外键 (FK) -> users.id | 任务创建人主管ID |
| `status` | `String` | 非空, (ENUM) | 任务状态 (CREATED, ASSIGNED, IN_PROGRESS等) |
| `title` | `String` | | 任务标题 |
| `description` | `String` | | 任务详细描述 |
| `pollution_type` | `String` | (ENUM) | 污染类型 |
| `severity_level` | `String` | (ENUM) | 严重程度 |
| `text_address` | `String` | | 任务地点文字描述 |
| `grid_x` | `Number` | | 任务地点网格X坐标 |
| `grid_y` | `Number` | | 任务地点网格Y坐标 |
| `latitude` | `Number` | | 任务地点纬度 |
| `longitude` | `Number` | | 任务地点经度 |
| `assigned_at` | `String` | | 任务分配时间 |
| `completed_at` | `String` | | 任务完成时间 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
| `deadline` | `String` | | 任务截止日期 |
| `history` | `Array` | | 任务状态历史记录 |
| `submissions` | `Array` | | 任务提交记录 |
##### `grids.json` - 业务网格数据
存储系统中定义的地理网格信息。
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| --------------- | --------------- | ------------------- | ---------------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 网格的唯一标识符 |
| `grid_x` | `Number` | | 网格X坐标 |
| `grid_y` | `Number` | | 网格Y坐标 |
| `city_name` | `String` | | 所属城市 |
| `district_name` | `String` | | 所属区县 |
| `description` | `String` | | 网格描述信息 |
| `is_obstacle` | `Boolean` | 默认 `false` | 是否为障碍物(如禁区) |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |
##### `assignments.json` - 任务分配记录数据
存储任务分配的详细记录。
| 字段名 | JSON数据类型 | 约束/说明 | 描述 |
| ---------------- | --------------- | ------------------------ | -------------------------- |
| `id` | `Number` | **唯一标识**, 自增 | 分配记录的唯一标识符 |
| `task_id` | `Number` | 非空, 外键 (FK) -> tasks.id | 关联的任务ID |
| `assigner_id` | `Number` | 非空, 外键 (FK) -> users.id | 分配者主管ID |
| `status` | `String` | 非空, (ENUM) | 分配状态 |
| `remarks` | `String` | | 分配备注 |
| `assignment_time`| `String` | 非空 | 分配时间 |
| `deadline` | `String` | | 任务截止日期 |
| `created_at` | `String` | 非空 | 记录创建时间 |
| `updated_at` | `String` | 非空 | 记录最后更新时间 |

36
Report/要求.md Normal file
View File

@@ -0,0 +1,36 @@
1\. 能够按照面向对象的思想对系统进行设计。使用UML类图对系统整体建模抽取出系统中的类、属性、方法、关联设计合理符合系统要求能够应用文件设计该系统的持久化存储结构和机制存储结构设计合理界面设计美观实用符合用户习惯
满足毕业要求3.1:掌握软件生命周期要素,了解软件开发过程管理模型,熟悉软件需求分析、设计、实现、测试、维护以及过程与管理的方法和技术)
2\. 能够在集成开发环境中使用Java语言实现设计的系统程序结构清晰代码书写规范、简洁能够对实现的系统进行调试、测试和评价保证实现的系统功能完善运行稳定
满足毕业要求3.3:能够设计满足特定功能需求与性能需求的解决方案,并体现创新意识)
3\. 深入理解面向对象的设计思想,掌握面向对象的设计方法,能够应用已有的框架、设计模式解决相应的问题。在解决问题的过程中有自己独到的见解,并能够有所创新;
满足毕业要求4.2:能够理解系统软件的设计思路和基本原理,掌握应用软件技术、科学方法,具备创新性地解决软件工程具体问题的能力)
4\. 能够自学Java语言中关于图形用户界面的知识并能为开发的系统构造图形用户界面。能够通过网络、课堂、书籍等多处获取所需的知识并应用这些知识解决相应的问题
满足毕业要求5.1:能够利用图书馆和互联网进行文献检索和资料查询,掌握获取技术、资源、现代工程工具和信息技术工具的能力;)
5\. 能够将设计、实现和测试过程总结形成实践报告,报告格式规范,报告内容充实、正确,报告叙述逻辑严密,可准确反映出设计和实现的结果,实践过程中出现的问题和解决方案,以及独立分析问题和解决问题的能力。
满足毕业要求10.1:具备一定的社交技能和技巧,能够就与本专业相关的当前热点问题发表自己的观点,能够以口头、文稿、图表等方式与业界同行进行技术交流与沟通,能使用通俗易懂的语言与社会公众进行表达与沟通)
本次实践的项目系统是《东软环保应急》的其中子模块《东软环保公众监督平台》。该系统用于建立环保公众监督平台,拓宽监督渠道,增加环保工作透明度,不断完善公众监督机制,切实增强环境保护实效。
主要考查线性结构(数组,链表,队列)、树、查找结构以及相关算法的设计与实现。
整体要求
1所有数据以文件格式保存文件存储在工程目录中。
2文件数据格式可以是JSON格式也可以是以对象序列化的方式存储。

315
Report/需求定义.md Normal file
View File

@@ -0,0 +1,315 @@
# 需求定义文档
## 1. 整体业务流程
下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程。
```mermaid
flowchart TD
%% 定义样式
classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333
classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333
classDef worker fill:#d5e8d4,stroke:#82b366,color:#333
classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333
classDef decision fill:#f8cecc,stroke:#b85450,color:#333
classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px
%% 流程开始
A([开始]) --> B["[公众端] 发现环境问题"]
B --> C["[公众端] 提交反馈<br>(标题/描述/图片/位置)"]
%% 平台接收与AI处理
C --> D["[平台] 接收反馈<br>生成唯一事件ID"]
D --> E{"[平台] AI自动审核<br>分析内容/分类"}
%% AI审核分支
E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"]
F1 --> F2["[平台] 通知提交者"]
F2 --> Z1([结束])
E -- "需人工确认" --> G["[主管] 查看反馈详情<br>进行人工审核"]
%% 主管审核分支
G --> H{"[主管] 审核决定"}
H -- "驳回" --> I1["[主管] 填写驳回理由"]
I1 --> I2["[平台] 更新状态为REJECTED"]
I2 --> I3["[平台] 通知提交者"]
I3 --> Z2([结束])
%% 审核通过,创建任务
H -- "通过" --> J1["[主管] 确认反馈有效"]
J1 --> J2["[平台] 自动创建结构化任务"]
J2 --> J3["[平台] 更新反馈状态为PROCESSED"]
%% 任务分配
J3 --> K1{"[主管] 选择分配方式"}
K1 -- "手动分配" --> K2["[主管] 选择特定网格员"]
K1 -- "智能推荐" --> K3["[平台] 运行分配算法<br>考虑位置/负载/专长"]
K3 --> K4["[平台] 推荐最佳人选"]
K4 --> K2
K2 --> K5["[平台] 创建任务分配记录<br>更新任务状态为ASSIGNED"]
K5 --> K6["[平台] 通知网格员"]
%% 网格员处理
K6 --> L1["[网格员] 接收任务通知"]
L1 --> L2{"[网格员] 接受任务?"}
L2 -- "拒绝" --> K1
L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"]
L3 --> L4["[网格员] 查看任务详情<br>获取路径规划"]
L4 --> L5["[网格员] 前往现场处理"]
L5 --> L6["[网格员] 记录处理过程<br>上传证明材料"]
L6 --> L7["[网格员] 提交处理结果<br>更新状态为SUBMITTED"]
%% 主管审核结果
L7 --> M1["[主管] 审核处理结果"]
M1 --> M2{"[主管] 结果是否合格?"}
M2 -- "不合格" --> M3["[主管] 填写原因<br>要求重新处理"]
M3 --> L4
%% 完成流程
M2 -- "合格" --> N1["[主管] 确认任务完成"]
N1 --> N2["[平台] 更新任务状态为APPROVED"]
N2 --> N3["[平台] 更新反馈状态为CLOSED"]
N3 --> N4["[平台] 通知反馈提交者"]
N4 --> N5["[平台] 更新统计数据"]
N5 --> O["[决策层] 查看数据看板<br>分析环境趋势"]
O --> Z3([结束])
%% 为节点添加类别
class A,Z1,Z2,Z3 start_end
class B,C,F2,I3,N4 public
class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform
class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor
class L1,L2,L3,L4,L5,L6,L7 worker
class O decision
```
## 2. 功能性需求
### 2.1 功能层次方框图
```mermaid
flowchart TD
%% 用户交互层
subgraph "用户交互层"
direction LR
C["公众服务模块(问题上报)"]
H["个人中心模块(我的反馈/资料)"]
D["管理驾驶舱(数据决策)"]
end
%% 核心业务层
subgraph "核心业务层"
direction LR
AI["AI分析模块(内容审核)"]
E["任务管理模块(分配、流转、执行)"]
end
%% 应用支撑层
subgraph "应用支撑层"
direction LR
F["网格与地图模块(LBS & 寻路)"]
I["文件服务模块(附件存取)"]
end
%% 基础服务层
subgraph "基础服务层"
direction LR
B["用户与认证模块"]
G["系统管理模块(用户/权限)"]
J["日志审计模块"]
end
%% 定义关系
C -- "提交反馈" --> AI
AI -- "分析结果" --> E
C -- "附件" --> I
E -- "调用" --> F
E -- "任务附件" --> I
E -- "统计数据" --> D
H -- "查询个人数据" --> E
%% 基础服务支撑所有上层模块 (关系隐含)
G -- "管理" --> B
classDef userLayer fill:#d4f1f9,stroke:#05a8e5;
classDef coreLayer fill:#ffe6cc,stroke:#f7a128;
classDef appSupportLayer fill:#d5e8d4,stroke:#82b366;
classDef baseLayer fill:#e1d5e7,stroke:#9673a6;
class C,H,D userLayer;
class AI,E coreLayer;
class F,I appSupportLayer;
class B,G,J baseLayer;
```
### 2.2 需求描述
#### 2.2.1 用户与认证模块
| 功能名称 | 用户与认证模块 |
| :--- | :--- |
| **优先级** | 高 |
| **业务背景** | 作为系统安全的基础,本模块负责管理所有用户的身份验证和访问控制,确保系统资源只能被授权用户访问,同时提供灵活的角色权限管理。 |
| **功能说明** | 1. **用户认证**基于JWT (JSON Web Token) 的安全认证机制,支持账号密码登录,提供令牌刷新功能。<br>2. **权限控制**基于RBAC (基于角色的访问控制) 模型预设管理员、主管、网格员等角色每个角色拥有特定的API访问权限。<br>3. **密码管理**:支持安全的密码重置流程,包括邮箱验证码验证,以及定期密码更新提醒。<br>4. **会话管理**:支持单点登录或多设备登录控制,可配置会话超时策略。 |
| **约束条件** | 1. 密码必须符合复杂度要求至少8位包含大小写字母、数字和特殊字符。<br>2. 敏感操作(如修改权限)需要二次验证。<br>3. 密码在数据库中必须使用BCrypt等强哈希算法加密存储。 |
| **相关查询** | 1. 按用户名、邮箱或手机号查询用户信息。<br>2. 查询特定角色的所有用户列表。<br>3. 查询用户的权限和访问历史。 |
| **其他需求** | 1. 登录失败超过预设次数后,账户应被临时锁定。<br>2. 系统应记录所有关键安全事件(登录、权限变更等)的审计日志。 |
| **裁剪说明** | 不可裁剪,此为系统安全的基础组件。 |
#### 2.2.2 反馈管理模块
| 功能名称 | 反馈管理模块 |
| :--- | :--- |
| **优先级** | 高 |
| **业务背景** | 作为系统的核心输入端口,本模块负责收集、处理和跟踪所有环境问题反馈,是连接公众与管理部门的桥梁,也是后续任务创建的数据源。 |
| **功能说明** | 1. **反馈提交**:提供结构化的表单接口,支持文字描述、污染类型分类、严重程度评估、地理位置标记和多媒体附件上传。<br>2. **AI内容审核**:集成智能审核服务,对反馈内容进行自动分析,识别垃圾信息、重复提交,并进行初步的分类和紧急程度评估。<br>3. **人工审核工作台**:为主管提供高效的反馈审核界面,支持批量处理、快速预览和详情查看。<br>4. **状态追踪**:完整记录反馈从提交到处理完成的全生命周期状态变更,支持多维度的统计和查询。 |
| **约束条件** | 1. 反馈提交必须包含至少一张图片和准确的地理位置信息。<br>2. AI审核结果仅作为参考最终决定权在人工审核者手中。<br>3. 对于紧急程度被标记为"高"的反馈系统应在1小时内完成审核。 |
| **相关查询** | 1. 按状态、时间段、区域、污染类型等多维度查询反馈列表。<br>2. 查看特定反馈的详细信息和处理历史。<br>3. 统计不同类型反馈的数量分布和处理效率。 |
| **其他需求** | 1. 支持反馈的优先级标记和升级处理。<br>2. 对于同一区域短时间内的多个相似反馈,系统应能智能识别并提示可能的重复。 |
| **裁剪说明** | AI审核功能可在初期简化实现但反馈的基本提交和人工审核流程不可裁剪。 |
#### 2.2.3 任务管理模块
| 功能名称 | 任务管理模块 |
| :--- | :--- |
| **优先级** | 高 |
| **业务背景** | 本模块是系统的核心业务处理单元,负责将审核通过的反馈转化为可执行的工作任务,并对任务的分配、执行和完成进行全流程管理。 |
| **功能说明** | 1. **任务创建**:支持从反馈自动生成任务,也支持主管手动创建临时任务,包含任务描述、位置、截止时间、优先级等信息。<br>2. **智能分配**:基于多因素(网格员位置、当前负载、专业技能、历史表现)的任务分配算法,为每个任务推荐最合适的处理人员。<br>3. **任务执行跟踪**:记录任务的每个状态变更(已分配、已接受、进行中、已提交、已审核),支持网格员实时上报处理进度。<br>4. **结果审核**:主管对网格员提交的处理结果进行审核,可以通过或驳回,并提供反馈意见。<br>5. **任务看板**:直观展示不同状态任务的数量和分布,支持拖拽操作进行状态更新。 |
| **约束条件** | 1. 任务必须关联到一个有效的反馈或由授权主管手动创建。<br>2. 任务分配时必须考虑网格员的工作区域和当前任务负载。<br>3. 高优先级任务应在24小时内分配并开始处理。<br>4. 任务提交时必须包含处理过程描述和至少一张结果照片。 |
| **相关查询** | 1. 按状态、负责人、时间段、区域等多维度查询任务列表。<br>2. 查看特定任务的详细信息、处理历史和相关反馈。<br>3. 统计不同网格员的任务完成率、平均处理时长等绩效指标。 |
| **其他需求** | 1. 支持任务的紧急程度升级和重新分配。<br>2. 对于长时间未处理的任务,系统应自动发送提醒通知。<br>3. 支持批量导出任务报告,用于绩效评估和工作汇报。 |
| **裁剪说明** | 智能分配算法可以在初期简化实现,但任务的基本创建、分配和状态管理功能不可裁剪。 |
#### 2.2.4 网格与地图模块
| 功能名称 | 网格与地图模块 |
| :--- | :--- |
| **优先级** | 中 |
| **业务背景** | 本模块负责对地理空间进行网格化管理,将城市区域划分为可管理的网格单元,并为任务执行提供地理位置支持和路径规划。 |
| **功能说明** | 1. **网格定义与管理**:支持管理员定义和维护城市网格系统,包括网格的坐标、属性(如是否为障碍物)和责任人分配。<br>2. **A*寻路算法服务**:基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划,考虑距离、交通状况和障碍物。<br>3. **地图可视化**:在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。<br>4. **区域统计**:基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 |
| **约束条件** | 1. 网格系统应支持多级划分最小网格单元不应大于500米×500米。<br>2. 路径规划应考虑实际道路情况和障碍物,避免不可通行区域。<br>3. 地图数据应定期更新,确保准确性。 |
| **相关查询** | 1. 查询特定区域内的网格定义和属性。<br>2. 查询特定网格的历史问题记录和统计数据。<br>3. 获取两点间的最优路径规划。 |
| **其他需求** | 1. 支持网格责任人的灵活调整和临时替换。<br>2. 地图界面应支持常见的交互操作(缩放、平移、点选)。<br>3. 支持离线地图数据缓存,保证在网络不稳定情况下的基本功能。 |
| **裁剪说明** | A*寻路算法的高级功能(如考虑实时交通)可以在后期迭代中实现,但基本的网格定义和地图展示功能不应裁剪。 |
#### 2.2.5 决策支持模块
| 功能名称 | 决策支持模块 (管理驾驶舱) |
| :--- | :--- |
| **优先级** | 中 |
| **业务背景** | 本模块旨在将系统中沉淀的大量业务数据转化为有价值的决策洞察,帮助管理层了解环境状况、评估治理效果、优化资源配置。 |
| **功能说明** | 1. **核心指标看板**:实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。<br>2. **多维度分析**:支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。<br>3. **热力图可视化**:在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。<br>4. **绩效评估**:对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。<br>5. **预警机制**:基于历史数据和趋势分析,对可能出现的环境风险提前预警。 |
| **约束条件** | 1. 数据分析应基于实时或准实时的业务数据,确保决策的时效性。<br>2. 图表和报表应支持多种导出格式PDF、Excel等便于进一步分析和汇报。<br>3. 敏感数据(如个人绩效)的访问应受到严格权限控制。 |
| **相关查询** | 1. 按自定义时间范围查询各类统计指标。<br>2. 查询特定区域或网格员的历史表现数据。<br>3. 生成定制化的数据报表和分析图表。 |
| **其他需求** | 1. 支持数据看板的个性化配置,满足不同管理者的关注点。<br>2. 提供数据异常检测和提醒功能,及时发现数据波动。<br>3. 支持定期自动生成并发送统计报告。 |
| **裁剪说明** | 高级分析功能(如预测模型)可以在后期迭代中实现,但基本的数据统计和可视化功能不应裁剪。 |
## 3. 需求规定
### 3.1 一般性需求
* **数据集中管理与共享**系统应采用统一的数据存储和访问机制确保各模块间的数据一致性和实时共享。所有业务数据应按照标准化格式进行存储并通过规范的API进行访问避免数据孤岛。
* **高可用性与可靠性**系统应保证7x24小时的稳定运行关键业务流程如反馈提交、任务分配的可用性应达到99.9%以上。应实现适当的错误处理和故障恢复机制,确保在出现异常情况时能够快速恢复。
* **安全性与合规性**
* 应用SSL/TLS加密保护所有网络通信。
* 实现严格的认证和授权机制,确保用户只能访问其权限范围内的数据和功能。
* 敏感数据(如用户密码)必须加密存储,并限制访问权限。
* 系统应保留完整的操作日志,支持安全审计和问题追溯。
* 符合相关的数据保护法规和隐私要求。
* **可扩展性与模块化**系统架构应支持水平扩展和功能模块的灵活添加。核心组件应设计为松耦合的便于独立升级和替换。API设计应考虑向后兼容性确保系统可以平滑演进。
* **用户体验优化**
* 界面设计应简洁直观符合现代Web应用的设计标准。
* 关键操作路径应尽量简化,减少用户点击次数。
* 系统响应时间应控制在可接受范围内核心API的平均响应时间不超过500ms。
* 提供适当的操作反馈和状态提示,增强用户对系统的信任感。
* 支持响应式设计确保在不同设备PC、平板、手机上的良好体验。
* **国际化与本地化**:系统应支持多语言界面,初期至少支持中英文切换。日期、时间、数字等格式应根据用户的区域设置进行适当显示。
* **可维护性与可测试性**:系统代码应遵循统一的编码规范和设计模式,保持良好的可读性和可维护性。核心业务逻辑应有充分的单元测试覆盖,便于后续的功能迭代和质量保障。
## 4. 数据描述
### 4.1 用户账户 (UserAccount) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 用户唯一标识符 | Long | 主键,自增 | 是 |
| `name` | 用户姓名 | String | 最大长度50 | 是 |
| `phone` | 手机号码 | String | 符合手机号格式 | 是 |
| `email` | 电子邮箱 | String | 符合邮箱格式 | 是 |
| `password` | 密码(加密存储) | String | 最小长度8包含字母、数字和特殊字符 | 是 |
| `gender` | 性别 | Enum | MALE, FEMALE, OTHER | 否 |
| `role` | 用户角色 | Enum | ADMIN, SUPERVISOR, GRID_WORKER, PUBLIC_USER | 是 |
| `status` | 账户状态 | Enum | ACTIVE, INACTIVE, LOCKED | 是 |
| `gridX` | 网格X坐标仅网格员 | Integer | 非负整数 | 否 |
| `gridY` | 网格Y坐标仅网格员 | Integer | 非负整数 | 否 |
| `createdAt` | 账户创建时间 | DateTime | ISO 8601格式 | 是 |
| `updatedAt` | 账户更新时间 | DateTime | ISO 8601格式 | 是 |
| `lastLoginAt` | 最后登录时间 | DateTime | ISO 8601格式 | 否 |
### 4.2 反馈 (Feedback) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 反馈唯一标识符 | Long | 主键,自增 | 是 |
| `eventId` | 事件ID业务编号 | String | UUID格式 | 是 |
| `title` | 反馈标题 | String | 最大长度100 | 是 |
| `description` | 问题详细描述 | String | 最大长度1000 | 是 |
| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 |
| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 |
| `longitude` | 地理位置经度 | Double | 有效范围 | 是 |
| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 |
| `imageUrls` | 图片URL列表 | Array | 至少一张图片 | 是 |
| `status` | 反馈状态 | Enum | PENDING_REVIEW, AI_REJECTED, PROCESSED, CLOSED | 是 |
| `submitterId` | 提交者ID | Long | 外键关联UserAccount | 是 |
| `reviewerId` | 审核者ID | Long | 外键关联UserAccount | 否 |
| `reviewNote` | 审核备注 | String | 最大长度500 | 否 |
| `createdAt` | 提交时间 | DateTime | ISO 8601格式 | 是 |
| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 |
| `processedAt` | 处理时间 | DateTime | ISO 8601格式 | 否 |
### 4.3 任务 (Task) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 任务唯一标识符 | Long | 主键,自增 | 是 |
| `feedbackId` | 关联的反馈ID | Long | 外键关联Feedback | 否 |
| `title` | 任务标题 | String | 最大长度100 | 是 |
| `description` | 任务描述 | String | 最大长度1000 | 是 |
| `assigneeId` | 被指派的网格员ID | Long | 外键关联UserAccount | 否 |
| `createdBy` | 创建者ID | Long | 外键关联UserAccount | 是 |
| `status` | 任务状态 | Enum | CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED | 是 |
| `priority` | 优先级 | Enum | LOW, MEDIUM, HIGH, URGENT | 是 |
| `longitude` | 任务地点经度 | Double | 有效范围 | 是 |
| `latitude` | 任务地点纬度 | Double | 有效范围 | 是 |
| `deadline` | 截止日期 | DateTime | ISO 8601格式 | 否 |
| `completionReport` | 完成报告 | String | 最大长度2000 | 否 |
| `resultImageUrls` | 结果图片URL列表 | Array | 可为空 | 否 |
| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 |
| `assignedAt` | 分配时间 | DateTime | ISO 8601格式 | 否 |
| `startedAt` | 开始时间 | DateTime | ISO 8601格式 | 否 |
| `submittedAt` | 提交时间 | DateTime | ISO 8601格式 | 否 |
| `approvedAt` | 审核通过时间 | DateTime | ISO 8601格式 | 否 |
| `rejectionReason` | 拒绝原因 | String | 最大长度500 | 否 |
### 4.4 网格 (Grid) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 网格唯一标识符 | Long | 主键,自增 | 是 |
| `gridX` | 网格X坐标 | Integer | 非负整数 | 是 |
| `gridY` | 网格Y坐标 | Integer | 非负整数 | 是 |
| `cityName` | 所属城市 | String | 最大长度50 | 是 |
| `districtName` | 所属区县 | String | 最大长度50 | 是 |
| `description` | 网格描述 | String | 最大长度200 | 否 |
| `isObstacle` | 是否为障碍物 | Boolean | true/false | 是 |
| `responsibleUserId` | 负责人ID | Long | 外键关联UserAccount | 否 |
| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 |
| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 |

614
Report/需求定义_v2.md Normal file
View File

@@ -0,0 +1,614 @@
# 需求定义文档
## 系统分析
### 1. 项目介绍
#### 1.1 项目背景
环境监测系统EMS是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速环境污染问题日益凸显传统的环境问题上报和处理机制存在流程繁琐、响应缓慢、缺乏透明度等问题。本系统旨在通过数字化手段构建一个连接公众、管理部门和执行人员的环境监测与治理平台实现环境问题的快速发现、高效处理和全程监督。
#### 1.2 项目目标
1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。
2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。
3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。
4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。
5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。
### 2. 业务分析
#### 2.1 业务痛点
1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。
2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。
3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。
4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。
5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。
#### 2.2 业务价值
1. **提升公众满意度**:通过快速响应和处理环境问题,提高公众对环境治理工作的满意度。
2. **优化资源配置**:基于数据分析和智能算法,实现人力资源的科学分配,提高资源利用效率。
3. **降低管理成本**:减少人工干预和纸质流程,降低管理成本,提高工作效率。
4. **提升环境质量**:通过高效处理环境问题,改善城市环境质量,提升居民生活品质。
5. **强化问责机制**:通过全流程记录和追踪,明确责任分工,强化问责机制。
#### 2.3 核心业务流程
环境监测系统的核心业务流程包括问题发现与上报、内容审核、任务创建与分配、任务执行与监督、结果审核与反馈等环节,形成一个完整的闭环管理体系。
1. **问题发现与上报**:公众通过移动端应用发现环境问题并提交反馈,包括问题描述、位置信息和图片证据。
2. **内容审核**系统通过AI技术对上报内容进行初步审核筛选出有效信息并由主管进行人工复核确认。
3. **任务创建与分配**:对于审核通过的反馈,系统自动创建任务,并基于智能算法为任务推荐最合适的处理人员。
4. **任务执行与监督**:网格员接收任务后,前往现场处理问题,并记录处理过程和结果。
5. **结果审核与反馈**:主管对处理结果进行审核,确认任务是否完成,并将处理结果反馈给问题上报者。
### 3. 功能分析
#### 3.1 用户角色分析
环境监测系统涉及四类主要用户角色,每个角色在系统中承担不同的职责:
1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。
2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。
3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。
4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。
#### 3.2 用例分析
##### 3.2.1 用例图
```mermaid
graph TD
%% 定义角色
PublicUser["公众用户"]
GridWorker["网格员"]
Supervisor["主管"]
Admin["管理员"]
%% 定义用例
UC1["注册与登录"]
UC2["提交环境问题反馈"]
UC3["查看反馈处理进度"]
UC4["接收任务通知"]
UC5["执行任务"]
UC6["提交处理结果"]
UC7["审核反馈内容"]
UC8["分配任务"]
UC9["审核处理结果"]
UC10["查看统计数据"]
UC11["管理用户账户"]
UC12["配置系统参数"]
%% 建立关系
PublicUser --> UC1
PublicUser --> UC2
PublicUser --> UC3
GridWorker --> UC1
GridWorker --> UC4
GridWorker --> UC5
GridWorker --> UC6
Supervisor --> UC1
Supervisor --> UC7
Supervisor --> UC8
Supervisor --> UC9
Supervisor --> UC10
Admin --> UC1
Admin --> UC10
Admin --> UC11
Admin --> UC12
%% 设置样式
classDef actor fill:#f9f,stroke:#333,stroke-width:2px
classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px
class PublicUser,GridWorker,Supervisor,Admin actor
class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12 usecase
```
##### 3.2.2 主要用例描述
**用例1提交环境问题反馈**
- **参与者**:公众用户
- **前置条件**:用户已登录系统
- **基本流程**
1. 用户选择"提交反馈"功能
2. 系统显示反馈提交表单
3. 用户填写问题标题、描述、污染类型、严重程度
4. 用户上传问题现场图片
5. 用户标记问题发生的地理位置
6. 用户提交表单
7. 系统验证表单数据
8. 系统生成唯一事件ID并保存反馈信息
9. 系统返回提交成功提示
- **替代流程**
- 如果表单验证失败,系统提示错误信息并返回表单页面
- 如果图片上传失败,系统提示重新上传
- **后置条件**:反馈信息被保存,状态设为"待审核"
**用例2审核反馈内容**
- **参与者**:主管
- **前置条件**:主管已登录系统,有待审核的反馈
- **基本流程**
1. 主管进入反馈审核页面
2. 系统显示待审核反馈列表
3. 主管选择一条反馈查看详情
4. 系统显示反馈详细信息和AI审核建议
5. 主管审核内容并做出决定(通过/驳回)
6. 如果通过,主管确认创建任务
7. 如果驳回,主管填写驳回理由
8. 系统更新反馈状态并通知相关人员
- **替代流程**
- 如果需要更多信息,主管可以暂缓决定,标记为"需补充信息"
- **后置条件**:反馈状态更新为"已处理"或"已驳回"
**用例3执行任务**
- **参与者**:网格员
- **前置条件**:网格员已登录系统,已接受任务
- **基本流程**
1. 网格员查看任务详情
2. 系统显示任务信息和位置
3. 网格员请求路径规划
4. 系统生成最优路径
5. 网格员前往现场处理问题
6. 网格员记录处理过程
7. 网格员上传处理结果和证明材料
8. 系统保存处理信息并更新任务状态
- **替代流程**
- 如果任务无法完成,网格员可提交说明并请求重新分配
- **后置条件**:任务状态更新为"已提交",等待主管审核
#### 3.3 活动图分析
##### 3.3.1 反馈提交与处理活动图
```mermaid
stateDiagram-v2
[*] --> 发现环境问题
发现环境问题 --> 填写反馈表单
填写反馈表单 --> 上传图片
上传图片 --> 标记位置
标记位置 --> 提交反馈
提交反馈 --> AI自动审核
state AI自动审核 {
[*] --> 内容分析
内容分析 --> 垃圾信息检测
垃圾信息检测 --> 分类与评级
分类与评级 --> [*]
}
AI自动审核 --> 判断AI审核结果
判断AI审核结果 --> 明显无效: AI拒绝
判断AI审核结果 --> 需人工确认: 需确认
明显无效 --> 标记为AI_REJECTED
标记为AI_REJECTED --> 通知提交者
通知提交者 --> [*]
需人工确认 --> 主管人工审核
主管人工审核 --> 判断审核结果
判断审核结果 --> 驳回: 不通过
判断审核结果 --> 通过: 通过
驳回 --> 填写驳回理由
填写驳回理由 --> 更新状态为REJECTED
更新状态为REJECTED --> 通知提交者反馈被驳回
通知提交者反馈被驳回 --> [*]
通过 --> 创建任务
创建任务 --> 更新反馈状态为PROCESSED
更新反馈状态为PROCESSED --> 任务分配流程
任务分配流程 --> [*]
```
##### 3.3.2 任务分配与执行活动图
```mermaid
stateDiagram-v2
[*] --> 任务创建完成
任务创建完成 --> 选择分配方式
state 选择分配方式 {
[*] --> 手动分配
[*] --> 智能推荐
智能推荐 --> 运行分配算法
运行分配算法 --> 推荐最佳人选
推荐最佳人选 --> 确认人选
手动分配 --> 选择特定网格员
选择特定网格员 --> 确认人选
确认人选 --> [*]
}
选择分配方式 --> 创建任务分配记录
创建任务分配记录 --> 更新任务状态为ASSIGNED
更新任务状态为ASSIGNED --> 通知网格员
通知网格员 --> 网格员接收通知
网格员接收通知 --> 判断是否接受
判断是否接受 --> 拒绝: 拒绝
判断是否接受 --> 接受: 接受
拒绝 --> 选择分配方式
接受 --> 更新状态为IN_PROGRESS
更新状态为IN_PROGRESS --> 获取路径规划
获取路径规划 --> 前往现场处理
前往现场处理 --> 记录处理过程
记录处理过程 --> 上传处理结果
上传处理结果 --> 提交处理结果
提交处理结果 --> 更新状态为SUBMITTED
更新状态为SUBMITTED --> 主管审核结果
主管审核结果 --> 判断结果是否合格
判断结果是否合格 --> 不合格: 不合格
判断结果是否合格 --> 合格: 合格
不合格 --> 填写原因要求重新处理
填写原因要求重新处理 --> 获取路径规划
合格 --> 确认任务完成
确认任务完成 --> 更新任务状态为APPROVED
更新任务状态为APPROVED --> 更新反馈状态为CLOSED
更新反馈状态为CLOSED --> 通知反馈提交者
通知反馈提交者 --> 更新统计数据
更新统计数据 --> [*]
```
### 4. 可行性分析
#### 4.1 技术可行性
1. **前端技术**采用Vue 3框架构建用户界面结合Element Plus组件库可以快速开发出美观、响应式的Web应用满足不同设备的访问需求。
2. **后端技术**基于Spring Boot 3框架和Java 17具备高性能、高并发处理能力能够满足系统的稳定性和扩展性需求。
3. **地图服务**可以集成百度地图、高德地图等成熟的地图API实现地理位置标记、路径规划等功能。
4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。
5. **数据存储**采用JSON文件存储方案简化部署和维护适合中小规模系统的快速实现。
#### 4.2 经济可行性
1. **开发成本**:采用主流开源框架和技术栈,降低开发成本和技术门槛。
2. **维护成本**:模块化设计和完善的文档,降低后期维护和升级成本。
3. **投资回报**:通过提高环境问题处理效率,减少人力资源浪费,长期来看具有良好的投资回报。
4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。
#### 4.3 操作可行性
1. **用户接受度**:系统界面设计简洁直观,操作流程符合用户习惯,易于被各类用户接受和使用。
2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。
3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。
#### 4.4 法律可行性
1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。
2. **知识产权**:系统开发过程中使用的第三方库和组件均为开源或已获得授权,不存在知识产权风险。
3. **合规性**:系统功能和流程设计符合相关法律法规和行业标准,确保合法合规运营。
## 5. 整体业务流程
下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程。
```mermaid
flowchart TD
%% 定义样式
classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333
classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333
classDef worker fill:#d5e8d4,stroke:#82b366,color:#333
classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333
classDef decision fill:#f8cecc,stroke:#b85450,color:#333
classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px
%% 流程开始
A([开始]) --> B["[公众端] 发现环境问题"]
B --> C["[公众端] 提交反馈<br>(标题/描述/图片/位置)"]
%% 平台接收与AI处理
C --> D["[平台] 接收反馈<br>生成唯一事件ID"]
D --> E{"[平台] AI自动审核<br>分析内容/分类"}
%% AI审核分支
E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"]
F1 --> F2["[平台] 通知提交者"]
F2 --> Z1([结束])
E -- "需人工确认" --> G["[主管] 查看反馈详情<br>进行人工审核"]
%% 主管审核分支
G --> H{"[主管] 审核决定"}
H -- "驳回" --> I1["[主管] 填写驳回理由"]
I1 --> I2["[平台] 更新状态为REJECTED"]
I2 --> I3["[平台] 通知提交者"]
I3 --> Z2([结束])
%% 审核通过,创建任务
H -- "通过" --> J1["[主管] 确认反馈有效"]
J1 --> J2["[平台] 自动创建结构化任务"]
J2 --> J3["[平台] 更新反馈状态为PROCESSED"]
%% 任务分配
J3 --> K1{"[主管] 选择分配方式"}
K1 -- "手动分配" --> K2["[主管] 选择特定网格员"]
K1 -- "智能推荐" --> K3["[平台] 运行分配算法<br>考虑位置/负载/专长"]
K3 --> K4["[平台] 推荐最佳人选"]
K4 --> K2
K2 --> K5["[平台] 创建任务分配记录<br>更新任务状态为ASSIGNED"]
K5 --> K6["[平台] 通知网格员"]
%% 网格员处理
K6 --> L1["[网格员] 接收任务通知"]
L1 --> L2{"[网格员] 接受任务?"}
L2 -- "拒绝" --> K1
L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"]
L3 --> L4["[网格员] 查看任务详情<br>获取路径规划"]
L4 --> L5["[网格员] 前往现场处理"]
L5 --> L6["[网格员] 记录处理过程<br>上传证明材料"]
L6 --> L7["[网格员] 提交处理结果<br>更新状态为SUBMITTED"]
%% 主管审核结果
L7 --> M1["[主管] 审核处理结果"]
M1 --> M2{"[主管] 结果是否合格?"}
M2 -- "不合格" --> M3["[主管] 填写原因<br>要求重新处理"]
M3 --> L4
%% 完成流程
M2 -- "合格" --> N1["[主管] 确认任务完成"]
N1 --> N2["[平台] 更新任务状态为APPROVED"]
N2 --> N3["[平台] 更新反馈状态为CLOSED"]
N3 --> N4["[平台] 通知反馈提交者"]
N4 --> N5["[平台] 更新统计数据"]
N5 --> O["[决策层] 查看数据看板<br>分析环境趋势"]
O --> Z3([结束])
%% 为节点添加类别
class A,Z1,Z2,Z3 start_end
class B,C,F2,I3,N4 public
class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform
class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor
class L1,L2,L3,L4,L5,L6,L7 worker
class O decision
```
## 6. 功能性需求
### 6.1 功能层次方框图
```mermaid
flowchart TD
%% 用户交互层
subgraph "用户交互层"
direction LR
C["公众服务模块\n(问题上报)"]
H["个人中心模块\n(我的反馈/资料)"]
D["管理驾驶舱\n(数据决策)"]
end
%% 核心业务层
subgraph "核心业务层"
direction LR
AI["AI分析模块\n(内容审核)"]
E["任务管理模块\n(分配、流转、执行)"]
end
%% 应用支撑层
subgraph "应用支撑层"
direction LR
F["网格与地图模块\n(LBS & 寻路)"]
I["文件服务模块\n(附件存取)"]
end
%% 基础服务层
subgraph "基础服务层"
direction LR
B["用户与认证模块"]
G["系统管理模块\n(用户/权限)"]
J["日志审计模块"]
end
%% 定义关系
C -- "提交反馈" --> AI
AI -- "分析结果" --> E
C -- "附件" --> I
E -- "调用" --> F
E -- "任务附件" --> I
E -- "统计数据" --> D
H -- "查询个人数据" --> E
%% 基础服务支撑所有上层模块 (关系隐含)
G -- "管理" --> B
classDef userLayer fill:#d4f1f9,stroke:#05a8e5;
classDef coreLayer fill:#ffe6cc,stroke:#f7a128;
classDef appSupportLayer fill:#d5e8d4,stroke:#82b366;
classDef baseLayer fill:#e1d5e7,stroke:#9673a6;
class C,H,D userLayer;
class AI,E coreLayer;
class F,I appSupportLayer;
class B,G,J baseLayer;
```
### 6.2 需求描述
#### 6.2.1 用户与认证模块
| 功能名称 | 用户与认证模块 |
| :--- | :--- |
| **优先级** | 高 |
| **业务背景** | 作为系统安全的基础,本模块负责管理所有用户的身份验证和访问控制,确保系统资源只能被授权用户访问,同时提供灵活的角色权限管理。 |
| **功能说明** | 1. **用户认证**基于JWT (JSON Web Token) 的安全认证机制,支持账号密码登录,提供令牌刷新功能。<br>2. **权限控制**基于RBAC (基于角色的访问控制) 模型预设管理员、主管、网格员等角色每个角色拥有特定的API访问权限。<br>3. **密码管理**:支持安全的密码重置流程,包括邮箱验证码验证,以及定期密码更新提醒。<br>4. **会话管理**:支持单点登录或多设备登录控制,可配置会话超时策略。 |
| **约束条件** | 1. 密码必须符合复杂度要求至少8位包含大小写字母、数字和特殊字符。<br>2. 敏感操作(如修改权限)需要二次验证。<br>3. 密码在数据库中必须使用BCrypt等强哈希算法加密存储。 |
| **相关查询** | 1. 按用户名、邮箱或手机号查询用户信息。<br>2. 查询特定角色的所有用户列表。<br>3. 查询用户的权限和访问历史。 |
| **其他需求** | 1. 登录失败超过预设次数后,账户应被临时锁定。<br>2. 系统应记录所有关键安全事件(登录、权限变更等)的审计日志。 |
| **裁剪说明** | 不可裁剪,此为系统安全的基础组件。 |
#### 6.2.2 反馈管理模块
| 功能名称 | 反馈管理模块 |
| :--- | :--- |
| **优先级** | 高 |
| **业务背景** | 作为系统的核心输入端口,本模块负责收集、处理和跟踪所有环境问题反馈,是连接公众与管理部门的桥梁,也是后续任务创建的数据源。 |
| **功能说明** | 1. **反馈提交**:提供结构化的表单接口,支持文字描述、污染类型分类、严重程度评估、地理位置标记和多媒体附件上传。<br>2. **AI内容审核**:集成智能审核服务,对反馈内容进行自动分析,识别垃圾信息、重复提交,并进行初步的分类和紧急程度评估。<br>3. **人工审核工作台**:为主管提供高效的反馈审核界面,支持批量处理、快速预览和详情查看。<br>4. **状态追踪**:完整记录反馈从提交到处理完成的全生命周期状态变更,支持多维度的统计和查询。 |
| **约束条件** | 1. 反馈提交必须包含至少一张图片和准确的地理位置信息。<br>2. AI审核结果仅作为参考最终决定权在人工审核者手中。<br>3. 对于紧急程度被标记为"高"的反馈系统应在1小时内完成审核。 |
| **相关查询** | 1. 按状态、时间段、区域、污染类型等多维度查询反馈列表。<br>2. 查看特定反馈的详细信息和处理历史。<br>3. 统计不同类型反馈的数量分布和处理效率。 |
| **其他需求** | 1. 支持反馈的优先级标记和升级处理。<br>2. 对于同一区域短时间内的多个相似反馈,系统应能智能识别并提示可能的重复。 |
| **裁剪说明** | AI审核功能可在初期简化实现但反馈的基本提交和人工审核流程不可裁剪。 |
#### 6.2.3 任务管理模块
| 功能名称 | 任务管理模块 |
| :--- | :--- |
| **优先级** | 高 |
| **业务背景** | 本模块是系统的核心业务处理单元,负责将审核通过的反馈转化为可执行的工作任务,并对任务的分配、执行和完成进行全流程管理。 |
| **功能说明** | 1. **任务创建**:支持从反馈自动生成任务,也支持主管手动创建临时任务,包含任务描述、位置、截止时间、优先级等信息。<br>2. **智能分配**:基于多因素(网格员位置、当前负载、专业技能、历史表现)的任务分配算法,为每个任务推荐最合适的处理人员。<br>3. **任务执行跟踪**:记录任务的每个状态变更(已分配、已接受、进行中、已提交、已审核),支持网格员实时上报处理进度。<br>4. **结果审核**:主管对网格员提交的处理结果进行审核,可以通过或驳回,并提供反馈意见。<br>5. **任务看板**:直观展示不同状态任务的数量和分布,支持拖拽操作进行状态更新。 |
| **约束条件** | 1. 任务必须关联到一个有效的反馈或由授权主管手动创建。<br>2. 任务分配时必须考虑网格员的工作区域和当前任务负载。<br>3. 高优先级任务应在24小时内分配并开始处理。<br>4. 任务提交时必须包含处理过程描述和至少一张结果照片。 |
| **相关查询** | 1. 按状态、负责人、时间段、区域等多维度查询任务列表。<br>2. 查看特定任务的详细信息、处理历史和相关反馈。<br>3. 统计不同网格员的任务完成率、平均处理时长等绩效指标。 |
| **其他需求** | 1. 支持任务的紧急程度升级和重新分配。<br>2. 对于长时间未处理的任务,系统应自动发送提醒通知。<br>3. 支持批量导出任务报告,用于绩效评估和工作汇报。 |
| **裁剪说明** | 智能分配算法可以在初期简化实现,但任务的基本创建、分配和状态管理功能不可裁剪。 |
#### 6.2.4 网格与地图模块
| 功能名称 | 网格与地图模块 |
| :--- | :--- |
| **优先级** | 中 |
| **业务背景** | 本模块负责对地理空间进行网格化管理,将城市区域划分为可管理的网格单元,并为任务执行提供地理位置支持和路径规划。 |
| **功能说明** | 1. **网格定义与管理**:支持管理员定义和维护城市网格系统,包括网格的坐标、属性(如是否为障碍物)和责任人分配。<br>2. **A*寻路算法服务**:基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划,考虑距离、交通状况和障碍物。<br>3. **地图可视化**:在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。<br>4. **区域统计**:基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 |
| **约束条件** | 1. 网格系统应支持多级划分最小网格单元不应大于500米×500米。<br>2. 路径规划应考虑实际道路情况和障碍物,避免不可通行区域。<br>3. 地图数据应定期更新,确保准确性。 |
| **相关查询** | 1. 查询特定区域内的网格定义和属性。<br>2. 查询特定网格的历史问题记录和统计数据。<br>3. 获取两点间的最优路径规划。 |
| **其他需求** | 1. 支持网格责任人的灵活调整和临时替换。<br>2. 地图界面应支持常见的交互操作(缩放、平移、点选)。<br>3. 支持离线地图数据缓存,保证在网络不稳定情况下的基本功能。 |
| **裁剪说明** | A*寻路算法的高级功能(如考虑实时交通)可以在后期迭代中实现,但基本的网格定义和地图展示功能不应裁剪。 |
#### 6.2.5 决策支持模块
| 功能名称 | 决策支持模块 (管理驾驶舱) |
| :--- | :--- |
| **优先级** | 中 |
| **业务背景** | 本模块旨在将系统中沉淀的大量业务数据转化为有价值的决策洞察,帮助管理层了解环境状况、评估治理效果、优化资源配置。 |
| **功能说明** | 1. **核心指标看板**:实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。<br>2. **多维度分析**:支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。<br>3. **热力图可视化**:在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。<br>4. **绩效评估**:对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。<br>5. **预警机制**:基于历史数据和趋势分析,对可能出现的环境风险提前预警。 |
| **约束条件** | 1. 数据分析应基于实时或准实时的业务数据,确保决策的时效性。<br>2. 图表和报表应支持多种导出格式PDF、Excel等便于进一步分析和汇报。<br>3. 敏感数据(如个人绩效)的访问应受到严格权限控制。 |
| **相关查询** | 1. 按自定义时间范围查询各类统计指标。<br>2. 查询特定区域或网格员的历史表现数据。<br>3. 生成定制化的数据报表和分析图表。 |
| **其他需求** | 1. 支持数据看板的个性化配置,满足不同管理者的关注点。<br>2. 提供数据异常检测和提醒功能,及时发现数据波动。<br>3. 支持定期自动生成并发送统计报告。 |
| **裁剪说明** | 高级分析功能(如预测模型)可以在后期迭代中实现,但基本的数据统计和可视化功能不应裁剪。 |
## 7. 需求规定
### 7.1 一般性需求
* **数据集中管理与共享**系统应采用统一的数据存储和访问机制确保各模块间的数据一致性和实时共享。所有业务数据应按照标准化格式进行存储并通过规范的API进行访问避免数据孤岛。
* **高可用性与可靠性**系统应保证7x24小时的稳定运行关键业务流程如反馈提交、任务分配的可用性应达到99.9%以上。应实现适当的错误处理和故障恢复机制,确保在出现异常情况时能够快速恢复。
* **安全性与合规性**
* 应用SSL/TLS加密保护所有网络通信。
* 实现严格的认证和授权机制,确保用户只能访问其权限范围内的数据和功能。
* 敏感数据(如用户密码)必须加密存储,并限制访问权限。
* 系统应保留完整的操作日志,支持安全审计和问题追溯。
* 符合相关的数据保护法规和隐私要求。
* **可扩展性与模块化**系统架构应支持水平扩展和功能模块的灵活添加。核心组件应设计为松耦合的便于独立升级和替换。API设计应考虑向后兼容性确保系统可以平滑演进。
* **用户体验优化**
* 界面设计应简洁直观符合现代Web应用的设计标准。
* 关键操作路径应尽量简化,减少用户点击次数。
* 系统响应时间应控制在可接受范围内核心API的平均响应时间不超过500ms。
* 提供适当的操作反馈和状态提示,增强用户对系统的信任感。
* 支持响应式设计确保在不同设备PC、平板、手机上的良好体验。
* **国际化与本地化**:系统应支持多语言界面,初期至少支持中英文切换。日期、时间、数字等格式应根据用户的区域设置进行适当显示。
* **可维护性与可测试性**:系统代码应遵循统一的编码规范和设计模式,保持良好的可读性和可维护性。核心业务逻辑应有充分的单元测试覆盖,便于后续的功能迭代和质量保障。
## 8. 数据描述
### 8.1 用户账户 (UserAccount) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 用户唯一标识符 | Long | 主键,自增 | 是 |
| `name` | 用户姓名 | String | 最大长度50 | 是 |
| `phone` | 手机号码 | String | 符合手机号格式 | 是 |
| `email` | 电子邮箱 | String | 符合邮箱格式 | 是 |
| `password` | 密码(加密存储) | String | 最小长度8包含字母、数字和特殊字符 | 是 |
| `gender` | 性别 | Enum | MALE, FEMALE, OTHER | 否 |
| `role` | 用户角色 | Enum | ADMIN, SUPERVISOR, GRID_WORKER, PUBLIC_USER | 是 |
| `status` | 账户状态 | Enum | ACTIVE, INACTIVE, LOCKED | 是 |
| `gridX` | 网格X坐标仅网格员 | Integer | 非负整数 | 否 |
| `gridY` | 网格Y坐标仅网格员 | Integer | 非负整数 | 否 |
| `createdAt` | 账户创建时间 | DateTime | ISO 8601格式 | 是 |
| `updatedAt` | 账户更新时间 | DateTime | ISO 8601格式 | 是 |
| `lastLoginAt` | 最后登录时间 | DateTime | ISO 8601格式 | 否 |
### 8.2 反馈 (Feedback) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 反馈唯一标识符 | Long | 主键,自增 | 是 |
| `eventId` | 事件ID业务编号 | String | UUID格式 | 是 |
| `title` | 反馈标题 | String | 最大长度100 | 是 |
| `description` | 问题详细描述 | String | 最大长度1000 | 是 |
| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 |
| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 |
| `longitude` | 地理位置经度 | Double | 有效范围 | 是 |
| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 |
| `imageUrls` | 图片URL列表 | Array | 至少一张图片 | 是 |
| `status` | 反馈状态 | Enum | PENDING_REVIEW, AI_REJECTED, PROCESSED, CLOSED | 是 |
| `submitterId` | 提交者ID | Long | 外键关联UserAccount | 是 |
| `reviewerId` | 审核者ID | Long | 外键关联UserAccount | 否 |
| `reviewNote` | 审核备注 | String | 最大长度500 | 否 |
| `createdAt` | 提交时间 | DateTime | ISO 8601格式 | 是 |
| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 |
| `processedAt` | 处理时间 | DateTime | ISO 8601格式 | 否 |
### 8.3 任务 (Task) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 任务唯一标识符 | Long | 主键,自增 | 是 |
| `feedbackId` | 关联的反馈ID | Long | 外键关联Feedback | 否 |
| `title` | 任务标题 | String | 最大长度100 | 是 |
| `description` | 任务描述 | String | 最大长度1000 | 是 |
| `assigneeId` | 被指派的网格员ID | Long | 外键关联UserAccount | 否 |
| `createdBy` | 创建者ID | Long | 外键关联UserAccount | 是 |
| `status` | 任务状态 | Enum | CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED | 是 |
| `priority` | 优先级 | Enum | LOW, MEDIUM, HIGH, URGENT | 是 |
| `longitude` | 任务地点经度 | Double | 有效范围 | 是 |
| `latitude` | 任务地点纬度 | Double | 有效范围 | 是 |
| `deadline` | 截止日期 | DateTime | ISO 8601格式 | 否 |
| `completionReport` | 完成报告 | String | 最大长度2000 | 否 |
| `resultImageUrls` | 结果图片URL列表 | Array | 可为空 | 否 |
| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 |
| `assignedAt` | 分配时间 | DateTime | ISO 8601格式 | 否 |
| `startedAt` | 开始时间 | DateTime | ISO 8601格式 | 否 |
| `submittedAt` | 提交时间 | DateTime | ISO 8601格式 | 否 |
| `approvedAt` | 审核通过时间 | DateTime | ISO 8601格式 | 否 |
| `rejectionReason` | 拒绝原因 | String | 最大长度500 | 否 |
### 8.4 网格 (Grid) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 网格唯一标识符 | Long | 主键,自增 | 是 |
| `gridX` | 网格X坐标 | Integer | 非负整数 | 是 |
| `gridY` | 网格Y坐标 | Integer | 非负整数 | 是 |
| `cityName` | 所属城市 | String | 最大长度50 | 是 |
| `districtName` | 所属区县 | String | 最大长度50 | 是 |
| `description` | 网格描述 | String | 最大长度200 | 否 |
| `isObstacle` | 是否为障碍物 | Boolean | true/false | 是 |
| `responsibleUserId` | 负责人ID | Long | 外键关联UserAccount | 否 |
| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 |
| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 |
</rewritten_file>

729
Report/需求定义_v3.md Normal file
View File

@@ -0,0 +1,729 @@
# 需求定义文档
## 1. 项目介绍
### 1.1 项目背景
环境监测系统EMS是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速环境污染问题日益凸显传统的环境问题上报和处理机制存在流程繁琐、响应缓慢、缺乏透明度等问题。本系统旨在通过数字化手段构建一个连接公众、管理部门和执行人员的环境监测与治理平台实现环境问题的快速发现、高效处理和全程监督。
### 1.2 项目目标
1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。
2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。
3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。
4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。
5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。
## 2. 系统分析
### 2.1 业务痛点分析
1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。
2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。
3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。
4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。
5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。
### 2.2 用户角色分析
环境监测系统涉及五类主要用户角色,每个角色在系统中承担不同的职责:
1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。
2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。
3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。
4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。
5. **决策者**:系统的战略端,通过分析系统生成的统计数据和趋势图表,制定环境管理策略和资源分配决策,是系统的最终受益者之一。
### 2.3 用例分析
#### 2.3.1 用例图
```mermaid
graph TD
%% 定义角色
PublicUser["公众用户"]
GridWorker["网格员"]
Supervisor["主管"]
Admin["管理员"]
%% 定义用例
UC1["注册与登录"]
UC2["提交环境问题反馈"]
UC3["查看反馈处理进度"]
UC4["接收任务通知"]
UC5["执行任务"]
UC6["提交处理结果"]
UC7["审核反馈内容"]
UC8["分配任务"]
UC9["审核处理结果"]
UC10["查看统计数据"]
UC11["管理用户账户"]
UC12["配置系统参数"]
%% 建立关系
PublicUser --> UC1
PublicUser --> UC2
PublicUser --> UC3
GridWorker --> UC1
GridWorker --> UC4
GridWorker --> UC5
GridWorker --> UC6
Supervisor --> UC1
Supervisor --> UC7
Supervisor --> UC8
Supervisor --> UC9
Supervisor --> UC10
Admin --> UC1
Admin --> UC10
Admin --> UC11
Admin --> UC12
%% 设置样式
classDef actor fill:#f9f,stroke:#333,stroke-width:2px
classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px
class PublicUser,GridWorker,Supervisor,Admin actor
class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12 usecase
```
#### 2.3.2 活动图:反馈提交与处理流程
```mermaid
stateDiagram-v2
[*] --> 发现环境问题
发现环境问题 --> 填写反馈表单
填写反馈表单 --> 上传图片
上传图片 --> 标记位置
标记位置 --> 提交反馈
提交反馈 --> AI自动审核
state AI自动审核 {
[*] --> 内容分析
内容分析 --> 垃圾信息检测
垃圾信息检测 --> 分类与评级
分类与评级 --> [*]
}
AI自动审核 --> 判断AI审核结果
判断AI审核结果 --> 明显无效: AI拒绝
判断AI审核结果 --> 需人工确认: 需确认
明显无效 --> 标记为AI_REJECTED
标记为AI_REJECTED --> 通知提交者
通知提交者 --> [*]
需人工确认 --> 主管人工审核
主管人工审核 --> 判断审核结果
判断审核结果 --> 驳回: 不通过
判断审核结果 --> 通过: 通过
驳回 --> 填写驳回理由
填写驳回理由 --> 更新状态为REJECTED
更新状态为REJECTED --> 通知提交者反馈被驳回
通知提交者反馈被驳回 --> [*]
通过 --> 创建任务
创建任务 --> 更新反馈状态为PROCESSED
更新反馈状态为PROCESSED --> 任务分配流程
任务分配流程 --> [*]
```
#### 2.3.3 活动图:任务分配与执行流程
```mermaid
stateDiagram-v2
[*] --> 任务创建完成
任务创建完成 --> 选择分配方式
state 选择分配方式 {
[*] --> 手动分配
[*] --> 智能推荐
智能推荐 --> 运行分配算法
运行分配算法 --> 推荐最佳人选
推荐最佳人选 --> 确认人选
手动分配 --> 选择特定网格员
选择特定网格员 --> 确认人选
确认人选 --> [*]
}
选择分配方式 --> 创建任务分配记录
创建任务分配记录 --> 更新任务状态为ASSIGNED
更新任务状态为ASSIGNED --> 通知网格员
通知网格员 --> 网格员接收通知
网格员接收通知 --> 判断是否接受
判断是否接受 --> 拒绝: 拒绝
判断是否接受 --> 接受: 接受
拒绝 --> 选择分配方式
接受 --> 更新状态为IN_PROGRESS
更新状态为IN_PROGRESS --> 获取路径规划
获取路径规划 --> 前往现场处理
前往现场处理 --> 记录处理过程
记录处理过程 --> 上传处理结果
上传处理结果 --> 提交处理结果
提交处理结果 --> 更新状态为SUBMITTED
更新状态为SUBMITTED --> 主管审核结果
主管审核结果 --> 判断结果是否合格
判断结果是否合格 --> 不合格: 不合格
判断结果是否合格 --> 合格: 合格
不合格 --> 填写原因要求重新处理
填写原因要求重新处理 --> 获取路径规划
合格 --> 确认任务完成
确认任务完成 --> 更新任务状态为APPROVED
更新任务状态为APPROVED --> 更新反馈状态为CLOSED
更新反馈状态为CLOSED --> 通知反馈提交者
通知反馈提交者 --> 更新统计数据
更新统计数据 --> [*]
```
### 2.4 系统可行性分析
#### 2.4.1 技术可行性
1. **前端技术**采用Vue 3框架构建用户界面结合Element Plus组件库可以快速开发出美观、响应式的Web应用满足不同设备的访问需求。
2. **后端技术**基于Spring Boot 3框架和Java 17具备高性能、高并发处理能力能够满足系统的稳定性和扩展性需求。
3. **地图服务**可以集成百度地图、高德地图等成熟的地图API实现地理位置标记、路径规划等功能。
4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。
5. **数据存储**采用JSON文件存储方案简化部署和维护适合中小规模系统的快速实现。
#### 2.4.2 经济可行性
1. **开发成本**:采用主流开源框架和技术栈,降低开发成本和技术门槛。
2. **维护成本**:模块化设计和完善的文档,降低后期维护和升级成本。
3. **投资回报**:通过提高环境问题处理效率,减少人力资源浪费,长期来看具有良好的投资回报。
4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。
#### 2.4.3 操作可行性
1. **用户接受度**:系统界面设计简洁直观,操作流程符合用户习惯,易于被各类用户接受和使用。
2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。
3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。
#### 2.4.4 法律可行性
1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。
2. **知识产权**:系统开发过程中使用的第三方库和组件均为开源或已获得授权,不存在知识产权风险。
3. **合规性**:系统功能和流程设计符合相关法律法规和行业标准,确保合法合规运营。
## 3. 功能性需求
### 3.1 功能层次方框图
```mermaid
flowchart TD
%% 用户交互层
subgraph "用户交互层"
direction LR
C["公众服务模块(问题上报)"]
H["个人中心模块(我的反馈/资料)"]
D["管理驾驶舱(数据决策)"]
end
%% 核心业务层
subgraph "核心业务层"
direction LR
AI["AI分析模块(内容审核)"]
E["任务管理模块(分配、流转、执行)"]
end
%% 应用支撑层
subgraph "应用支撑层"
direction LR
F["网格与地图模块(LBS & 寻路)"]
I["文件服务模块(附件存取)"]
end
%% 基础服务层
subgraph "基础服务层"
direction LR
B["用户与认证模块"]
G["系统管理模块(用户/权限)"]
J["日志审计模块"]
end
%% 定义关系
C -- "提交反馈" --> AI
AI -- "分析结果" --> E
C -- "附件" --> I
E -- "调用" --> F
E -- "任务附件" --> I
E -- "统计数据" --> D
H -- "查询个人数据" --> E
%% 基础服务支撑所有上层模块 (关系隐含)
G -- "管理" --> B
classDef userLayer fill:#d4f1f9,stroke:#05a8e5;
classDef coreLayer fill:#ffe6cc,stroke:#f7a128;
classDef appSupportLayer fill:#d5e8d4,stroke:#82b366;
classDef baseLayer fill:#e1d5e7,stroke:#9673a6;
class C,H,D userLayer;
class AI,E coreLayer;
class F,I appSupportLayer;
class B,G,J baseLayer;
```
### 3.2 模块功能概述
| 模块名称 | 功能概述 |
| :--- | :--- |
| **用户与认证模块** | 管理用户身份验证、权限控制和会话管理,确保系统安全性。提供用户注册、登录、密码重置等功能。 |
| **反馈管理模块** | 处理公众提交的环境问题反馈,包括提交、审核、状态追踪和查询统计等功能。 |
| **任务管理模块** | 将审核通过的反馈转化为工作任务,并管理任务的分配、执行和完成全流程。 |
| **网格与地图模块** | 提供地理空间的网格化管理,支持路径规划和地图可视化,辅助任务执行。 |
| **决策支持模块** | 通过数据分析和可视化,为管理层提供决策支持,帮助优化资源配置和治理策略。 |
| **系统管理模块** | 提供系统配置、用户管理、权限设置等基础功能,保障系统正常运行。 |
| **文件服务模块** | 处理系统中的文件上传、存储和访问,支持反馈和任务的附件管理。 |
| **日志审计模块** | 记录系统操作日志,支持安全审计和问题追溯,确保系统运行的可追溯性。 |
## 4. 整体业务流程
下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程。
```mermaid
flowchart TD
%% 定义样式
classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333
classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333
classDef worker fill:#d5e8d4,stroke:#82b366,color:#333
classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333
classDef decision fill:#f8cecc,stroke:#b85450,color:#333
classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px
%% 流程开始
A([开始]) --> B["[公众端] 发现环境问题"]
B --> C["[公众端] 提交反馈<br>(标题/描述/图片/位置)"]
%% 平台接收与AI处理
C --> D["[平台] 接收反馈<br>生成唯一事件ID"]
D --> E{"[平台] AI自动审核<br>分析内容/分类"}
%% AI审核分支
E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"]
F1 --> F2["[平台] 通知提交者"]
F2 --> Z1([结束])
E -- "需人工确认" --> G["[主管] 查看反馈详情<br>进行人工审核"]
%% 主管审核分支
G --> H{"[主管] 审核决定"}
H -- "驳回" --> I1["[主管] 填写驳回理由"]
I1 --> I2["[平台] 更新状态为REJECTED"]
I2 --> I3["[平台] 通知提交者"]
I3 --> Z2([结束])
%% 审核通过,创建任务
H -- "通过" --> J1["[主管] 确认反馈有效"]
J1 --> J2["[平台] 自动创建结构化任务"]
J2 --> J3["[平台] 更新反馈状态为PROCESSED"]
%% 任务分配
J3 --> K1{"[主管] 选择分配方式"}
K1 -- "手动分配" --> K2["[主管] 选择特定网格员"]
K1 -- "智能推荐" --> K3["[平台] 运行分配算法<br>考虑位置/负载/专长"]
K3 --> K4["[平台] 推荐最佳人选"]
K4 --> K2
K2 --> K5["[平台] 创建任务分配记录<br>更新任务状态为ASSIGNED"]
K5 --> K6["[平台] 通知网格员"]
%% 网格员处理
K6 --> L1["[网格员] 接收任务通知"]
L1 --> L2{"[网格员] 接受任务?"}
L2 -- "拒绝" --> K1
L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"]
L3 --> L4["[网格员] 查看任务详情<br>获取路径规划"]
L4 --> L5["[网格员] 前往现场处理"]
L5 --> L6["[网格员] 记录处理过程<br>上传证明材料"]
L6 --> L7["[网格员] 提交处理结果<br>更新状态为SUBMITTED"]
%% 主管审核结果
L7 --> M1["[主管] 审核处理结果"]
M1 --> M2{"[主管] 结果是否合格?"}
M2 -- "不合格" --> M3["[主管] 填写原因<br>要求重新处理"]
M3 --> L4
%% 完成流程
M2 -- "合格" --> N1["[主管] 确认任务完成"]
N1 --> N2["[平台] 更新任务状态为APPROVED"]
N2 --> N3["[平台] 更新反馈状态为CLOSED"]
N3 --> N4["[平台] 通知反馈提交者"]
N4 --> N5["[平台] 更新统计数据"]
N5 --> O["[决策层] 查看数据看板<br>分析环境趋势"]
O --> Z3([结束])
%% 为节点添加类别
class A,Z1,Z2,Z3 start_end
class B,C,F2,I3,N4 public
class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform
class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor
class L1,L2,L3,L4,L5,L6,L7 worker
class O decision
```
## 5. 详细需求描述
### 5.1 用户与认证模块
| 功能名称 | 用户与认证模块 |
| :--- | :--- |
| **优先级** | 高 |
| **业务背景** | 作为系统安全的基础,本模块负责管理所有用户的身份验证和访问控制,确保系统资源只能被授权用户访问,同时提供灵活的角色权限管理。 |
| **功能说明** | 1. **用户认证**基于JWT (JSON Web Token) 的安全认证机制,支持账号密码登录,提供令牌刷新功能。<br>2. **权限控制**基于RBAC (基于角色的访问控制) 模型预设管理员、主管、网格员等角色每个角色拥有特定的API访问权限。<br>3. **密码管理**:支持安全的密码重置流程,包括邮箱验证码验证,以及定期密码更新提醒。<br>4. **会话管理**:支持单点登录或多设备登录控制,可配置会话超时策略。 |
| **约束条件** | 1. 密码必须符合复杂度要求至少8位包含大小写字母、数字和特殊字符。<br>2. 敏感操作(如修改权限)需要二次验证。<br>3. 密码在数据库中必须使用BCrypt等强哈希算法加密存储。<br>4. 登录失败超过预设次数后,账户应被临时锁定。 |
| **相关查询** | 1. 按用户名、邮箱或手机号查询用户信息。<br>2. 查询特定角色的所有用户列表。<br>3. 查询用户的权限和访问历史。 |
| **其他需求** | 1. 系统应记录所有关键安全事件(登录、权限变更等)的审计日志。<br>2. 支持用户个人资料的维护和更新。 |
**用户认证流程图**
```mermaid
sequenceDiagram
participant User as 用户
participant AuthController as 认证控制器
participant AuthService as 认证服务
participant UserRepository as 用户仓库
participant JwtUtil as JWT工具
participant Database as JSON持久化存储
User->>AuthController: POST /api/auth/login
AuthController->>AuthService: signIn(LoginRequest)
AuthService->>UserRepository: findByEmailOrPhone()
UserRepository->>Database: 查询用户信息
Database-->>UserRepository: 返回用户数据
UserRepository-->>AuthService: 返回UserAccount
AuthService->>AuthService: 验证密码
AuthService->>JwtUtil: 生成JWT令牌
JwtUtil-->>AuthService: 返回JWT
AuthService-->>AuthController: JwtAuthenticationResponse
AuthController-->>User: 返回JWT令牌
```
### 5.2 反馈管理模块
| 功能名称 | 反馈管理模块 |
| :--- | :--- |
| **优先级** | 高 |
| **业务背景** | 作为系统的核心输入端口,本模块负责收集、处理和跟踪所有环境问题反馈,是连接公众与管理部门的桥梁,也是后续任务创建的数据源。 |
| **功能说明** | 1. **反馈提交**:提供结构化的表单接口,支持文字描述、污染类型分类、严重程度评估、地理位置标记和多媒体附件上传。<br>2. **AI内容审核**:集成智能审核服务,对反馈内容进行自动分析,识别垃圾信息、重复提交,并进行初步的分类和紧急程度评估。<br>3. **人工审核工作台**:为主管提供高效的反馈审核界面,支持批量处理、快速预览和详情查看。<br>4. **状态追踪**:完整记录反馈从提交到处理完成的全生命周期状态变更,支持多维度的统计和查询。 |
| **约束条件** | 1. 反馈提交必须包含至少一张图片和准确的地理位置信息。<br>2. AI审核结果仅作为参考最终决定权在人工审核者手中。<br>3. 对于紧急程度被标记为"高"的反馈系统应在1小时内完成审核。<br>4. 同一用户在短时间内如5分钟不能重复提交内容高度相似的反馈。 |
| **相关查询** | 1. 按状态、时间段、区域、污染类型等多维度查询反馈列表。<br>2. 查看特定反馈的详细信息和处理历史。<br>3. 统计不同类型反馈的数量分布和处理效率。 |
| **其他需求** | 1. 支持反馈的优先级标记和升级处理。<br>2. 对于同一区域短时间内的多个相似反馈,系统应能智能识别并提示可能的重复。<br>3. 支持反馈附件的预览和下载。 |
**反馈提交处理流程图**
```mermaid
sequenceDiagram
participant User as 用户
participant FeedbackController as 反馈控制器
participant FeedbackService as 反馈服务
participant FeedbackRepository as 反馈仓库
participant AIService as AI服务
participant EventPublisher as 事件发布器
participant Database as JSON持久化存储
User->>FeedbackController: POST /api/feedback/submit
FeedbackController->>FeedbackService: submitFeedback(request, files)
FeedbackService->>FeedbackService: 生成事件ID
FeedbackService->>FeedbackRepository: save(feedback)
FeedbackRepository->>Database: 保存反馈数据
Database-->>FeedbackRepository: 返回保存结果
FeedbackRepository-->>FeedbackService: 返回Feedback实体
FeedbackService->>EventPublisher: 发布反馈创建事件
EventPublisher->>AIService: 触发AI处理
AIService->>AIService: 分析反馈内容
FeedbackService-->>FeedbackController: 返回Feedback
FeedbackController-->>User: 201 Created
```
### 5.3 任务管理模块
| 功能名称 | 任务管理模块 |
| :--- | :--- |
| **优先级** | 高 |
| **业务背景** | 本模块是系统的核心业务处理单元,负责将审核通过的反馈转化为可执行的工作任务,并对任务的分配、执行和完成进行全流程管理。 |
| **功能说明** | 1. **任务创建**:支持从反馈自动生成任务,也支持主管手动创建临时任务,包含任务描述、位置、截止时间、优先级等信息。<br>2. **智能分配**:基于多因素(网格员位置、当前负载、专业技能、历史表现)的任务分配算法,为每个任务推荐最合适的处理人员。<br>3. **任务执行跟踪**:记录任务的每个状态变更(已分配、已接受、进行中、已提交、已审核),支持网格员实时上报处理进度。<br>4. **结果审核**:主管对网格员提交的处理结果进行审核,可以通过或驳回,并提供反馈意见。<br>5. **任务看板**:直观展示不同状态任务的数量和分布,支持拖拽操作进行状态更新。 |
| **约束条件** | 1. 任务必须关联到一个有效的反馈或由授权主管手动创建。<br>2. 任务分配时必须考虑网格员的工作区域和当前任务负载。<br>3. 高优先级任务应在24小时内分配并开始处理。<br>4. 任务提交时必须包含处理过程描述和至少一张结果照片。<br>5. 已完成的任务不能重新分配或修改。 |
| **相关查询** | 1. 按状态、负责人、时间段、区域等多维度查询任务列表。<br>2. 查看特定任务的详细信息、处理历史和相关反馈。<br>3. 统计不同网格员的任务完成率、平均处理时长等绩效指标。 |
| **其他需求** | 1. 支持任务的紧急程度升级和重新分配。<br>2. 对于长时间未处理的任务,系统应自动发送提醒通知。<br>3. 支持批量导出任务报告,用于绩效评估和工作汇报。 |
**任务分配流程图**
```mermaid
sequenceDiagram
participant Supervisor as 主管
participant TaskController as 任务控制器
participant TaskService as 任务服务
participant AssignmentService as 分配服务
participant UserRepository as 用户仓库
participant TaskRepository as 任务仓库
participant GridWorker as 网格工作人员
Supervisor->>TaskController: POST /api/tasks/assign
TaskController->>TaskService: assignTask(taskId, workerId)
TaskService->>UserRepository: findById(workerId)
UserRepository-->>TaskService: 返回GridWorker
TaskService->>AssignmentService: createAssignment()
AssignmentService->>TaskRepository: updateTaskStatus()
TaskRepository-->>AssignmentService: 更新成功
AssignmentService-->>TaskService: Assignment创建成功
TaskService->>TaskService: 发送通知给工作人员
TaskService-->>TaskController: 分配成功
TaskController-->>Supervisor: 200 OK
Note over GridWorker: 接收任务通知
```
### 5.4 网格与地图模块
| 功能名称 | 网格与地图模块 |
| :--- | :--- |
| **优先级** | 中 |
| **业务背景** | 本模块负责对地理空间进行网格化管理,将城市区域划分为可管理的网格单元,并为任务执行提供地理位置支持和路径规划。 |
| **功能说明** | 1. **网格定义与管理**:支持管理员定义和维护城市网格系统,包括网格的坐标、属性(如是否为障碍物)和责任人分配。<br>2. **A*寻路算法服务**:基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划,考虑距离、交通状况和障碍物。<br>3. **地图可视化**:在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。<br>4. **区域统计**:基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 |
| **约束条件** | 1. 网格系统应支持多级划分最小网格单元不应大于500米×500米。<br>2. 路径规划应考虑实际道路情况和障碍物,避免不可通行区域。<br>3. 地图数据应定期更新,确保准确性。<br>4. 网格坐标系统必须与地理坐标系统(经纬度)有明确的转换关系。 |
| **相关查询** | 1. 查询特定区域内的网格定义和属性。<br>2. 查询特定网格的历史问题记录和统计数据。<br>3. 获取两点间的最优路径规划。<br>4. 查询特定区域内的网格员分布情况。 |
| **其他需求** | 1. 支持网格责任人的灵活调整和临时替换。<br>2. 地图界面应支持常见的交互操作(缩放、平移、点选)。<br>3. 支持离线地图数据缓存,保证在网络不稳定情况下的基本功能。 |
**路径规划流程图**
```mermaid
sequenceDiagram
participant User as 用户
participant PathfindingController as 寻路控制器
participant AStarService as A*服务
participant MapData as 地图数据
User->>+PathfindingController: POST /api/pathfinding/find (起点, 终点)
PathfindingController->>+AStarService: findPath(start, end)
AStarService->>+MapData: getObstacles()
MapData-->>-AStarService: 返回障碍物信息
AStarService->>AStarService: 执行A*算法计算路径
AStarService-->>-PathfindingController: 返回计算出的路径
PathfindingController-->>-User: 200 OK (路径坐标列表)
```
### 5.5 决策支持模块
| 功能名称 | 决策支持模块 (管理驾驶舱) |
| :--- | :--- |
| **优先级** | 中 |
| **业务背景** | 本模块旨在将系统中沉淀的大量业务数据转化为有价值的决策洞察,帮助管理层了解环境状况、评估治理效果、优化资源配置。 |
| **功能说明** | 1. **核心指标看板**:实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。<br>2. **多维度分析**:支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。<br>3. **热力图可视化**:在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。<br>4. **绩效评估**:对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。<br>5. **预警机制**:基于历史数据和趋势分析,对可能出现的环境风险提前预警。 |
| **约束条件** | 1. 数据分析应基于实时或准实时的业务数据,确保决策的时效性。<br>2. 图表和报表应支持多种导出格式PDF、Excel等便于进一步分析和汇报。<br>3. 敏感数据(如个人绩效)的访问应受到严格权限控制。<br>4. 系统应能处理和分析至少一年的历史数据。 |
| **相关查询** | 1. 按自定义时间范围查询各类统计指标。<br>2. 查询特定区域或网格员的历史表现数据。<br>3. 生成定制化的数据报表和分析图表。<br>4. 查询环境问题的时间分布和地理分布。 |
| **其他需求** | 1. 支持数据看板的个性化配置,满足不同管理者的关注点。<br>2. 提供数据异常检测和提醒功能,及时发现数据波动。<br>3. 支持定期自动生成并发送统计报告。 |
**决策者获取仪表盘数据流程图**
```mermaid
sequenceDiagram
participant DecisionMaker as 决策者
participant DashboardController as 仪表盘控制器
participant DashboardService as 仪表盘服务
participant variousRepositories as 各类仓库
participant Database as JSON持久化存储
DecisionMaker->>DashboardController: GET /api/dashboard/stats
DashboardController->>DashboardService: getDashboardStats()
DashboardService->>variousRepositories: (并行)调用多个仓库方法获取数据
variousRepositories->>Database: 查询统计数据
Database-->>variousRepositories: 返回数据
variousRepositories-->>DashboardService: 返回统计结果
DashboardService->>DashboardService: 聚合数据为DashboardStatsDTO
DashboardService-->>DashboardController: 返回DashboardStatsDTO
DashboardController-->>DecisionMaker: 返回核心统计数据
```
### 5.6 决策者角色需求
| 功能名称 | 决策者角色需求 |
| :--- | :--- |
| **优先级** | 中 |
| **业务背景** | 决策者是环境监测系统的战略用户,负责根据系统提供的数据分析结果制定环境管理策略和资源分配决策。他们需要全局视角的数据展示和深度分析功能,以支持科学决策。 |
| **功能说明** | 1. **综合仪表盘**:提供环境状况、处理效率、资源利用等多维度的综合数据视图,支持按时间、区域进行筛选。<br>2. **趋势分析**:展示关键指标的历史变化趋势,支持多指标对比和季节性分析。<br>3. **资源分配建议**:基于历史数据和预测模型,为人力资源调配和预算分配提供决策建议。<br>4. **绩效评估报告**:生成网格员、区域、团队的绩效评估报告,支持多维度比较。<br>5. **预警监控**:实时监控环境问题高发区域和异常情况,支持预警规则配置。 |
| **约束条件** | 1. 决策者界面应简洁明了,突出关键指标和异常情况。<br>2. 复杂分析应提供直观的可视化展示,避免过多的数字和表格。<br>3. 报表生成不应影响系统的整体性能。<br>4. 敏感数据的访问应受到严格的权限控制。 |
| **相关查询** | 1. 按时间段、区域、问题类型等维度查询统计数据。<br>2. 查询资源利用效率和投入产出比。<br>3. 查询环境问题的地理分布和时间分布热点。<br>4. 查询预测模型生成的趋势预测结果。 |
| **其他需求** | 1. 支持报表的导出和分享功能。<br>2. 提供决策建议的解释性说明,增强决策透明度。<br>3. 支持自定义关注指标和提醒阈值。<br>4. 提供移动端访问能力,满足随时随地查看关键数据的需求。 |
**决策者使用流程图**
```mermaid
sequenceDiagram
participant DecisionMaker as 决策者
participant Dashboard as 综合仪表盘
participant ReportGenerator as 报表生成器
participant AnalyticsEngine as 分析引擎
participant Database as 数据存储
DecisionMaker->>Dashboard: 登录系统
Dashboard->>AnalyticsEngine: 请求核心指标数据
AnalyticsEngine->>Database: 查询原始数据
Database-->>AnalyticsEngine: 返回数据
AnalyticsEngine->>AnalyticsEngine: 计算指标和趋势
AnalyticsEngine-->>Dashboard: 返回处理后的数据
Dashboard-->>DecisionMaker: 展示综合仪表盘
DecisionMaker->>Dashboard: 调整筛选条件(时间/区域)
Dashboard->>AnalyticsEngine: 请求筛选后的数据
AnalyticsEngine->>Database: 查询筛选数据
Database-->>AnalyticsEngine: 返回筛选结果
AnalyticsEngine-->>Dashboard: 返回处理后的数据
Dashboard-->>DecisionMaker: 更新仪表盘显示
DecisionMaker->>ReportGenerator: 请求生成绩效报告
ReportGenerator->>AnalyticsEngine: 获取绩效数据
AnalyticsEngine->>Database: 查询绩效相关数据
Database-->>AnalyticsEngine: 返回数据
AnalyticsEngine-->>ReportGenerator: 提供分析结果
ReportGenerator-->>DecisionMaker: 生成并下载报告
DecisionMaker->>Dashboard: 查看资源分配建议
Dashboard->>AnalyticsEngine: 请求优化建议
AnalyticsEngine->>AnalyticsEngine: 运行资源优化算法
AnalyticsEngine-->>Dashboard: 返回建议结果
Dashboard-->>DecisionMaker: 展示资源分配建议
```
## 6. 需求规定
### 6.2 技术规格需求
* **前端技术栈**
* 核心框架Vue.js 3.x
* 构建工具Vite
* UI组件库Element Plus
* 状态管理Pinia
* 路由管理Vue Router
* HTTP客户端Axios
* **后端技术栈**
* 核心框架Spring Boot 3.x
* 编程语言Java 17
* 安全框架Spring Security 6.x
* API文档SpringDoc (OpenAPI)
* 数据验证Jakarta Bean Validation (Hibernate Validator)
* 日志系统SLF4J & Logback
* **数据存储**
* 采用JSON文件存储方案
* 实现自定义的泛型JSON仓储层
* 确保数据操作的线程安全性
* **部署环境**
* 支持Docker容器化部署
* 支持常见的Web服务器如Nginx、Apache
* 最低硬件要求4核CPU、8GB内存、50GB存储空间
## 7. 数据描述
### 7.1 用户账户 (UserAccount) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 用户唯一标识符 | Long | 主键,自增 | 是 |
| `name` | 用户姓名 | String | 最大长度50 | 是 |
| `phone` | 手机号码 | String | 符合手机号格式 | 是 |
| `email` | 电子邮箱 | String | 符合邮箱格式 | 是 |
| `password` | 密码(加密存储) | String | 最小长度8包含字母、数字和特殊字符 | 是 |
| `gender` | 性别 | Enum | MALE, FEMALE, OTHER | 否 |
| `role` | 用户角色 | Enum | ADMIN, SUPERVISOR, GRID_WORKER, PUBLIC_USER | 是 |
| `status` | 账户状态 | Enum | ACTIVE, INACTIVE, LOCKED | 是 |
| `gridX` | 网格X坐标仅网格员 | Integer | 非负整数 | 否 |
| `gridY` | 网格Y坐标仅网格员 | Integer | 非负整数 | 否 |
| `region` | 地理区域或区县 | String | 最大长度255 | 否 |
| `level` | 熟练度级别(仅网格员) | Enum | JUNIOR, INTERMEDIATE, SENIOR, EXPERT | 否 |
| `skills` | 技能列表JSON格式 | List<String> | - | 否 |
| `createdAt` | 账户创建时间 | DateTime | ISO 8601格式 | 是 |
| `updatedAt` | 账户更新时间 | DateTime | ISO 8601格式 | 是 |
| `enabled` | 账户是否启用 | Boolean | true/false | 是 |
| `currentLatitude` | 当前纬度(仅网格员) | Double | 有效范围 | 否 |
| `currentLongitude` | 当前经度(仅网格员) | Double | 有效范围 | 否 |
| `failedLoginAttempts` | 连续登录失败次数 | Integer | 非负整数 | 是 |
| `lockoutEndTime` | 锁定结束时间 | DateTime | ISO 8601格式 | 否 |
### 7.2 反馈 (Feedback) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 反馈唯一标识符 | Long | 主键,自增 | 是 |
| `eventId` | 事件ID业务编号 | String | UUID格式 | 是 |
| `title` | 反馈标题 | String | 最大长度100 | 是 |
| `description` | 问题详细描述 | String | 最大长度1000 | 是 |
| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 |
| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 |
| `status` | 反馈状态 | Enum | PENDING_REVIEW, AI_REJECTED, PROCESSED, CLOSED | 是 |
| `textAddress` | 文本地址描述 | String | 最大长度200 | 否 |
| `gridX` | 网格X坐标 | Integer | 非负整数 | 否 |
| `gridY` | 网格Y坐标 | Integer | 非负整数 | 否 |
| `submitterId` | 提交者ID | Long | 外键关联UserAccount | 是 |
| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 |
| `longitude` | 地理位置经度 | Double | 有效范围 | 是 |
| `user` | 提交者用户对象 | UserAccount | 外键关联 | 是 |
| `attachments` | 附件列表 | List<Attachment> | 至少一张图片 | 是 |
| `task` | 关联的任务 | Task | 一对一关联 | 否 |
| `createdAt` | 提交时间 | DateTime | ISO 8601格式 | 是 |
| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 |
### 7.3 任务 (Task) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 任务唯一标识符 | Long | 主键,自增 | 是 |
| `feedback` | 关联的反馈 | Feedback | 外键关联Feedback | 否 |
| `assignee` | 被指派的网格员 | UserAccount | 外键关联UserAccount | 否 |
| `createdBy` | 创建者 | UserAccount | 外键关联UserAccount | 是 |
| `status` | 任务状态 | Enum | CREATED, ASSIGNED, IN_PROGRESS, SUBMITTED, APPROVED, REJECTED | 是 |
| `assignedAt` | 分配时间 | DateTime | ISO 8601格式 | 否 |
| `completedAt` | 完成时间 | DateTime | ISO 8601格式 | 否 |
| `createdAt` | 创建时间 | DateTime | ISO 8601格式 | 是 |
| `updatedAt` | 更新时间 | DateTime | ISO 8601格式 | 是 |
| `title` | 任务标题 | String | 最大长度100 | 是 |
| `description` | 任务描述 | String | 最大长度1000 | 是 |
| `pollutionType` | 污染类型 | Enum | AIR, WATER, SOIL, NOISE, OTHER | 是 |
| `severityLevel` | 严重程度 | Enum | LOW, MEDIUM, HIGH, CRITICAL | 是 |
| `textAddress` | 文本地址描述 | String | 最大长度200 | 否 |
| `gridX` | 网格X坐标 | Integer | 非负整数 | 否 |
| `gridY` | 网格Y坐标 | Integer | 非负整数 | 否 |
| `latitude` | 地理位置纬度 | Double | 有效范围 | 是 |
| `longitude` | 地理位置经度 | Double | 有效范围 | 是 |
| `history` | 任务历史记录 | List<TaskHistory> | - | 否 |
| `assignment` | 任务分配信息 | Assignment | 一对一关联 | 否 |
| `submissions` | 任务提交记录 | List<TaskSubmission> | - | 否 |
### 7.4 网格 (Grid) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 网格唯一标识符 | Long | 主键,自增 | 是 |
| `gridX` | 网格X坐标 | Integer | 非负整数 | 是 |
| `gridY` | 网格Y坐标 | Integer | 非负整数 | 是 |
| `cityName` | 所属城市 | String | 最大长度50 | 是 |
| `districtName` | 所属区县 | String | 最大长度50 | 是 |
| `description` | 网格描述 | String | 最大长度200 | 否 |
| `isObstacle` | 是否为障碍物 | Boolean | true/false | 是 |
### 7.5 任务分配 (Assignment) 数据结构
| 字段名 | 描述 | 数据类型 | 约束条件 | 是否必填 |
| :--- | :--- | :--- | :--- | :--- |
| `id` | 分配唯一标识符 | Long | 主键,自增 | 是 |
| `task` | 关联的任务 | Task | 外键关联Task | 是 |
| `assigner` | 分配人 | UserAccount | 外键关联UserAccount | 是 |
| `assignmentTime` | 分配时间 | DateTime | ISO 8601格式 | 是 |
| `deadline` | 截止日期 | DateTime | ISO 8601格式 | 否 |
| `status` | 分配状态 | Enum | PENDING, ACCEPTED, REJECTED, COMPLETED | 是 |
| `remarks` | 备注说明 | String | 最大长度500 | 否 |

840
Report/需求定义_v4.md Normal file
View File

@@ -0,0 +1,840 @@
# 需求定义文档
## 1. 项目介绍
### 1.1 项目背景
环境监测系统EMS是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速环境污染问题日益凸显传统的环境问题上报和处理机制存在以下痛点
- **流程繁琐**:公众发现环境问题后,需要通过多个渠道和部门层层上报,处理流程不透明
- **响应缓慢**:从问题发现到最终解决,往往需要经历漫长的等待时间
- **缺乏透明度**:公众难以了解问题处理进度,无法有效监督
- **资源分配不合理**:缺乏科学的任务分配机制,导致人力资源利用不均衡
本系统旨在通过数字化手段,构建一个连接公众、管理部门和执行人员的环境监测与治理平台,实现环境问题的快速发现、高效处理和全程监督。
### 1.2 项目目标
1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。
2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。
3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。
4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。
5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。
## 2. 系统分析
### 2.1 业务痛点分析
1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。
2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。
3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。
4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。
5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。
### 2.2 用户角色分析
环境监测系统涉及五类主要用户角色,每个角色在系统中承担不同的职责:
1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。
- 需求:简单便捷的问题上报方式、透明的处理进度查询
- 痛点:传统上报渠道繁琐、反馈周期长、处理结果不透明
2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。
- 需求:清晰的任务指派、便捷的结果上报、高效的路径规划
- 痛点:任务分配不合理、工作量分布不均、缺乏高效导航
3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。
- 需求:高效的任务管理、智能的人员调配、直观的进度监控
- 痛点:人工分配任务效率低、缺乏全局视角、绩效评估困难
4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。
- 需求:灵活的权限配置、完善的日志审计、便捷的系统维护
- 痛点:账户管理繁琐、权限控制粗放、系统维护成本高
5. **决策者**:系统的战略端,通过分析系统生成的统计数据和趋势图表,制定环境管理策略和资源分配决策,是系统的最终受益者之一。
- 需求:多维度的数据分析、直观的可视化展示、科学的决策支持
- 痛点:数据获取困难、分析维度单一、缺乏预测能力
### 2.3 用例分析
#### 2.3.1 用例图
以下用例图展示了系统中各角色可以执行的主要操作:
```mermaid
graph TD
%% 定义角色
PublicUser["公众用户"]
GridWorker["网格员"]
Supervisor["主管"]
Admin["管理员"]
DecisionMaker["决策者"]
%% 定义用例
UC1["注册与登录"]
UC2["提交环境问题反馈"]
UC3["查看反馈处理进度"]
UC4["接收任务通知"]
UC5["执行任务"]
UC6["提交处理结果"]
UC7["审核反馈内容"]
UC8["分配任务"]
UC9["审核处理结果"]
UC10["查看统计数据"]
UC11["管理用户账户"]
UC12["配置系统参数"]
UC13["查看决策仪表盘"]
UC14["生成分析报告"]
%% 建立关系
PublicUser --> UC1
PublicUser --> UC2
PublicUser --> UC3
GridWorker --> UC1
GridWorker --> UC4
GridWorker --> UC5
GridWorker --> UC6
Supervisor --> UC1
Supervisor --> UC7
Supervisor --> UC8
Supervisor --> UC9
Supervisor --> UC10
Admin --> UC1
Admin --> UC10
Admin --> UC11
Admin --> UC12
DecisionMaker --> UC1
DecisionMaker --> UC10
DecisionMaker --> UC13
DecisionMaker --> UC14
%% 设置样式
classDef actor fill:#f9f,stroke:#333,stroke-width:2px
classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px
class PublicUser,GridWorker,Supervisor,Admin,DecisionMaker actor
class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12,UC13,UC14 usecase
```
#### 2.3.2 活动图:反馈提交与处理流程
以下活动图展示了从公众发现环境问题到反馈处理完成的完整业务流程:
```mermaid
stateDiagram-v2
[*] --> 发现环境问题
发现环境问题 --> 填写反馈表单
填写反馈表单 --> 上传图片
上传图片 --> 标记位置
标记位置 --> 提交反馈
提交反馈 --> AI自动审核
state AI自动审核 {
[*] --> 内容分析
内容分析 --> 垃圾信息检测
垃圾信息检测 --> 分类与评级
分类与评级 --> [*]
}
AI自动审核 --> 判断AI审核结果
判断AI审核结果 --> 明显无效: AI拒绝
判断AI审核结果 --> 需人工确认: 需确认
明显无效 --> 标记为AI_REJECTED
标记为AI_REJECTED --> 通知提交者
通知提交者 --> [*]
需人工确认 --> 主管人工审核
主管人工审核 --> 判断审核结果
判断审核结果 --> 驳回: 不通过
判断审核结果 --> 通过: 通过
驳回 --> 填写驳回理由
填写驳回理由 --> 更新状态为REJECTED
更新状态为REJECTED --> 通知提交者反馈被驳回
通知提交者反馈被驳回 --> [*]
通过 --> 创建任务
创建任务 --> 更新反馈状态为PROCESSED
更新反馈状态为PROCESSED --> 任务分配流程
任务分配流程 --> [*]
```
# 需求定义文档(续)
#### 2.3.3 活动图:任务分配与执行流程
以下活动图展示了从任务创建到完成的完整业务流程:
```mermaid
stateDiagram-v2
[*] --> 任务创建完成
任务创建完成 --> 选择分配方式
state 选择分配方式 {
[*] --> 手动分配
[*] --> 智能推荐
智能推荐 --> 运行分配算法
运行分配算法 --> 推荐最佳人选
推荐最佳人选 --> 确认人选
手动分配 --> 选择特定网格员
选择特定网格员 --> 确认人选
确认人选 --> [*]
}
选择分配方式 --> 创建任务分配记录
创建任务分配记录 --> 更新任务状态为ASSIGNED
更新任务状态为ASSIGNED --> 通知网格员
通知网格员 --> 网格员接收通知
网格员接收通知 --> 判断是否接受
判断是否接受 --> 拒绝: 拒绝
判断是否接受 --> 接受: 接受
拒绝 --> 选择分配方式
接受 --> 更新状态为IN_PROGRESS
更新状态为IN_PROGRESS --> 获取路径规划
获取路径规划 --> 前往现场处理
前往现场处理 --> 记录处理过程
记录处理过程 --> 上传处理结果
上传处理结果 --> 提交处理结果
提交处理结果 --> 更新状态为SUBMITTED
更新状态为SUBMITTED --> 主管审核结果
主管审核结果 --> 判断结果是否合格
判断结果是否合格 --> 不合格: 不合格
判断结果是否合格 --> 合格: 合格
不合格 --> 填写原因要求重新处理
填写原因要求重新处理 --> 获取路径规划
合格 --> 确认任务完成
确认任务完成 --> 更新任务状态为APPROVED
更新任务状态为APPROVED --> 更新反馈状态为CLOSED
更新反馈状态为CLOSED --> 通知反馈提交者
通知反馈提交者 --> 更新统计数据
更新统计数据 --> [*]
```
### 2.4 系统可行性分析
#### 2.4.1 技术可行性
本系统的技术实现是可行的,主要基于以下分析:
1. **前端技术**市场上已有成熟的前端框架和组件库可以快速开发出美观、响应式的Web应用满足不同设备的访问需求。
2. **后端技术**:现有的后端框架具备高性能、高并发处理能力,能够满足系统的稳定性和扩展性需求。
3. **地图服务**可以集成成熟的地图API实现地理位置标记、路径规划等功能无需从零开发。
4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。
5. **数据存储**:可采用多种数据存储方案,根据系统规模和性能需求灵活选择。
#### 2.4.2 经济可行性
从经济角度看,本系统的开发和运营是可行的:
1. **开发成本**:可以采用主流开源框架和技术栈,降低开发成本和技术门槛。
2. **维护成本**:通过模块化设计和完善的文档,可以降低后期维护和升级成本。
3. **投资回报**
- 提高环境问题处理效率,减少人力资源浪费
- 降低环境问题带来的经济损失
- 提升城市环境质量,间接促进经济发展
- 长期来看具有良好的投资回报
4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。
#### 2.4.3 操作可行性
从操作角度看,本系统具有良好的可行性:
1. **用户接受度**
- 系统界面设计简洁直观,符合用户习惯
- 操作流程简化,降低学习成本
- 移动端支持,满足随时随地使用需求
2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。
3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。
4. **组织支持**:系统实施需要相关部门的协调配合,但总体上不会对现有组织架构产生颠覆性影响。
#### 2.4.4 法律可行性
从法律合规角度看,本系统的实施不存在重大障碍:
1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。
2. **知识产权**:系统开发过程中使用的第三方库和组件均可采用开源或获得授权的方式,避免知识产权风险。
3. **合规性**:系统功能和流程设计可以确保符合相关法律法规和行业标准,确保合法合规运营。
## 3. 功能性需求
### 3.1 功能层次方框图
以下功能层次图展示了系统的整体功能架构:
```mermaid
flowchart TD
%% 用户交互层
subgraph "用户交互层"
direction LR
C["公众服务模块\n(问题上报)"]
H["个人中心模块\n(我的反馈/资料)"]
D["管理驾驶舱\n(数据决策)"]
end
%% 核心业务层
subgraph "核心业务层"
direction LR
AI["AI分析模块\n(内容审核)"]
E["任务管理模块\n(分配、流转、执行)"]
end
%% 应用支撑层
subgraph "应用支撑层"
direction LR
F["网格与地图模块\n(LBS & 寻路)"]
I["文件服务模块\n(附件存取)"]
end
%% 基础服务层
subgraph "基础服务层"
direction LR
B["用户与认证模块"]
G["系统管理模块\n(用户/权限)"]
J["日志审计模块"]
end
%% 定义关系
C -- "提交反馈" --> AI
AI -- "分析结果" --> E
C -- "附件" --> I
E -- "调用" --> F
E -- "任务附件" --> I
E -- "统计数据" --> D
H -- "查询个人数据" --> E
%% 基础服务支撑所有上层模块 (关系隐含)
G -- "管理" --> B
classDef userLayer fill:#d4f1f9,stroke:#05a8e5;
classDef coreLayer fill:#ffe6cc,stroke:#f7a128;
classDef appSupportLayer fill:#d5e8d4,stroke:#82b366;
classDef baseLayer fill:#e1d5e7,stroke:#9673a6;
class C,H,D userLayer;
class AI,E coreLayer;
class F,I appSupportLayer;
class B,G,J baseLayer;
```
# 需求定义文档(续)
### 3.2 模块功能概述
| 模块名称 | 功能概述 |
| :--- | :--- |
| **用户与认证模块** | 负责用户身份验证和权限控制,提供注册、登录、密码重置等功能,确保系统安全性。 |
| **反馈管理模块** | 处理公众提交的环境问题反馈,包括提交、审核、状态追踪和查询统计等功能。 |
| **任务管理模块** | 将审核通过的反馈转化为工作任务,管理任务的分配、执行和完成全流程。 |
| **网格与地图模块** | 提供地理空间的网格化管理,支持路径规划和地图可视化,辅助任务执行。 |
| **决策支持模块** | 通过数据分析和可视化,为管理层提供决策支持,帮助优化资源配置和治理策略。 |
| **系统管理模块** | 提供系统配置、用户管理、权限设置等基础功能,保障系统正常运行。 |
| **文件服务模块** | 处理系统中的文件上传、存储和访问,支持反馈和任务的附件管理。 |
| **日志审计模块** | 记录系统操作日志,支持安全审计和问题追溯,确保系统运行的可追溯性。 |
## 4. 整体业务流程
下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程:
```mermaid
flowchart TD
%% 定义样式
classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333
classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333
classDef worker fill:#d5e8d4,stroke:#82b366,color:#333
classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333
classDef decision fill:#f8cecc,stroke:#b85450,color:#333
classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px
%% 流程开始
A([开始]) --> B["[公众端] 发现环境问题"]
B --> C["[公众端] 提交反馈<br>(标题/描述/图片/位置)"]
%% 平台接收与AI处理
C --> D["[平台] 接收反馈<br>生成唯一事件ID"]
D --> E{"[平台] AI自动审核<br>分析内容/分类"}
%% AI审核分支
E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"]
F1 --> F2["[平台] 通知提交者"]
F2 --> Z1([结束])
E -- "需人工确认" --> G["[主管] 查看反馈详情<br>进行人工审核"]
%% 主管审核分支
G --> H{"[主管] 审核决定"}
H -- "驳回" --> I1["[主管] 填写驳回理由"]
I1 --> I2["[平台] 更新状态为REJECTED"]
I2 --> I3["[平台] 通知提交者"]
I3 --> Z2([结束])
%% 审核通过,创建任务
H -- "通过" --> J1["[主管] 确认反馈有效"]
J1 --> J2["[平台] 自动创建结构化任务"]
J2 --> J3["[平台] 更新反馈状态为PROCESSED"]
%% 任务分配
J3 --> K1{"[主管] 选择分配方式"}
K1 -- "手动分配" --> K2["[主管] 选择特定网格员"]
K1 -- "智能推荐" --> K3["[平台] 运行分配算法<br>考虑位置/负载/专长"]
K3 --> K4["[平台] 推荐最佳人选"]
K4 --> K2
K2 --> K5["[平台] 创建任务分配记录<br>更新任务状态为ASSIGNED"]
K5 --> K6["[平台] 通知网格员"]
%% 网格员处理
K6 --> L1["[网格员] 接收任务通知"]
L1 --> L2{"[网格员] 接受任务?"}
L2 -- "拒绝" --> K1
L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"]
L3 --> L4["[网格员] 查看任务详情<br>获取路径规划"]
L4 --> L5["[网格员] 前往现场处理"]
L5 --> L6["[网格员] 记录处理过程<br>上传证明材料"]
L6 --> L7["[网格员] 提交处理结果<br>更新状态为SUBMITTED"]
%% 主管审核结果
L7 --> M1["[主管] 审核处理结果"]
M1 --> M2{"[主管] 结果是否合格?"}
M2 -- "不合格" --> M3["[主管] 填写原因<br>要求重新处理"]
M3 --> L4
%% 完成流程
M2 -- "合格" --> N1["[主管] 确认任务完成"]
N1 --> N2["[平台] 更新任务状态为APPROVED"]
N2 --> N3["[平台] 更新反馈状态为CLOSED"]
N3 --> N4["[平台] 通知反馈提交者"]
N4 --> N5["[平台] 更新统计数据"]
N5 --> O["[决策层] 查看数据看板<br>分析环境趋势"]
O --> Z3([结束])
%% 为节点添加类别
class A,Z1,Z2,Z3 start_end
class B,C,F2,I3,N4 public
class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform
class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor
class L1,L2,L3,L4,L5,L6,L7 worker
class O decision
```
## 5. 功能需求详细描述
### 5.1 用户与认证模块需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| AUTH-01 | 用户注册 | 系统应允许新用户通过提供基本信息(姓名、手机号、邮箱、密码等)进行注册,并验证信息的有效性和唯一性。 | 高 |
| AUTH-02 | 用户登录 | 系统应支持用户通过邮箱/手机号和密码进行身份验证,登录成功后生成安全令牌用于后续请求。 | 高 |
| AUTH-03 | 密码重置 | 系统应提供安全的密码重置机制,包括通过邮箱或手机验证码验证身份。 | 中 |
| AUTH-04 | 权限控制 | 系统应基于用户角色(公众用户、网格员、主管、管理员、决策者)实施访问控制,确保用户只能访问其权限范围内的功能和数据。 | 高 |
| AUTH-05 | 会话管理 | 系统应维护和管理用户会话状态,包括会话创建、验证和超时处理。 | 中 |
| AUTH-06 | 个人资料管理 | 系统应允许用户查看和更新其个人资料,包括基本信息、联系方式和偏好设置。 | 低 |
| AUTH-07 | 安全日志 | 系统应记录所有关键安全事件(登录尝试、权限变更等),支持安全审计。 | 中 |
**用户与认证流程图**
```mermaid
graph TD
A[用户访问系统] --> B{是否已登录?}
B -- 否 --> C{是否有账号?}
B -- 是 --> D[访问系统功能]
C -- 否 --> E[注册新账号]
C -- 是 --> F[登录系统]
E --> G[填写注册信息]
G --> H[验证邮箱/手机]
H --> I[创建用户账号]
I --> F
F --> J{认证是否成功?}
J -- 否 --> K{是否忘记密码?}
J -- 是 --> L[生成安全令牌]
K -- 是 --> M[密码重置流程]
K -- 否 --> F
M --> N[验证身份]
N --> O[设置新密码]
O --> F
L --> P[加载用户权限]
P --> D
D --> Q[系统记录操作日志]
R[用户请求退出] --> S[清除会话信息]
S --> T[返回登录页面]
```
### 5.2 反馈管理模块需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| FEED-01 | 反馈提交 | 系统应允许公众用户提交环境问题反馈,包括问题描述、位置标记、污染类型分类和图片上传。 | 高 |
| FEED-02 | AI内容审核 | 系统应自动分析反馈内容,识别垃圾信息、重复提交,并进行初步分类和紧急程度评估。 | 中 |
| FEED-03 | 人工审核 | 系统应提供主管审核反馈的工作台,支持查看详情、批准或驳回反馈。 | 高 |
| FEED-04 | 状态追踪 | 系统应记录和展示反馈的全生命周期状态变更,允许用户查询处理进度。 | 高 |
| FEED-05 | 反馈查询 | 系统应支持按多种条件(状态、时间、区域、类型等)查询和筛选反馈列表。 | 中 |
| FEED-06 | 反馈统计 | 系统应生成反馈数据的统计分析,包括数量分布、处理效率等指标。 | 低 |
| FEED-07 | 通知提醒 | 系统应在反馈状态变更时通知相关用户,保持信息透明。 | 中 |
**反馈管理流程图**
```mermaid
graph TD
A[公众用户发现环境问题] --> B[打开反馈提交界面]
B --> C[填写问题描述]
C --> D[选择污染类型]
D --> E[评估严重程度]
E --> F[标记地理位置]
F --> G[上传现场照片]
G --> H[提交反馈]
H --> I[系统生成唯一事件ID]
I --> J[AI自动审核内容]
J --> K{AI审核结果}
K -- 明显无效 --> L[标记为AI_REJECTED]
K -- 需人工确认 --> M[进入主管审核队列]
L --> N[通知用户反馈被拒绝]
M --> O[主管查看反馈详情]
O --> P{审核决定}
P -- 驳回 --> Q[填写驳回理由]
P -- 通过 --> R[标记为有效反馈]
Q --> S[更新状态为REJECTED]
S --> T[通知用户反馈被驳回]
R --> U[创建关联任务]
U --> V[更新状态为PROCESSED]
W[用户查询反馈状态] --> X[系统展示处理进度]
Y[管理员查看统计数据] --> Z[系统生成反馈分析报表]
```
# 需求定义文档(续)
### 5.3 任务管理模块需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| TASK-01 | 任务创建 | 系统应支持从审核通过的反馈自动生成任务,也支持主管手动创建临时任务。 | 高 |
| TASK-02 | 智能分配 | 系统应基于多因素(网格员位置、当前负载、专业技能、历史表现)推荐最合适的处理人员。 | 中 |
| TASK-03 | 任务分配 | 系统应支持主管手动选择或确认系统推荐的网格员,并将任务分配给他们。 | 高 |
| TASK-04 | 任务通知 | 系统应在任务分配后立即通知相关网格员,并提供任务详情查看途径。 | 高 |
| TASK-05 | 任务执行跟踪 | 系统应记录任务的每个状态变更,支持网格员实时上报处理进度。 | 中 |
| TASK-06 | 路径规划 | 系统应为网格员提供从当前位置到任务地点的最优路径规划。 | 中 |
| TASK-07 | 结果提交 | 系统应支持网格员提交处理结果,包括文字描述和图片证明。 | 高 |
| TASK-08 | 结果审核 | 系统应支持主管审核网格员提交的处理结果,可以通过或驳回并提供反馈。 | 高 |
| TASK-09 | 任务查询 | 系统应支持按多种条件(状态、负责人、时间、区域等)查询和筛选任务列表。 | 中 |
| TASK-10 | 任务统计 | 系统应生成任务数据的统计分析,包括完成率、平均处理时长等绩效指标。 | 低 |
| TASK-11 | 任务看板 | 系统应提供直观的任务看板,展示不同状态任务的数量和分布。 | 低 |
**任务管理流程图**
```mermaid
graph TD
A[反馈通过审核] --> B[自动创建任务]
C[主管手动创建任务] --> D[任务创建完成]
B --> D
D --> E{选择分配方式}
E -- 手动分配 --> F[主管选择网格员]
E -- 智能推荐 --> G[系统推荐最佳人选]
G --> F
F --> H[分配任务给网格员]
H --> I[通知网格员]
I --> J{网格员接受?}
J -- 否 --> E
J -- 是 --> K[更新任务状态为进行中]
K --> L[网格员获取路径规划]
L --> M[网格员处理任务]
M --> N[提交处理结果]
N --> O[主管审核结果]
O --> P{结果合格?}
P -- 否 --> Q[填写原因]
Q --> L
P -- 是 --> R[更新任务状态为已完成]
R --> S[通知反馈提交者]
S --> T[更新统计数据]
```
### 5.4 网格与地图模块需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| GRID-01 | 网格定义 | 系统应支持管理员定义和维护城市网格系统,包括网格的坐标、属性和责任人分配。 | 中 |
| GRID-02 | 地图可视化 | 系统应在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。 | 中 |
| GRID-03 | 位置标记 | 系统应支持用户在地图上精确标记环境问题位置,并自动关联到对应网格。 | 高 |
| GRID-04 | A*寻路算法 | 系统应基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划。 | 中 |
| GRID-05 | 区域统计 | 系统应基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | 低 |
| GRID-06 | 网格查询 | 系统应支持查询特定区域内的网格定义、属性和历史问题记录。 | 低 |
| GRID-07 | 离线地图 | 系统应支持地图数据的离线缓存,保证在网络不稳定情况下的基本功能。 | 低 |
**网格与地图流程图**
```mermaid
graph TD
A[管理员定义网格系统] --> B[划分城市区域为网格单元]
B --> C[设置网格属性]
C --> D[分配网格责任人]
E[用户标记环境问题位置] --> F[系统关联到对应网格]
F --> G[存储地理位置信息]
H[网格员接收任务] --> I[查看任务位置]
I --> J[请求路径规划]
J --> K[A*算法计算最优路径]
K --> L[展示导航路线]
M[管理员查看统计数据] --> N[生成环境问题热力图]
N --> O[识别问题高发区域]
O --> P[调整资源分配策略]
```
### 5.5 决策支持模块需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| DECI-01 | 核心指标看板 | 系统应实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。 | 中 |
| DECI-02 | 多维度分析 | 系统应支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。 | 中 |
| DECI-03 | 热力图可视化 | 系统应在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。 | 中 |
| DECI-04 | 绩效评估 | 系统应对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。 | 低 |
| DECI-05 | 预警机制 | 系统应基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | 低 |
| DECI-06 | 报表导出 | 系统应支持将分析结果导出为多种格式PDF、Excel等便于进一步分析和汇报。 | 低 |
| DECI-07 | 个性化配置 | 系统应支持决策者个性化配置数据看板,满足不同管理者的关注点。 | 低 |
**决策支持流程图**
```mermaid
graph TD
A[系统收集业务数据] --> B[实时计算核心指标]
B --> C[展示核心指标看板]
D[决策者选择分析维度] --> E[系统执行多维度分析]
E --> F[生成趋势图表]
G[系统分析地理数据] --> H[生成环境问题热力图]
H --> I[标识高发区域]
J[系统收集网格员工作数据] --> K[计算绩效指标]
K --> L[生成绩效排名]
M[系统分析历史数据] --> N[识别异常趋势]
N --> O{是否达到预警阈值?}
O -- 是 --> P[触发预警通知]
O -- 否 --> Q[继续监控]
R[决策者查看分析结果] --> S[选择导出格式]
S --> T[导出分析报表]
```
## 6. 非功能性需求
### 6.1 性能需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| PERF-01 | 响应时间 | 系统的页面加载时间应不超过3秒API响应时间应不超过500毫秒不包括文件上传下载。 | 高 |
| PERF-02 | 并发用户 | 系统应能同时支持至少100个并发用户正常操作不出现明显延迟。 | 中 |
| PERF-03 | 数据处理量 | 系统应能处理每日至少1000条反馈和500个任务的创建、更新和查询操作。 | 中 |
| PERF-04 | 文件处理 | 系统应支持单个文件最大10MB总附件大小不超过50MB的上传和处理。 | 中 |
| PERF-05 | 地图渲染 | 地图界面应能在1秒内完成初始加载并在500毫秒内响应用户的缩放和平移操作。 | 中 |
### 6.2 可用性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| USAB-01 | 界面一致性 | 系统界面应保持风格一致,包括颜色方案、按钮位置、导航结构等,减少用户学习成本。 | 中 |
| USAB-02 | 错误处理 | 系统应提供清晰的错误提示和恢复建议,避免用户操作中断。 | 高 |
| USAB-03 | 帮助系统 | 系统应提供上下文相关的帮助信息和操作指南,支持用户自助解决问题。 | 低 |
| USAB-04 | 响应式设计 | 系统界面应适应不同设备PC、平板、手机的屏幕尺寸提供一致的用户体验。 | 高 |
| USAB-05 | 操作简化 | 核心功能的操作路径应尽量简化,减少用户点击次数和输入量。 | 中 |
### 6.3 安全性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| SECU-01 | 数据加密 | 敏感数据如用户密码必须加密存储传输过程中应使用HTTPS加密。 | 高 |
| SECU-02 | 访问控制 | 系统应实施严格的基于角色的访问控制,确保用户只能访问其权限范围内的数据和功能。 | 高 |
| SECU-03 | 输入验证 | 系统应对所有用户输入进行验证和过滤防止SQL注入、XSS等常见安全攻击。 | 高 |
| SECU-04 | 会话管理 | 系统应安全管理用户会话,包括会话创建、验证、超时和销毁,防止会话劫持。 | 中 |
| SECU-05 | 审计日志 | 系统应记录所有关键操作的审计日志,包括用户登录、数据修改、权限变更等。 | 中 |
# 需求定义文档(续)
### 6.4 可靠性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| RELI-01 | 系统可用性 | 系统应保证7x24小时的稳定运行计划内维护时间除外系统可用性应达到99.5%以上。 | 高 |
| RELI-02 | 数据备份 | 系统应定期(至少每日一次)对全部业务数据进行备份,并支持数据恢复。 | 高 |
| RELI-03 | 故障恢复 | 系统应具备故障检测和自动恢复机制,在出现异常情况时能够快速恢复正常运行。 | 中 |
| RELI-04 | 数据一致性 | 系统应确保在各种操作条件下(包括并发访问和异常中断)保持数据的一致性和完整性。 | 高 |
| RELI-05 | 容错能力 | 系统应能够处理常见的错误情况(如网络中断、数据异常),并提供适当的降级服务。 | 中 |
### 6.5 可维护性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| MAIN-01 | 模块化设计 | 系统应采用模块化设计,各功能模块之间松耦合,便于独立开发、测试和维护。 | 中 |
| MAIN-02 | 配置管理 | 系统应支持通过配置文件或管理界面调整系统参数,无需修改代码和重新部署。 | 中 |
| MAIN-03 | 日志记录 | 系统应记录详细的运行日志,包括错误信息、性能指标和关键操作,便于问题诊断。 | 高 |
| MAIN-04 | 版本升级 | 系统应支持平滑的版本升级机制,最小化升级对用户的影响。 | 低 |
| MAIN-05 | 代码规范 | 系统开发应遵循统一的编码规范和设计模式,提高代码可读性和可维护性。 | 中 |
### 6.6 可扩展性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| SCAL-01 | 水平扩展 | 系统架构应支持通过增加服务器节点进行水平扩展,以应对用户量和数据量的增长。 | 低 |
| SCAL-02 | 功能扩展 | 系统应支持在不影响现有功能的情况下,添加新的功能模块和业务流程。 | 中 |
| SCAL-03 | 接口开放 | 系统应提供标准化的API接口便于与其他系统集成和数据交换。 | 中 |
| SCAL-04 | 多租户支持 | 系统应具备支持多租户(如不同城市或区域)的潜力,能够在未来进行扩展。 | 低 |
| SCAL-05 | 数据量增长 | 系统应能够处理数据量的持续增长,性能不应随着数据量的增加而明显下降。 | 中 |
## 7. 数据需求
### 7.1 数据实体
系统需要管理以下核心数据实体:
1. **用户账户 (UserAccount)**:存储系统用户的基本信息、认证信息和角色权限。
2. **反馈 (Feedback)**:存储公众提交的环境问题反馈,包括描述、位置、图片等信息。
3. **任务 (Task)**:存储从反馈生成的工作任务,包括任务详情、状态和处理记录。
4. **网格 (Grid)**:存储城市区域的网格化管理数据,包括网格坐标、属性和责任人。
5. **任务分配 (Assignment)**:存储任务与网格员之间的分配关系,包括分配时间、状态等。
6. **附件 (Attachment)**:存储反馈和任务相关的图片、文档等附件文件。
7. **操作日志 (OperationLog)**:存储系统中的关键操作记录,用于审计和问题追溯。
### 7.2 数据量估计
基于系统预期使用规模,估计以下数据量:
1. **用户账户**预计1000个用户账户包括公众用户、网格员、主管、管理员和决策者。
2. **反馈**预计每日100-200条新增反馈年增长约5万条。
3. **任务**预计每日50-100个新增任务年增长约2.5万个。
4. **网格**预计1000-5000个网格单元根据城市规模和网格粒度而定。
5. **附件**预计每条反馈平均2张图片每个任务平均2张图片年增长约15万个文件。
6. **操作日志**预计每日1万条日志记录主要用于短期审计长期可归档。
### 7.3 数据保留策略
1. **核心业务数据**用户、反馈、任务、网格长期保留至少保留3年。
2. **附件文件**保留1年可根据存储容量考虑适当延长或缩短。
3. **操作日志**
- 详细日志保留30天
- 关键审计日志保留1年
- 统计汇总数据长期保留
### 7.4 数据备份要求
1. **备份频率**
- 核心业务数据:每日全量备份,每小时增量备份
- 附件文件:每周全量备份,每日增量备份
- 配置数据:每次变更后备份
2. **备份保留**
- 每日备份保留7天
- 每周备份保留1个月
- 每月备份保留6个月
- 每年备份永久保留
3. **恢复能力**系统应支持将数据恢复到任意备份点恢复时间目标RTO不超过4小时。
## 8. 结论
### 8.1 需求优先级总结
基于业务重要性和实现复杂度,系统需求按以下优先级排序:
1. **最高优先级**
- 用户认证和权限控制
- 反馈提交和审核
- 任务创建和分配
- 数据安全和完整性
2. **高优先级**
- 任务执行和结果审核
- 地图标记和路径规划
- 系统性能和可用性
- 用户界面和体验
3. **中等优先级**
- AI内容审核
- 决策支持和数据分析
- 通知和提醒功能
- 系统可维护性
4. **低优先级**
- 高级统计和报表
- 个性化配置
- 系统扩展性
- 辅助功能和优化
### 8.2 实施建议
为确保系统成功实施,建议采取以下策略:
1. **分阶段实施**
- 第一阶段:核心功能(用户管理、反馈、任务)
- 第二阶段支撑功能地图、AI审核
- 第三阶段:高级功能(决策支持、报表)
2. **持续迭代**:采用敏捷开发方法,快速交付可用版本,根据用户反馈持续改进。
3. **用户参与**:在开发过程中邀请各类用户参与测试和评估,确保系统满足实际需求。
4. **技术选型**:选择成熟稳定的技术栈,平衡创新性和可靠性,降低开发和维护风险。
5. **数据治理**:建立完善的数据管理和治理机制,确保数据质量和安全。
### 8.3 预期效益
成功实施环境监测系统预期将带来以下效益:
1. **业务效益**
- 环境问题处理效率提升50%以上
- 问题解决质量显著提高
- 资源利用更加合理高效
2. **管理效益**
- 实现环境问题全流程可视化管理
- 提供科学决策的数据支持
- 优化工作流程和资源配置
3. **社会效益**
- 提升公众参与环境治理的积极性
- 增强政府工作透明度和公信力
- 改善城市环境质量和居民生活满意度
通过环境监测系统的建设和应用,将有效解决当前环境问题管理中的痛点,构建起连接公众、管理部门和执行人员的桥梁,实现环境问题的快速发现、高效处理和全程监督,为城市环境治理提供有力支撑。

View File

@@ -0,0 +1,166 @@
# 需求定义文档
## 1. 项目介绍
### 1.1 项目背景
环境监测系统EMS是为解决城市环境问题而设计的综合性管理平台。随着城市化进程加速环境污染问题日益凸显传统的环境问题上报和处理机制存在以下痛点
- **流程繁琐**:公众发现环境问题后,需要通过多个渠道和部门层层上报,处理流程不透明
- **响应缓慢**:从问题发现到最终解决,往往需要经历漫长的等待时间
- **缺乏透明度**:公众难以了解问题处理进度,无法有效监督
- **资源分配不合理**:缺乏科学的任务分配机制,导致人力资源利用不均衡
本系统旨在通过数字化手段,构建一个连接公众、管理部门和执行人员的环境监测与治理平台,实现环境问题的快速发现、高效处理和全程监督。
### 1.2 项目目标
1. **建立闭环管理机制**:构建从问题发现、上报、审核、分配、处理到结果反馈的完整闭环流程,确保每个环境问题都能得到妥善解决。
2. **提高处理效率**:通过流程优化和智能算法,缩短环境问题从发现到解决的时间,提高环境治理效率。
3. **增强公众参与**:为公众提供便捷的问题上报渠道,增强公众参与环境治理的积极性和获得感。
4. **辅助决策分析**:通过数据可视化和多维度分析,为管理层提供决策支持,优化资源配置和治理策略。
5. **提升治理透明度**:实现环境问题处理全过程可追踪、可监督,增强政府工作透明度和公信力。
## 2. 系统分析
### 2.1 业务痛点分析
1. **信息孤岛**:环境问题信息分散在不同部门和系统中,缺乏统一管理和共享机制。
2. **流程断裂**:传统环境问题处理流程存在多个环节,各环节之间衔接不畅,容易导致问题处理延误或遗漏。
3. **资源分配不均**:缺乏科学的任务分配机制,导致人力资源利用不均衡,部分区域问题积压严重。
4. **监督机制不足**:公众难以了解问题处理进度和结果,缺乏有效的监督渠道。
5. **数据分析不足**:未能充分利用环境问题数据进行趋势分析和预测,难以支持科学决策。
### 2.2 用户角色分析
环境监测系统涉及五类主要用户角色,每个角色在系统中承担不同的职责:
1. **公众用户**:系统的信息输入端,负责发现和上报环境问题,是系统的主要服务对象。
- 需求:简单便捷的问题上报方式、透明的处理进度查询
- 痛点:传统上报渠道繁琐、反馈周期长、处理结果不透明
2. **网格员**:系统的执行端,负责接收任务并前往现场处理环境问题,是系统的核心操作人员。
- 需求:清晰的任务指派、便捷的结果上报、高效的路径规划
- 痛点:任务分配不合理、工作量分布不均、缺乏高效导航
3. **主管**:系统的管理端,负责审核反馈、分配任务、审核结果,是系统的关键决策者。
- 需求:高效的任务管理、智能的人员调配、直观的进度监控
- 痛点:人工分配任务效率低、缺乏全局视角、绩效评估困难
4. **管理员**:系统的维护端,负责用户管理、权限设置、系统配置等基础支撑工作。
- 需求:灵活的权限配置、完善的日志审计、便捷的系统维护
- 痛点:账户管理繁琐、权限控制粗放、系统维护成本高
5. **决策者**:系统的战略端,通过分析系统生成的统计数据和趋势图表,制定环境管理策略和资源分配决策,是系统的最终受益者之一。
- 需求:多维度的数据分析、直观的可视化展示、科学的决策支持
- 痛点:数据获取困难、分析维度单一、缺乏预测能力
### 2.3 用例分析
#### 2.3.1 用例图
以下用例图展示了系统中各角色可以执行的主要操作:
```mermaid
graph TD
%% 定义角色
PublicUser["公众用户"]
GridWorker["网格员"]
Supervisor["主管"]
Admin["管理员"]
DecisionMaker["决策者"]
%% 定义用例
UC1["注册与登录"]
UC2["提交环境问题反馈"]
UC3["查看反馈处理进度"]
UC4["接收任务通知"]
UC5["执行任务"]
UC6["提交处理结果"]
UC7["审核反馈内容"]
UC8["分配任务"]
UC9["审核处理结果"]
UC10["查看统计数据"]
UC11["管理用户账户"]
UC12["配置系统参数"]
UC13["查看决策仪表盘"]
UC14["生成分析报告"]
%% 建立关系
PublicUser --> UC1
PublicUser --> UC2
PublicUser --> UC3
GridWorker --> UC1
GridWorker --> UC4
GridWorker --> UC5
GridWorker --> UC6
Supervisor --> UC1
Supervisor --> UC7
Supervisor --> UC8
Supervisor --> UC9
Supervisor --> UC10
Admin --> UC1
Admin --> UC10
Admin --> UC11
Admin --> UC12
DecisionMaker --> UC1
DecisionMaker --> UC10
DecisionMaker --> UC13
DecisionMaker --> UC14
%% 设置样式
classDef actor fill:#f9f,stroke:#333,stroke-width:2px
classDef usecase fill:#ccf,stroke:#33f,stroke-width:1px
class PublicUser,GridWorker,Supervisor,Admin,DecisionMaker actor
class UC1,UC2,UC3,UC4,UC5,UC6,UC7,UC8,UC9,UC10,UC11,UC12,UC13,UC14 usecase
```
#### 2.3.2 活动图:反馈提交与处理流程
以下活动图展示了从公众发现环境问题到反馈处理完成的完整业务流程:
```mermaid
stateDiagram-v2
[*] --> 发现环境问题
发现环境问题 --> 填写反馈表单
填写反馈表单 --> 上传图片
上传图片 --> 标记位置
标记位置 --> 提交反馈
提交反馈 --> AI自动审核
state AI自动审核 {
[*] --> 内容分析
内容分析 --> 垃圾信息检测
垃圾信息检测 --> 分类与评级
分类与评级 --> [*]
}
AI自动审核 --> 判断AI审核结果
判断AI审核结果 --> 明显无效: AI拒绝
判断AI审核结果 --> 需人工确认: 需确认
明显无效 --> 标记为AI_REJECTED
标记为AI_REJECTED --> 通知提交者
通知提交者 --> [*]
需人工确认 --> 主管人工审核
主管人工审核 --> 判断审核结果
判断审核结果 --> 驳回: 不通过
判断审核结果 --> 通过: 通过
驳回 --> 填写驳回理由
填写驳回理由 --> 更新状态为REJECTED
更新状态为REJECTED --> 通知提交者反馈被驳回
通知提交者反馈被驳回 --> [*]
通过 --> 创建任务
创建任务 --> 更新反馈状态为PROCESSED
更新反馈状态为PROCESSED --> 任务分配流程
任务分配流程 --> [*]
```

View File

@@ -0,0 +1,197 @@
# 需求定义文档(续)
### 3.2 模块功能概述
| 模块名称 | 功能概述 |
| :--- | :--- |
| **用户与认证模块** | 负责用户身份验证和权限控制,提供注册、登录、密码重置等功能,确保系统安全性。 |
| **反馈管理模块** | 处理公众提交的环境问题反馈,包括提交、审核、状态追踪和查询统计等功能。 |
| **任务管理模块** | 将审核通过的反馈转化为工作任务,管理任务的分配、执行和完成全流程。 |
| **网格与地图模块** | 提供地理空间的网格化管理,支持路径规划和地图可视化,辅助任务执行。 |
| **决策支持模块** | 通过数据分析和可视化,为管理层提供决策支持,帮助优化资源配置和治理策略。 |
| **系统管理模块** | 提供系统配置、用户管理、权限设置等基础功能,保障系统正常运行。 |
| **文件服务模块** | 处理系统中的文件上传、存储和访问,支持反馈和任务的附件管理。 |
| **日志审计模块** | 记录系统操作日志,支持安全审计和问题追溯,确保系统运行的可追溯性。 |
## 4. 整体业务流程
下图描述了从公众发现问题、上报、到平台内部流转、处理、并最终反馈结果的完整闭环业务流程:
```mermaid
flowchart TD
%% 定义样式
classDef public fill:#d4f1f9,stroke:#05a8e5,color:#333
classDef platform fill:#ffe6cc,stroke:#f7a128,color:#333
classDef worker fill:#d5e8d4,stroke:#82b366,color:#333
classDef supervisor fill:#e1d5e7,stroke:#9673a6,color:#333
classDef decision fill:#f8cecc,stroke:#b85450,color:#333
classDef start_end fill:#f5f5f5,stroke:#666666,color:#333,stroke-width:2px
%% 流程开始
A([开始]) --> B["[公众端] 发现环境问题"]
B --> C["[公众端] 提交反馈<br>(标题/描述/图片/位置)"]
%% 平台接收与AI处理
C --> D["[平台] 接收反馈<br>生成唯一事件ID"]
D --> E{"[平台] AI自动审核<br>分析内容/分类"}
%% AI审核分支
E -- "明显无效" --> F1["[平台] 标记为AI_REJECTED"]
F1 --> F2["[平台] 通知提交者"]
F2 --> Z1([结束])
E -- "需人工确认" --> G["[主管] 查看反馈详情<br>进行人工审核"]
%% 主管审核分支
G --> H{"[主管] 审核决定"}
H -- "驳回" --> I1["[主管] 填写驳回理由"]
I1 --> I2["[平台] 更新状态为REJECTED"]
I2 --> I3["[平台] 通知提交者"]
I3 --> Z2([结束])
%% 审核通过,创建任务
H -- "通过" --> J1["[主管] 确认反馈有效"]
J1 --> J2["[平台] 自动创建结构化任务"]
J2 --> J3["[平台] 更新反馈状态为PROCESSED"]
%% 任务分配
J3 --> K1{"[主管] 选择分配方式"}
K1 -- "手动分配" --> K2["[主管] 选择特定网格员"]
K1 -- "智能推荐" --> K3["[平台] 运行分配算法<br>考虑位置/负载/专长"]
K3 --> K4["[平台] 推荐最佳人选"]
K4 --> K2
K2 --> K5["[平台] 创建任务分配记录<br>更新任务状态为ASSIGNED"]
K5 --> K6["[平台] 通知网格员"]
%% 网格员处理
K6 --> L1["[网格员] 接收任务通知"]
L1 --> L2{"[网格员] 接受任务?"}
L2 -- "拒绝" --> K1
L2 -- "接受" --> L3["[网格员] 更新任务状态为IN_PROGRESS"]
L3 --> L4["[网格员] 查看任务详情<br>获取路径规划"]
L4 --> L5["[网格员] 前往现场处理"]
L5 --> L6["[网格员] 记录处理过程<br>上传证明材料"]
L6 --> L7["[网格员] 提交处理结果<br>更新状态为SUBMITTED"]
%% 主管审核结果
L7 --> M1["[主管] 审核处理结果"]
M1 --> M2{"[主管] 结果是否合格?"}
M2 -- "不合格" --> M3["[主管] 填写原因<br>要求重新处理"]
M3 --> L4
%% 完成流程
M2 -- "合格" --> N1["[主管] 确认任务完成"]
N1 --> N2["[平台] 更新任务状态为APPROVED"]
N2 --> N3["[平台] 更新反馈状态为CLOSED"]
N3 --> N4["[平台] 通知反馈提交者"]
N4 --> N5["[平台] 更新统计数据"]
N5 --> O["[决策层] 查看数据看板<br>分析环境趋势"]
O --> Z3([结束])
%% 为节点添加类别
class A,Z1,Z2,Z3 start_end
class B,C,F2,I3,N4 public
class D,E,F1,I2,J2,J3,K3,K4,K5,K6,N2,N3,N5 platform
class G,H,I1,J1,K1,K2,M1,M2,M3,N1 supervisor
class L1,L2,L3,L4,L5,L6,L7 worker
class O decision
```
## 5. 功能需求详细描述
### 5.1 用户与认证模块需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| AUTH-01 | 用户注册 | 系统应允许新用户通过提供基本信息(姓名、手机号、邮箱、密码等)进行注册,并验证信息的有效性和唯一性。 | 高 |
| AUTH-02 | 用户登录 | 系统应支持用户通过邮箱/手机号和密码进行身份验证,登录成功后生成安全令牌用于后续请求。 | 高 |
| AUTH-03 | 密码重置 | 系统应提供安全的密码重置机制,包括通过邮箱或手机验证码验证身份。 | 中 |
| AUTH-04 | 权限控制 | 系统应基于用户角色(公众用户、网格员、主管、管理员、决策者)实施访问控制,确保用户只能访问其权限范围内的功能和数据。 | 高 |
| AUTH-05 | 会话管理 | 系统应维护和管理用户会话状态,包括会话创建、验证和超时处理。 | 中 |
| AUTH-06 | 个人资料管理 | 系统应允许用户查看和更新其个人资料,包括基本信息、联系方式和偏好设置。 | 低 |
| AUTH-07 | 安全日志 | 系统应记录所有关键安全事件(登录尝试、权限变更等),支持安全审计。 | 中 |
**用户与认证流程图**
```mermaid
graph TD
A[用户访问系统] --> B{是否已登录?}
B -- 否 --> C{是否有账号?}
B -- 是 --> D[访问系统功能]
C -- 否 --> E[注册新账号]
C -- 是 --> F[登录系统]
E --> G[填写注册信息]
G --> H[验证邮箱/手机]
H --> I[创建用户账号]
I --> F
F --> J{认证是否成功?}
J -- 否 --> K{是否忘记密码?}
J -- 是 --> L[生成安全令牌]
K -- 是 --> M[密码重置流程]
K -- 否 --> F
M --> N[验证身份]
N --> O[设置新密码]
O --> F
L --> P[加载用户权限]
P --> D
D --> Q[系统记录操作日志]
R[用户请求退出] --> S[清除会话信息]
S --> T[返回登录页面]
```
### 5.2 反馈管理模块需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| FEED-01 | 反馈提交 | 系统应允许公众用户提交环境问题反馈,包括问题描述、位置标记、污染类型分类和图片上传。 | 高 |
| FEED-02 | AI内容审核 | 系统应自动分析反馈内容,识别垃圾信息、重复提交,并进行初步分类和紧急程度评估。 | 中 |
| FEED-03 | 人工审核 | 系统应提供主管审核反馈的工作台,支持查看详情、批准或驳回反馈。 | 高 |
| FEED-04 | 状态追踪 | 系统应记录和展示反馈的全生命周期状态变更,允许用户查询处理进度。 | 高 |
| FEED-05 | 反馈查询 | 系统应支持按多种条件(状态、时间、区域、类型等)查询和筛选反馈列表。 | 中 |
| FEED-06 | 反馈统计 | 系统应生成反馈数据的统计分析,包括数量分布、处理效率等指标。 | 低 |
| FEED-07 | 通知提醒 | 系统应在反馈状态变更时通知相关用户,保持信息透明。 | 中 |
**反馈管理流程图**
```mermaid
graph TD
A[公众用户发现环境问题] --> B[打开反馈提交界面]
B --> C[填写问题描述]
C --> D[选择污染类型]
D --> E[评估严重程度]
E --> F[标记地理位置]
F --> G[上传现场照片]
G --> H[提交反馈]
H --> I[系统生成唯一事件ID]
I --> J[AI自动审核内容]
J --> K{AI审核结果}
K -- 明显无效 --> L[标记为AI_REJECTED]
K -- 需人工确认 --> M[进入主管审核队列]
L --> N[通知用户反馈被拒绝]
M --> O[主管查看反馈详情]
O --> P{审核决定}
P -- 驳回 --> Q[填写驳回理由]
P -- 通过 --> R[标记为有效反馈]
Q --> S[更新状态为REJECTED]
S --> T[通知用户反馈被驳回]
R --> U[创建关联任务]
U --> V[更新状态为PROCESSED]
W[用户查询反馈状态] --> X[系统展示处理进度]
Y[管理员查看统计数据] --> Z[系统生成反馈分析报表]
```

View File

@@ -0,0 +1,180 @@
# 需求定义文档(续)
#### 2.3.3 活动图:任务分配与执行流程
以下活动图展示了从任务创建到完成的完整业务流程:
```mermaid
stateDiagram-v2
[*] --> 任务创建完成
任务创建完成 --> 选择分配方式
state 选择分配方式 {
[*] --> 手动分配
[*] --> 智能推荐
智能推荐 --> 运行分配算法
运行分配算法 --> 推荐最佳人选
推荐最佳人选 --> 确认人选
手动分配 --> 选择特定网格员
选择特定网格员 --> 确认人选
确认人选 --> [*]
}
选择分配方式 --> 创建任务分配记录
创建任务分配记录 --> 更新任务状态为ASSIGNED
更新任务状态为ASSIGNED --> 通知网格员
通知网格员 --> 网格员接收通知
网格员接收通知 --> 判断是否接受
判断是否接受 --> 拒绝: 拒绝
判断是否接受 --> 接受: 接受
拒绝 --> 选择分配方式
接受 --> 更新状态为IN_PROGRESS
更新状态为IN_PROGRESS --> 获取路径规划
获取路径规划 --> 前往现场处理
前往现场处理 --> 记录处理过程
记录处理过程 --> 上传处理结果
上传处理结果 --> 提交处理结果
提交处理结果 --> 更新状态为SUBMITTED
更新状态为SUBMITTED --> 主管审核结果
主管审核结果 --> 判断结果是否合格
判断结果是否合格 --> 不合格: 不合格
判断结果是否合格 --> 合格: 合格
不合格 --> 填写原因要求重新处理
填写原因要求重新处理 --> 获取路径规划
合格 --> 确认任务完成
确认任务完成 --> 更新任务状态为APPROVED
更新任务状态为APPROVED --> 更新反馈状态为CLOSED
更新反馈状态为CLOSED --> 通知反馈提交者
通知反馈提交者 --> 更新统计数据
更新统计数据 --> [*]
```
### 2.4 系统可行性分析
#### 2.4.1 技术可行性
本系统的技术实现是可行的,主要基于以下分析:
1. **前端技术**市场上已有成熟的前端框架和组件库可以快速开发出美观、响应式的Web应用满足不同设备的访问需求。
2. **后端技术**:现有的后端框架具备高性能、高并发处理能力,能够满足系统的稳定性和扩展性需求。
3. **地图服务**可以集成成熟的地图API实现地理位置标记、路径规划等功能无需从零开发。
4. **AI技术**:可以利用现有的自然语言处理和图像识别技术,实现对反馈内容的智能分析和审核。
5. **数据存储**:可采用多种数据存储方案,根据系统规模和性能需求灵活选择。
#### 2.4.2 经济可行性
从经济角度看,本系统的开发和运营是可行的:
1. **开发成本**:可以采用主流开源框架和技术栈,降低开发成本和技术门槛。
2. **维护成本**:通过模块化设计和完善的文档,可以降低后期维护和升级成本。
3. **投资回报**
- 提高环境问题处理效率,减少人力资源浪费
- 降低环境问题带来的经济损失
- 提升城市环境质量,间接促进经济发展
- 长期来看具有良好的投资回报
4. **社会效益**:改善城市环境质量,提升居民生活满意度,产生显著的社会效益。
#### 2.4.3 操作可行性
从操作角度看,本系统具有良好的可行性:
1. **用户接受度**
- 系统界面设计简洁直观,符合用户习惯
- 操作流程简化,降低学习成本
- 移动端支持,满足随时随地使用需求
2. **培训需求**:系统操作简单,只需简单培训即可上手,降低推广和应用门槛。
3. **业务适应性**:系统流程设计符合环境问题处理的实际业务需求,能够无缝融入现有工作流程。
4. **组织支持**:系统实施需要相关部门的协调配合,但总体上不会对现有组织架构产生颠覆性影响。
#### 2.4.4 法律可行性
从法律合规角度看,本系统的实施不存在重大障碍:
1. **数据隐私**:系统设计符合数据保护法规要求,对用户隐私数据进行加密存储和严格权限控制。
2. **知识产权**:系统开发过程中使用的第三方库和组件均可采用开源或获得授权的方式,避免知识产权风险。
3. **合规性**:系统功能和流程设计可以确保符合相关法律法规和行业标准,确保合法合规运营。
## 3. 功能性需求
### 3.1 功能层次方框图
以下功能层次图展示了系统的整体功能架构:
```mermaid
flowchart TD
%% 用户交互层
subgraph "用户交互层"
direction LR
C["公众服务模块\n(问题上报)"]
H["个人中心模块\n(我的反馈/资料)"]
D["管理驾驶舱\n(数据决策)"]
end
%% 核心业务层
subgraph "核心业务层"
direction LR
AI["AI分析模块\n(内容审核)"]
E["任务管理模块\n(分配、流转、执行)"]
end
%% 应用支撑层
subgraph "应用支撑层"
direction LR
F["网格与地图模块\n(LBS & 寻路)"]
I["文件服务模块\n(附件存取)"]
end
%% 基础服务层
subgraph "基础服务层"
direction LR
B["用户与认证模块"]
G["系统管理模块\n(用户/权限)"]
J["日志审计模块"]
end
%% 定义关系
C -- "提交反馈" --> AI
AI -- "分析结果" --> E
C -- "附件" --> I
E -- "调用" --> F
E -- "任务附件" --> I
E -- "统计数据" --> D
H -- "查询个人数据" --> E
%% 基础服务支撑所有上层模块 (关系隐含)
G -- "管理" --> B
classDef userLayer fill:#d4f1f9,stroke:#05a8e5;
classDef coreLayer fill:#ffe6cc,stroke:#f7a128;
classDef appSupportLayer fill:#d5e8d4,stroke:#82b366;
classDef baseLayer fill:#e1d5e7,stroke:#9673a6;
class C,H,D userLayer;
class AI,E coreLayer;
class F,I appSupportLayer;
class B,G,J baseLayer;
```

View File

@@ -0,0 +1,148 @@
# 需求定义文档(续)
### 6.4 可靠性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| RELI-01 | 系统可用性 | 系统应保证7x24小时的稳定运行计划内维护时间除外系统可用性应达到99.5%以上。 | 高 |
| RELI-02 | 数据备份 | 系统应定期(至少每日一次)对全部业务数据进行备份,并支持数据恢复。 | 高 |
| RELI-03 | 故障恢复 | 系统应具备故障检测和自动恢复机制,在出现异常情况时能够快速恢复正常运行。 | 中 |
| RELI-04 | 数据一致性 | 系统应确保在各种操作条件下(包括并发访问和异常中断)保持数据的一致性和完整性。 | 高 |
| RELI-05 | 容错能力 | 系统应能够处理常见的错误情况(如网络中断、数据异常),并提供适当的降级服务。 | 中 |
### 6.5 可维护性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| MAIN-01 | 模块化设计 | 系统应采用模块化设计,各功能模块之间松耦合,便于独立开发、测试和维护。 | 中 |
| MAIN-02 | 配置管理 | 系统应支持通过配置文件或管理界面调整系统参数,无需修改代码和重新部署。 | 中 |
| MAIN-03 | 日志记录 | 系统应记录详细的运行日志,包括错误信息、性能指标和关键操作,便于问题诊断。 | 高 |
| MAIN-04 | 版本升级 | 系统应支持平滑的版本升级机制,最小化升级对用户的影响。 | 低 |
| MAIN-05 | 代码规范 | 系统开发应遵循统一的编码规范和设计模式,提高代码可读性和可维护性。 | 中 |
### 6.6 可扩展性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| SCAL-01 | 水平扩展 | 系统架构应支持通过增加服务器节点进行水平扩展,以应对用户量和数据量的增长。 | 低 |
| SCAL-02 | 功能扩展 | 系统应支持在不影响现有功能的情况下,添加新的功能模块和业务流程。 | 中 |
| SCAL-03 | 接口开放 | 系统应提供标准化的API接口便于与其他系统集成和数据交换。 | 中 |
| SCAL-04 | 多租户支持 | 系统应具备支持多租户(如不同城市或区域)的潜力,能够在未来进行扩展。 | 低 |
| SCAL-05 | 数据量增长 | 系统应能够处理数据量的持续增长,性能不应随着数据量的增加而明显下降。 | 中 |
## 7. 数据需求
### 7.1 数据实体
系统需要管理以下核心数据实体:
1. **用户账户 (UserAccount)**:存储系统用户的基本信息、认证信息和角色权限。
2. **反馈 (Feedback)**:存储公众提交的环境问题反馈,包括描述、位置、图片等信息。
3. **任务 (Task)**:存储从反馈生成的工作任务,包括任务详情、状态和处理记录。
4. **网格 (Grid)**:存储城市区域的网格化管理数据,包括网格坐标、属性和责任人。
5. **任务分配 (Assignment)**:存储任务与网格员之间的分配关系,包括分配时间、状态等。
6. **附件 (Attachment)**:存储反馈和任务相关的图片、文档等附件文件。
7. **操作日志 (OperationLog)**:存储系统中的关键操作记录,用于审计和问题追溯。
### 7.2 数据量估计
基于系统预期使用规模,估计以下数据量:
1. **用户账户**预计1000个用户账户包括公众用户、网格员、主管、管理员和决策者。
2. **反馈**预计每日100-200条新增反馈年增长约5万条。
3. **任务**预计每日50-100个新增任务年增长约2.5万个。
4. **网格**预计1000-5000个网格单元根据城市规模和网格粒度而定。
5. **附件**预计每条反馈平均2张图片每个任务平均2张图片年增长约15万个文件。
6. **操作日志**预计每日1万条日志记录主要用于短期审计长期可归档。
### 7.3 数据保留策略
1. **核心业务数据**用户、反馈、任务、网格长期保留至少保留3年。
2. **附件文件**保留1年可根据存储容量考虑适当延长或缩短。
3. **操作日志**
- 详细日志保留30天
- 关键审计日志保留1年
- 统计汇总数据长期保留
### 7.4 数据备份要求
1. **备份频率**
- 核心业务数据:每日全量备份,每小时增量备份
- 附件文件:每周全量备份,每日增量备份
- 配置数据:每次变更后备份
2. **备份保留**
- 每日备份保留7天
- 每周备份保留1个月
- 每月备份保留6个月
- 每年备份永久保留
3. **恢复能力**系统应支持将数据恢复到任意备份点恢复时间目标RTO不超过4小时。
## 8. 结论
### 8.1 需求优先级总结
基于业务重要性和实现复杂度,系统需求按以下优先级排序:
1. **最高优先级**
- 用户认证和权限控制
- 反馈提交和审核
- 任务创建和分配
- 数据安全和完整性
2. **高优先级**
- 任务执行和结果审核
- 地图标记和路径规划
- 系统性能和可用性
- 用户界面和体验
3. **中等优先级**
- AI内容审核
- 决策支持和数据分析
- 通知和提醒功能
- 系统可维护性
4. **低优先级**
- 高级统计和报表
- 个性化配置
- 系统扩展性
- 辅助功能和优化
### 8.2 实施建议
为确保系统成功实施,建议采取以下策略:
1. **分阶段实施**
- 第一阶段:核心功能(用户管理、反馈、任务)
- 第二阶段支撑功能地图、AI审核
- 第三阶段:高级功能(决策支持、报表)
2. **持续迭代**:采用敏捷开发方法,快速交付可用版本,根据用户反馈持续改进。
3. **用户参与**:在开发过程中邀请各类用户参与测试和评估,确保系统满足实际需求。
4. **技术选型**:选择成熟稳定的技术栈,平衡创新性和可靠性,降低开发和维护风险。
5. **数据治理**:建立完善的数据管理和治理机制,确保数据质量和安全。
### 8.3 预期效益
成功实施环境监测系统预期将带来以下效益:
1. **业务效益**
- 环境问题处理效率提升50%以上
- 问题解决质量显著提高
- 资源利用更加合理高效
2. **管理效益**
- 实现环境问题全流程可视化管理
- 提供科学决策的数据支持
- 优化工作流程和资源配置
3. **社会效益**
- 提升公众参与环境治理的积极性
- 增强政府工作透明度和公信力
- 改善城市环境质量和居民生活满意度
通过环境监测系统的建设和应用,将有效解决当前环境问题管理中的痛点,构建起连接公众、管理部门和执行人员的桥梁,实现环境问题的快速发现、高效处理和全程监督,为城市环境治理提供有力支撑。

View File

@@ -0,0 +1,147 @@
# 需求定义文档(续)
### 5.3 任务管理模块需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| TASK-01 | 任务创建 | 系统应支持从审核通过的反馈自动生成任务,也支持主管手动创建临时任务。 | 高 |
| TASK-02 | 智能分配 | 系统应基于多因素(网格员位置、当前负载、专业技能、历史表现)推荐最合适的处理人员。 | 中 |
| TASK-03 | 任务分配 | 系统应支持主管手动选择或确认系统推荐的网格员,并将任务分配给他们。 | 高 |
| TASK-04 | 任务通知 | 系统应在任务分配后立即通知相关网格员,并提供任务详情查看途径。 | 高 |
| TASK-05 | 任务执行跟踪 | 系统应记录任务的每个状态变更,支持网格员实时上报处理进度。 | 中 |
| TASK-06 | 路径规划 | 系统应为网格员提供从当前位置到任务地点的最优路径规划。 | 中 |
| TASK-07 | 结果提交 | 系统应支持网格员提交处理结果,包括文字描述和图片证明。 | 高 |
| TASK-08 | 结果审核 | 系统应支持主管审核网格员提交的处理结果,可以通过或驳回并提供反馈。 | 高 |
| TASK-09 | 任务查询 | 系统应支持按多种条件(状态、负责人、时间、区域等)查询和筛选任务列表。 | 中 |
| TASK-10 | 任务统计 | 系统应生成任务数据的统计分析,包括完成率、平均处理时长等绩效指标。 | 低 |
| TASK-11 | 任务看板 | 系统应提供直观的任务看板,展示不同状态任务的数量和分布。 | 低 |
**任务管理流程图**
```mermaid
graph TD
A[反馈通过审核] --> B[自动创建任务]
C[主管手动创建任务] --> D[任务创建完成]
B --> D
D --> E{选择分配方式}
E -- 手动分配 --> F[主管选择网格员]
E -- 智能推荐 --> G[系统推荐最佳人选]
G --> F
F --> H[分配任务给网格员]
H --> I[通知网格员]
I --> J{网格员接受?}
J -- 否 --> E
J -- 是 --> K[更新任务状态为进行中]
K --> L[网格员获取路径规划]
L --> M[网格员处理任务]
M --> N[提交处理结果]
N --> O[主管审核结果]
O --> P{结果合格?}
P -- 否 --> Q[填写原因]
Q --> L
P -- 是 --> R[更新任务状态为已完成]
R --> S[通知反馈提交者]
S --> T[更新统计数据]
```
### 5.4 网格与地图模块需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| GRID-01 | 网格定义 | 系统应支持管理员定义和维护城市网格系统,包括网格的坐标、属性和责任人分配。 | 中 |
| GRID-02 | 地图可视化 | 系统应在地图上直观展示反馈点、任务分布和网格员位置,支持多种筛选条件和图层切换。 | 中 |
| GRID-03 | 位置标记 | 系统应支持用户在地图上精确标记环境问题位置,并自动关联到对应网格。 | 高 |
| GRID-04 | A*寻路算法 | 系统应基于网格系统和实时路况,为网格员提供从当前位置到任务地点的最优路径规划。 | 中 |
| GRID-05 | 区域统计 | 系统应基于网格系统,生成环境问题热力图,识别高发区域和问题类型分布。 | 低 |
| GRID-06 | 网格查询 | 系统应支持查询特定区域内的网格定义、属性和历史问题记录。 | 低 |
| GRID-07 | 离线地图 | 系统应支持地图数据的离线缓存,保证在网络不稳定情况下的基本功能。 | 低 |
**网格与地图流程图**
```mermaid
graph TD
A[管理员定义网格系统] --> B[划分城市区域为网格单元]
B --> C[设置网格属性]
C --> D[分配网格责任人]
E[用户标记环境问题位置] --> F[系统关联到对应网格]
F --> G[存储地理位置信息]
H[网格员接收任务] --> I[查看任务位置]
I --> J[请求路径规划]
J --> K[A*算法计算最优路径]
K --> L[展示导航路线]
M[管理员查看统计数据] --> N[生成环境问题热力图]
N --> O[识别问题高发区域]
O --> P[调整资源分配策略]
```
### 5.5 决策支持模块需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| DECI-01 | 核心指标看板 | 系统应实时展示关键业务指标,如待处理反馈数、进行中任务数、平均处理时长、按时完成率等。 | 中 |
| DECI-02 | 多维度分析 | 系统应支持按时间、区域、污染类型、处理人等多个维度对数据进行交叉分析和趋势展示。 | 中 |
| DECI-03 | 热力图可视化 | 系统应在地图上以热力图形式展示环境问题的分布密度,直观识别高发区域。 | 中 |
| DECI-04 | 绩效评估 | 系统应对网格员和区域的工作效率、问题解决质量等进行量化评估,生成排名和对比分析。 | 低 |
| DECI-05 | 预警机制 | 系统应基于历史数据和趋势分析,对可能出现的环境风险提前预警。 | 低 |
| DECI-06 | 报表导出 | 系统应支持将分析结果导出为多种格式PDF、Excel等便于进一步分析和汇报。 | 低 |
| DECI-07 | 个性化配置 | 系统应支持决策者个性化配置数据看板,满足不同管理者的关注点。 | 低 |
**决策支持流程图**
```mermaid
graph TD
A[系统收集业务数据] --> B[实时计算核心指标]
B --> C[展示核心指标看板]
D[决策者选择分析维度] --> E[系统执行多维度分析]
E --> F[生成趋势图表]
G[系统分析地理数据] --> H[生成环境问题热力图]
H --> I[标识高发区域]
J[系统收集网格员工作数据] --> K[计算绩效指标]
K --> L[生成绩效排名]
M[系统分析历史数据] --> N[识别异常趋势]
N --> O{是否达到预警阈值?}
O -- 是 --> P[触发预警通知]
O -- 否 --> Q[继续监控]
R[决策者查看分析结果] --> S[选择导出格式]
S --> T[导出分析报表]
```
## 6. 非功能性需求
### 6.1 性能需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| PERF-01 | 响应时间 | 系统的页面加载时间应不超过3秒API响应时间应不超过500毫秒不包括文件上传下载。 | 高 |
| PERF-02 | 并发用户 | 系统应能同时支持至少100个并发用户正常操作不出现明显延迟。 | 中 |
| PERF-03 | 数据处理量 | 系统应能处理每日至少1000条反馈和500个任务的创建、更新和查询操作。 | 中 |
| PERF-04 | 文件处理 | 系统应支持单个文件最大10MB总附件大小不超过50MB的上传和处理。 | 中 |
| PERF-05 | 地图渲染 | 地图界面应能在1秒内完成初始加载并在500毫秒内响应用户的缩放和平移操作。 | 中 |
### 6.2 可用性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| USAB-01 | 界面一致性 | 系统界面应保持风格一致,包括颜色方案、按钮位置、导航结构等,减少用户学习成本。 | 中 |
| USAB-02 | 错误处理 | 系统应提供清晰的错误提示和恢复建议,避免用户操作中断。 | 高 |
| USAB-03 | 帮助系统 | 系统应提供上下文相关的帮助信息和操作指南,支持用户自助解决问题。 | 低 |
| USAB-04 | 响应式设计 | 系统界面应适应不同设备PC、平板、手机的屏幕尺寸提供一致的用户体验。 | 高 |
| USAB-05 | 操作简化 | 核心功能的操作路径应尽量简化,减少用户点击次数和输入量。 | 中 |
### 6.3 安全性需求
| 需求ID | 需求名称 | 需求描述 | 优先级 |
| :--- | :--- | :--- | :--- |
| SECU-01 | 数据加密 | 敏感数据如用户密码必须加密存储传输过程中应使用HTTPS加密。 | 高 |
| SECU-02 | 访问控制 | 系统应实施严格的基于角色的访问控制,确保用户只能访问其权限范围内的数据和功能。 | 高 |
| SECU-03 | 输入验证 | 系统应对所有用户输入进行验证和过滤防止SQL注入、XSS等常见安全攻击。 | 高 |
| SECU-04 | 会话管理 | 系统应安全管理用户会话,包括会话创建、验证、超时和销毁,防止会话劫持。 | 中 |
| SECU-05 | 审计日志 | 系统应记录所有关键操作的审计日志,包括用户登录、数据修改、权限变更等。 | 中 |

0
Report/需求规约.md Normal file
View File

View File

@@ -598,4 +598,214 @@
"targetType" : "UserAccount",
"ipAddress" : "0:0:0:0:0:0:0:1",
"createdAt" : "2025-06-27T23:57:30.9731674"
}, {
"id" : 21,
"user" : {
"id" : 1,
"name" : "系统管理员",
"phone" : "13800138000",
"email" : "admin@example.com",
"password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK",
"gender" : null,
"role" : "ADMIN",
"status" : "ACTIVE",
"gridX" : null,
"gridY" : null,
"region" : null,
"level" : null,
"skills" : null,
"createdAt" : "2025-06-24T23:33:58.3224414",
"updatedAt" : "2025-06-24T23:33:58.323441",
"enabled" : true,
"currentLatitude" : null,
"currentLongitude" : null,
"failedLoginAttempts" : 0,
"lockoutEndTime" : null
},
"operationType" : "LOGIN",
"description" : "用户登录成功",
"targetId" : "1",
"targetType" : "UserAccount",
"ipAddress" : "0:0:0:0:0:0:0:1",
"createdAt" : "2025-10-25T19:09:54.3566996"
}, {
"id" : 22,
"user" : {
"id" : 9,
"name" : "网格员3",
"phone" : "14700000002",
"email" : "worker3@example.com",
"password" : "$2a$10$DozEFhdm56LYCGAj/Op60OWgp7pN22R9vB6HvDfJJUsEV/bfThhIq",
"gender" : "MALE",
"role" : "GRID_WORKER",
"status" : "ACTIVE",
"gridX" : 0,
"gridY" : 2,
"region" : "苏州市",
"level" : null,
"skills" : null,
"createdAt" : "2025-06-24T23:33:59.0083253",
"updatedAt" : "2025-06-24T23:33:59.0083253",
"enabled" : true,
"currentLatitude" : null,
"currentLongitude" : null,
"failedLoginAttempts" : 0,
"lockoutEndTime" : null
},
"operationType" : "LOGIN",
"description" : "用户登录成功",
"targetId" : "9",
"targetType" : "UserAccount",
"ipAddress" : "0:0:0:0:0:0:0:1",
"createdAt" : "2025-10-25T19:11:31.9871387"
}, {
"id" : 23,
"user" : {
"id" : 2,
"name" : "决策者",
"phone" : "13900139000",
"email" : "decision@example.com",
"password" : "$2a$10$RyY5edaCBC2FdiXsdkz5Ae/bHs4FOQDMMg4U/1lRbNyGoXqDmKWIa",
"gender" : null,
"role" : "DECISION_MAKER",
"status" : "ACTIVE",
"gridX" : null,
"gridY" : null,
"region" : null,
"level" : null,
"skills" : null,
"createdAt" : "2025-06-24T23:33:58.4177379",
"updatedAt" : "2025-06-24T23:33:58.4177379",
"enabled" : true,
"currentLatitude" : null,
"currentLongitude" : null,
"failedLoginAttempts" : 0,
"lockoutEndTime" : null
},
"operationType" : "LOGIN",
"description" : "用户登录成功",
"targetId" : "2",
"targetType" : "UserAccount",
"ipAddress" : "0:0:0:0:0:0:0:1",
"createdAt" : "2025-10-25T19:11:44.6822258"
}, {
"id" : 24,
"user" : {
"id" : 57,
"name" : "特定网格员",
"phone" : "14700009999",
"email" : "worker@aizhangz.top",
"password" : "$2a$10$BESSBI.5BicU8NbFp.HG8envauXACHo/sDe20XvFFezGJWqbI7v1i",
"gender" : "MALE",
"role" : "GRID_WORKER",
"status" : "ACTIVE",
"gridX" : 10,
"gridY" : 10,
"region" : "苏州市",
"level" : null,
"skills" : null,
"createdAt" : "2025-06-24T23:34:02.2703065",
"updatedAt" : "2025-06-24T23:34:02.2703065",
"enabled" : true,
"currentLatitude" : null,
"currentLongitude" : null,
"failedLoginAttempts" : 0,
"lockoutEndTime" : null
},
"operationType" : "LOGIN",
"description" : "用户登录失败",
"targetId" : "57",
"targetType" : "UserAccount",
"ipAddress" : "0:0:0:0:0:0:0:1",
"createdAt" : "2025-10-25T19:12:28.9176313"
}, {
"id" : 25,
"user" : {
"id" : 1,
"name" : "系统管理员",
"phone" : "13800138000",
"email" : "admin@example.com",
"password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK",
"gender" : null,
"role" : "ADMIN",
"status" : "ACTIVE",
"gridX" : null,
"gridY" : null,
"region" : null,
"level" : null,
"skills" : null,
"createdAt" : "2025-06-24T23:33:58.3224414",
"updatedAt" : "2025-06-24T23:33:58.323441",
"enabled" : true,
"currentLatitude" : null,
"currentLongitude" : null,
"failedLoginAttempts" : 0,
"lockoutEndTime" : null
},
"operationType" : "LOGIN",
"description" : "用户登录成功",
"targetId" : "1",
"targetType" : "UserAccount",
"ipAddress" : "0:0:0:0:0:0:0:1",
"createdAt" : "2025-10-25T19:13:39.6709067"
}, {
"id" : 26,
"user" : {
"id" : 1,
"name" : "系统管理员",
"phone" : "13800138000",
"email" : "admin@example.com",
"password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK",
"gender" : null,
"role" : "ADMIN",
"status" : "ACTIVE",
"gridX" : null,
"gridY" : null,
"region" : null,
"level" : null,
"skills" : null,
"createdAt" : "2025-06-24T23:33:58.3224414",
"updatedAt" : "2025-06-24T23:33:58.323441",
"enabled" : true,
"currentLatitude" : null,
"currentLongitude" : null,
"failedLoginAttempts" : 0,
"lockoutEndTime" : null
},
"operationType" : "LOGIN",
"description" : "用户登录成功",
"targetId" : "1",
"targetType" : "UserAccount",
"ipAddress" : "0:0:0:0:0:0:0:1",
"createdAt" : "2025-10-25T19:14:22.7981559"
}, {
"id" : 27,
"user" : {
"id" : 1,
"name" : "系统管理员",
"phone" : "13800138000",
"email" : "admin@example.com",
"password" : "$2a$10$PjqWk7oBYmN2ecjNd6njFuS125JSGyb9A8v7HAWJjTzTH5fvLDgLK",
"gender" : null,
"role" : "ADMIN",
"status" : "ACTIVE",
"gridX" : null,
"gridY" : null,
"region" : null,
"level" : null,
"skills" : null,
"createdAt" : "2025-06-24T23:33:58.3224414",
"updatedAt" : "2025-06-24T23:33:58.323441",
"enabled" : true,
"currentLatitude" : null,
"currentLongitude" : null,
"failedLoginAttempts" : 0,
"lockoutEndTime" : null
},
"operationType" : "LOGIN",
"description" : "用户登录成功",
"targetId" : "1",
"targetType" : "UserAccount",
"ipAddress" : "0:0:0:0:0:0:0:1",
"createdAt" : "2025-10-25T19:14:34.822513"
} ]

View File

@@ -0,0 +1,33 @@
# ems-monitoring-system
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
{
"name": "ems-monitoring-system",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"format": "prettier --write src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"animate.css": "^4.1.1",
"axios": "^1.10.0",
"date-fns": "^4.1.0",
"element-plus": "^2.10.2",
"pinia": "^3.0.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.1",
"@types/node": "^22.14.0",
"@vitejs/plugin-vue": "^5.2.3",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^7.0.2",
"prettier": "3.5.3",
"typescript": "~5.8.0",
"vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,29 @@
<template>
<router-view v-slot="{ Component, route }">
<transition
enter-active-class="animate__animated animate__fadeIn"
leave-active-class="animate__animated animate__fadeOut"
mode="out-in"
>
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</template>
<script setup lang="ts">
</script>
<style>
html, body, #app {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Globally disable scrolling */
}
/* You might want to adjust the animation speed */
:root {
--animate-duration: 0.35s;
}
</style>

View File

@@ -0,0 +1,99 @@
// @/api/auth.ts
import apiClient from './index';
import type {
LoginRequest,
LoginResponse,
SignUpRequest,
ResetPasswordWithCodeRequest
} from './types';
/**
* 用户登录(使用 Fetch API
* @param credentials - 登录凭据 (邮箱和密码)
* @returns Promise<LoginResponse>
*/
export const loginWithFetch = async (credentials: LoginRequest): Promise<LoginResponse> => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
});
if (!response.ok) {
let errorMessage = `HTTP error ${response.status}: ${response.statusText}`;
const responseText = await response.text();
try {
const errorBody = JSON.parse(responseText);
if (errorBody && errorBody.message) {
errorMessage = errorBody.message;
} else if (responseText) {
errorMessage = responseText;
}
} catch (e) {
if (responseText) {
errorMessage = responseText;
}
}
const error: any = new Error(errorMessage);
error.status = response.status;
throw error;
}
// If response is OK, we need to parse it as JSON.
// The stream hasn't been read if response.ok is true.
const data = await response.json();
return data;
};
/**
* 用户登录
* @param credentials - 登录凭据 (邮箱和密码)
* @returns Promise<AxiosResponse<LoginResponse>>
*/
export const login = (credentials: LoginRequest) => {
return apiClient.post<LoginResponse>('/auth/login', credentials);
};
/**
* 用户注册
* @param data - 注册所需信息
*/
export const signup = (data: SignUpRequest) => {
return apiClient.post('/auth/signup', data);
};
/**
* 发送【注册】验证码
* @param email - 目标邮箱
*/
export const sendVerificationCode = (email: string) => {
return apiClient.post(`/auth/send-verification-code?email=${email}`);
};
/**
* 发送【密码重置】验证码
* @param email - 目标邮箱
*/
export const sendPasswordResetCode = (email: string) => {
return apiClient.post(`/auth/send-password-reset-code?email=${email}`);
};
/**
* 使用验证码重置密码
* @param data - 包含邮箱、验证码和新密码的对象
*/
export const resetPasswordWithCode = (data: ResetPasswordWithCodeRequest) => {
return apiClient.post('/auth/reset-password-with-code', data);
};
/**
* 用户登出
* 通知后端记录登出操作
*/
export const logout = () => {
return apiClient.post('/auth/logout');
};
// ... 其他认证相关的API调用

View File

@@ -0,0 +1,147 @@
import apiClient from './index';
import type {
DashboardStatsDTO,
AqiDistributionDTO,
TaskStatsDTO,
TrendDataPointDTO,
GridCoverageDTO,
HeatmapPointDTO,
PollutionStatsDTO,
AqiHeatmapPointDTO,
PollutantThresholdDTO,
Grid
} from './types';
/**
* Fetches the main dashboard statistics.
* @returns A promise that resolves to the dashboard stats.
*/
export const getDashboardStats = (): Promise<DashboardStatsDTO> => {
return apiClient.get('/dashboard/stats');
};
/**
* Fetches the AQI level distribution.
* @returns A promise that resolves to an array of AQI distribution data.
*/
export const getAqiDistribution = (): Promise<AqiDistributionDTO[]> => {
return apiClient.get('/dashboard/reports/aqi-distribution');
};
/**
* Fetches the monthly trend of exceedance events.
* @returns A promise that resolves to an array of trend data points.
*/
export const getMonthlyExceedanceTrend = (): Promise<TrendDataPointDTO[]> => {
return apiClient.get('/dashboard/reports/monthly-exceedance-trend')
.then(data => {
console.log('原始趋势数据:', data);
// 确保数据格式正确
if (Array.isArray(data)) {
return data.map(item => ({
yearMonth: item.yearMonth || '',
count: item.count || 0
}));
}
return [];
});
};
/**
* Fetches the grid coverage statistics by city.
* @returns A promise that resolves to an array of grid coverage data.
*/
export const getGridCoverageByCity = (): Promise<GridCoverageDTO[]> => {
return apiClient.get('/dashboard/reports/grid-coverage');
};
/**
* Fetches the heatmap data for pollution incidents.
* @returns A promise that resolves to an array of heatmap points.
*/
export const getHeatmapData = (): Promise<HeatmapPointDTO[]> => {
return apiClient.get('/dashboard/map/heatmap');
};
/**
* Fetches the pollution statistics.
* @returns A promise that resolves to an array of pollution stats.
*/
export const getPollutionStats = (): Promise<PollutionStatsDTO[]> => {
return apiClient.get('/dashboard/reports/pollution-stats');
};
/**
* Fetches the task completion statistics.
* @returns A promise that resolves to the task stats.
*/
export const getTaskCompletionStats = (): Promise<TaskStatsDTO> => {
return apiClient.get('/dashboard/reports/task-completion-stats');
};
/**
* Fetches the AQI heatmap data.
* @returns A promise that resolves to an array of AQI heatmap points.
*/
export const getAqiHeatmapData = (): Promise<AqiHeatmapPointDTO[]> => {
return apiClient.get('/dashboard/map/aqi-heatmap');
};
/**
* Fetches all pollutant thresholds.
* @returns A promise that resolves to an array of pollutant thresholds.
*/
export const getPollutantThresholds = (): Promise<PollutantThresholdDTO[]> => {
return apiClient.get('/dashboard/thresholds');
};
/**
* Fetches a specific pollutant threshold.
* @param pollutantName The name of the pollutant.
* @returns A promise that resolves to the pollutant threshold.
*/
export const getPollutantThreshold = (pollutantName: string): Promise<PollutantThresholdDTO> => {
return apiClient.get(`/dashboard/thresholds/${pollutantName}`);
};
/**
* Saves a pollutant threshold.
* @param threshold The pollutant threshold to save.
* @returns A promise that resolves to the saved pollutant threshold.
*/
export const savePollutantThreshold = (threshold: PollutantThresholdDTO): Promise<PollutantThresholdDTO> => {
return apiClient.post('/dashboard/thresholds', threshold);
};
/**
* 获取每种污染物的月度趋势数据
* @returns 每种污染物的月度趋势数据
*/
export const getPollutantMonthlyTrends = (value: string): Promise<Record<string, TrendDataPointDTO[]>> => {
return apiClient.get('/dashboard/reports/pollutant-monthly-trends')
.then(data => {
console.log('原始污染物趋势数据:', data);
// 确保数据格式正确
if (data && typeof data === 'object') {
const result: Record<string, TrendDataPointDTO[]> = {};
Object.entries(data).forEach(([key, value]) => {
if (Array.isArray(value)) {
result[key] = value.map(item => ({
yearMonth: item.yearMonth || '',
count: item.count || 0
}));
}
});
return result;
}
return {};
});
};
/**
* Fetches all grid data from the server.
* @returns A promise that resolves to an array of Grid objects.
*/
export const getGrids = (): Promise<Grid[]> => {
return apiClient.get('/grids');
};

View File

@@ -0,0 +1,292 @@
import apiClient from './index';
import type { Page, AssigneeInfoDTO, UserInfoDTO, TaskInfoDTO, AttachmentDTO } from './types';
import { PollutionType } from './types';
import type { AxiosResponse } from 'axios';
// 导出必要的类型
export { PollutionType };
// API URL
const API_URL = '/feedback';
// --- Start: Copied from TaskDetailView.vue for now ---
// This is technical debt and should be refactored into a central types file.
interface FeedbackDTO {
feedbackId: number;
eventId: string;
title: string;
}
interface AssigneeDTO {
id: number;
name: string;
phone: string;
}
interface TaskHistoryDTO {
id: number;
oldStatus: string | null;
newStatus: string;
comments: string;
changedAt: string;
changedBy: {
id: number;
name:string;
};
}
interface SubmissionInfoDTO {
comments: string;
attachments: AttachmentDTO[];
}
export interface TaskDetail {
id: number;
feedback: FeedbackDTO;
title: string;
description: string;
status: string;
assignee: AssigneeDTO;
assignedAt: string | null;
completedAt: string | null;
history: TaskHistoryDTO[];
submissionInfo: SubmissionInfoDTO | null;
}
// --- End: Copied from TaskDetailView.vue ---
/**
* 反馈状态枚举
*/
export enum FeedbackStatus {
AI_REVIEWING = 'AI_REVIEWING', // AI审核中
PENDING_REVIEW = 'PENDING_REVIEW', // 待人工审核
REJECTED = 'REJECTED', // 已拒绝
CONFIRMED = 'CONFIRMED', // 已确认
ASSIGNED = 'ASSIGNED', // 已分配
IN_PROGRESS = 'IN_PROGRESS', // 处理中
RESOLVED = 'RESOLVED', // 已解决
CLOSED = 'CLOSED', // 已关闭
PENDING_ASSIGNMENT = 'PENDING_ASSIGNMENT', // 待分配
PROCESSED = 'PROCESSED' // 已处理
}
/**
* 严重程度枚举
*/
export enum SeverityLevel {
LOW = 'LOW', // 低
MEDIUM = 'MEDIUM', // 中
HIGH = 'HIGH' // 高
}
/**
* 反馈列表项接口 (现在匹配后端的 FeedbackResponseDTO)
*/
export interface FeedbackResponse {
id: number;
eventId: string;
title: string;
description: string;
pollutionType: PollutionType;
severityLevel: SeverityLevel;
status: FeedbackStatus;
textAddress: string;
gridX: number;
gridY: number;
latitude: number;
longitude: number;
createdAt: string; // ISO date string
updatedAt: string; // ISO date string
submitterId: number;
user: UserInfoDTO;
task: TaskDetail | null; // Task can be null if not assigned
attachments: AttachmentDTO[];
}
/**
* 反馈详情接口 (为了简化,我们可以让它继承自 FeedbackResponse)
*/
export interface FeedbackDetail extends FeedbackResponse {
aiAnalysis?: string;
reviewNotes?: string;
assignmentId?: number;
assignmentMethod?: string;
assignmentTime?: string;
assignedWorkerId?: number;
resolutionNotes?: string;
resolutionTime?: string;
// `images` is now `attachments`, which is already in FeedbackResponse
}
/**
* 反馈筛选参数接口
*/
export interface FeedbackFilters {
status?: FeedbackStatus;
pollutionType?: PollutionType;
severityLevel?: SeverityLevel;
cityName?: string;
districtName?: string;
startDate?: string;
endDate?: string;
keyword?: string;
page?: number;
size?: number;
}
/**
* 反馈处理请求接口
*/
export interface ProcessFeedbackRequest {
status: FeedbackStatus;
notes?: string;
}
/**
* 反馈分配请求接口
*/
export interface AssignFeedbackRequest {
gridWorkerId: number;
notes?: string;
}
/**
* 反馈拒绝请求接口
*/
export interface RejectFeedbackRequest {
notes: string;
}
export interface LocationInfo {
latitude: number;
longitude: number;
textAddress: string;
gridX: number;
gridY: number;
}
export interface FeedbackSubmissionRequest {
title: string;
description: string;
pollutionType: PollutionType;
severityLevel: SeverityLevel;
location: LocationInfo;
}
/**
* 获取反馈列表
* @param params 筛选参数
* @returns 分页反馈列表
*/
export function getFeedbackList(params?: FeedbackFilters): Promise<Page<FeedbackResponse>> {
return apiClient.get(API_URL, { params });
}
/**
* 获取反馈详情
* @param id 反馈ID
* @returns 反馈详情
*/
export function getFeedbackDetail(id: number): Promise<AxiosResponse<FeedbackDetail>> {
return apiClient.get(`/feedback/${id}`);
}
/**
* 处理反馈
* @param id 反馈ID
* @param data 处理数据
* @returns 处理结果
*/
export function processFeedback(id: number, request: { status: string; notes?: string }): Promise<AxiosResponse<FeedbackDetail>> {
return apiClient.post(`/feedback/${id}/process`, request);
}
/**
* 分配反馈给网格员
* @param id 反馈ID
* @param data 分配数据
* @returns 分配结果
*/
export function assignFeedback(id: number, data: AssignFeedbackRequest): Promise<any> {
return apiClient.post(`${API_URL}/${id}/assign`, data);
}
/**
* 标记反馈为已解决
* @param id 反馈ID
* @param notes 解决说明
* @returns 操作结果
*/
export function resolveFeedback(id: number, notes?: string): Promise<any> {
return apiClient.post(`${API_URL}/${id}/resolve`, { notes });
}
/**
* 关闭反馈
* @param id 反馈ID
* @param notes 关闭说明
* @returns 操作结果
*/
export function closeFeedback(id: number, notes?: string): Promise<any> {
return apiClient.post(`${API_URL}/${id}/close`, { notes });
}
/**
* 拒绝反馈
* @param id 反馈ID
* @param data 拒绝数据
* @returns 操作结果
*/
export function rejectFeedback(id: number, data: RejectFeedbackRequest): Promise<any> {
return apiClient.post(`/supervisor/reviews/${id}/reject`, data);
}
/**
* 确认反馈
* @param id 反馈ID
* @returns 响应体
*/
export function confirmFeedback(id: number): Promise<any> {
return apiClient.post(`${API_URL}/${id}/confirm`);
}
/**
* 提交新的反馈
* @param formData 包含反馈数据和文件的表单
* @returns 提交结果
*/
export function submitFeedback(formData: FormData): Promise<any> {
return apiClient.post(`${API_URL}/submit`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
/**
* 获取反馈统计数据
* @returns 反馈统计数据
*/
export const getFeedbackStats = async () => {
try {
const response = await apiClient.get(`${API_URL}/stats`);
return response || {}; // 如果响应为空,返回一个空对象
} catch (error) {
console.error('获取反馈统计数据API错误:', error);
// 即使API失败也返回一个默认的统计对象结构防止UI崩溃
return {
total: 0,
pending: 0,
confirmed: 0,
assigned: 0,
inProgress: 0,
resolved: 0,
closed: 0,
rejected: 0,
};
}
};
/**
* 获取待处理的反馈列表
* @returns 待处理的反馈列表
*/
export function getPendingFeedback(): Promise<AxiosResponse<FeedbackResponse[]>> {
return apiClient.get('/feedback/pending');
}

View File

@@ -0,0 +1,42 @@
import apiClient from './index';
import type { Grid } from './types';
/**
* DTO for updating a grid's core properties.
* This should align with GridUpdateRequest in the backend.
*/
export interface GridUpdateRequest {
isObstacle?: boolean;
description?: string;
}
/**
* 更新网格信息
* @param gridId - 要更新的网格ID
* @param data - 包含更新数据的对象
* @returns 更新后的网格对象
*/
export const updateGrid = (gridId: number, data: GridUpdateRequest): Promise<Grid> => {
return apiClient.patch(`/grids/${gridId}`, data);
};
/**
* 通过坐标将网格员分配到网格
* @param gridX - 网格X坐标
* @param gridY - 网格Y坐标
* @param userId - 要分配的用户ID
* @returns 成功则返回void
*/
export const assignWorkerByCoordinates = (gridX: number, gridY: number, userId: number): Promise<void> => {
return apiClient.post(`/grids/coordinates/${gridX}/${gridY}/assign`, { userId });
};
/**
* 通过坐标从网格中移除网格员
* @param gridX - 网格X坐标
* @param gridY - 网格Y坐标
* @returns 成功则返回void
*/
export const unassignWorkerByCoordinates = (gridX: number, gridY: number): Promise<void> => {
return apiClient.post(`/grids/coordinates/${gridX}/${gridY}/unassign`);
};

View File

@@ -0,0 +1,50 @@
import axios from 'axios';
import { useAuthStore } from '@/stores/auth';
// 创建 Axios 实例
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // 从环境变量或默认值设置基础URL
headers: {
'Content-Type': 'application/json',
},
});
// 添加请求拦截器
apiClient.interceptors.request.use(
(config) => {
// 这个拦截器会在每个请求发送前执行
const token = localStorage.getItem('token');
if (token) {
// 为请求头添加Authorization字段
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 响应拦截器
apiClient.interceptors.response.use(
(response) => {
// 对响应数据做点什么
return response.data;
},
(error) => {
// 对响应错误做点什么
if (error.response && error.response.status === 401) {
// 如果是401错误说明token无效或过期
// 我们需要调用auth store的logout方法
// 为了避免循环依赖我们在函数内部获取store实例
const authStore = useAuthStore();
authStore.logout();
// 在这里重定向到登录页
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default apiClient;

View File

@@ -0,0 +1,10 @@
import apiClient from '.';
import type { OperationLog } from './types';
export function getOperationLogs(params: any): Promise<OperationLog[]> {
return apiClient.get('/logs', { params });
}
export function getMyOperationLogs(): Promise<OperationLog[]> {
return apiClient.get('/logs/my-logs');
}

View File

@@ -0,0 +1,30 @@
import apiClient from './index';
/**
* Represents a point with x and y coordinates.
* Matches the backend Point DTO.
*/
export interface Point {
x: number;
y: number;
}
/**
* Represents the request payload for finding a path.
* Matches the backend PathfindingRequest DTO.
*/
export interface PathfindingRequest {
startX: number;
startY: number;
endX: number;
endY: number;
}
/**
* Calls the backend API to find a path between two points.
* @param request The pathfinding request containing start and end coordinates.
* @returns A promise that resolves to a list of points representing the path.
*/
export const findPath = (request: PathfindingRequest): Promise<Point[]> => {
return apiClient.post('/pathfinding/find', request);
};

View File

@@ -0,0 +1,40 @@
import apiClient from './index';
import type { Page, Pageable, UserAccount, UserUpdateRequest } from './types';
/**
* 获取用户列表(可分页、可筛选)
* @param params - 包含分页和筛选条件的参数
* @returns 用户数据分页对象
*/
export const getUsers = (params: { role?: string; name?: string; } & Pageable): Promise<Page<UserAccount>> => {
return apiClient.get('/personnel/users', { params });
};
/**
* 获取所有网格员信息
* @returns 所有网格员的列表
*/
export const getAllGridWorkers = async (): Promise<UserAccount[]> => {
// We fetch with a large size to get all workers, assuming the number is manageable.
// A more robust solution might involve paginating through all results if the number of workers is very large.
const response = await getUsers({ role: 'GRID_WORKER', page: 0, size: 1000 });
return response.content;
};
/**
* 根据ID更新用户信息
* @param userId - 用户ID
* @param data - 需要更新的用户信息
* @returns 更新后的用户信息
*/
export const updateUser = (userId: number, data: UserUpdateRequest): Promise<UserAccount> => {
return apiClient.patch(`/personnel/users/${userId}`, data);
};
/**
* 根据ID删除用户
* @param userId - 用户ID
*/
export const deleteUser = (userId: number): Promise<void> => {
return apiClient.delete(`/personnel/users/${userId}`);
};

View File

@@ -0,0 +1,15 @@
import apiClient from './index';
/**
* Submits public feedback.
* This endpoint is for unauthenticated users (guests).
* @param formData The form data containing the feedback details and any files.
* @returns A promise that resolves on successful submission.
*/
export const submitPublicFeedback = (formData: FormData): Promise<any> => {
return apiClient.post('/public/feedback', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
};

View File

@@ -0,0 +1,37 @@
import type { AxiosResponse, AxiosError } from 'axios';
import apiClient from './index';
import type { UserAccount } from './types';
export const getAvailableGridWorkers = (): Promise<UserAccount[]> => {
// 根据TaskAssignmentController的路径应为/tasks/grid-workers
return apiClient.get<UserAccount[]>('/tasks/grid-workers')
.then((response: AxiosResponse<UserAccount[]>) => response.data)
.catch((error: AxiosError) => {
console.error('获取可用网格员API错误:', error);
// 如果API调用失败返回空数组而不是模拟数据
return [];
});
};
export const assignTask = (feedbackId: number, assigneeId: number): Promise<any> => {
return apiClient.post('/tasks/assign', { feedbackId, assigneeId });
};
/**
* Approves a submitted task.
* @param taskId The ID of the task to approve.
* @returns A promise that resolves with the updated task details.
*/
export const approveTask = (taskId: number): Promise<any> => {
return apiClient.post(`/management/tasks/${taskId}/approve`);
};
/**
* Rejects a submitted task.
* @param taskId The ID of the task to reject.
* @param reason The reason for rejection.
* @returns A promise that resolves with the updated task details.
*/
export const rejectTask = (taskId: number, reason: string): Promise<any> => {
return apiClient.post(`/management/tasks/${taskId}/reject`, { reason });
};

View File

@@ -0,0 +1,307 @@
/**
* 通用用户信息类型, 反映了JWT中包含的声明
*/
export interface User {
id: number;
name: string;
email: string;
role: string;
phone?: string;
status?: string;
region?: string;
skills?: string[];
gridX?: number;
gridY?: number;
}
/**
* 用户登录请求体
*/
export interface LoginRequest {
email: string;
password: string;
}
/**
* 登录成功响应体, 与后端完全匹配
*/
export interface LoginResponse {
accessToken: string;
}
/**
* 用户注册请求体
*/
export interface SignUpRequest {
name: string;
email: string;
phone: string;
password: string;
verificationCode: string;
role: string;
}
/**
* 使用验证码重置密码请求体, 对应后端的 PasswordResetWithCodeDto
*/
export interface ResetPasswordWithCodeRequest {
email: string;
code: string;
newPassword: string;
}
/**
* 分页请求参数
*/
export interface Pageable {
page?: number;
size?: number;
sort?: string;
}
/**
* 分页响应体
*/
export interface Page<T> {
content: T[];
totalPages: number;
totalElements: number;
size: number;
number: number;
}
/**
* 后端 UserAccount 实体类的完整映射
*/
export interface UserAccount {
id: number;
name: string;
email: string;
phone: string;
role: string;
status: string;
gender?: string;
region?: string;
gridX?: number;
gridY?: number;
level?: string;
skills?: string[];
currentLatitude?: number;
currentLongitude?: number;
createdAt: string; // Assuming ISO date string
updatedAt: string; // Assuming ISO date string
}
/**
* 用户更新请求体, 对应后端的 UserUpdateRequest
*/
export interface UserUpdateRequest {
name?: string;
phone?: string;
region?: string;
role?: string;
status?: string;
gender?: 'MALE' | 'FEMALE' | 'OTHER';
gridX?: number;
gridY?: number;
level?: string;
skills?: string[];
currentLatitude?: number;
currentLongitude?: number;
}
// --- Dashboard DTOs ---
/**
* Key statistics for the main dashboard.
* @param totalFeedbacks - Total number of feedbacks.
* @param confirmedFeedbacks - Number of confirmed feedbacks.
* @param totalAqiRecords - Total number of AQI records.
* @param activeGridWorkers - Number of active grid workers.
*/
export type DashboardStatsDTO = {
totalFeedbacks: number;
confirmedFeedbacks: number;
totalAqiRecords: number;
activeGridWorkers: number;
};
/**
* Data for AQI level distribution chart.
* @param level - The AQI descriptive level (e.g., "Good", "Moderate").
* @param count - The number of monitoring points at this level.
*/
export type AqiDistributionDTO = {
level: string;
count: number;
};
/**
* A single data point for a time-series trend chart.
* @param yearMonth - The date for the data point in "YYYY-MM" format.
* @param count - The value for that date (e.g., number of exceedances).
*/
export type TrendDataPointDTO = {
yearMonth: string;
count: number;
};
/**
* Grid coverage statistics for a specific city.
* @param city - The name of the city.
* @param totalGrids - The total number of grids in the city.
* @param coveredGrids - The number of grids with assigned personnel.
* @param coverageRate - The calculated coverage percentage.
*/
export type GridCoverageDTO = {
city: string;
totalGrids: number;
coveredGrids: number;
coverageRate: number;
};
/**
* A single point for a heatmap visualization.
* @param lat - Latitude of the data point.
* @param lng - Longitude of the data point.
* @param value - The intensity value for the heatmap.
*/
export type HeatmapPointDTO = {
lat: number;
lng: number;
value: number;
};
/**
* Detailed statistics for a specific pollutant.
* @param pollutantName - The name of the pollutant (e.g., "PM2.5").
* @param averageValue - The average value over a period.
* @param maxValue - The maximum recorded value.
* @param unit - The measurement unit (e.g., "µg/m³").
*/
export type PollutionStatsDTO = {
pollutantName: string;
averageValue: number;
maxValue: number;
unit: string;
};
/**
* Statistics on task completion status.
* @param totalTasks - Total number of tasks.
* @param completedTasks - Number of completed tasks.
* @param completionRate - Completion rate of tasks.
*/
export type TaskStatsDTO = {
totalTasks: number;
completedTasks: number;
completionRate: number;
};
/**
* A single point for the AQI heatmap, including pollutant info.
* Extends HeatmapPointDTO with additional details.
*/
export type AqiHeatmapPointDTO = HeatmapPointDTO & {
primaryPollutant: string;
aqi: number;
};
/**
* 污染物类型枚举
*/
export enum PollutionType {
PM25 = 'PM25',
O3 = 'O3',
NO2 = 'NO2',
SO2 = 'SO2',
OTHER = 'OTHER'
}
/**
* 污染物阈值设置
* @param pollutionType - 污染物类型
* @param pollutantName - 污染物名称
* @param threshold - 阈值
* @param unit - 单位
* @param description - 描述
*/
export type PollutantThresholdDTO = {
pollutionType: PollutionType;
pollutantName: string;
threshold: number;
unit: string;
description?: string;
};
/**
* Represents a geographical grid cell.
* Matches the backend Grid entity.
*/
export interface Grid {
id: number;
gridX: number;
gridY: number;
cityName: string;
districtName?: string;
description?: string;
isObstacle: boolean;
}
// ... 您可以根据API文档继续添加其他类型定义
// --- Task and Assignment DTOs ---
export interface AssigneeInfoDTO {
id: number;
name: string;
phone: string;
}
export interface UserInfoDTO {
id: number;
name: string;
phone: string;
email: string;
}
export interface TaskInfoDTO {
id: number;
status: string; // Should match TaskStatus enum from backend
assignee: AssigneeInfoDTO;
submissionInfo: SubmissionInfoDTO | null;
}
export interface AttachmentDTO {
id: number;
fileName: string;
fileType: string;
url: string;
uploadedAt: string; // ISO date string
}
export interface SubmissionInfoDTO {
comments: string;
attachments: AttachmentDTO[];
}
/**
* 操作日志类型
*/
export interface OperationLog {
id: number;
userId: number;
userName: string;
operationType: string;
operationTypeDesc: string;
description: string;
targetId: string;
targetType: string;
ipAddress: string;
createdAt: string;
}
/**
* Task Summary DTO for supervisor view.
* This should match `TaskSummaryDTO` from the backend.
*/

View File

@@ -0,0 +1,74 @@
import apiClient from './index';
import type { GridWorker } from './grid';
/**
* 用户信息接口
*/
export interface UserInfo {
id: number;
name: string;
email: string;
phone: string;
role: string;
avatar?: string;
permissions?: string[];
}
/**
* 用户更新请求接口
*/
export interface UserUpdateRequest {
name?: string;
phone?: string;
email?: string;
region?: string;
level?: string;
gridX?: number | null;
gridY?: number | null;
currentLatitude?: number | null;
currentLongitude?: number | null;
skills?: string[];
status?: string;
gender?: string;
}
/**
* 获取当前登录用户信息
* @returns 用户信息
*/
export function getCurrentUser(): Promise<UserInfo> {
return apiClient.get('/auth/profile');
}
/**
* 更新用户资料
* @param userId 用户ID
* @param data 更新数据
* @returns 更新后的用户信息
*/
export function updateUserProfile(userId: number, data: UserUpdateRequest): Promise<GridWorker> {
console.log(`API调用: 更新用户资料, userId=${userId}`, data);
return apiClient.patch(`/personnel/users/${userId}`, data)
.then(response => {
console.log('更新用户资料API调用成功:', response.data);
return response.data;
})
.catch(error => {
console.error('更新用户资料API调用失败:', error);
console.error('错误详情:', error.response?.data || error.message);
throw error;
});
}
/**
* 更新用户角色
* @param userId 用户ID
* @param role 角色名称
* @param gridX 网格X坐标可选
* @param gridY 网格Y坐标可选
* @returns 更新后的用户信息
*/
export function updateUserRole(userId: number, role: string, gridX?: number, gridY?: number): Promise<GridWorker> {
const data = { role, gridX, gridY };
return apiClient.put(`/personnel/users/${userId}/role`, data).then(response => response.data);
}

View File

@@ -0,0 +1,90 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-green-dark: #2E7D32;
--vt-c-green: #4CAF50;
--vt-c-green-light: #81C784;
--vt-c-green-soft: #C8E6C9;
--vt-c-green-mute: #E8F5E9;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-green-dark);
--vt-c-text-light-2: rgba(46, 125, 50, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-green-mute);
--color-background-soft: var(--vt-c-green-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-green-light);
--color-text: var(--vt-c-green-soft);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,607 @@
<template>
<el-dialog
:model-value="visible"
title="反馈详情"
width="70%"
:before-close="handleClose"
class="feedback-detail-dialog"
top="5vh"
>
<template #header>
<div class="dialog-header">
<span class="el-dialog__title">反馈详情</span>
<el-button
type="primary"
:icon="Refresh"
circle
@click="handleRefresh"
:loading="loading"
title="刷新数据"
/>
</div>
</template>
<div v-if="loading" v-loading="loading" class="loading-container"></div>
<div v-if="!loading && currentFeedback" class="detail-container">
<el-descriptions :column="2" border>
<el-descriptions-item label="ID">{{ currentFeedback.id }}</el-descriptions-item>
<el-descriptions-item label="事件编号">{{ currentFeedback.eventId || '无' }}</el-descriptions-item>
<el-descriptions-item label="标题" :span="2">{{ currentFeedback.title }}</el-descriptions-item>
<el-descriptions-item label="污染类型">{{ formatPollutionType(currentFeedback.pollutionType) }}</el-descriptions-item>
<el-descriptions-item label="严重程度">
<el-tag :type="getSeverityTagType(currentFeedback.severityLevel)">
{{ formatSeverityLevel(currentFeedback.severityLevel) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusTagType(currentFeedback.status)">
{{ formatStatus(currentFeedback.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ formatDate(currentFeedback.createdAt) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDate(currentFeedback.updatedAt) }}</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="1" border class="mt-4">
<el-descriptions-item label="详细描述">
<div class="description-content">{{ currentFeedback.description }}</div>
</el-descriptions-item>
<el-descriptions-item label="提交位置" v-if="currentFeedback.latitude && currentFeedback.longitude">
{{ currentFeedback.textAddress || `经纬度: (${currentFeedback.longitude}, ${currentFeedback.latitude})` }}
</el-descriptions-item>
<el-descriptions-item label="报告人信息" v-if="currentFeedback.user">
{{ currentFeedback.user.name }}
<el-tag size="small" style="margin-left: 8px;">{{ formatRole(currentFeedback.user.role) }}</el-tag>
<div class="mt-1">ID: {{ currentFeedback.user.id }}, 电话: {{ currentFeedback.reporterPhone || '未提供' }}</div>
<div class="user-note mt-1" v-if="currentFeedback.user.role === 'GRID_WORKER'">
<el-alert
type="warning"
:closable="false"
show-icon
title="注意:通常反馈应由公众监督员提交,此处显示为网格员可能是测试数据"
/>
</div>
</el-descriptions-item>
</el-descriptions>
<div class="task-details mt-4" v-if="currentFeedback.task">
<h4>任务详情</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="任务ID">{{ currentFeedback.task.id }}</el-descriptions-item>
<el-descriptions-item label="任务状态">
<el-tag :type="getStatusTagType(currentFeedback.task.status)">
{{ formatStatus(currentFeedback.task.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="处理人" v-if="currentFeedback.task.assignee">
{{ currentFeedback.task.assignee.name }} (ID: {{ currentFeedback.task.assignee.id }})
</el-descriptions-item>
<el-descriptions-item label="处理人电话" v-if="currentFeedback.task.assignee">
{{ currentFeedback.task.assignee.phone || '未提供' }}
</el-descriptions-item>
<el-descriptions-item label="网格坐标" :span="2" v-if="currentFeedback.task.gridX !== null && currentFeedback.task.gridY !== null">
X: {{ currentFeedback.task.gridX }}, Y: {{ currentFeedback.task.gridY }}
</el-descriptions-item>
<el-descriptions-item label="分配时间" v-if="currentFeedback.task.assignment?.assignmentTime">
{{ formatDate(currentFeedback.task.assignment.assignmentTime) }}
</el-descriptions-item>
<el-descriptions-item label="分配说明" v-if="currentFeedback.task.assignment?.remarks">
{{ currentFeedback.task.assignment.remarks }}
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 任务提交详情 -->
<div class="submission-details mt-4" v-if="currentFeedback.task && currentFeedback.task.submissionInfo">
<h4>任务提交详情</h4>
<el-descriptions :column="1" border>
<el-descriptions-item label="提交说明">
{{ currentFeedback.task.submissionInfo.comments }}
</el-descriptions-item>
<el-descriptions-item label="提交附件" v-if="currentFeedback.task.submissionInfo.attachments && currentFeedback.task.submissionInfo.attachments.length > 0">
<div v-for="att in currentFeedback.task.submissionInfo.attachments" :key="att.id">
<el-link :href="att.url" type="primary" target="_blank" :icon="Document">{{ att.fileName }}</el-link>
</div>
</el-descriptions-item>
<el-descriptions-item label="无附件" v-else>
未提交任何附件
</el-descriptions-item>
</el-descriptions>
</div>
<div class="image-gallery mt-4" v-if="currentFeedback.attachments && currentFeedback.attachments.length > 0">
<h4>相关图片</h4>
<el-image
v-for="att in currentFeedback.attachments"
:key="att.id"
:src="att.url"
:preview-src-list="currentFeedback.attachments.map(a => a.url)"
:initial-index="currentFeedback.attachments.findIndex(a => a.id === att.id)"
fit="cover"
class="feedback-image"
lazy
/>
</div>
</div>
<div v-if="!loading && !currentFeedback" class="empty-state">
<el-empty description="无法加载反馈详情" />
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
<!-- 任务审批按钮 -->
<template v-if="currentFeedback?.task?.status === 'SUBMITTED' && canManageSubmittedTask">
<el-button type="success" @click="handleApprove" :loading="approving">批准通过</el-button>
<el-button type="danger" @click="handleRejectTask" :loading="rejecting">打回任务</el-button>
</template>
<!-- 根据状态条件性显示操作按钮 -->
<el-button type="primary" @click="handleProcess" v-if="canProcess">处理</el-button>
<el-button type="success" @click="handleAssign" v-if="canAssign">分配任务</el-button>
<el-button type="warning" @click="handleRejectFeedback" v-if="canReject">拒绝反馈</el-button>
</div>
</template>
</el-dialog>
<!-- 分配任务对话框 -->
<el-dialog
v-model="assignDialogVisible"
title="分配任务"
width="30%"
:before-close="() => assignDialogVisible = false"
>
<div v-if="loadingGridWorkers">正在加载网格员...</div>
<el-select
v-else
v-model="selectedGridWorkerId"
placeholder="请选择网格员"
style="width: 100%;"
>
<el-option
v-for="worker in gridWorkers"
:key="worker.id"
:label="`${worker.name} (ID: ${worker.id})${worker.phone ? ' - ' + worker.phone : ''}`"
:value="worker.id"
/>
</el-select>
<template #footer>
<span class="dialog-footer">
<el-button @click="assignDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmAssign" :disabled="!selectedGridWorkerId">
确认分配
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed, onMounted, h } from 'vue';
import { useFeedbackStore } from '@/store/feedback';
import { storeToRefs } from 'pinia';
import { ElMessage, ElMessageBox, ElSelect, ElOption } from 'element-plus';
import { FeedbackStatus, PollutionType, SeverityLevel, processFeedback, assignFeedback, rejectFeedback, resolveFeedback } from '@/api/feedback';
import { approveTask, rejectTask } from '@/api/tasks';
import type { UserAccount } from '@/api/types';
import apiClient from '@/api/index';
import { Document, Refresh } from '@element-plus/icons-vue';
import { useAuthStore } from '@/stores/auth';
const props = defineProps<{
visible: boolean;
feedbackId: number | null;
}>();
const emit = defineEmits(['close', 'refresh']);
const feedbackStore = useFeedbackStore();
const { currentFeedbackDetail: currentFeedback, detailLoading: loading } = storeToRefs(feedbackStore);
const authStore = useAuthStore();
const currentUser = computed(() => authStore.user);
const approving = ref(false);
const rejecting = ref(false);
// 添加网格员列表状态
const gridWorkers = ref<UserAccount[]>([]);
const loadingGridWorkers = ref(false);
const selectedGridWorkerId = ref<number | null>(null);
const assignDialogVisible = ref(false);
const handleRefresh = () => {
if (props.feedbackId) {
feedbackStore.fetchFeedbackDetail(props.feedbackId);
}
};
// 加载网格员列表
const loadGridWorkers = async () => {
loadingGridWorkers.value = true;
try {
// 直接调用API
const response = await apiClient.get('/tasks/grid-workers');
// 根据apiClient的拦截器response已经是处理过的数据了
gridWorkers.value = response as unknown as UserAccount[];
console.log('成功获取到网格员列表:', gridWorkers.value);
if (gridWorkers.value.length === 0) {
ElMessage.info('当前没有可用的网格员。');
}
} catch (error) {
console.error('加载网格员列表失败:', error);
ElMessage.error('加载网格员列表失败,请检查后端服务是否正常。');
gridWorkers.value = []; // 失败时清空数组
} finally {
loadingGridWorkers.value = false;
}
};
// 获取模拟网格员数据
const getMockGridWorkers = (): UserAccount[] => {
return [
{
id: 1,
name: '张三',
email: 'zhangsan@example.com',
phone: '13800000001',
role: 'GRID_WORKER',
status: 'ACTIVE',
gender: 'MALE',
region: '北京市朝阳区',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 2,
name: '李四',
email: 'lisi@example.com',
phone: '13800000002',
role: 'GRID_WORKER',
status: 'ACTIVE',
gender: 'FEMALE',
region: '北京市海淀区',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 3,
name: '王五',
email: 'wangwu@example.com',
phone: '13800000003',
role: 'GRID_WORKER',
status: 'ACTIVE',
gender: 'MALE',
region: '北京市西城区',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
];
};
// 在对话框显示时加载网格员列表
watch(
() => props.visible,
(newVisible) => {
if (newVisible && canAssign.value) {
// 移除这里的加载,改为在点击分配按钮时加载
}
}
);
// --- Computed Properties for Action Buttons ---
const canManageSubmittedTask = computed(() => {
// Per user requirements, only ADMINs can manage tasks submitted by Grid Workers.
return authStore.user?.role === 'ADMIN';
});
const isActionableBySupervisor = computed(() => {
// This property now simply checks if the user has the authority to perform
// pre-assignment actions like processing or rejecting feedback.
if (!authStore.user) return false;
const viewerRole = authStore.user.role;
return viewerRole === 'ADMIN' || viewerRole === 'SUPERVISOR';
});
const canProcess = computed(() => {
if (!currentFeedback.value || !isActionableBySupervisor.value) return false;
const processableStatus = [
FeedbackStatus.AI_REVIEWING,
FeedbackStatus.PENDING_REVIEW,
FeedbackStatus.CONFIRMED,
];
return processableStatus.includes(currentFeedback.value.status);
});
const canAssign = computed(() => {
if (!currentFeedback.value || !authStore.user) return false;
const viewerRole = authStore.user.role;
const isEligibleRole = viewerRole === 'ADMIN' || viewerRole === 'SUPERVISOR';
if (!isEligibleRole) {
return false;
}
// As per user feedback, supervisors and admins should be able to assign tasks.
// The button should appear when the feedback is ready for assignment.
// Based on the 'handleProcess' function, this status is PENDING_ASSIGNMENT.
return currentFeedback.value.status === 'PENDING_ASSIGNMENT';
});
const canReject = computed(() => {
if (!currentFeedback.value || !isActionableBySupervisor.value) return false;
const rejectableStatus = [
FeedbackStatus.AI_REVIEWING,
FeedbackStatus.PENDING_REVIEW,
];
return rejectableStatus.includes(currentFeedback.value.status);
});
// 格式化显示内容的辅助函数
const formatPollutionType = (type: string) => {
const map: Record<string, string> = {
'PM25': 'PM2.5',
'O3': '臭氧',
'NO2': '二氧化氮',
'SO2': '二氧化硫',
'OTHER': '其他'
};
return map[type] || type;
};
const formatSeverityLevel = (level: string) => {
const map: Record<string, string> = {
'LOW': '低',
'MEDIUM': '中',
'HIGH': '高'
};
return map[level] || level;
};
const formatStatus = (status: string) => {
const map: Record<string, string> = {
'AI_REVIEWING': 'AI审核中',
'PENDING_REVIEW': '待人工审核',
'REJECTED': '已拒绝',
'CONFIRMED': '已确认',
'ASSIGNED': '已分配',
'IN_PROGRESS': '处理中',
'RESOLVED': '已处理',
'CLOSED': '已关闭',
'PENDING_ASSIGNMENT': '待分配',
'PROCESSED': '已处理',
'COMPLETED': '已处理',
'SUBMITTED': '已提交'
};
return map[status] || status;
};
const getSeverityTagType = (level: string) => {
const map: Record<string, 'info' | 'warning' | 'danger'> = {
'LOW': 'info',
'MEDIUM': 'warning',
'HIGH': 'danger'
};
return map[level] || 'info';
};
const getStatusTagType = (status: string) => {
const map: Record<string, 'info' | 'warning' | 'danger' | 'primary' | 'success'> = {
'AI_REVIEWING': 'info',
'PENDING_REVIEW': 'warning',
'REJECTED': 'danger',
'CONFIRMED': 'primary',
'ASSIGNED': 'primary',
'IN_PROGRESS': 'warning',
'RESOLVED': 'success',
'CLOSED': 'info',
'PENDING_ASSIGNMENT': 'warning',
'PROCESSED': 'success',
'COMPLETED': 'success',
'SUBMITTED': 'info'
};
return map[status] || 'info';
};
const getFeedbackDisplayStatus = (feedback: any): string => {
if (!feedback) return '未知状态';
if (feedback.task && feedback.task.status === 'COMPLETED') {
return '已处理';
}
return formatStatus(feedback.status);
};
const getFeedbackDisplayTagType = (feedback: any): 'info' | 'warning' | 'danger' | 'primary' | 'success' => {
if (!feedback) return 'info';
if (feedback.task && feedback.task.status === 'COMPLETED') {
return 'success';
}
return getStatusTagType(feedback.status);
};
const formatDate = (dateStr: string) => {
try {
return new Date(dateStr).toLocaleString();
} catch (e) {
return dateStr;
}
};
const formatRole = (role: string) => {
const map: Record<string, string> = {
'ADMIN': '管理员',
'DECISION_MAKER': '决策人',
'SUPERVISOR': '监督员',
'GRID_WORKER': '网格员',
'PUBLIC_SUPERVISOR': '公众监督员'
};
return map[role] || role;
};
watch(
() => props.feedbackId,
(newId) => {
if (newId && props.visible) {
feedbackStore.fetchFeedbackDetail(newId);
}
},
{ immediate: true }
);
const handleClose = () => {
// 清空选中的网格员
selectedGridWorkerId.value = null;
emit('close');
};
const handleProcess = async () => {
if (!currentFeedback.value) return;
try {
await ElMessageBox.confirm(
'此操作将批准反馈并创建一个待分配的任务。',
'确认处理反馈',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'success',
}
);
await processFeedback(currentFeedback.value.id, {
status: FeedbackStatus.PENDING_ASSIGNMENT,
notes: '管理员已批准该反馈,准备分配任务。'
});
ElMessage.success('反馈已批准,进入待分配状态');
emit('refresh');
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('处理反馈失败');
}
}
};
const handleAssign = () => {
if (!currentFeedback.value) return;
loadGridWorkers();
assignDialogVisible.value = true;
};
const confirmAssign = async () => {
if (!currentFeedback.value || !selectedGridWorkerId.value) {
ElMessage.warning('请选择一个网格员');
return;
}
try {
await apiClient.post('/tasks/assign', {
feedbackId: currentFeedback.value.id,
assigneeId: selectedGridWorkerId.value
});
ElMessage.success('分配成功');
assignDialogVisible.value = false;
emit('refresh');
handleClose();
} catch (apiError) {
console.error('API分配失败:', apiError);
ElMessage.error('分配失败,请稍后重试');
}
};
const handleRejectTask = async () => {
const taskId = currentFeedback.value?.task?.id;
if (!taskId) {
ElMessage.error('无法获取任务ID操作取消');
return;
}
ElMessageBox.prompt('请输入打回任务的理由:', '打回任务', {
confirmButtonText: '确认打回',
cancelButtonText: '取消',
inputPattern: /.+/,
inputErrorMessage: '理由不能为空',
})
.then(async ({ value }) => {
rejecting.value = true;
try {
await rejectTask(taskId, value);
ElMessage.success('任务已打回');
emit('refresh');
handleClose();
} catch (error) {
console.error('打回任务失败:', error);
ElMessage.error('操作失败,请重试');
} finally {
rejecting.value = false;
}
})
.catch(() => {
ElMessage.info('已取消操作');
});
};
const handleApprove = async () => {
if (!currentFeedback.value?.task) return;
approving.value = true;
try {
await approveTask(currentFeedback.value.task.id);
ElMessage.success('任务已批准');
emit('refresh');
} catch (error) {
ElMessage.error('批准任务失败');
} finally {
approving.value = false;
}
};
const handleRejectFeedback = async () => {
if (!currentFeedback.value) return;
if (props.feedbackId) {
ElMessageBox.prompt('请输入拒绝此反馈的理由:', '拒绝反馈', {
confirmButtonText: '确认拒绝',
cancelButtonText: '取消',
})
.then(async ({ value }) => {
try {
await rejectFeedback(props.feedbackId!, { notes: value || '无明确理由' });
ElMessage.success('反馈已拒绝');
emit('refresh');
handleClose();
} catch (error) {
console.error('拒绝反馈失败:', error);
ElMessage.error('操作失败');
}
})
.catch(() => {
ElMessage.info('已取消拒绝操作');
});
}
};
</script>
<style scoped>
.feedback-detail-dialog .loading-container {
height: 300px;
}
.detail-container {
max-height: 75vh;
overflow-y: auto;
}
.mt-4 {
margin-top: 1.5rem;
}
.mt-1 {
margin-top: 0.25rem;
}
.description-content {
white-space: pre-wrap;
}
.image-gallery {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.feedback-image {
width: 100px;
height: 100px;
border-radius: 4px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,333 @@
<template>
<div class="grid-map-container">
<div class="map-header">
<h3>环境监测网格地图</h3>
<div class="map-controls">
<el-select v-model="selectedCity" placeholder="选择城市" @change="handleCityChange" size="small">
<el-option v-for="city in cities" :key="city" :label="city" :value="city" />
</el-select>
<el-button type="primary" size="small" :icon="Loading" :loading="loading" @click="refreshData">
刷新数据
</el-button>
</div>
</div>
<div class="map-content">
<div class="map-container" ref="mapContainer">
<div v-if="loading" class="loading-overlay">
<el-icon class="loading-icon" :size="32"><Loading /></el-icon>
<div class="loading-text">加载中...</div>
</div>
</div>
<div class="map-legend">
<div v-for="city in Object.keys(cityColorPalette)" :key="city" class="legend-item">
<div class="legend-color" :style="{ backgroundColor: cityColorPalette[city] }"></div>
<span>{{ city }}</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #333333;"></div>
<span>障碍区域</span>
</div>
</div>
<!-- 网格编辑/详情一体化对话框 -->
<el-dialog
v-model="gridEditDialogVisible"
:title="editingGrid ? `编辑网格 (${editingGrid.gridX}, ${editingGrid.gridY})` : '网格详情'"
width="600px"
destroy-on-close
>
<el-form v-if="editingGrid" :model="editFormData" label-width="120px">
<el-descriptions :column="1" border class="detail-view" v-if="!isEditMode">
<el-descriptions-item label="城市">{{ editingGrid.cityName }}</el-descriptions-item>
<el-descriptions-item label="区域">{{ editingGrid.districtName || '未指定' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="editingGrid.isObstacle ? 'danger' : currentWorkerInDialog ? 'success' : 'warning'" effect="dark">
{{ editingGrid.isObstacle ? '障碍区域' : currentWorkerInDialog ? '已分配' : '未分配' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="描述">{{ editFormData.description || '无' }}</el-descriptions-item>
<el-descriptions-item label="网格员">
<div v-if="currentWorkerInDialog">
<strong>{{ currentWorkerInDialog.name }}</strong> (ID: {{ currentWorkerInDialog.id }})
</div>
<div v-else>未分配</div>
</el-descriptions-item>
</el-descriptions>
<div v-if="isEditMode">
<el-form-item label="描述">
<el-input v-model="editFormData.description" type="textarea" :rows="3" placeholder="请输入网格描述" />
</el-form-item>
<el-form-item label="是否为障碍物">
<el-switch v-model="editFormData.isObstacle" />
</el-form-item>
<div v-if="!editFormData.isObstacle">
<el-divider>网格员分配</el-divider>
<el-form-item label="分配网格员">
<el-select v-model="editFormData.workerId" placeholder="选择或搜索网格员" clearable filterable style="width: 100%;">
<el-option
v-for="worker in availableGridWorkers"
:key="worker.id"
:label="`${worker.name} (ID: ${worker.id})`"
:value="worker.id"
/>
</el-select>
</el-form-item>
</div>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="isEditMode = !isEditMode">{{ isEditMode ? '返回详情' : '编辑网格' }}</el-button>
<el-button @click="gridEditDialogVisible = false">关闭</el-button>
<el-button v-if="isEditMode" type="primary" @click="handleUpdateGrid" :loading="loading">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
import {
getGrids,
getGridWorkers,
updateGrid,
assignGridWorkerByCoordinates,
removeGridWorkerByCoordinates
} from '@/api/grid';
import type { GridData, GridWorker } from '@/api/grid';
import {
ElMessage,
ElDialog,
ElDescriptions,
ElDescriptionsItem,
ElButton,
ElDivider,
ElTag,
ElSelect,
ElOption,
ElForm,
ElFormItem,
ElInput,
ElSwitch,
ElAlert
} from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
const loading = ref(false);
const selectedCity = ref<string>('北京市');
const cities = ref<string[]>(['北京市']);
const mapContainer = ref<HTMLDivElement | null>(null);
const gridData = ref<GridData[]>([]);
const gridWorkers = ref<GridWorker[]>([]);
const availableGridWorkers = ref<GridWorker[]>([]);
const gridEditDialogVisible = ref(false);
const editingGrid = ref<GridData | null>(null);
const isEditMode = ref(false);
const editFormData = ref({
description: '',
isObstacle: false,
workerId: null as number | string | null,
});
const currentWorkerInDialog = computed(() => {
if (!editingGrid.value) return null;
return gridWorkers.value.find(w => w.gridX === editingGrid.value!.gridX && w.gridY === editingGrid.value!.gridY) || null;
});
let gridSize = 20;
const cityColorPalette: Record<string, string> = {};
const getCityColor = (cityName: string): string => {
if (!cityColorPalette[cityName]) {
const predefinedColors = ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac'];
const allCityNames = [...new Set(gridData.value.map(g => g.cityName))];
const cityIndex = allCityNames.indexOf(cityName);
cityColorPalette[cityName] = predefinedColors[cityIndex % predefinedColors.length];
}
return cityColorPalette[cityName];
};
const loadData = async () => {
loading.value = true;
try {
const [grids, workers] = await Promise.all([
getGrids(),
getGridWorkers()
]);
if (Array.isArray(grids)) {
gridData.value = grids;
const uniqueCities = [...new Set(grids.map(g => g.cityName))];
if (uniqueCities.length > 0) {
cities.value = uniqueCities;
selectedCity.value = uniqueCities[0];
}
}
if (Array.isArray(workers)) {
gridWorkers.value = workers;
}
renderMap();
} catch (error) {
console.error('加载数据失败:', error);
ElMessage.error('加载数据失败,请检查后端服务');
} finally {
loading.value = false;
}
};
const fetchAvailableGridWorkers = async () => {
try {
const response = await getGridWorkers({ unassigned: true });
availableGridWorkers.value = Array.isArray(response) ? response : [];
} catch (error) {
console.error('获取可用网格员数据失败:', error);
ElMessage.error('获取可用网格员列表失败');
}
};
const renderMap = () => {
const container = mapContainer.value;
if (!container || !gridData.value || gridData.value.length === 0) {
if (container) container.innerHTML = '<div class="empty-state">没有可显示的网格数据</div>';
return;
}
container.innerHTML = '';
const allX = gridData.value.map(g => g.gridX);
const allY = gridData.value.map(g => g.gridY);
const minX = Math.min(...allX);
const maxX = Math.max(...allX);
const minY = Math.min(...allY);
const maxY = Math.max(...allY);
const worldWidth = maxX - minX + 1;
const worldHeight = maxY - minY + 1;
gridSize = Math.max(5, Math.floor(Math.min(container.clientWidth / worldWidth, container.clientHeight / worldHeight)));
const svgWidth = worldWidth * gridSize;
const svgHeight = worldHeight * gridSize;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', `${svgWidth}px`);
svg.setAttribute('height', `${svgHeight}px`);
svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
gridData.value.forEach(grid => {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', `${(grid.gridX - minX) * gridSize}`);
rect.setAttribute('y', `${(grid.gridY - minY) * gridSize}`);
rect.setAttribute('width', `${gridSize}`);
rect.setAttribute('height', `${gridSize}`);
rect.setAttribute('fill', grid.isObstacle ? '#333333' : getCityColor(grid.cityName));
rect.setAttribute('stroke-width', '0.5');
rect.setAttribute('stroke', '#fff');
rect.setAttribute('cursor', 'pointer');
rect.addEventListener('click', () => showGridDialog(grid));
svg.appendChild(rect);
});
container.appendChild(svg);
};
const showGridDialog = async (grid: GridData) => {
isEditMode.value = false;
editingGrid.value = grid;
const currentWorker = gridWorkers.value.find(w => w.gridX === grid.gridX && w.gridY === grid.gridY);
editFormData.value = {
description: grid.description || '',
isObstacle: grid.isObstacle,
workerId: currentWorker ? currentWorker.id : '',
};
await fetchAvailableGridWorkers();
gridEditDialogVisible.value = true;
};
const handleUpdateGrid = async () => {
if (!editingGrid.value) return;
loading.value = true;
const grid = editingGrid.value;
const { isObstacle, description, workerId } = editFormData.value;
const newWorkerId = workerId ? Number(workerId) : null;
try {
await updateGrid({ ...grid, isObstacle, description });
const workerAssignmentChanged = (currentWorkerInDialog.value?.id ?? null) !== newWorkerId;
if (isObstacle && currentWorkerInDialog.value) {
await removeGridWorkerByCoordinates(grid.gridX, grid.gridY);
} else if (!isObstacle && workerAssignmentChanged) {
if (newWorkerId) {
await assignGridWorkerByCoordinates(grid.gridX, grid.gridY, newWorkerId);
} else if (currentWorkerInDialog.value) {
await removeGridWorkerByCoordinates(grid.gridX, grid.gridY);
}
}
ElMessage.success('网格信息更新成功');
gridEditDialogVisible.value = false;
await loadData();
} catch (error: any) {
ElMessage.error('更新网格失败: ' + (error?.response?.data?.message || error.message));
} finally {
loading.value = false;
}
};
const refreshData = () => loadData();
const handleCityChange = () => { /* Future implementation for city-specific view */ };
onMounted(() => {
loadData();
window.addEventListener('resize', renderMap);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', renderMap);
});
</script>
<style scoped>
.grid-map-container {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
border-radius: 8px;
}
.map-header {
padding: 15px 20px;
border-bottom: 1px solid #ebeef5;
}
.map-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 15px;
overflow: hidden;
}
.map-container {
flex: 1;
position: relative;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: auto;
}
.map-legend {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
padding-top: 15px;
}
.legend-item { display: flex; align-items: center; }
.legend-color { width: 16px; height: 16px; margin-right: 8px; border-radius: 4px; }
.dialog-footer { text-align: right; }
.detail-view { margin-bottom: 20px; }
</style>

View File

@@ -0,0 +1,99 @@
<template>
<el-dialog
:model-value="visible"
title="提交任务进度"
width="50%"
@update:model-value="$emit('update:visible', $event)"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" label-width="80px">
<el-form-item label="进度说明" prop="comments">
<el-input
v-model="form.comments"
type="textarea"
rows="4"
placeholder="请输入详细的进度说明..."
/>
</el-form-item>
<el-form-item label="上传图片" prop="files">
<el-upload
v-model:file-list="form.files"
action="#"
list-type="picture-card"
:auto-upload="false"
multiple
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="$emit('update:visible', false)">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">
提交
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, UploadUserFile } from 'element-plus';
import { ElMessage } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import apiClient from '@/api';
const props = defineProps<{
visible: boolean;
taskId: number | null;
}>();
const emit = defineEmits(['update:visible', 'submitted']);
const formRef = ref<FormInstance>();
const loading = ref(false);
const form = reactive({
comments: '',
files: [] as UploadUserFile[],
});
watch(() => props.visible, (newValue) => {
if (newValue) {
formRef.value?.resetFields();
}
});
const handleSubmit = async () => {
if (!props.taskId) {
ElMessage.error('任务ID无效');
return;
}
loading.value = true;
try {
const formData = new FormData();
formData.append('comments', form.comments);
form.files.forEach(file => {
if (file.raw) {
formData.append('files', file.raw);
}
});
await apiClient.post(`/worker/${props.taskId}/submit`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
ElMessage.success('进度提交成功');
emit('submitted');
emit('update:visible', false);
} catch (error) {
console.error('提交失败:', error);
ElMessage.error('提交失败');
} finally {
loading.value = false;
}
};
</script>

View File

@@ -0,0 +1,27 @@
/**
* @file Pollution related constants, aligned with backend enums.
*/
/**
* Represents the types of pollution, consistent with the `PollutionType` enum in the backend.
* This ensures that frontend displays and data submissions are aligned with backend expectations.
*/
export const POLLUTION_TYPES = ['PM25', 'O3', 'NO2', 'SO2', 'OTHER'] as const;
/**
* Type definition for a single pollution type.
* Ensures type safety when working with pollution constants.
*/
export type PollutionType = typeof POLLUTION_TYPES[number];
/**
* A map to provide human-readable names for pollution types.
* Useful for displaying in UI components like chart legends or labels.
*/
export const POLLUTION_TYPE_MAP: Record<PollutionType, string> = {
PM25: 'PM2.5',
O3: 'O₃',
NO2: 'NO₂',
SO2: 'SO₂',
OTHER: '其他',
};

View File

@@ -0,0 +1,428 @@
<template>
<el-container class="layout-container" :class="layoutClass">
<el-header class="layout-header">
<div class="header-title">环境监督系统</div>
<div class="header-right">
<el-dropdown @command="handleCommand">
<span class="el-dropdown-link">
{{ userInfoDisplay }}
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-container class="main-body-container">
<el-aside width="220px" class="layout-aside">
<el-menu
:default-active="$route.path"
class="el-menu-vertical"
@select="handleMenuSelect"
>
<template v-for="menu in visibleMenu" :key="menu.path">
<el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.path">
<template #title>
<el-icon><component :is="menu.meta.icon" /></el-icon>
<span>{{ menu.meta.title }}</span>
</template>
<el-menu-item v-for="child in menu.children" :key="child.path" :index="child.path">
{{ child.meta.title }}
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="menu.path">
<el-icon><component :is="menu.meta.icon" /></el-icon>
<span>{{ menu.meta.title }}</span>
</el-menu-item>
</template>
</el-menu>
</el-aside>
<el-main class="layout-main">
<router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import {
HomeFilled,
InfoFilled,
ArrowDown,
Location,
List,
ChatDotSquare,
Setting,
User,
DataAnalysis,
Grid,
} from '@element-plus/icons-vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth';
import { computed, ref, markRaw, watch } from 'vue';
const router = useRouter();
const route = useRoute();
const authStore = useAuthStore();
const userRole = computed(() => authStore.user?.role);
const layoutClass = computed(() => {
if (userRole.value) {
// a simple convention would be `role-<role-name-in-lowercase>`
return `role-${userRole.value.toLowerCase()}`;
}
return '';
});
watch(
() => route.path,
(newPath, oldPath) => {
console.log(`路由从 ${oldPath} 切换到 ${newPath}`);
}
);
const allMenus = ref([
{
path: '/dashboard',
meta: { title: '仪表盘', icon: markRaw(DataAnalysis), roles: ['DECISION_MAKER', 'ADMIN'] }
},
{
path: '/map',
meta: { title: '网格地图', icon: markRaw(Grid), roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER'] }
},
{
path: '/my-tasks',
meta: { title: '我的任务', icon: markRaw(List), roles: ['GRID_WORKER'] }
},
{
path: '/worker-map',
meta: { title: '地图管理', icon: markRaw(Grid), roles: ['GRID_WORKER'] }
},
{
path: '/submit-feedback',
meta: { title: '提交反馈', icon: markRaw(ChatDotSquare), roles: ['PUBLIC_SUPERVISOR'] }
},
{
path: '/feedback',
meta: { title: '我的反馈', icon: markRaw(List), roles: ['PUBLIC_SUPERVISOR'] }
},
{
path: '/feedback',
meta: { title: '反馈管理', icon: markRaw(ChatDotSquare), roles: ['ADMIN', 'SUPERVISOR'] }
},
{
path: '/system',
meta: { title: '系统管理', icon: markRaw(Setting), roles: ['ADMIN'] },
children: [
{ path: '/system/users', meta: { title: '人员管理', roles: ['ADMIN'] } },
{ path: '/system/roles', meta: { title: '操作日志', roles: ['ADMIN'] } },
]
},
{
path: '/settings',
meta: { title: '设置', icon: markRaw(User), roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER', 'PUBLIC_SUPERVISOR'] },
children: [
{ path: '/settings/profile', meta: { title: '个人中心', roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER', 'PUBLIC_SUPERVISOR'] } },
{ path: '/settings/my-logs', meta: { title: '我的操作日志', roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER', 'PUBLIC_SUPERVISOR'] } },
{ path: '/settings/system', meta: { title: '系统设置', roles: ['ADMIN'] } },
]
},
]);
const visibleMenu = computed(() => {
if (!userRole.value) {
return [];
}
const role = userRole.value;
function filterMenu(menuItems: any[]) {
const accessibleMenu: any[] = [];
for (const item of menuItems) {
if (item.meta && item.meta.roles && item.meta.roles.includes(role)) {
const newItem = { ...item };
if (item.children) {
newItem.children = filterMenu(item.children);
if (newItem.children.length > 0 || newItem.path === '/settings') {
accessibleMenu.push(newItem);
}
} else {
accessibleMenu.push(newItem);
}
}
}
return accessibleMenu;
}
return filterMenu(allMenus.value);
});
const userInfoDisplay = computed(() => {
if (authStore.user) {
return `${authStore.user.name} (${authStore.user.role})`;
}
return '未登录';
});
const handleCommand = (command: string | number | object) => {
if (command === 'logout') {
authStore.logout();
router.push('/login');
}
}
const handleMenuSelect = (path: string) => {
console.log('菜单选择:', path);
if (route.path === '/grid-management' && path !== '/grid-management') {
console.log('从网格管理页面切换到其他页面使用location.href');
window.location.href = path;
} else {
router.push(path);
}
};
</script>
<style scoped>
.layout-container {
height: 100vh;
display: flex;
flex-direction: column;
/* The background color will be set by role-specific styles */
}
.layout-header {
/* background: #ffffff; */ /* Will be set by role-specific styles */
color: #303133;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 22px;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
z-index: 10;
flex-shrink: 0;
}
.header-title {
font-weight: 600;
letter-spacing: 1px;
/* color: #004d40; */ /* Will be set by role-specific styles */
}
.header-right {
display: flex;
align-items: center;
}
.el-dropdown-link {
cursor: pointer;
color: #606266;
display: flex;
align-items: center;
font-size: 16px;
}
.main-body-container {
flex: 1;
overflow: hidden;
}
.layout-aside {
/* background-color: #ffffff; */ /* Will be set by role-specific styles */
box-shadow: 2px 0 6px rgba(0,21,41,.08);
transition: width 0.3s;
border-right: 1px solid #e6e6e6;
overflow-y: auto;
}
.el-menu {
border-right: none;
background-color: transparent;
}
.el-menu-item, .el-sub-menu__title {
/* color: #303133; */ /* Will be set by role-specific styles */
font-size: 15px;
height: 50px;
line-height: 50px;
}
.el-menu-item:hover, .el-sub-menu__title:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.el-menu-item.is-active {
/* background-color: #ecf5ff; */ /* Will be set by role-specific styles */
font-weight: bold;
}
.layout-main {
padding: 20px;
background-color: #f0f2f5;
flex: 1;
overflow-y: auto;
position: relative;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.5s;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
<style>
/* Role-based theming */
/* ADMIN THEME (Dark Green) */
.role-admin .layout-header {
background-color: #2E7D32; /* Dark Green */
color: white;
}
.role-admin .header-title {
color: white;
}
.role-admin .el-dropdown-link {
color: white;
}
.role-admin .layout-aside {
background-color: #388E3C; /* Slightly lighter green */
}
.role-admin .el-menu-item,
.role-admin .el-sub-menu__title {
color: #E8F5E9; /* Muted green text */
}
.role-admin .el-menu-item:hover,
.role-admin .el-sub-menu__title:hover {
background-color: #45a049;
}
.role-admin .el-menu-item.is-active {
background-color: #2E7D32; /* Dark Green */
color: #fff;
}
/* DECISION_MAKER THEME (Corporate Green) */
.role-decision_maker .layout-header {
background-color: #00695C; /* Teal/Corporate Green */
color: white;
}
.role-decision_maker .header-title {
color: white;
}
.role-decision_maker .el-dropdown-link {
color: white;
}
.role-decision_maker .layout-aside {
background-color: #00796B;
}
.role-decision_maker .el-menu-item,
.role-decision_maker .el-sub-menu__title {
color: #B2DFDB;
}
.role-decision_maker .el-menu-item:hover,
.role-decision_maker .el-sub-menu__title:hover {
background-color: #00897b;
}
.role-decision_maker .el-menu-item.is-active {
background-color: #00695C;
color: #fff;
}
/* GRID_WORKER THEME (Vibrant Green) */
.role-grid_worker .layout-header {
background-color: #689F38; /* Vibrant Green */
color: white;
}
.role-grid_worker .header-title {
color: white;
}
.role-grid_worker .el-dropdown-link {
color: white;
}
.role-grid_worker .layout-aside {
background-color: #7CB342;
}
.role-grid_worker .el-menu-item,
.role-grid_worker .el-sub-menu__title {
color: #F1F8E9;
}
.role-grid_worker .el-menu-item:hover,
.role-grid_worker .el-sub-menu__title:hover {
background-color: #8bc34a;
}
.role-grid_worker .el-menu-item.is-active {
background-color: #689F38;
color: #fff;
}
/* SUPERVISOR THEME (Calming Green) */
.role-supervisor .layout-header {
background-color: #39796b; /* Calming Green */
color: white;
}
.role-supervisor .header-title {
color: white;
}
.role-supervisor .el-dropdown-link {
color: white;
}
.role-supervisor .layout-aside {
background-color: #4DB6AC;
}
.role-supervisor .el-menu-item,
.role-supervisor .el-sub-menu__title {
color: #E0F2F1;
}
.role-supervisor .el-menu-item:hover,
.role-supervisor .el-sub-menu__title:hover {
background-color: #26a69a;
}
.role-supervisor .el-menu-item.is-active {
background-color: #39796b;
color: #fff;
}
/* PUBLIC_SUPERVISOR THEME (Light Green) */
.role-public_supervisor .layout-header {
background-color: #AED581; /* Light Green */
color: #333;
}
.role-public_supervisor .header-title {
color: #333;
}
.role-public_supervisor .el-dropdown-link {
color: #333;
}
.role-public_supervisor .layout-aside {
background-color: #DCEDC8;
}
.role-public_supervisor .el-menu-item,
.role-public_supervisor .el-sub-menu__title {
color: #556B2F;
}
.role-public_supervisor .el-menu-item:hover,
.role-public_supervisor .el-sub-menu__title:hover {
background-color: #9ccc65;
}
.role-public_supervisor .el-menu-item.is-active {
background-color: #AED581;
color: #333;
}
</style>

View File

@@ -0,0 +1,31 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './assets/main.css'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'animate.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, {
locale: zhCn,
zIndex: 3000,
popperOptions: {
modifiers: [
{
name: 'computeStyles',
options: {
gpuAcceleration: false
}
}
]
}
})
app.mount('#app')

View File

@@ -0,0 +1,163 @@
import { createRouter, createWebHistory } from 'vue-router'
import MainLayout from '../layouts/MainLayout.vue'
import { useAuthStore } from '@/stores/auth';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue')
},
{
path: '/register',
name: 'register',
component: () => import('../views/RegisterView.vue')
},
{
path: '/forgot-password',
name: 'forgot-password',
component: () => import('../views/ForgotPasswordView.vue')
},
{
path: '/reset-password',
name: 'reset-password',
component: () => import('../views/ResetPasswordView.vue')
},
{
path: '/public-feedback',
name: 'public-feedback',
component: () => import('../views/PublicFeedbackSubmitView.vue')
},
{
path: '/',
component: MainLayout,
redirect: '/dashboard',
meta: { requiresAuth: true },
children: [
{
path: 'dashboard',
name: 'dashboard',
component: () => import('../views/HomeView.vue'),
meta: { roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR'] }
},
{
path: 'map',
name: 'map',
component: () => import('../views/GridManagementView.vue'),
meta: { roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER'] }
},
{
path: 'tasks',
name: 'tasks',
component: () => import('../views/TaskView.vue'),
meta: { roles: ['ADMIN', 'SUPERVISOR'] }
},
{
path: 'my-tasks',
name: 'my-tasks',
component: () => import('../views/MyTasksView.vue'),
meta: { roles: ['GRID_WORKER'] }
},
{
path: 'worker-map',
name: 'worker-map',
component: () => import('../views/WorkerMapView.vue'),
meta: { requiresAuth: true, roles: ['GRID_WORKER'] }
},
{
path: 'my-tasks/:id',
name: 'my-task-detail',
component: () => import('../views/TaskDetailView.vue'),
props: true,
meta: { roles: ['GRID_WORKER'] }
},
{
path: 'feedback',
name: 'feedback',
component: () => import('../views/FeedbackView.vue'),
meta: { roles: ['PUBLIC_SUPERVISOR', 'ADMIN', 'SUPERVISOR'] }
},
{
path: 'submit-feedback',
name: 'submit-feedback',
component: () => import('../views/SubmitFeedbackView.vue'),
meta: { roles: ['PUBLIC_SUPERVISOR'] }
},
{
path: 'system/users',
name: 'system-users',
component: () => import('../views/UserManagementView.vue'),
meta: { roles: ['ADMIN'] }
},
{
path: 'system/roles',
name: 'system-roles',
component: () => import('../views/OperationLogView.vue'),
meta: { roles: ['ADMIN'] }
},
{
path: 'settings/system',
name: 'settings-system',
component: () => import('../views/SystemSettingsView.vue'),
meta: { roles: ['ADMIN'] }
},
{
path: 'settings/profile',
name: 'settings-profile',
component: () => import('../views/UserProfileView.vue'),
meta: { roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER', 'PUBLIC_SUPERVISOR'] }
},
{
path: 'settings/my-logs',
name: 'settings-my-logs',
component: () => import('../views/MyOperationLogView.vue'),
meta: { roles: ['ADMIN', 'DECISION_MAKER', 'SUPERVISOR', 'GRID_WORKER', 'PUBLIC_SUPERVISOR'] }
},
{
path: 'about',
name: 'about',
component: () => import('../views/AboutView.vue')
}
]
}
]
})
router.beforeEach((to, from, next) => {
const publicPages = ['/login', '/register', '/forgot-password', '/reset-password', '/public-feedback'];
const authRequired = !publicPages.includes(to.path);
const authStore = useAuthStore();
if (authRequired && !authStore.isLoggedIn) {
return next('/login');
}
// 检查路由是否需要特定角色
if (to.meta.roles) {
const userRole = authStore.user?.role;
if (userRole && (to.meta.roles as string[]).includes(userRole)) {
next();
} else {
// 如果用户角色不被允许,可以重定向到 403 页面或主页
// 这里我们简单地重定向到用户各自的主页
if(userRole) {
switch (userRole) {
case 'GRID_WORKER':
return next('/my-tasks');
case 'PUBLIC_SUPERVISOR':
return next('/submit-feedback');
default:
return next('/dashboard');
}
}
return next('/login'); // 如果没有角色信息,则返回登录页
}
} else {
// 如果路由没有 meta.roles则允许所有已登录用户访问
next();
}
});
export default router

View File

@@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@@ -0,0 +1,124 @@
import { defineStore } from 'pinia';
import {
getFeedbackList,
getFeedbackDetail,
processFeedback,
assignFeedback,
resolveFeedback,
closeFeedback,
rejectFeedback,
confirmFeedback,
getFeedbackStats,
submitFeedback,
FeedbackStatus
} from '@/api/feedback';
import type {
FeedbackResponse,
FeedbackDetail,
FeedbackFilters,
Page
} from '@/api/feedback';
import { ElMessage } from 'element-plus';
import { useAuthStore } from '../stores/auth';
interface FeedbackState {
feedbackList: FeedbackResponse[];
total: number;
loading: boolean;
currentFeedbackDetail: FeedbackDetail | null;
detailLoading: boolean;
stats: {
total: number;
pending: number;
confirmed: number;
assigned: number;
inProgress: number;
resolved: number;
closed: number;
rejected: number;
};
}
export const useFeedbackStore = defineStore('feedback', {
state: (): FeedbackState => ({
feedbackList: [],
total: 0,
loading: false,
currentFeedbackDetail: null,
detailLoading: false,
stats: {
total: 0,
pending: 0,
confirmed: 0,
assigned: 0,
inProgress: 0,
resolved: 0,
closed: 0,
rejected: 0
}
}),
actions: {
async fetchFeedbackList(params: FeedbackFilters) {
this.loading = true;
const authStore = useAuthStore();
const user = authStore.user;
let apiParams: FeedbackFilters = { ...params };
if (user && user.role === 'PUBLIC_SUPERVISOR') {
apiParams.submitterId = user.id;
}
try {
const response = await getFeedbackList(apiParams);
this.feedbackList = response.content;
this.total = response.totalElements;
} catch (error) {
console.error('获取反馈列表失败:', error);
ElMessage.error('获取反馈列表失败,请检查网络或联系管理员');
this.feedbackList = [];
this.total = 0;
} finally {
this.loading = false;
}
},
async fetchFeedbackDetail(id: number) {
this.detailLoading = true;
try {
this.currentFeedbackDetail = await getFeedbackDetail(id);
} catch (error) {
console.error('获取反馈详情失败:', error);
ElMessage.error('无法加载反馈详情');
this.currentFeedbackDetail = null;
} finally {
this.detailLoading = false;
}
},
async fetchFeedbackStats() {
try {
const statsData = await getFeedbackStats();
this.stats = { ...this.stats, ...statsData };
} catch (error) {
console.error('获取反馈统计数据API错误:', error);
ElMessage.error('获取反馈统计数据失败');
}
},
async submitFeedback(formData: FormData) {
try {
await submitFeedback(formData);
this.fetchFeedbackStats();
} catch (error) {
console.error('提交反馈失败:', error);
throw error;
}
},
// 可以在这里添加更多处理反馈的 actions, 例如:
// async process(id: number, data: ProcessFeedbackRequest) { ... }
// async assign(id: number, data: AssignFeedbackRequest) { ... }
}
});

View File

@@ -0,0 +1,127 @@
import { defineStore } from 'pinia';
import { ref, computed, watch } from 'vue';
import { jwtDecode } from 'jwt-decode';
import type { LoginRequest, User } from '@/api/types';
import apiClient from '@/api';
import { loginWithFetch, logout as apiLogout } from '@/api/auth';
import router from '@/router'; // 导入 Vue Router 实例
// 定义从JWT解码出的负载的类型, 它继承自 User 类型
interface JwtPayload extends User {
iat: number; // Issued At
exp: number; // Expiration Time
sub: string; // Subject, typically the user's email or ID
}
export const useAuthStore = defineStore('auth', () => {
const token = ref<string | null>(localStorage.getItem('token'));
const user = ref<User | null>(null);
/**
* 从 token 解码并设置 user state
* @param tokenValue - JWT token string
*/
function setUserFromToken(tokenValue: string) {
try {
const decoded = jwtDecode<JwtPayload>(tokenValue);
// 解码出的信息直接赋值给 user state
user.value = decoded;
} catch (error) {
console.error('Failed to decode token or token is invalid:', error);
// 解码失败时, 清理状态
user.value = null;
token.value = null;
localStorage.removeItem('token');
}
}
// 初始化时如果localStorage中存在 token则从中恢复用户信息
if (token.value) {
setUserFromToken(token.value);
}
const isLoggedIn = computed(() => !!token.value && !!user.value);
/**
* 设置认证信息
* @param newToken - JWT
*/
function setAuth(newToken: string) {
token.value = newToken;
localStorage.setItem('token', newToken);
setUserFromToken(newToken);
}
/**
* 用户登录
* @param credentials - 登录凭据
*/
async function login(credentials: LoginRequest) {
try {
const response = await loginWithFetch(credentials);
const newToken = response.accessToken;
if (!newToken) {
throw new Error('accessToken not found in login response');
}
setAuth(newToken);
// 登录成功后,根据用户角色进行重定向
if (user.value) {
switch (user.value.role) {
case 'GRID_WORKER':
router.push('/my-tasks');
break;
case 'ADMIN':
case 'DECISION_MAKER':
router.push('/dashboard');
break;
case 'SUPERVISOR':
case 'PUBLIC_SUPERVISOR':
router.push('/feedback');
break;
default:
// 对于其他未知角色,可以重定向到登录页或一个通用的欢迎页
router.push('/login');
break;
}
} else {
// 如果用户信息未设置,则默认重定向到主页
router.push('/');
}
} catch (error: any) {
console.error('Login process failed:', error);
delete apiClient.defaults.headers.common['Authorization'];
throw error;
}
}
/**
* 用户登出
*/
async function logout() {
try {
// 调用后端的登出接口以记录日志
await apiLogout();
} catch (error) {
console.error('Failed to call logout API, but proceeding with client-side logout:', error);
} finally {
// 无论后端调用是否成功,都清理前端状态
user.value = null;
token.value = null;
localStorage.removeItem('token');
// 登出后重定向到登录页
router.push('/login');
}
}
return {
token,
user,
isLoggedIn,
login,
logout,
};
});

View File

@@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@@ -0,0 +1,20 @@
import { format } from 'date-fns';
/**
* Formats a date-time string or Date object into a standardized "yyyy-MM-dd HH:mm:ss" format.
* If the input is invalid or null, it returns an empty string.
*
* @param dateTime The date-time to format (string, Date, or null/undefined).
* @returns The formatted date-time string or an empty string.
*/
export function formatDateTime(dateTime: string | Date | null | undefined): string {
if (!dateTime) {
return '';
}
try {
return format(new Date(dateTime), 'yyyy-MM-dd HH:mm:ss');
} catch (error) {
console.error('Error formatting date:', error);
return '';
}
}

View File

@@ -0,0 +1,12 @@
<template>
<div>
<h1>关于页面</h1>
<p>这是一个使用 Vue 3 Element Plus 构建的环境监督系统</p>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,459 @@
<template>
<div class="feedback-management-page">
<!-- 统计面板 -->
<el-row :gutter="20" class="stats-row">
<el-col :span="4">
<el-card shadow="hover" class="stats-card">
<div class="stats-item">
<div class="stats-value">{{ stats.total }}</div>
<div class="stats-label">总反馈</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stats-card pending">
<div class="stats-item">
<div class="stats-value">{{ stats.pending }}</div>
<div class="stats-label">待处理</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stats-card confirmed">
<div class="stats-item">
<div class="stats-value">{{ stats.confirmed }}</div>
<div class="stats-label">已确认</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stats-card assigned">
<div class="stats-item">
<div class="stats-value">{{ stats.assigned }}</div>
<div class="stats-label">已分配</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stats-card resolved">
<div class="stats-item">
<div class="stats-value">{{ stats.resolved }}</div>
<div class="stats-label">已解决</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stats-card rejected">
<div class="stats-item">
<div class="stats-value">{{ stats.rejected }}</div>
<div class="stats-label">已拒绝</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card class="page-card">
<template #header>
<div class="card-header">
<span>反馈管理</span>
</div>
</template>
<!-- 筛选区域 -->
<div class="filter-container">
<el-form :inline="true" :model="filters" class="filter-form">
<el-form-item label="状态">
<el-select v-model="filters.status" placeholder="请选择状态" clearable>
<el-option
v-for="item in filterOptions.status"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="污染类型">
<el-select v-model="filters.pollutionType" placeholder="请选择污染类型" clearable>
<el-option
v-for="item in filterOptions.pollutionType"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="严重程度">
<el-select
v-model="filters.severityLevel"
placeholder="所有程度"
clearable
:popper-append-to-body="false"
>
<template #default>
<el-option
v-for="(text, key) in severityLevelMap"
:key="key"
:label="text"
:value="key"
/>
</template>
<template #prefix>{{ selectedSeverityLabel }}</template>
</el-select>
</el-form-item>
<el-form-item label="提交时间">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="关键词">
<el-input v-model="filters.keyword" placeholder="标题、描述、报告人" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 反馈列表 -->
<el-table :data="feedbackList" v-loading="loading" stripe class="feedback-table">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="标题" min-width="150" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="120">
<template #default="scope">
<el-tag :type="statusTagMap[scope.row.status] || 'info'">
{{ statusMap[scope.row.status] || '未知状态' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="pollutionType" label="污染类型" width="120">
<template #default="{ row }: { row: FeedbackResponse }">
<span>{{ pollutionTypeMap[row.pollutionType] }}</span>
</template>
</el-table-column>
<el-table-column prop="severityLevel" label="严重程度" width="100">
<template #default="{ row }: { row: FeedbackResponse }">
<el-tag :type="severityTagMap[row.severityLevel]">{{ severityLevelMap[row.severityLevel] }}</el-tag>
</template>
</el-table-column>
<el-table-column label="网格坐标" width="120">
<template #default="{ row }: { row: FeedbackResponse }">
<span v-if="row.gridX !== null && row.gridY !== null">
{{ `X: ${row.gridX}, Y: ${row.gridY}` }}
</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column label="处理人" width="120">
<template #default="{ row }: { row: FeedbackResponse }">
<span v-if="row.task && row.task.assignee">
{{ row.task.assignee.name }}
</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column prop="user.name" label="报告人" width="120">
<template #default="{ row }: { row: FeedbackResponse }">
<div>
<span>{{ row.user ? row.user.name : '未知' }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="提交时间" width="180">
<template #default="{ row }">
<span>{{ formatDate(row.createdAt) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="handleViewDetails(row)">查看详情</el-button>
<!-- 其他操作按钮如处理分配等 -->
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="total > 0"
class="pagination-container"
:current-page="pagination.page"
:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
<!-- 详情对话框 -->
<FeedbackDetailDialog
:visible="detailDialogVisible"
:feedback-id="selectedFeedbackId"
@close="detailDialogVisible = false"
@refresh="fetchFeedbackList"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch, computed } from 'vue';
import { useFeedbackStore } from '@/store/feedback';
import { storeToRefs } from 'pinia';
import { FeedbackStatus, SeverityLevel } from '@/api/feedback';
import { PollutionType } from '@/api/types';
import type { FeedbackResponse, FeedbackFilters } from '@/api/feedback';
import FeedbackDetailDialog from '@/components/FeedbackDetailDialog.vue';
import { useAuthStore } from '@/stores/auth';
const authStore = useAuthStore();
const feedbackStore = useFeedbackStore();
const { feedbackList, total, loading, stats } = storeToRefs(feedbackStore);
// 使用 reactive 创建过滤条件对象
const filters = reactive<Omit<FeedbackFilters, 'page' | 'size'>>({
status: undefined,
pollutionType: undefined,
severityLevel: undefined,
startDate: undefined,
endDate: undefined,
keyword: undefined,
});
// 用于筛选器的状态子集
const filterableStatusMap: Partial<Record<FeedbackStatus, string>> = {
[FeedbackStatus.AI_REVIEWING]: 'AI审核中',
[FeedbackStatus.IN_PROGRESS]: '处理中',
[FeedbackStatus.PENDING_ASSIGNMENT]: '待分配',
[FeedbackStatus.PROCESSED]: '已处理',
};
// 使用计算属性来确保状态的显示名称
const selectedStatusLabel = computed(() => {
return filters.status ? statusMap[filters.status] : '全部状态';
});
const selectedPollutionTypeLabel = computed(() => {
return filters.pollutionType ? pollutionTypeMap[filters.pollutionType] : '所有类型';
});
const selectedSeverityLabel = computed(() => {
return filters.severityLevel ? severityLevelMap[filters.severityLevel] : '所有程度';
});
// 日期范围
const dateRange = ref<[string, string] | null>(null);
const pagination = reactive({
page: 1,
size: 10,
});
const detailDialogVisible = ref(false);
const selectedFeedbackId = ref<number | null>(null);
// 映射关系
const statusMap: Record<string, string> = {
'AI_REVIEWING': 'AI审核中',
'AI_REVIEW_FAILED': 'AI审核失败',
'PENDING_REVIEW': '待人工审核',
'PENDING_ASSIGNMENT': '待分配',
'ASSIGNED': '已分配',
'CONFIRMED': '已确认',
'CLOSED_INVALID': '无效关闭',
'PROCESSED': '已处理',
'IN_PROGRESS': '处理中',
'RESOLVED': '已处理',
'COMPLETED': '已处理'
};
const statusTagMap: Record<string, 'info' | 'warning' | 'danger' | 'success'> = {
'AI_REVIEWING': 'info',
'AI_REVIEW_FAILED': 'danger',
'PENDING_REVIEW': 'warning',
'PENDING_ASSIGNMENT': 'warning',
'ASSIGNED': 'warning',
'CONFIRMED': 'success',
'CLOSED_INVALID': 'info',
'PROCESSED': 'success',
'IN_PROGRESS': 'warning',
'RESOLVED': 'success',
'COMPLETED': 'success'
};
const pollutionTypeMap: Record<PollutionType, string> = {
[PollutionType.PM25]: 'PM2.5',
[PollutionType.O3]: '臭氧',
[PollutionType.SO2]: '二氧化硫',
[PollutionType.NO2]: '二氧化氮',
[PollutionType.OTHER]: '其他'
};
const severityLevelMap: Record<SeverityLevel, string> = {
[SeverityLevel.LOW]: '低',
[SeverityLevel.MEDIUM]: '中',
[SeverityLevel.HIGH]: '高',
};
const severityTagMap: Record<SeverityLevel, 'info' | 'warning' | 'danger'> = {
[SeverityLevel.LOW]: 'info',
[SeverityLevel.MEDIUM]: 'warning',
[SeverityLevel.HIGH]: 'danger',
};
const formatDate = (date: string | null) => {
if (!date) return 'N/A';
return new Date(date).toLocaleString();
};
const fetchFeedbackList = () => {
const params = {
...filters,
page: pagination.page - 1, // 后端分页从0开始
size: pagination.size,
};
feedbackStore.fetchFeedbackList(params);
};
const handleSearch = () => {
pagination.page = 1;
fetchFeedbackList();
};
const handleReset = () => {
Object.assign(filters, {
status: undefined,
pollutionType: undefined,
severityLevel: undefined,
startDate: undefined,
endDate: undefined,
keyword: undefined,
});
dateRange.value = null;
fetchFeedbackList();
};
const handleSizeChange = (size: number) => {
pagination.size = size;
fetchFeedbackList();
};
const handleCurrentChange = (page: number) => {
pagination.page = page;
fetchFeedbackList();
};
const handleViewDetails = (row: FeedbackResponse) => {
selectedFeedbackId.value = row.id;
detailDialogVisible.value = true;
};
const fetchFeedbackStats = () => {
feedbackStore.fetchFeedbackStats();
};
const filterOptions = {
status: [
{ value: 'PENDING_REVIEW', label: '待人工审核' },
{ value: 'PENDING_ASSIGNMENT', label: '待分配' },
{ value: 'ASSIGNED', label: '已分配' },
{ value: 'IN_PROGRESS', label: '处理中' },
{ value: 'PROCESSED', label: '已处理' },
{ value: 'CLOSED_INVALID', label: '无效关闭' }
],
pollutionType: Object.entries(pollutionTypeMap).map(([value, label]) => ({ value, label })),
// ... more filter options
};
onMounted(() => {
fetchFeedbackList();
fetchFeedbackStats();
});
</script>
<style scoped>
.feedback-management-page {
padding: 20px;
}
.stats-row {
margin-bottom: 20px;
}
.stats-card {
border-radius: 8px;
transition: all 0.3s;
cursor: pointer;
}
.stats-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
.stats-item {
text-align: center;
padding: 10px;
}
.stats-value {
font-size: 24px;
font-weight: bold;
margin-bottom: 5px;
}
.stats-label {
font-size: 14px;
color: #606266;
}
.pending .stats-value {
color: #e6a23c;
}
.confirmed .stats-value {
color: #409eff;
}
.assigned .stats-value {
color: #67c23a;
}
.resolved .stats-value {
color: #67c23a;
}
.rejected .stats-value {
color: #f56c6c;
}
.page-card {
border-radius: 8px;
}
.card-header {
font-size: 18px;
font-weight: 600;
}
.filter-container {
margin-bottom: 20px;
}
.filter-form .el-form-item {
margin-bottom: 10px;
}
.feedback-table {
width: 100%;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
/* 选中的过滤器样式 */
:deep(.el-select) .el-input__prefix {
font-weight: bold;
color: #409eff !important;
}
:deep(.el-input--prefix .el-input__inner) {
color: #409eff;
font-weight: bold;
}
:deep(.el-date-editor.is-active) {
color: #409eff;
font-weight: bold;
}
:deep(.el-input.is-active .el-input__inner) {
color: #409eff;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,187 @@
<template>
<div class="forgot-password-container">
<el-card class="forgot-password-card">
<div class="card-header">
<h2>找回密码</h2>
<p>请输入您的注册邮箱以接收密码重置邮件</p>
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
hide-required-asterisk
@submit.prevent="handleSubmit"
autocomplete="off"
>
<el-form-item label="邮箱地址" prop="email">
<el-input
v-model="form.email"
placeholder="请输入您注册时使用的邮箱"
size="large"
autocomplete="off"
>
<template #append>
<el-button
:disabled="isSending"
@click="handleSubmit"
>
{{ buttonText }}
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
<div class="card-footer">
<el-link @click="$router.push('/login')">返回登录</el-link>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { useRouter } from 'vue-router';
import type { FormInstance, FormRules } from 'element-plus';
import { sendPasswordResetCode } from '@/api/auth';
const router = useRouter();
const formRef = ref<FormInstance>();
const isSending = ref(false);
const countdown = ref(0);
const form = reactive({
email: '',
});
const rules = reactive({
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] }
],
});
const buttonText = computed(() => {
if (isSending.value) {
return `${countdown.value}s 后可重发`;
}
return '发送重置邮件';
});
const handleSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid) => {
if (valid) {
isSending.value = true;
countdown.value = 60;
try {
await sendPasswordResetCode(form.email);
ElMessage.success('密码重置邮件已发送,即将跳转...');
const timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(timer);
isSending.value = false;
}
}, 1000);
setTimeout(() => {
router.push({ path: '/reset-password', query: { email: form.email } });
}, 2000);
} catch (error) {
ElMessage.error('邮件发送失败,请检查邮箱地址是否正确。');
isSending.value = false;
countdown.value = 0;
}
}
});
};
</script>
<style scoped>
.forgot-password-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-image: url('https://images.unsplash.com/photo-1506744038136-46273834b3fb?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80');
background-size: cover;
background-position: center;
}
.forgot-password-card {
width: 480px;
border-radius: 25px;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
padding: 20px;
}
.card-header {
text-align: center;
margin-bottom: 2rem;
}
.card-header h2 {
color: #004d40;
margin-bottom: 0.5rem;
font-weight: 600;
}
.card-header p {
color: #607d8b;
font-size: 0.9rem;
}
.card-footer {
margin-top: 1.5rem;
text-align: center;
}
.submit-button {
width: 100%;
border-radius: 10px;
background-color: #004d40;
border-color: #004d40;
transition: background-color 0.3s ease;
font-size: 1rem;
padding: 1.2rem 0;
}
.submit-button:hover {
background-color: #00695c;
border-color: #00695c;
}
:deep(.el-input__wrapper) {
border-top-left-radius: 15px;
border-bottom-left-radius: 15px;
}
:deep(.el-input-group__append) {
background-color: #004d40;
color: white;
border: none;
cursor: pointer;
border-top-right-radius: 15px;
border-bottom-right-radius: 15px;
}
:deep(.el-input-group__append:hover) {
background-color: #00695c;
}
:deep(.el-input-group__append .el-button) {
background: transparent;
color: white;
border: none;
box-shadow: none;
}
</style>

View File

@@ -0,0 +1,731 @@
<template>
<div class="grid-management-container">
<!-- Page Header -->
<div class="page-header">
<h2>网格地图</h2>
<div class="header-actions">
<el-button type="primary" @click="refreshData" :loading="loading || isLoadingWorkers" icon="Refresh">
刷新数据
</el-button>
</div>
</div>
<!-- Main Content -->
<div class="main-content-grid">
<!-- Grid Map Area -->
<div class="map-area" v-loading="loading">
<div v-if="error" class="error-message">
<el-alert title="地图数据加载失败" :description="error" type="error" show-icon :closable="false" />
<el-button @click="fetchAllGrids" type="primary" class="retry-button">重试</el-button>
</div>
<svg v-else-if="displayGrids.length" :width="svgDimensions.width" :height="svgDimensions.height" class="grid-svg">
<defs>
<pattern id="grid-pattern" :width="CELL_SIZE" :height="CELL_SIZE" patternUnits="userSpaceOnUse">
<path :d="`M ${CELL_SIZE} 0 L 0 0 0 ${CELL_SIZE}`" fill="none" stroke="#e9e9e9" stroke-width="0.5" />
</pattern>
<!-- Softer Gradient for the path -->
<linearGradient id="soft-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#89f7fe; stop-opacity:1" /> <!-- Light Sky Blue -->
<stop offset="100%" style="stop-color:#66a6ff; stop-opacity:1" /> <!-- Soft Lavender Blue -->
</linearGradient>
</defs>
<rect :width="svgDimensions.width" :height="svgDimensions.height" fill="url(#grid-pattern)" />
<g v-for="grid in displayGrids" :key="grid.id">
<!-- Replace rect with foreignObject to allow for CSS pseudo-elements -->
<foreignObject
:x="grid.displayX * CELL_SIZE"
:y="grid.displayY * CELL_SIZE"
:width="CELL_SIZE"
:height="CELL_SIZE"
>
<div
xmlns="http://www.w3.org/1999/xhtml"
class="grid-cell-wrapper"
@click="selectGrid(grid)"
>
<div
:class="{
'grid-cell': true,
'selected': selectedGrid && selectedGrid.id === grid.id,
'is-path': pathInfo.pathSet.has(`${grid.gridX},${grid.gridY}`),
'is-start': pathInfo.start === `${grid.gridX},${grid.gridY}`,
'is-end': pathInfo.end === `${grid.gridX},${grid.gridY}`
}"
:style="{
'animation-delay': `${(pathInfo.pathMap.get(`${grid.gridX},${grid.gridY}`) || 0) * 50}ms`,
'background-color': getGridFillColor(grid)
}"
></div>
</div>
</foreignObject>
<!-- Path Number Text remains as SVG text -->
<text
v-if="pathInfo.pathSet.has(`${grid.gridX},${grid.gridY}`)"
:x="grid.displayX * CELL_SIZE + CELL_SIZE / 2"
:y="grid.displayY * CELL_SIZE + CELL_SIZE / 2"
class="path-text"
:style="{ 'animation-delay': `${(pathInfo.pathMap.get(`${grid.gridX},${grid.gridY}`) || 0) * 50}ms` }"
>
{{ (pathInfo.pathMap.get(`${grid.gridX},${grid.gridY}`) || 0) + 1 }}
</text>
<!-- Border Rendering -->
<line v-if="getBorder(grid, 'top')" :x1="grid.displayX * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="grid.displayY * CELL_SIZE" class="border-line" />
<line v-if="getBorder(grid, 'right')" :x1="(grid.displayX + 1) * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
<line v-if="getBorder(grid, 'bottom')" :x1="grid.displayX * CELL_SIZE" :y1="(grid.displayY + 1) * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
<line v-if="getBorder(grid, 'left')" :x1="grid.displayX * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="grid.displayX * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
</g>
</svg>
<el-empty v-else description="没有可显示的网格数据" />
</div>
<!-- Details Sidebar -->
<div class="sidebar-area">
<el-card shadow="never" class="details-card">
<template #header>
<div class="card-header">
<span>网格详情</span>
<el-button v-if="selectedGrid && !isEditing && canEdit" type="primary" link @click="enterEditMode" icon="Edit">
编辑
</el-button>
</div>
</template>
<div v-if="isSaving" v-loading="true" class="details-content-loading"></div>
<div v-else-if="selectedGrid" class="details-content">
<!-- Display Mode -->
<template v-if="!isEditing">
<p><strong>网格ID:</strong> {{ selectedGrid.id }}</p>
<p><strong>城市:</strong> <el-tag :color="getCityColor(selectedGrid.cityName)" effect="light">{{ selectedGrid.cityName }}</el-tag></p>
<p><strong>区县:</strong> {{ selectedGrid.districtName || 'N/A' }}</p>
<p><strong>原始坐标:</strong> ({{ selectedGrid.gridX }}, {{ selectedGrid.gridY }})</p>
<p><strong>是否为障碍物:</strong> <el-tag :type="selectedGrid.isObstacle ? 'danger' : 'success'">{{ selectedGrid.isObstacle ? '是' : '否' }}</el-tag></p>
<p><strong>描述:</strong> {{ selectedGrid.description || '无' }}</p>
<el-divider />
<p><strong>负责人:</strong></p>
<div v-if="selectedGridWorker">
<el-tag effect="plain" type="info">
<el-icon><User /></el-icon>
{{ selectedGridWorker.name }} ({{ selectedGridWorker.phone }})
</el-tag>
</div>
<div v-else>
<el-tag type="warning">未分配</el-tag>
</div>
</template>
<!-- Editing Mode -->
<template v-else-if="canEdit">
<el-form label-position="top" label-width="100px">
<el-form-item label="是否为障碍物">
<el-switch v-model="editableIsObstacle" />
</el-form-item>
<el-form-item label="描述">
<el-input type="textarea" v-model="editableDescription" :rows="3" />
</el-form-item>
<el-form-item label="分配网格员">
<el-select
v-model="selectedWorkerIdToAssign"
placeholder="请选择网格员"
clearable
filterable
style="width: 100%;"
>
<el-option label="-- 未分配 --" value="unassigned"></el-option>
<el-option
v-for="worker in allWorkers"
:key="worker.id"
:label="`${worker.name} (${worker.phone})`"
:value="worker.id"
/>
</el-select>
</el-form-item>
</el-form>
<div class="edit-actions">
<el-button @click="cancelEdit">取消</el-button>
<el-button type="primary" @click="saveChanges" :loading="isSaving">保存</el-button>
</div>
</template>
</div>
<el-empty v-else description="请在左侧地图上选择一个网格" />
</el-card>
<!-- Pathfinding Card -->
<el-card shadow="never" class="details-card pathfinding-card">
<template #header>
<div class="card-header">
<span>路径规划</span>
</div>
</template>
<div v-if="!authStore.user" class="pathfinding-content">
<el-empty description="请先登录以使用路径规划功能" />
</div>
<div v-else class="pathfinding-content">
<el-form label-position="top" :model="pathfinding" @submit.prevent="handleFindPath">
<el-form-item label="起点坐标 (X, Y)">
<div style="display: flex; gap: 8px; width: 100%;">
<el-input-number v-model="pathfinding.startX" :controls="false" placeholder="X" style="width: 100%" />
<el-input-number v-model="pathfinding.startY" :controls="false" placeholder="Y" style="width: 100%" />
</div>
<small class="form-help-text">默认为您分配的网格 (0,0)</small>
</el-form-item>
<el-form-item label="终点坐标 (X, Y)">
<div style="display: flex; gap: 8px; width: 100%;">
<el-input-number v-model="pathfinding.endX" :controls="false" placeholder="X" style="width: 100%" />
<el-input-number v-model="pathfinding.endY" :controls="false" placeholder="Y" style="width: 100%" />
</div>
<small class="form-help-text">点击地图上的网格可快速设置终点</small>
</el-form-item>
</el-form>
<div class="pathfinding-actions">
<el-button @click="clearPath" :disabled="!calculatedPath.length">清除路径</el-button>
<el-button type="primary" @click="handleFindPath" :loading="isPathfindingLoading">规划路径</el-button>
</div>
</div>
</el-card>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useAuthStore } from '@/stores/auth';
import type { Grid, UserAccount } from '@/api/types';
import { getGrids } from '@/api/dashboard';
import { getAllGridWorkers } from '@/api/personnel';
import { updateGrid, assignWorkerByCoordinates, unassignWorkerByCoordinates } from '@/api/grid';
import { findPath, type Point, type PathfindingRequest } from '@/api/pathfinding';
import { User } from '@element-plus/icons-vue';
const CELL_SIZE = 25;
const PADDING_CELLS = 2;
const authStore = useAuthStore();
// --- State ---
const gridData = ref<Grid[]>([]);
const allWorkers = ref<UserAccount[]>([]);
const loading = ref(false); // For main grid loading
const isLoadingWorkers = ref(false);
const isSaving = ref(false);
const error = ref<string | null>(null);
const selectedGrid = ref<DisplayGrid | null>(null);
const isEditing = ref(false);
// --- Editing State ---
const editableDescription = ref('');
const editableIsObstacle = ref(false);
const selectedWorkerIdToAssign = ref<number | 'unassigned' | null>(null);
// --- Pathfinding State ---
const pathfinding = ref({
startX: 0,
startY: 0,
endX: undefined as number | undefined,
endY: undefined as number | undefined
});
const calculatedPath = ref<Point[]>([]);
const isPathfindingLoading = ref(false);
// --- Data Fetching & Refresh ---
const fetchAllGrids = async () => {
loading.value = true;
error.value = null;
selectedGrid.value = null; // Reset selection
try {
const response = await getGrids();
gridData.value = Array.isArray(response) ? response : (response as any).content || [];
} catch (e: any) {
const errorMessage = e.message || '获取网格数据时发生未知错误';
error.value = errorMessage;
ElMessage.error(errorMessage);
} finally {
loading.value = false;
}
};
const fetchAllWorkers = async () => {
const userRole = authStore.user?.role;
// 只有管理员和主管需要获取完整的网格员列表用于管理
if (userRole !== 'ADMIN' && userRole !== 'SUPERVISOR') {
allWorkers.value = []; // 其他角色无需加载网格员数据
return;
}
isLoadingWorkers.value = true;
try {
allWorkers.value = await getAllGridWorkers();
} catch (e: any) {
ElMessage.error('获取网格员列表失败: ' + e.message);
} finally {
isLoadingWorkers.value = false;
}
};
const refreshData = async () => {
await Promise.all([fetchAllGrids(), fetchAllWorkers()]);
ElMessage.success('数据已刷新');
// Set default start point for pathfinding if user has one
const user = authStore.user;
if (user && typeof user.gridX === 'number' && typeof user.gridY === 'number') {
pathfinding.value.startX = user.gridX;
pathfinding.value.startY = user.gridY;
}
};
onMounted(() => {
refreshData();
});
// --- Computed Properties ---
const canEdit = computed(() => {
const role = authStore.user?.role;
return role === 'ADMIN' || role === 'SUPERVISOR';
});
const workersByCoord = computed<Map<string, UserAccount>>(() => {
const map = new Map<string, UserAccount>();
allWorkers.value.forEach(worker => {
if (worker.gridX !== null && worker.gridY !== null) {
map.set(`${worker.gridX},${worker.gridY}`, worker);
}
});
return map;
});
const selectedGridWorker = computed<UserAccount | undefined>(() => {
if (!selectedGrid.value || !allWorkers.value.length) {
return undefined;
}
return allWorkers.value.find(
worker => worker.gridX === selectedGrid.value?.gridX && worker.gridY === selectedGrid.value?.gridY
);
});
const pathInfo = computed(() => {
const pathSet = new Set<string>();
const pathMap = new Map<string, number>(); // Stores coordinate -> index for animation delay
let start = '';
let end = '';
if (calculatedPath.value.length > 0) {
calculatedPath.value.forEach((point, index) => {
const coord = `${point.x},${point.y}`;
pathSet.add(coord);
pathMap.set(coord, index);
});
const startPoint = calculatedPath.value[0];
const endPoint = calculatedPath.value[calculatedPath.value.length - 1];
start = `${startPoint.x},${startPoint.y}`;
end = `${endPoint.x},${endPoint.y}`;
}
return { pathSet, pathMap, start, end };
});
// --- City Color Logic ---
const cityColors = ref<Map<string, string>>(new Map());
const getCityColor = (cityName: string): string => {
if (!cityColors.value.has(cityName)) {
let hash = 0;
for (let i = 0; i < cityName.length; i++) {
hash = cityName.charCodeAt(i) + ((hash << 5) - hash);
}
const color = `hsl(${hash % 360}, 80%, 85%)`;
cityColors.value.set(cityName, color);
}
return cityColors.value.get(cityName)!;
};
const getGridFillColor = (grid: Grid): string => {
const isPath = pathInfo.value.pathSet.has(`${grid.gridX},${grid.gridY}`);
// If it's part of the path, let the CSS class handle the gradient background
if (isPath) {
return 'transparent';
}
if (grid.isObstacle) {
return '#000000'; // Black for obstacles
}
if (workersByCoord.value.has(`${grid.gridX},${grid.gridY}`)) {
return '#FF0000'; // Red for grids with workers
}
return getCityColor(grid.cityName);
};
// --- Display Logic ---
interface DisplayGrid extends Grid {
displayX: number;
displayY: number;
}
const displayGrids = computed<DisplayGrid[]>(() => {
if (!gridData.value.length) return [];
// Determine the top-left corner of the entire map to normalize coordinates
const minX = Math.min(...gridData.value.map(g => g.gridX));
const minY = Math.min(...gridData.value.map(g => g.gridY));
// Render all grids based on their absolute positions, normalized to start at (0,0)
return gridData.value.map(grid => ({
...grid,
displayX: grid.gridX - minX,
displayY: grid.gridY - minY,
}));
});
const gridMap = computed(() => {
const map = new Map<string, DisplayGrid>();
displayGrids.value.forEach(grid => {
map.set(`${grid.displayX},${grid.displayY}`, grid);
});
return map;
});
const svgDimensions = computed(() => {
if (!displayGrids.value.length) return { width: 0, height: 0 };
const maxX = Math.max(...displayGrids.value.map(g => g.displayX));
const maxY = Math.max(...displayGrids.value.map(g => g.displayY));
return {
width: (maxX + 1) * CELL_SIZE,
height: (maxY + 1) * CELL_SIZE,
};
});
const getBorder = (grid: DisplayGrid, side: 'top' | 'right' | 'bottom' | 'left'): boolean => {
const { displayX, displayY, cityName } = grid;
let neighbor: DisplayGrid | undefined;
switch (side) {
case 'top':
neighbor = gridMap.value.get(`${displayX},${displayY - 1}`);
break;
case 'right':
neighbor = gridMap.value.get(`${displayX + 1},${displayY}`);
break;
case 'bottom':
neighbor = gridMap.value.get(`${displayX},${displayY + 1}`);
break;
case 'left':
neighbor = gridMap.value.get(`${displayX - 1},${displayY}`);
break;
}
return !neighbor || neighbor.cityName !== cityName;
};
const selectGrid = (grid: DisplayGrid) => {
selectedGrid.value = grid;
// Also set pathfinding endpoint for convenience
pathfinding.value.endX = grid.gridX;
pathfinding.value.endY = grid.gridY;
// If in edit mode, populate fields
if (isEditing.value) {
// populateEditFields(grid); // This function was removed, keep it commented or remove it if not needed
}
};
// --- Editing Logic ---
const enterEditMode = () => {
if (!selectedGrid.value) return;
isEditing.value = true;
editableDescription.value = selectedGrid.value.description || '';
editableIsObstacle.value = selectedGrid.value.isObstacle;
selectedWorkerIdToAssign.value = selectedGridWorker.value?.id ?? 'unassigned';
};
const cancelEdit = () => {
isEditing.value = false;
};
const saveChanges = async () => {
if (!selectedGrid.value) return;
isSaving.value = true;
try {
const { id, gridX, gridY } = selectedGrid.value;
let needsGridRefresh = false;
let needsWorkerRefresh = false;
// 1. Update grid details (obstacle, description)
const originalDescription = selectedGrid.value.description || '';
if (editableIsObstacle.value !== selectedGrid.value.isObstacle || editableDescription.value !== originalDescription) {
await updateGrid(id, {
isObstacle: editableIsObstacle.value,
description: editableDescription.value,
});
needsGridRefresh = true;
}
// 2. Update worker assignment
const originalWorkerId = selectedGridWorker.value?.id;
const newWorkerId = selectedWorkerIdToAssign.value;
if (newWorkerId !== (originalWorkerId ?? 'unassigned')) {
if (newWorkerId === 'unassigned') {
await unassignWorkerByCoordinates(gridX, gridY);
} else if (newWorkerId) {
await assignWorkerByCoordinates(gridX, gridY, newWorkerId);
}
needsWorkerRefresh = true;
}
ElMessage.success('网格信息更新成功!');
// Refresh data
if (needsGridRefresh) await fetchAllGrids();
if (needsWorkerRefresh) await fetchAllWorkers();
isEditing.value = false;
} catch (e: any) {
ElMessage.error('保存失败: ' + (e.response?.data?.message || e.message));
} finally {
isSaving.value = false;
}
};
// --- Pathfinding Methods ---
const handleFindPath = async () => {
if (typeof pathfinding.value.endX !== 'number' || typeof pathfinding.value.endY !== 'number') {
ElMessage.warning('请输入有效的终点坐标。');
return;
}
isPathfindingLoading.value = true;
calculatedPath.value = []; // Clear previous path
const request: PathfindingRequest = {
startX: pathfinding.value.startX,
startY: pathfinding.value.startY,
endX: pathfinding.value.endX,
endY: pathfinding.value.endY,
};
try {
const path = await findPath(request);
if (path && path.length > 0) {
calculatedPath.value = path;
ElMessage.success(`路径规划成功,共 ${path.length} 步。`);
} else {
ElMessage.error('未找到有效路径,请检查起终点或地图障碍物。');
}
} catch (e: any) {
ElMessage.error('路径规划失败: ' + (e.response?.data?.message || e.message));
} finally {
isPathfindingLoading.value = false;
}
};
const clearPath = () => {
calculatedPath.value = [];
ElMessage.info('路径已清除。');
};
</script>
<style scoped>
.grid-management-container {
padding: 20px;
background-color: #f7f8fa;
height: 100%;
display: flex;
flex-direction: column;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-shrink: 0;
}
.page-header h2 {
margin: 0;
font-size: 24px;
}
.main-content-grid {
display: flex;
gap: 20px;
flex: 1;
overflow: hidden;
}
.map-area {
flex: 3;
background-color: #fff;
border-radius: 8px;
padding: 10px;
overflow: auto;
border: 1px solid #e9e9e9;
}
.sidebar-area {
flex: 1;
min-width: 300px;
}
.details-card .card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.grid-svg {
display: block;
}
.grid-cell-wrapper {
width: 100%;
height: 100%;
cursor: pointer;
}
.grid-cell {
width: 100%;
height: 100%;
background-color: transparent; /* Default state */
border: 0.5px solid #dcdfe6;
box-sizing: border-box;
transition: all 0.2s ease;
position: relative;
overflow: hidden; /* Important for pseudo-element animation */
}
.grid-cell:hover {
filter: brightness(0.9);
}
.grid-cell.selected {
border: 2px solid #ff4d4f;
}
.grid-cell.is-start {
background-color: #00e676;
box-shadow: 0 0 8px #00e676, inset 0 0 5px rgba(255,255,255,0.7);
border-color: #fff;
}
.grid-cell.is-end {
background-color: #ff5252;
box-shadow: 0 0 8px #ff5252, inset 0 0 5px rgba(255,255,255,0.7);
border-color: #fff;
}
.grid-cell.is-path {
background-image: linear-gradient(to bottom right, #89f7fe, #66a6ff);
border-color: #66a6ff;
opacity: 0;
animation: draw-path-reveal 0.4s forwards ease-out;
}
.grid-cell.is-path::before {
content: '';
position: absolute;
top: 0;
left: -150%;
width: 50%;
height: 100%;
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0) 100%);
transform: skewX(-25deg);
animation: light-sweep 1.2s ease-in-out 3 forwards; /* Play 3 times and stop */
animation-delay: inherit; /* Inherit delay from parent */
}
@keyframes light-sweep {
100% {
left: 150%;
}
}
.path-text {
font-size: 10px;
fill: white;
font-weight: bold;
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
animation: draw-path-reveal 0.4s forwards ease-out;
}
.border-line {
stroke: #000;
stroke-width: 2.5px;
margin-top: 15px;
width: 100%; /* Ensure it takes full width */
}
.error-message {
padding: 20px;
}
.retry-button {
margin-top: 15px;
}
.details-content p {
margin: 0 0 12px;
color: #606266;
margin-top: 4px;
line-height: 1.2;
width: 100%; /* Ensure it takes full width */
}
.details-content p strong {
color: #303133;
margin-right: 8px;
}
.details-content-loading {
height: 200px;
}
.edit-actions {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.pathfinding-card {
margin-top: 20px;
}
.pathfinding-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.form-help-text {
font-size: 12px;
color: #909399;
margin-top: 4px;
line-height: 1.2;
width: 100%; /* Ensure it takes full width */
}
@keyframes draw-path-reveal {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes pulse {
0%, 100% {
filter: brightness(1);
transform: scale(1);
}
50% {
filter: brightness(1.7);
transform: scale(1.05);
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,240 @@
<template>
<div class="login-container">
<el-card class="login-card">
<template #header>
<div class="card-header">
<span>环境监督系统</span>
</div>
</template>
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" class="login-form" label-position="top" autocomplete="off" @submit.prevent="handleLogin">
<el-form-item label="邮箱" prop="email" class="form-item-tight">
<el-input v-model="loginForm.email" placeholder="请输入邮箱" size="large" :prefix-icon="User" clearable autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" class="form-item-tight">
<el-input type="password" v-model="loginForm.password" placeholder="请输入密码" size="large" :prefix-icon="Lock" show-password autocomplete="new-password"></el-input>
</el-form-item>
<el-form-item class="login-btn-item">
<el-button type="primary" @click="handleLogin" :loading="loading" class="login-button" size="large"> </el-button>
</el-form-item>
<div class="extra-links">
<el-link type="info" @click="$router.push('/register')">注册账号</el-link>
<el-link type="info" @click="$router.push('/forgot-password')">找回密码</el-link>
</div>
<el-form-item class="guest-btn-item">
<el-button @click="handleGuestAccess" class="guest-button" size="large" :icon="Promotion">作为游客提交反馈</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import type { FormInstance, FormRules } from 'element-plus';
import { ElMessage } from 'element-plus';
import { useAuthStore } from '@/stores/auth';
import { User, Lock, Promotion } from '@element-plus/icons-vue';
const router = useRouter();
const authStore = useAuthStore();
const loginFormRef = ref<FormInstance>();
const loading = ref(false);
const loginForm = reactive({
email: '',
password: '',
});
const loginRules = reactive<FormRules>({
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] }
],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
});
const handleLogin = async () => {
if (!loginFormRef.value) return;
await loginFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true;
try {
await authStore.login(loginForm);
ElMessage.success('登录成功,正在跳转...');
} catch (error) {
ElMessage.error('登录失败,请检查您的邮箱和密码。');
console.error(error);
} finally {
loading.value = false;
}
}
});
};
const handleGuestAccess = () => {
router.push('/public-feedback');
};
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
overflow: hidden; /* Prevent scrolling */
background-image: url('https://images.unsplash.com/photo-1506744038136-46273834b3fb?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80');
background-size: cover;
background-position: center;
}
.login-card {
position: relative;
width: 480px;
border-radius: 25px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
animation: fadeIn 0.7s ease-in-out;
background: rgba(245, 245, 245, 0.85); /* Light warm gray */
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.5);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card-header {
text-align: center;
font-size: 26px;
font-weight: 600;
color: #004d40; /* Teal Gold color */
padding: 25px 0 0 0; /* Removed bottom padding */
border-bottom: 1px solid rgba(0,77,64,0.2);
}
.login-form {
padding: 10px 35px 20px 35px; /* Minimal top padding */
}
.form-item-tight {
margin-bottom: 18px;
}
.login-button {
width: 100%;
border: none;
background: #004d40; /* Teal Gold color */
color: white;
letter-spacing: 5px;
padding-left: 10px; /* to compensate letter-spacing */
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 77, 64, 0.4);
border-radius: 10px;
}
.login-button:hover {
background: #00695c;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 77, 64, 0.5);
}
.login-btn-item {
margin-top: 25px;
margin-bottom: 0;
}
.el-input__wrapper {
transition: all 0.3s ease;
}
.el-input__wrapper:hover {
box-shadow: 0 0 0 1px var(--el-color-primary) inset !important;
}
.el-input__wrapper.is-focus {
box-shadow: 0 0 0 1px var(--el-color-primary) inset !important;
}
.extra-links {
display: flex;
justify-content: space-between;
margin-top: 20px;
padding: 0 10px;
}
.divider-text {
color: #888;
font-size: 14px;
}
.guest-btn-item {
margin-top: 15px;
}
.guest-button {
width: 100%;
border-color: #004d40;
color: #004d40;
}
.guest-button:hover {
background-color: #e0f2f1;
border-color: #00695c;
color: #00695c;
}
.extra-links .el-link {
color: #bbb;
font-size: 15px; /* Increased font size */
}
.extra-links .el-link:hover {
color: #fff;
}
</style>
<style>
/* This is a global style override to achieve the desired hover/focus effect */
.el-input__wrapper {
transition: all 0.3s ease !important;
}
.el-input__wrapper:hover {
box-shadow: 0 0 0 1px var(--el-color-primary, #409eff) inset !important;
}
.el-input__wrapper.is-focus {
box-shadow: 0 0 0 1px var(--el-color-primary, #409eff) inset !important;
}
.el-form-item__label {
color: #555 !important;
font-weight: 500 !important;
letter-spacing: 0.5px !important;
font-size: 15px !important;
}
.el-input__inner {
color: #333 !important;
font-size: 15px !important; /* Matched font size to label */
}
.el-input__icon {
font-size: 18px !important; /* Icon size can be slightly larger */
}
.el-input__wrapper {
background-color: rgba(255, 255, 255, 0.9) !important;
border-radius: 10px !important;
}
</style>

View File

@@ -0,0 +1,12 @@
<template>
<div>
<h1>地图模块</h1>
<p>这里是地图模块页面</p>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,58 @@
<template>
<el-card>
<template #header>
<div class="card-header">
<span>我的操作日志</span>
</div>
</template>
<el-table :data="logs" v-loading="loading" style="width: 100%" border stripe>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="ipAddress" label="IP地址" width="150"></el-table-column>
<el-table-column prop="operationTypeDesc" label="操作类型" width="120"></el-table-column>
<el-table-column prop="description" label="描述" min-width="200"></el-table-column>
<el-table-column prop="targetId" label="对象ID" width="100"></el-table-column>
<el-table-column prop="targetType" label="对象类型" width="120"></el-table-column>
<el-table-column prop="createdAt" label="操作时间" width="180">
<template #default="scope">
{{ formatTime(scope.row.createdAt) }}
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { getMyOperationLogs } from '@/api/log';
import type { OperationLog } from '@/api/types';
const loading = ref(true);
const logs = ref<OperationLog[]>([]);
const fetchLogs = async () => {
loading.value = true;
try {
logs.value = await getMyOperationLogs();
} catch (error) {
ElMessage.error('获取操作日志失败');
console.error(error);
} finally {
loading.value = false;
}
};
const formatTime = (timeStr: string) => {
if (!timeStr) return '';
return new Date(timeStr).toLocaleString();
};
onMounted(() => {
fetchLogs();
});
</script>
<style scoped>
/* You can add styles if needed */
</style>

View File

@@ -0,0 +1,228 @@
<template>
<div class="my-tasks-view">
<el-card class="page-card">
<template #header>
<div class="card-header">
<span>我的任务</span>
</div>
</template>
<!-- 筛选区域 -->
<div class="filter-container">
<el-radio-group v-model="selectedStatus" @change="fetchTasks">
<el-radio-button value="">全部</el-radio-button>
<el-radio-button value="ASSIGNED">待接受</el-radio-button>
<el-radio-button value="IN_PROGRESS">进行中</el-radio-button>
<el-radio-button value="SUBMITTED">已提交</el-radio-button>
<el-radio-button value="COMPLETED">已完成</el-radio-button>
</el-radio-group>
</div>
<!-- 任务列表 -->
<el-table :data="tasks" v-loading="loading" stripe class="task-table" @row-click="handleRowClick">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">{{ formatStatus(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="textAddress" label="地址" min-width="250" show-overflow-tooltip />
<el-table-column prop="severity" label="严重程度" width="100">
<template #default="{ row }">
<el-tag :type="getSeverityTagType(row.severity)">{{ formatSeverity(row.severity) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="分配时间" width="180">
<template #default="{ row }">
<span>{{ new Date(row.createdAt).toLocaleString() }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="viewTaskDetails(row.id)">查看详情</el-button>
<el-button v-if="row.status === 'ASSIGNED'" type="success" link size="small" @click="acceptTask(row.id)">接受</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="total > 0"
class="pagination-container"
:current-page="pagination.page"
:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue';
import apiClient from '@/api';
import { ElMessage, ElMessageBox } from 'element-plus';
import type { Page } from '@/api/types';
import { useRouter } from 'vue-router';
// --- 类型定义 ---
interface TaskSummary {
id: number;
title: string;
description: string;
status: string;
assigneeName: string | null;
createdAt: string;
textAddress: string;
imageUrl: string;
severity: string;
}
// --- 响应式状态 ---
const tasks = ref<TaskSummary[]>([]);
const loading = ref(false);
const selectedStatus = ref<string | null>(null);
const total = ref(0);
const pagination = reactive({
page: 1,
size: 10,
});
const router = useRouter();
// --- API 调用 ---
const fetchTasks = async () => {
loading.value = true;
try {
const params: any = {
page: pagination.page - 1,
size: pagination.size,
sort: 'createdAt,desc'
};
if (selectedStatus.value) {
params.status = selectedStatus.value;
}
const response = await apiClient.get<Page<TaskSummary>>('/worker', { params });
tasks.value = response.content;
total.value = response.totalElements;
} catch (error) {
console.error('获取任务列表失败:', error);
ElMessage.error('获取任务列表失败');
tasks.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
const acceptTask = async (taskId: number) => {
try {
await ElMessageBox.confirm('确定要接受这个任务吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
});
await apiClient.post(`/worker/${taskId}/accept`);
ElMessage.success('任务已接受');
fetchTasks(); // 刷新列表
} catch (error) {
if (error !== 'cancel') {
console.error('接受任务失败:', error);
ElMessage.error('接受任务失败');
}
}
};
// --- 辅助函数 ---
const formatStatus = (status: string) => {
const map: Record<string, string> = {
'ASSIGNED': '待接受',
'IN_PROGRESS': '进行中',
'SUBMITTED': '已提交',
'COMPLETED': '已完成',
'REJECTED': '已拒绝',
};
return map[status] || status;
};
const getStatusTagType = (status: string): 'primary' | 'warning' | 'info' | 'success' | 'danger' => {
const map: Record<string, 'primary' | 'warning' | 'info' | 'success' | 'danger'> = {
'ASSIGNED': 'primary',
'IN_PROGRESS': 'warning',
'SUBMITTED': 'info',
'COMPLETED': 'success',
'REJECTED': 'danger',
};
return map[status] || 'info';
};
const formatSeverity = (severity: string) => {
const map: Record<string, string> = {
'LOW': '低',
'MEDIUM': '中',
'HIGH': '高',
};
return map[severity] || severity;
};
const getSeverityTagType = (severity: string): 'info' | 'warning' | 'danger' => {
const map: Record<string, 'info' | 'warning' | 'danger'> = {
'LOW': 'info',
'MEDIUM': 'warning',
'HIGH': 'danger',
};
return map[severity] || 'info';
};
// --- 事件处理 ---
const viewTaskDetails = (taskId: number) => {
router.push(`/my-tasks/${taskId}`);
};
const handleRowClick = (row: TaskSummary) => {
viewTaskDetails(row.id);
};
const handleSizeChange = (size: number) => {
pagination.size = size;
fetchTasks();
};
const handleCurrentChange = (page: number) => {
pagination.page = page;
fetchTasks();
};
// --- 生命周期钩子 ---
onMounted(() => {
fetchTasks();
});
</script>
<style scoped>
.my-tasks-view {
padding: 20px;
}
.page-card {
border-radius: 8px;
}
.card-header {
font-size: 18px;
font-weight: 600;
}
.filter-container {
margin-bottom: 20px;
}
.task-table {
width: 100%;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<el-card>
<template #header>
<div class="card-header">
<span>操作日志</span>
</div>
</template>
<div class="filter-container">
<el-input v-model="filters.userId" placeholder="按用户ID搜索" style="width: 200px;"></el-input>
<el-select v-model="filters.operationType" placeholder="按操作类型筛选" clearable>
<el-option v-for="item in operationTypes" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<el-date-picker
v-model="filters.timeRange"
type="datetimerange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期">
</el-date-picker>
<el-button type="primary" :icon="Search" @click="fetchLogs">搜索</el-button>
</div>
<el-table :data="logs" v-loading="loading" style="width: 100%" border stripe>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="userName" label="用户名" width="120"></el-table-column>
<el-table-column prop="ipAddress" label="IP地址" width="150"></el-table-column>
<el-table-column prop="operationTypeDesc" label="操作类型" width="120"></el-table-column>
<el-table-column prop="description" label="描述" min-width="200"></el-table-column>
<el-table-column prop="targetId" label="对象ID" width="100"></el-table-column>
<el-table-column prop="targetType" label="对象类型" width="120"></el-table-column>
<el-table-column prop="createdAt" label="操作时间" width="180">
<template #default="scope">
{{ formatTime(scope.row.createdAt) }}
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { Search } from '@element-plus/icons-vue';
import { getOperationLogs } from '@/api/log';
import type { OperationLog } from '@/api/types';
const loading = ref(true);
const logs = ref<OperationLog[]>([]);
const filters = reactive({
userId: '',
operationType: '',
timeRange: [] as [Date, Date] | [],
});
const operationTypes = [
{ label: "登录", value: "LOGIN" },
{ label: "登出", value: "LOGOUT" },
{ label: "创建", value: "CREATE" },
{ label: "更新", value: "UPDATE" },
{ label: "删除", value: "DELETE" },
{ label: "分配任务", value: "ASSIGN_TASK" },
{ label: "提交任务", value: "SUBMIT_TASK" },
{ label: "审批任务", value: "APPROVE_TASK" },
{ label: "拒绝任务", value: "REJECT_TASK" },
{ label: "提交反馈", value: "SUBMIT_FEEDBACK" },
{ label: "审批反馈", value: "APPROVE_FEEDBACK" },
{ label: "拒绝反馈", value: "REJECT_FEEDBACK" },
{ label: "其他操作", value: "OTHER" }
];
const fetchLogs = async () => {
loading.value = true;
try {
const params: any = {
userId: filters.userId || undefined,
operationType: filters.operationType || undefined,
startTime: filters.timeRange?.[0]?.toISOString(),
endTime: filters.timeRange?.[1]?.toISOString(),
};
logs.value = await getOperationLogs(params);
} catch (error) {
ElMessage.error('获取操作日志失败');
console.error(error);
} finally {
loading.value = false;
}
};
const formatTime = (timeStr: string) => {
if (!timeStr) return '';
return new Date(timeStr).toLocaleString();
};
onMounted(() => {
fetchLogs();
});
</script>
<style scoped>
.filter-container {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,204 @@
<template>
<div class="public-submit-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>公共环境问题反馈</span>
<p>感谢您为环境保护出一份力您可以在此提交发现的环境问题无需登录</p>
</div>
</template>
<el-form :model="form" :rules="rules" ref="feedbackForm" label-width="120px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入反馈标题"></el-input>
</el-form-item>
<el-form-item label="问题描述" prop="description">
<el-input type="textarea" :rows="4" v-model="form.description" placeholder="请详细描述您发现的问题"></el-input>
</el-form-item>
<el-form-item label="污染类型" prop="pollutionType">
<el-select v-model="form.pollutionType" placeholder="请选择污染类型">
<el-option v-for="(label, key) in pollutionTypeMap" :key="key" :label="label" :value="key"></el-option>
</el-select>
</el-form-item>
<el-form-item label="严重程度" prop="severityLevel">
<el-rate v-model="rating" :max="3" :texts="['低', '中', '高']" show-text></el-rate>
</el-form-item>
<el-form-item label="联系方式" prop="contactInfo">
<el-input v-model="form.contactInfo" placeholder="选填,便于我们与您联系(邮箱或电话)"></el-input>
</el-form-item>
<el-form-item label="问题地址" prop="textAddress">
<el-input v-model="form.textAddress" placeholder="请输入问题发生的地址"></el-input>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="经度" prop="longitude">
<el-input-number v-model="form.longitude" :precision="6" :step="0.000001" placeholder="请输入经度"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="纬度" prop="latitude">
<el-input-number v-model="form.latitude" :precision="6" :step="0.000001" placeholder="请输入纬度"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="网格X" prop="gridX">
<el-input-number v-model="form.gridX" :precision="0" :step="1" placeholder="请输入网格X坐标"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="网格Y" prop="gridY">
<el-input-number v-model="form.gridY" :precision="0" :step="1" placeholder="请输入网格Y坐标"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="照片/视频附件">
<el-upload
ref="upload"
class="upload-demo"
action="#"
:on-remove="handleRemove"
:on-change="handleChange"
:file-list="fileList"
list-type="picture-card"
:auto-upload="false"
multiple
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :loading="loading">立即提交</el-button>
<el-button @click="resetForm">重置表单</el-button>
<el-button @click="$router.push('/login')">返回登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { PollutionType, SeverityLevel } from '@/api/feedback';
import { submitPublicFeedback } from '@/api/public'; // Assuming the function will be in a new api/public.ts
const feedbackForm = ref<FormInstance>();
const loading = ref(false);
const fileList = ref([]);
const rating = ref(2);
interface PublicFeedbackForm {
title: string;
description: string;
pollutionType: PollutionType;
severityLevel: SeverityLevel;
contactInfo?: string;
textAddress: string;
latitude?: number;
longitude?: number;
gridX?: number;
gridY?: number;
}
const form = reactive({
title: '',
description: '',
pollutionType: PollutionType.OTHER,
contactInfo: '',
textAddress: '',
latitude: 0,
longitude: 0,
gridX: 0,
gridY: 0,
});
const pollutionTypeMap: Record<PollutionType, string> = {
[PollutionType.PM25]: 'PM2.5',
[PollutionType.O3]: '臭氧 (O3)',
[PollutionType.NO2]: '二氧化氮 (NO2)',
[PollutionType.SO2]: '二氧化硫 (SO2)',
[PollutionType.OTHER]: '其他',
};
const rules = reactive<FormRules>({
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
description: [{ required: true, message: '请输入问题描述', trigger: 'blur' }],
pollutionType: [{ required: true, message: '请选择污染类型', trigger: 'change' }],
textAddress: [{ required: true, message: '请输入问题地址', trigger: 'blur' }],
gridX: [{ required: true, type: 'number', message: '网格X必须为数字' }],
gridY: [{ required: true, type: 'number', message: '网格Y必须为数字' }],
});
const handleRemove = (file, fileListUpdated) => {
fileList.value = fileListUpdated;
};
const handleChange = (file, fileListUpdated) => {
fileList.value = fileListUpdated;
}
const submitForm = async () => {
if (!feedbackForm.value) return;
await feedbackForm.value.validate(async (valid) => {
if (valid) {
loading.value = true;
try {
let severity: SeverityLevel;
if (rating.value === 1) severity = SeverityLevel.LOW;
else if (rating.value === 2) severity = SeverityLevel.MEDIUM;
else severity = SeverityLevel.HIGH;
const submissionData: PublicFeedbackForm = { ...form, severityLevel: severity };
const formData = new FormData();
formData.append('feedback', new Blob([JSON.stringify(submissionData)], { type: 'application/json' }));
fileList.value.forEach(file => {
formData.append('files', file.raw);
});
await submitPublicFeedback(formData);
ElMessage.success('反馈提交成功!感谢您的贡献。');
resetForm();
} catch (error) {
ElMessage.error('提交失败,请稍后再试。');
} finally {
loading.value = false;
}
}
});
};
const resetForm = () => {
feedbackForm.value?.resetFields();
fileList.value = [];
rating.value = 2;
};
</script>
<style scoped>
.public-submit-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
background-color: #f0f2f5;
}
.box-card {
width: 800px;
}
.card-header {
font-size: 1.2em;
font-weight: bold;
text-align: center;
}
.card-header p {
font-size: 0.8em;
font-weight: normal;
color: #666;
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,299 @@
<template>
<div class="register-container">
<el-card class="register-card">
<template #header>
<div class="card-header">
<span>创建您的账户</span>
</div>
</template>
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" label-position="top" hide-required-asterisk autocomplete="off">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="name">
<el-input v-model="registerForm.name" placeholder="您的真实姓名" size="large" autocomplete="off"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<el-input v-model="registerForm.phone" placeholder="您的手机号" size="large" autocomplete="off"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="邮箱" prop="email">
<el-input v-model="registerForm.email" placeholder="您的邮箱" size="large" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="验证码" prop="verificationCode">
<el-row :gutter="10" style="width: 100%;">
<el-col :span="15" class="verification-code-input">
<el-input v-model="registerForm.verificationCode" placeholder="请输入6位验证码" size="large"></el-input>
</el-col>
<el-col :span="9">
<el-button @click="sendVerificationCode" :disabled="isSendingCode || countdown > 0" :loading="isSendingCode" class="send-code-button" size="large">
{{ countdown > 0 ? `${countdown}秒后重发` : '发送验证码' }}
</el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="registerForm.password" placeholder="设置您的密码" show-password size="large" autocomplete="new-password"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="registerForm.confirmPassword" placeholder="请再次输入您的密码" show-password size="large" autocomplete="new-password"></el-input>
</el-form-item>
<el-form-item label="注册身份" prop="role">
<el-select v-model="registerForm.role" placeholder="请选择您的身份" style="width: 100%;" size="large">
<el-option label="公众监督员" value="PUBLIC_SUPERVISOR"></el-option>
<el-option label="网格员" value="GRID_WORKER"></el-option>
<el-option label="业务主管" value="SUPERVISOR"></el-option>
<el-option label="系统管理员" value="ADMIN"></el-option>
<el-option label="决策者" value="DECISION_MAKER"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleRegister" :loading="isRegistering" class="register-button">立即注册</el-button>
</el-form-item>
<div class="extra-links">
<el-link type="info" @click="$router.push('/login')">已有账户返回登录</el-link>
</div>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import type { FormInstance, FormRules } from 'element-plus';
import { ElMessage } from 'element-plus';
import { signup, sendVerificationCode as apiSendCode } from '@/api/auth';
const router = useRouter();
const registerFormRef = ref<FormInstance>();
const isRegistering = ref(false);
const registerForm = reactive({
name: '',
phone: '',
email: '',
password: '',
confirmPassword: '',
role: '',
verificationCode: '',
});
const validatePass2 = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== registerForm.password) {
callback(new Error("两次输入的密码不一致!"));
} else {
callback();
}
};
const validatePhone = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('请输入手机号'));
} else if (!/^1\d{10}$/.test(value)) {
callback(new Error('手机号必须是以1开头的11位数字'));
} else {
callback();
}
};
const registerRules = reactive<FormRules>({
email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }, { type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] }],
phone: [{ required: true, validator: validatePhone, trigger: 'blur' }],
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
confirmPassword: [{ validator: validatePass2, trigger: 'blur' }],
role: [{ required: true, message: '请选择注册身份', trigger: 'change' }],
verificationCode: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
});
const isSendingCode = ref(false);
const countdown = ref(0);
let timer: number | null = null;
const sendVerificationCode = async () => {
if (!registerForm.email) {
ElMessage.warning('请输入您的邮箱地址');
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(registerForm.email)) {
ElMessage.warning('请输入有效的邮箱地址');
return;
}
isSendingCode.value = true;
try {
await apiSendCode(registerForm.email);
ElMessage.success('验证码已发送,请注意查收');
countdown.value = 60;
if(timer) clearInterval(timer);
timer = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
if(timer) clearInterval(timer);
timer = null;
}
}, 1000);
} catch (error) {
ElMessage.error('验证码发送失败,请稍后重试');
console.error(error);
} finally {
isSendingCode.value = false;
}
};
const handleRegister = async () => {
if (!registerFormRef.value) return;
await registerFormRef.value.validate(async (valid) => {
if (valid) {
isRegistering.value = true;
try {
const { confirmPassword, ...signupData } = registerForm;
await signup(signupData);
ElMessage.success('注册成功!正在跳转到登录页面...');
setTimeout(() => {
router.push('/login');
}, 1500);
} catch (error) {
ElMessage.error('注册失败,请检查您填写的信息。');
console.error(error);
} finally {
isRegistering.value = false;
}
}
});
};
</script>
<style scoped>
.register-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
width: 100vw;
overflow-y: auto; /* Allow scrolling only if card is too tall */
padding: 40px 0;
background-image: url('https://images.unsplash.com/photo-1506744038136-46273834b3fb?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80');
background-size: cover;
background-position: center;
}
.register-card {
width: 550px; /* Wider card for more fields */
border-radius: 25px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
background: rgba(245, 245, 245, 0.85);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.5);
}
.card-header {
text-align: center;
font-size: 26px;
font-weight: 600;
color: #004d40;
padding: 25px 0 15px 0;
border-bottom: 1px solid rgba(0,77,64,0.2);
}
.register-button {
width: 100%;
border: none;
background: #004d40;
color: white;
letter-spacing: 5px;
padding-left: 10px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 77, 64, 0.4);
border-radius: 10px;
font-size: 16px;
height: 45px;
}
.register-button:hover {
background: #00695c;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 77, 64, 0.5);
}
.extra-links {
text-align: center;
margin-top: 15px;
}
.extra-links .el-link {
color: #555;
font-size: 15px;
}
.extra-links .el-link:hover {
color: #004d40;
}
/* Add scoped styles for the verification code input and button */
:deep(.verification-code-input .el-input__wrapper) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.send-code-button {
width: 100%;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background-color: #004d40;
color: white;
border-color: #004d40;
}
.send-code-button:hover {
background-color: #00695c;
border-color: #00695c;
}
</style>
<style>
/* Global overrides to match login page style */
:deep(.register-container .el-input__wrapper),
:deep(.register-container .el-select .el-input__wrapper) {
background-color: rgba(255, 255, 255, 0.9) !important;
border-radius: 10px !important;
height: 45px;
box-shadow: none !important;
border: 1px solid transparent !important;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
:deep(.register-container .el-input__wrapper:hover),
:deep(.register-container .el-select .el-input__wrapper:hover) {
border-color: #ccc !important;
}
:deep(.register-container .el-input__wrapper.is-focus),
:deep(.register-container .el-select .el-input__wrapper.is-focused) {
border-color: #004d40 !important;
box-shadow: 0 0 0 1px rgba(0, 77, 64, 0.2) !important;
}
.register-container .el-form-item__label {
color: #555 !important;
font-weight: 500 !important;
letter-spacing: 0.5px !important;
font-size: 15px !important;
}
.register-container .el-input__inner {
color: #333 !important;
font-size: 16px !important;
}
.register-container .el-button {
height: 45px;
border-radius: 10px;
}
</style>

View File

@@ -0,0 +1,217 @@
<template>
<div class="reset-password-container">
<el-card class="reset-password-card">
<div class="card-header">
<h2>重置密码</h2>
<p>请输入您的验证码和新密码</p>
</div>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
hide-required-asterisk
@submit.prevent="handleSubmit"
autocomplete="off"
>
<el-form-item label="邮箱地址" prop="email">
<el-input
v-model="form.email"
placeholder="请输入您的邮箱地址"
size="large"
autocomplete="email"
disabled
/>
</el-form-item>
<el-form-item label="验证码" prop="code">
<el-input
v-model="form.code"
placeholder="请输入6位邮箱验证码"
size="large"
/>
</el-form-item>
<el-form-item label="新密码" prop="password">
<el-input
type="password"
v-model="form.password"
placeholder="请输入您的新密码"
show-password
size="large"
autocomplete="new-password"
/>
</el-form-item>
<el-form-item label="确认新密码" prop="confirmPassword">
<el-input
type="password"
v-model="form.confirmPassword"
placeholder="请再次输入新密码"
show-password
size="large"
autocomplete="new-password"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
native-type="submit"
:loading="isLoading"
class="submit-button"
size="large"
>
确认重置
</el-button>
</el-form-item>
</el-form>
<div class="card-footer">
<el-link @click="$router.push('/login')">返回登录</el-link>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { useRouter, useRoute } from 'vue-router';
import type { FormInstance, FormRules } from 'element-plus';
import { resetPasswordWithCode } from '@/api/auth';
const router = useRouter();
const route = useRoute();
const formRef = ref<FormInstance>();
const isLoading = ref(false);
const form = reactive({
email: '',
code: '',
password: '',
confirmPassword: '',
});
onMounted(() => {
if (route.query.email) {
form.email = route.query.email as string;
} else {
ElMessage.error('无效的重置链接,请返回重试。');
router.push('/forgot-password');
}
});
const validatePass = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== form.password) {
callback(new Error("两次输入的密码不一致!"));
} else {
callback();
}
};
const rules = reactive<FormRules>({
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入有效的邮箱地址', trigger: ['blur', 'change'] }
],
code: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ len: 6, message: '验证码必须是6位', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' },
],
confirmPassword: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{ validator: validatePass, trigger: 'blur' }
],
});
const handleSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid) => {
if (valid) {
isLoading.value = true;
try {
await resetPasswordWithCode({
email: form.email,
code: form.code,
newPassword: form.password,
});
ElMessage.success('密码重置成功!即将跳转到登录页...');
setTimeout(() => router.push('/login'), 2000);
} catch (error) {
ElMessage.error('密码重置失败,请检查验证码是否正确。');
console.error(error);
} finally {
isLoading.value = false;
}
}
});
};
</script>
<style scoped>
.reset-password-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-image: url('https://images.unsplash.com/photo-1506744038136-46273834b3fb?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80');
background-size: cover;
background-position: center;
}
.reset-password-card {
width: 480px;
border-radius: 25px;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
padding: 20px;
}
.card-header {
text-align: center;
margin-bottom: 2rem;
}
.card-header h2 {
color: #004d40;
margin-bottom: 0.5rem;
font-weight: 600;
}
.card-header p {
color: #607d8b;
font-size: 0.9rem;
}
.el-form-item {
margin-bottom: 1.8rem;
}
.submit-button {
width: 100%;
border-radius: 10px;
background-color: #004d40;
border-color: #004d40;
transition: background-color 0.3s ease;
font-size: 1rem;
padding: 1.2rem 0;
}
.submit-button:hover {
background-color: #00695c;
border-color: #00695c;
}
.card-footer {
margin-top: 1rem;
text-align: center;
}
</style>

View File

@@ -0,0 +1,190 @@
<template>
<div class="submit-feedback-container">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>提交新的环境问题反馈</span>
</div>
</template>
<el-form :model="form" :rules="rules" ref="feedbackForm" label-width="120px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入反馈标题"></el-input>
</el-form-item>
<el-form-item label="问题描述" prop="description">
<el-input type="textarea" :rows="4" v-model="form.description" placeholder="请详细描述您发现的问题"></el-input>
</el-form-item>
<el-form-item label="污染类型" prop="pollutionType">
<el-select v-model="form.pollutionType" placeholder="请选择污染类型">
<el-option v-for="(label, key) in pollutionTypeMap" :key="key" :label="label" :value="key"></el-option>
</el-select>
</el-form-item>
<el-form-item label="严重程度" prop="severityLevel">
<el-rate v-model="rating" :max="3" :texts="['低', '中', '高']" show-text></el-rate>
</el-form-item>
<el-form-item label="问题地址" prop="location.textAddress">
<el-input v-model="form.location.textAddress" placeholder="请输入问题发生的地址"></el-input>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="经度" prop="location.longitude">
<el-input-number v-model="form.location.longitude" :precision="6" :step="0.000001" placeholder="请输入经度"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="纬度" prop="location.latitude">
<el-input-number v-model="form.location.latitude" :precision="6" :step="0.000001" placeholder="请输入纬度"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="网格X" prop="location.gridX">
<el-input-number v-model="form.location.gridX" :precision="0" :step="1" placeholder="请输入网格X坐标"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="网格Y" prop="location.gridY">
<el-input-number v-model="form.location.gridY" :precision="0" :step="1" placeholder="请输入网格Y坐标"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="照片/视频附件">
<el-upload
ref="upload"
class="upload-demo"
action="#"
:on-preview="handlePreview"
:on-remove="handleRemove"
:on-change="handleChange"
:file-list="fileList"
list-type="picture-card"
:auto-upload="false"
multiple
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :loading="loading">立即提交</el-button>
<el-button @click="resetForm">重置表单</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { useFeedbackStore } from '@/store/feedback';
import { PollutionType } from '@/api/types';
import { SeverityLevel } from '@/api/feedback';
import type { FeedbackSubmissionRequest, LocationInfo } from '@/api/feedback';
const feedbackForm = ref<FormInstance>();
const loading = ref(false);
const fileList = ref([]);
const rating = ref(2); // For el-rate which uses a number. 1=LOW, 2=MEDIUM, 3=HIGH
const form = reactive<Omit<FeedbackSubmissionRequest, 'severityLevel'>>({
title: '',
description: '',
pollutionType: PollutionType.PM25,
location: {
latitude: 0,
longitude: 0,
textAddress: '',
gridX: 0,
gridY: 0,
},
});
const pollutionTypeMap: Record<PollutionType, string> = {
[PollutionType.PM25]: 'PM2.5',
[PollutionType.O3]: '臭氧 (O3)',
[PollutionType.NO2]: '二氧化氮 (NO2)',
[PollutionType.SO2]: '二氧化硫 (SO2)',
[PollutionType.OTHER]: '其他',
};
const rules = reactive<FormRules>({
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
description: [{ required: true, message: '请输入问题描述', trigger: 'blur' }],
pollutionType: [{ required: true, message: '请选择污染类型', trigger: 'change' }],
'location.textAddress': [{ required: true, message: '请输入问题地址', trigger: 'blur' }],
'location.latitude': [{ required: true, type: 'number', message: '纬度必须为数字' }],
'location.longitude': [{ required: true, type: 'number', message: '经度必须为数字' }],
'location.gridX': [{ required: true, type: 'number', message: '网格X必须为数字' }],
'location.gridY': [{ required: true, type: 'number', message: '网格Y必须为数字' }],
});
const feedbackStore = useFeedbackStore();
const handleRemove = (file, fileListUpdated) => {
fileList.value = fileListUpdated;
};
const handlePreview = (file) => {
console.log(file);
};
const handleChange = (file, fileListUpdated) => {
fileList.value = fileListUpdated;
}
const submitForm = async () => {
if (!feedbackForm.value) return;
await feedbackForm.value.validate(async (valid) => {
if (valid) {
loading.value = true;
try {
let severity: SeverityLevel;
if (rating.value === 1) {
severity = SeverityLevel.LOW;
} else if (rating.value === 2) {
severity = SeverityLevel.MEDIUM;
} else {
severity = SeverityLevel.HIGH;
}
const submissionData: FeedbackSubmissionRequest = {
...form,
severityLevel: severity,
};
const formData = new FormData();
formData.append('feedback', new Blob([JSON.stringify(submissionData)], { type: 'application/json' }));
fileList.value.forEach(file => {
formData.append('files', file.raw);
});
await feedbackStore.submitFeedback(formData);
ElMessage.success('反馈提交成功!感谢您的贡献。');
resetForm();
} catch (error) {
ElMessage.error('提交失败,请稍后再试。');
} finally {
loading.value = false;
}
}
});
};
const resetForm = () => {
feedbackForm.value?.resetFields();
fileList.value = [];
rating.value = 2;
};
</script>
<style scoped>
.submit-feedback-container {
padding: 20px;
}
.card-header {
font-size: 1.2em;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<div class="supervisor-view">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>待审核反馈</span>
</div>
</template>
<el-table :data="pendingFeedback" style="width: 100%" v-loading="loading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="title" label="标题"></el-table-column>
<el-table-column prop="description" label="描述" show-overflow-tooltip></el-table-column>
<el-table-column prop="pollutionType" label="污染类型" width="120"></el-table-column>
<el-table-column prop="severityLevel" label="严重等级" width="120"></el-table-column>
<el-table-column prop="createdAt" label="提交时间" width="180">
<template #default="scope">
{{ formatDateTime(scope.row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button size="small" @click="handleCreateTask(scope.row)">创建任务</el-button>
<el-button size="small" type="danger" @click="handleRejectFeedback(scope.row)">拒绝</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- TODO: Add Task Management Section -->
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import type { Feedback } from '@/api/types';
import { getPendingFeedback, processFeedback } from '@/api/feedback'; // Assuming processFeedback exists
import { formatDateTime } from '@/utils/formatter';
const pendingFeedback = ref<Feedback[]>([]);
const loading = ref(false);
const fetchPendingFeedback = async () => {
loading.value = true;
try {
const response = await getPendingFeedback();
pendingFeedback.value = response.data;
} catch (error) {
ElMessage.error('获取待审核反馈失败');
console.error(error);
} finally {
loading.value = false;
}
};
const handleCreateTask = (feedback: Feedback) => {
// TODO: Implement task creation logic
console.log('Create task from feedback:', feedback.id);
ElMessage.info(`准备为反馈 #${feedback.id} 创建任务`);
};
const handleRejectFeedback = async (feedback: Feedback) => {
try {
await ElMessageBox.confirm(`确定要拒绝这条反馈吗? (ID: ${feedback.id})`, '确认操作', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
// This assumes a 'processFeedback' function exists to update feedback status
// to something like 'REJECTED' or 'CLOSED_INVALID'.
// We may need to create or adjust this backend logic.
await processFeedback(feedback.id, { status: 'CLOSED_INVALID' }); // Placeholder status
ElMessage.success('反馈已拒绝');
fetchPendingFeedback(); // Refresh list
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('操作失败');
}
}
};
onMounted(() => {
fetchPendingFeedback();
});
</script>
<style scoped>
.supervisor-view {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -0,0 +1,12 @@
<template>
<div>
<h1>系统设置</h1>
<p>这里是系统设置页面</p>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,289 @@
<template>
<div class="task-detail-view" v-if="task">
<el-card class="page-card">
<template #header>
<div class="card-header">
<span>任务详情: {{ task.title }}</span>
<el-tag :type="getStatusTagType(task.status)">{{ formatStatus(task.status) }}</el-tag>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="任务ID">{{ task.id }}</el-descriptions-item>
<el-descriptions-item label="严重程度">
<el-tag :type="getSeverityTagType(task.severityLevel)">{{ formatSeverity(task.severityLevel) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="污染类型">{{ task.pollutionType || '未指定' }}</el-descriptions-item>
<el-descriptions-item label="网格坐标">X: {{ task.gridX }}, Y: {{ task.gridY }}</el-descriptions-item>
<el-descriptions-item label="分配时间">{{ task.assignedAt ? new Date(task.assignedAt).toLocaleString() : 'N/A' }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ task.completedAt ? new Date(task.completedAt).toLocaleString() : 'N/A' }}</el-descriptions-item>
<el-descriptions-item label="地址" :span="2">{{ task.textAddress || '未提供' }}</el-descriptions-item>
<el-descriptions-item label="任务描述" :span="2">{{ task.description }}</el-descriptions-item>
</el-descriptions>
<div v-if="task.feedback">
<el-divider />
<h3>关联反馈信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="反馈ID">{{ task.feedback.feedbackId }}</el-descriptions-item>
<el-descriptions-item label="提交人">{{ task.feedback.submitterName }}</el-descriptions-item>
<el-descriptions-item label="反馈时间">{{ new Date(task.feedback.createdAt).toLocaleString() }}</el-descriptions-item>
<el-descriptions-item label="反馈标题">{{ task.feedback.title }}</el-descriptions-item>
<el-descriptions-item label="反馈描述" :span="2">{{ task.feedback.description }}</el-descriptions-item>
<el-descriptions-item label="现场图片" :span="2" v-if="task.feedback.imageUrls && task.feedback.imageUrls.length > 0">
<el-image
v-for="url in task.feedback.imageUrls"
:key="url"
:src="url"
:preview-src-list="task.feedback.imageUrls"
style="width: 100px; height: 100px; margin-right: 10px;"
fit="cover"
/>
</el-descriptions-item>
</el-descriptions>
</div>
<el-divider />
<h3>任务历史</h3>
<div v-if="task.history && task.history.length > 0">
<el-timeline>
<el-timeline-item
v-for="item in task.history"
:key="item.id"
:timestamp="new Date(item.changedAt).toLocaleString()"
:type="getHistoryType(item.newStatus)"
>
<strong>{{ formatStatus(item.newStatus) }}</strong>
<p>{{ item.comments }}</p>
</el-timeline-item>
</el-timeline>
</div>
<el-empty v-else description="暂无历史记录" />
<div v-if="task.submissionInfo">
<el-divider />
<h3>任务提交详情</h3>
<el-descriptions :column="1" border>
<el-descriptions-item label="提交说明">{{ task.submissionInfo.comments }}</el-descriptions-item>
<el-descriptions-item label="提交附件" v-if="task.submissionInfo.attachments && task.submissionInfo.attachments.length > 0">
<div v-for="att in task.submissionInfo.attachments" :key="att.id">
<el-link :href="att.url" type="primary" target="_blank">{{ att.fileName }}</el-link>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
<div class="action-buttons">
<el-button v-if="task.status === 'ASSIGNED'" type="success" @click="acceptTask" :loading="actionLoading">接受任务</el-button>
<el-button v-if="task.status === 'IN_PROGRESS'" type="primary" @click="openSubmitDialog" :loading="actionLoading">提交进度</el-button>
<el-button @click="$router.back()">返回列表</el-button>
</div>
</el-card>
</div>
<div v-else-if="loading">
<el-card class="page-card" v-loading="true" style="min-height: 400px;"></el-card>
</div>
<div v-else>
<el-card class="page-card">
<el-empty description="无法加载任务详情" />
</el-card>
</div>
<SubmitProgressDialog
v-if="task"
v-model:visible="isSubmitDialogVisible"
:task-id="task.id"
@submitted="handleSubmitted"
/>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import apiClient from '@/api';
import { ElMessage, ElMessageBox } from 'element-plus';
import SubmitProgressDialog from '@/components/SubmitProgressDialog.vue';
// --- 类型定义 ---
interface TaskHistoryDTO {
id: number;
changedAt: string;
newStatus: string;
comments: string;
}
interface FeedbackDTO {
feedbackId: number;
eventId: string;
title: string;
description: string;
severityLevel: string;
textAddress: string;
submitterId: number;
submitterName: string;
createdAt: string;
imageUrls: string[];
}
interface AssigneeDTO {
id: number;
name: string;
phone: string;
}
interface AttachmentDTO {
id: number;
fileName: string;
fileType: string;
url: string;
}
interface SubmissionInfoDTO {
comments: string;
attachments: AttachmentDTO[];
}
interface TaskDetail {
id: number;
feedback: FeedbackDTO;
title: string;
description: string;
severityLevel: string;
pollutionType: string;
textAddress: string;
gridX: number;
gridY: number;
status: string;
assignee: AssigneeDTO;
assignedAt: string | null;
completedAt: string | null;
history: TaskHistoryDTO[];
submissionInfo: SubmissionInfoDTO | null;
}
// --- 响应式状态 ---
const route = useRoute();
const router = useRouter();
const taskId = ref(route.params.id as string);
const task = ref<TaskDetail | null>(null);
const loading = ref(false);
const actionLoading = ref(false);
const isSubmitDialogVisible = ref(false);
// --- API 调用 ---
const fetchTaskDetails = async () => {
loading.value = true;
try {
const response = await apiClient.get(`/worker/${taskId.value}`);
task.value = response as TaskDetail;
} catch (error) {
console.error(`获取任务详情失败 (ID: ${taskId.value}):`, error);
ElMessage.error('获取任务详情失败');
} finally {
loading.value = false;
}
};
const acceptTask = async () => {
actionLoading.value = true;
try {
await ElMessageBox.confirm('确定要接受这个任务吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
});
await apiClient.post(`/worker/${taskId.value}/accept`);
ElMessage.success('任务已接受');
fetchTaskDetails(); // Refresh details
} catch (error) {
if (error !== 'cancel') {
console.error('接受任务失败:', error);
ElMessage.error('接受任务失败');
}
} finally {
actionLoading.value = false;
}
};
const openSubmitDialog = () => {
isSubmitDialogVisible.value = true;
};
const handleSubmitted = () => {
fetchTaskDetails();
};
// --- 辅助函数 ---
const formatStatus = (status: string) => {
const map: Record<string, string> = {
'ASSIGNED': '待接受',
'IN_PROGRESS': '进行中',
'SUBMITTED': '已提交',
'COMPLETED': '已完成',
'REJECTED': '已拒绝',
};
return map[status] || status;
};
const getStatusTagType = (status: string): 'primary' | 'warning' | 'info' | 'success' | 'danger' => {
const map: Record<string, 'primary' | 'warning' | 'info' | 'success' | 'danger'> = {
'ASSIGNED': 'primary',
'IN_PROGRESS': 'warning',
'SUBMITTED': 'info',
'COMPLETED': 'success',
'REJECTED': 'danger',
};
return map[status] || 'info';
};
const formatSeverity = (severity: string) => {
const map: Record<string, string> = {
'LOW': '低',
'MEDIUM': '中',
'HIGH': '高',
};
return map[severity] || severity;
};
const getSeverityTagType = (severity: string): 'info' | 'warning' | 'danger' => {
const map: Record<string, 'info' | 'warning' | 'danger'> = {
'LOW': 'info',
'MEDIUM': 'warning',
'HIGH': 'danger',
};
return map[severity] || 'info';
};
const getHistoryType = (status: string): 'primary' | 'success' | 'info' => {
switch (status) {
case 'ASSIGNED':
case 'IN_PROGRESS':
return 'primary';
case 'COMPLETED':
return 'success';
default:
return 'info';
}
};
onMounted(() => {
fetchTaskDetails();
});
</script>
<style scoped>
.task-detail-view {
padding: 20px;
}
.page-card {
border-radius: 8px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
font-weight: 600;
}
.action-buttons {
margin-top: 20px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,12 @@
<template>
<div>
<h1>任务管理</h1>
<p>这里是任务管理页面</p>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,437 @@
<template>
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>人员管理</span>
</div>
</template>
<div class="table-header">
<el-input v-model="searchName" placeholder="按姓名搜索" clearable @clear="fetchUsers" @keyup.enter="fetchUsers" style="width: 200px;"></el-input>
<el-select v-model="searchRole" placeholder="按角色筛选" clearable @change="fetchUsers" style="width: 150px;">
<el-option label="管理员" value="ADMIN"></el-option>
<el-option label="决策者" value="DECISION_MAKER"></el-option>
<el-option label="主管" value="SUPERVISOR"></el-option>
<el-option label="网格员" value="GRID_WORKER"></el-option>
<el-option label="公众监督员" value="PUBLIC_SUPERVISOR"></el-option>
</el-select>
<el-button type="primary" @click="fetchUsers" :icon="Search">搜索</el-button>
</div>
<el-table :data="users" v-loading="loading" style="width: 100%" border stripe table-layout="auto" class="user-table compact-table">
<el-table-column prop="id" label="ID" width="60" align="center"></el-table-column>
<el-table-column prop="name" label="姓名" width="120" align="center"></el-table-column>
<el-table-column prop="email" label="邮箱" min-width="180"></el-table-column>
<el-table-column prop="phone" label="手机号" width="120" align="center"></el-table-column>
<el-table-column prop="gender" label="性别" width="70" align="center">
<template #default="scope">
{{ formatGender(scope.row.gender) }}
</template>
</el-table-column>
<el-table-column prop="role" label="角色" width="150" align="center">
<template #default="scope">
<el-tag :type="roleTagType(scope.row.role)" size="small">{{ formatRole(scope.row.role) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="statusTagType(scope.row.status)" size="small" disable-transitions>{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-popconfirm title="确定要删除这位用户吗?" @confirm="handleDelete(scope.row.id)">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="total > 0"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-sizes="[10, 20, 50, 100]"
v-model:page-size="pagination.size"
v-model:current-page="pagination.page"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
style="margin-top: 20px; justify-content: flex-end;"
></el-pagination>
<!-- Edit User Dialog -->
<el-dialog v-model="editDialogVisible" title="编辑用户信息" width="700px" :show-close="false" class="edit-dialog ultimate-dialog">
<template #header="{ close, titleId, titleClass }">
<div class="ultimate-dialog-header">
<h4 :id="titleId" :class="titleClass">
<el-icon class="header-icon"><EditPen /></el-icon>
编辑用户信息
</h4>
<el-button circle :icon="Close" type="danger" class="header-close-btn" @click="close"></el-button>
</div>
</template>
<el-form :model="editForm" ref="editFormRef" label-width="90px" label-position="top">
<div class="form-section">
<h3 class="section-title">身份凭证</h3>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item>
<template #label>
<div class="form-label-icon">
<el-icon><User /></el-icon>
<span>用户ID</span>
</div>
</template>
<el-input v-model="editForm.id" disabled class="readonly-input"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<div class="form-label-icon">
<el-icon><Avatar /></el-icon>
<span>姓名</span>
</div>
</template>
<el-input v-model="editForm.name" disabled class="readonly-input"></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="form-section">
<h3 class="section-title">详细信息</h3>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item prop="phone">
<template #label>
<div class="form-label-icon">
<el-icon><Phone /></el-icon>
<span>手机号</span>
</div>
</template>
<el-input v-model="editForm.phone" placeholder="请输入手机号"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="gender">
<template #label>
<div class="form-label-icon">
<el-icon><Male /></el-icon>
<span>性别</span>
</div>
</template>
<el-radio-group v-model="editForm.gender">
<el-radio label="MALE"></el-radio>
<el-radio label="FEMALE"></el-radio>
<el-radio label="OTHER">其他</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item prop="role">
<template #label>
<div class="form-label-icon">
<el-icon><Key /></el-icon>
<span>角色</span>
</div>
</template>
<el-select v-model="editForm.role" placeholder="选择角色" style="width:100%;">
<el-option label="管理员" value="ADMIN"></el-option>
<el-option label="决策者" value="DECISION_MAKER"></el-option>
<el-option label="主管" value="SUPERVISOR"></el-option>
<el-option label="网格员" value="GRID_WORKER"></el-option>
<el-option label="公众监督员" value="PUBLIC_SUPERVISOR"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="status">
<template #label>
<div class="form-label-icon">
<el-icon><Switch /></el-icon>
<span>状态</span>
</div>
</template>
<el-select v-model="editForm.status" placeholder="选择状态" style="width:100%;">
<el-option label="ACTIVE" value="ACTIVE"></el-option>
<el-option label="INACTIVE" value="INACTIVE"></el-option>
<el-option label="SUSPENDED" value="SUSPENDED"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false" plain> </el-button>
<el-button type="primary" @click="submitEdit" :loading="isSaving" :icon="Check"> </el-button>
</span>
</template>
</el-dialog>
</el-card>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Search, User, Avatar, Phone, Male, Key, Switch, Close, Check, EditPen } from '@element-plus/icons-vue';
import { getUsers, updateUser, deleteUser } from '@/api/personnel';
import type { UserAccount, UserUpdateRequest } from '@/api/types';
const users = ref<UserAccount[]>([]);
const loading = ref(true);
const total = ref(0);
const searchName = ref('');
const searchRole = ref('');
const pagination = reactive({
page: 1,
size: 10,
});
const editDialogVisible = ref(false);
const isSaving = ref(false);
const editFormRef = ref();
const editForm = reactive<Partial<UserAccount>>({});
const roleMap: Record<string, string> = {
ADMIN: '管理员',
DECISION_MAKER: '决策者',
SUPERVISOR: '主管',
GRID_WORKER: '网格员',
PUBLIC_SUPERVISOR: '公众监督员'
};
const formatRole = (role: string) => roleMap[role] || role;
const formatGender = (gender: 'MALE' | 'FEMALE' | 'OTHER') => {
if (gender === 'MALE') return '男';
if (gender === 'FEMALE') return '女';
return '未知';
}
const roleTagType = (role: string) => {
switch (role) {
case 'ADMIN': return 'danger';
case 'DECISION_MAKER': return 'warning';
case 'SUPERVISOR': return 'primary';
default: return 'info';
}
};
const statusTagType = (status: string) => {
if (status === 'ACTIVE') return 'success';
if (status === 'INACTIVE') return 'info';
if (status === 'SUSPENDED') return 'warning';
return '';
};
const fetchUsers = async () => {
loading.value = true;
try {
const params = {
page: pagination.page - 1,
size: pagination.size,
name: searchName.value || undefined,
role: searchRole.value || undefined,
};
const response = await getUsers(params);
users.value = response.content;
total.value = response.totalElements;
} catch (error) {
console.error("获取用户列表时出错:", error);
ElMessage.error('获取用户列表失败,请检查网络或联系管理员。');
} finally {
loading.value = false;
}
};
const handleEdit = (user: UserAccount) => {
Object.assign(editForm, user);
editDialogVisible.value = true;
};
const submitEdit = async () => {
if (!editForm.id) return;
isSaving.value = true;
const updateData: UserUpdateRequest = {
phone: editForm.phone,
gender: editForm.gender as 'MALE' | 'FEMALE' | 'OTHER',
role: editForm.role,
status: editForm.status,
};
try {
await updateUser(editForm.id, updateData);
ElMessage.success('用户信息更新成功');
editDialogVisible.value = false;
await fetchUsers();
} catch (error) {
console.error(`更新用户 #${editForm.id} 失败:`, error);
ElMessage.error('更新失败,请重试。');
} finally {
isSaving.value = false;
}
};
const handleDelete = async (userId: number) => {
try {
await deleteUser(userId);
ElMessage.success('删除成功');
await fetchUsers();
} catch (error) {
console.error(`删除用户 #${userId} 失败:`, error);
ElMessage.error('删除失败,请重试。');
}
};
const handleSizeChange = (size: number) => {
pagination.size = size;
fetchUsers();
};
const handleCurrentChange = (page: number) => {
pagination.page = page;
fetchUsers();
};
onMounted(() => {
fetchUsers();
});
</script>
<style scoped>
.box-card {
width: 100%;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 20px;
font-weight: bold;
}
.table-header {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.user-table {
font-size: 14px;
}
.compact-table :deep(.el-table__cell) {
padding: 6px 0;
}
.user-table .el-button {
margin: 0 4px;
}
.el-pagination {
margin-top: 20px;
justify-content: flex-end;
}
:deep(.el-table__row .el-button) {
visibility: hidden;
}
:deep(.el-table__row:hover .el-button) {
visibility: visible;
}
/* Ultimate Dialog Styles */
.ultimate-dialog .el-dialog__header {
display: none; /* Hide original header */
}
.ultimate-dialog {
--el-dialog-padding-primary: 0;
background: linear-gradient(to top right, #e0f2f1, #ffffff);
}
.ultimate-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border-bottom: 1px solid var(--el-border-color-lighter);
}
.ultimate-dialog-header h4 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.header-icon {
font-size: 1.3rem;
}
.header-close-btn {
--el-button-bg-color: transparent;
--el-button-border-color: transparent;
--el-button-hover-bg-color: var(--el-color-danger-light-8);
--el-button-hover-border-color: var(--el-color-danger-light-8);
}
.ultimate-dialog .el-dialog__body {
padding: 24px 30px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%);
}
.form-section {
background-color: rgba(255, 255, 255, 0.7);
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.05);
margin-bottom: 25px;
border: 1px solid rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
}
.section-title {
font-size: 1rem;
font-weight: 600;
color: #495057;
margin-top: 0;
margin-bottom: 24px;
border-left: 4px solid var(--el-color-primary);
padding-left: 12px;
}
.form-label-icon {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
color: #343a40;
}
.readonly-input {
--el-input-disabled-bg-color: #eef1f6;
--el-input-disabled-text-color: #303133;
--el-input-disabled-border-color: #dcdfe6;
}
.dialog-footer {
text-align: right;
padding: 10px 30px 20px;
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<div class="profile-container">
<el-card class="profile-card">
<template #header>
<div class="card-header">
<span>个人信息</span>
</div>
</template>
<div v-if="user" class="profile-content">
<el-descriptions :column="2" border>
<el-descriptions-item label="用户ID" label-align="right" align="center" label-class-name="my-label">
{{ user.id }}
</el-descriptions-item>
<el-descriptions-item label="姓名" label-align="right" align="center">
{{ user.name }}
</el-descriptions-item>
<el-descriptions-item label="邮箱" label-align="right" align="center">
{{ user.email }}
</el-descriptions-item>
<el-descriptions-item label="电话" label-align="right" align="center">
{{ user.phone || 'N/A' }}
</el-descriptions-item>
<el-descriptions-item label="角色" label-align="right" align="center">
<el-tag>{{ user.role }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态" label-align="right" align="center">
<el-tag :type="user.status === 'ACTIVE' ? 'success' : 'danger'">{{ user.status }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="所属区域" label-align="right" align="center" :span="2">
{{ user.region || 'N/A' }}
</el-descriptions-item>
<el-descriptions-item label="技能" label-align="right" align="center" :span="2">
<template v-if="user.skills && user.skills.length">
<el-tag v-for="skill in user.skills" :key="skill" type="info" style="margin-right: 8px;">
{{ skill }}
</el-tag>
</template>
<span v-else>N/A</span>
</el-descriptions-item>
</el-descriptions>
</div>
<div v-else>
<p>正在加载用户信息...</p>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth';
import { storeToRefs } from 'pinia';
// 从 Pinia store 中获取用户信息
const authStore = useAuthStore();
// 使用 storeToRefs 来保持响应性
const { user } = storeToRefs(authStore);
</script>
<style scoped>
.profile-container {
padding: 20px;
}
.profile-card {
max-width: 800px;
margin: 0 auto;
}
.card-header {
font-size: 20px;
font-weight: bold;
}
.profile-content {
padding: 20px;
}
.my-label {
background: var(--el-color-success-light-9) !important;
}
</style>

View File

@@ -0,0 +1,337 @@
<template>
<div class="grid-management-container">
<!-- Page Header -->
<div class="page-header">
<h2>我的网格地图</h2>
<div class="header-actions">
<el-button type="primary" @click="refreshData" :loading="loading || isLoadingWorkers" icon="Refresh">
刷新数据
</el-button>
</div>
</div>
<!-- Main Content -->
<div class="main-content-grid">
<!-- Grid Map Area -->
<div class="map-area" v-loading="loading">
<div v-if="error" class="error-message">
<el-alert title="地图数据加载失败" :description="error" type="error" show-icon :closable="false" />
<el-button @click="fetchAllGrids" type="primary" class="retry-button">重试</el-button>
</div>
<svg v-else-if="displayGrids.length" :width="svgDimensions.width" :height="svgDimensions.height" class="grid-svg">
<defs>
<pattern id="grid-pattern-worker" :width="CELL_SIZE" :height="CELL_SIZE" patternUnits="userSpaceOnUse">
<path :d="`M ${CELL_SIZE} 0 L 0 0 0 ${CELL_SIZE}`" fill="none" stroke="#e9e9e9" stroke-width="0.5" />
</pattern>
</defs>
<rect :width="svgDimensions.width" :height="svgDimensions.height" fill="url(#grid-pattern-worker)" />
<g v-for="grid in displayGrids" :key="grid.id">
<rect
:x="grid.displayX * CELL_SIZE"
:y="grid.displayY * CELL_SIZE"
:width="CELL_SIZE"
:height="CELL_SIZE"
:fill="getGridFillColor(grid)"
@click="selectGrid(grid)"
:class="{ 'selected': selectedGrid && selectedGrid.id === grid.id }"
class="grid-cell"
/>
<!-- Border Rendering -->
<line v-if="getBorder(grid, 'top')" :x1="grid.displayX * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="grid.displayY * CELL_SIZE" class="border-line" />
<line v-if="getBorder(grid, 'right')" :x1="(grid.displayX + 1) * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
<line v-if="getBorder(grid, 'bottom')" :x1="grid.displayX * CELL_SIZE" :y1="(grid.displayY + 1) * CELL_SIZE" :x2="(grid.displayX + 1) * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
<line v-if="getBorder(grid, 'left')" :x1="grid.displayX * CELL_SIZE" :y1="grid.displayY * CELL_SIZE" :x2="grid.displayX * CELL_SIZE" :y2="(grid.displayY + 1) * CELL_SIZE" class="border-line" />
</g>
</svg>
<el-empty v-else description="没有可显示的网格数据" />
</div>
<!-- Details Sidebar -->
<div class="sidebar-area">
<el-card shadow="never" class="details-card">
<template #header>
<div class="card-header">
<span>网格详情</span>
</div>
</template>
<div v-if="selectedGrid" class="details-content">
<p><strong>网格ID:</strong> {{ selectedGrid.id }}</p>
<p><strong>城市:</strong> <el-tag :color="getCityColor(selectedGrid.cityName)" effect="light">{{ selectedGrid.cityName }}</el-tag></p>
<p><strong>区县:</strong> {{ selectedGrid.districtName || 'N/A' }}</p>
<p><strong>原始坐标:</strong> ({{ selectedGrid.gridX }}, {{ selectedGrid.gridY }})</p>
<p><strong>是否为障碍物:</strong> <el-tag :type="selectedGrid.isObstacle ? 'danger' : 'success'">{{ selectedGrid.isObstacle ? '是' : '否' }}</el-tag></p>
<p><strong>描述:</strong> {{ selectedGrid.description || '无' }}</p>
<el-divider />
<p><strong>负责人:</strong></p>
<div v-if="selectedGridWorker">
<el-tag effect="plain" type="info">
<el-icon><User /></el-icon>
{{ selectedGridWorker.name }} ({{ selectedGridWorker.phone }})
</el-tag>
</div>
<div v-else>
<el-tag type="warning">未分配</el-tag>
</div>
</div>
<el-empty v-else description="请在左侧地图上选择一个网格" />
</el-card>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { ElMessage } from 'element-plus';
import type { Grid, UserAccount } from '@/api/types';
import { getGrids } from '@/api/dashboard';
import { getAllGridWorkers } from '@/api/personnel';
import { User } from '@element-plus/icons-vue';
const CELL_SIZE = 25;
const PADDING_CELLS = 2;
// --- State ---
const gridData = ref<Grid[]>([]);
const allWorkers = ref<UserAccount[]>([]);
const loading = ref(false);
const isLoadingWorkers = ref(false);
const error = ref<string | null>(null);
const selectedGrid = ref<DisplayGrid | null>(null);
// --- Data Fetching & Refresh ---
const fetchAllGrids = async () => {
loading.value = true;
error.value = null;
selectedGrid.value = null;
try {
const response = await getGrids();
gridData.value = Array.isArray(response) ? response : (response as any).content || [];
} catch (e: any) {
error.value = e.message || '获取网格数据时发生未知错误';
ElMessage.error(error.value);
} finally {
loading.value = false;
}
};
const fetchAllWorkers = async () => {
isLoadingWorkers.value = true;
try {
allWorkers.value = await getAllGridWorkers();
} catch (e: any) {
ElMessage.error('获取网格员列表失败: ' + e.message);
} finally {
isLoadingWorkers.value = false;
}
};
const refreshData = async () => {
await Promise.all([fetchAllGrids(), fetchAllWorkers()]);
ElMessage.success('数据已刷新');
}
onMounted(() => {
refreshData();
});
// --- Computed Properties ---
const workersByCoord = computed<Map<string, UserAccount>>(() => {
const map = new Map<string, UserAccount>();
allWorkers.value.forEach(worker => {
if (worker.gridX !== null && worker.gridY !== null) {
map.set(`${worker.gridX},${worker.gridY}`, worker);
}
});
return map;
});
const selectedGridWorker = computed<UserAccount | undefined>(() => {
if (!selectedGrid.value || !allWorkers.value.length) {
return undefined;
}
return allWorkers.value.find(
worker => worker.gridX === selectedGrid.value?.gridX && worker.gridY === selectedGrid.value?.gridY
);
});
// --- City Color Logic ---
const cityColors = ref<Map<string, string>>(new Map());
const getCityColor = (cityName: string): string => {
if (!cityColors.value.has(cityName)) {
let hash = 0;
for (let i = 0; i < cityName.length; i++) {
hash = cityName.charCodeAt(i) + ((hash << 5) - hash);
}
const color = `hsl(${hash % 360}, 80%, 85%)`;
cityColors.value.set(cityName, color);
}
return cityColors.value.get(cityName)!;
};
const getGridFillColor = (grid: Grid): string => {
if (grid.isObstacle) {
return '#000000';
}
if (workersByCoord.value.has(`${grid.gridX},${grid.gridY}`)) {
return '#FF0000';
}
return getCityColor(grid.cityName);
};
// --- Display Logic ---
interface DisplayGrid extends Grid {
displayX: number;
displayY: number;
}
const displayGrids = computed<DisplayGrid[]>(() => {
if (!gridData.value.length) return [];
// Determine the top-left corner of the entire map to normalize coordinates
const minX = Math.min(...gridData.value.map(g => g.gridX));
const minY = Math.min(...gridData.value.map(g => g.gridY));
// Render all grids based on their absolute positions, normalized to start at (0,0)
return gridData.value.map(grid => ({
...grid,
displayX: grid.gridX - minX,
displayY: grid.gridY - minY,
}));
});
const gridMap = computed(() => {
const map = new Map<string, DisplayGrid>();
displayGrids.value.forEach(grid => {
map.set(`${grid.displayX},${grid.displayY}`, grid);
});
return map;
});
const svgDimensions = computed(() => {
if (!displayGrids.value.length) return { width: 0, height: 0 };
const maxX = Math.max(...displayGrids.value.map(g => g.displayX));
const maxY = Math.max(...displayGrids.value.map(g => g.displayY));
return {
width: (maxX + 1) * CELL_SIZE,
height: (maxY + 1) * CELL_SIZE,
};
});
const getBorder = (grid: DisplayGrid, side: 'top' | 'right' | 'bottom' | 'left'): boolean => {
const { displayX, displayY, cityName } = grid;
let neighbor: DisplayGrid | undefined;
switch (side) {
case 'top':
neighbor = gridMap.value.get(`${displayX},${displayY - 1}`);
break;
case 'right':
neighbor = gridMap.value.get(`${displayX + 1},${displayY}`);
break;
case 'bottom':
neighbor = gridMap.value.get(`${displayX},${displayY + 1}`);
break;
case 'left':
neighbor = gridMap.value.get(`${displayX - 1},${displayY}`);
break;
}
return !neighbor || neighbor.cityName !== cityName;
};
const selectGrid = (grid: DisplayGrid) => {
selectedGrid.value = grid;
};
</script>
<style scoped>
.grid-management-container {
padding: 20px;
background-color: #f7f8fa;
height: 100%;
display: flex;
flex-direction: column;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-shrink: 0;
}
.page-header h2 {
margin: 0;
font-size: 24px;
}
.main-content-grid {
display: flex;
gap: 20px;
flex: 1;
overflow: hidden;
}
.map-area {
flex: 3;
background-color: #fff;
border-radius: 8px;
padding: 10px;
overflow: auto;
border: 1px solid #e9e9e9;
}
.sidebar-area {
flex: 1;
min-width: 300px;
}
.details-card .card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.grid-svg {
display: block;
}
.grid-cell {
stroke: #fff;
stroke-width: 1;
transition: filter 0.2s ease-in-out;
}
.grid-cell:hover {
filter: brightness(0.9);
}
.grid-cell.selected {
stroke: #ff4d4f;
stroke-width: 2;
}
.border-line {
stroke: #333;
stroke-width: 1.5;
}
.error-message {
padding: 20px;
}
.retry-button {
margin-top: 15px;
}
.details-content p {
margin: 0 0 12px;
color: #606266;
}
.details-content p strong {
color: #303133;
margin-right: 8px;
}
</style>

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