195 lines
12 KiB
Markdown
195 lines
12 KiB
Markdown
# 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生成与校验
|
||
|
||
系统的认证和授权机制基于JWT(JSON Web Token)。我实现了一个`JwtTokenProvider`来封装JWT的生成和校验逻辑,使其与业务代码分离。
|
||
|
||
```java:ems-backend/src/main/java/com/dne/ems/security/JwtTokenProvider.java
|
||
@Component
|
||
public class JwtTokenProvider {
|
||
|
||
@Value("${app.jwtSecret}")
|
||
private String jwtSecret;
|
||
|
||
@Value("${app.jwtExpirationInMs}")
|
||
private int jwtExpirationInMs;
|
||
|
||
// 根据用户信息生成JWT
|
||
public String generateToken(Authentication authentication) {
|
||
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
|
||
Date now = new Date();
|
||
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
|
||
|
||
return Jwts.builder()
|
||
.setSubject(Long.toString(userPrincipal.getId()))
|
||
.claim("role", userPrincipal.getAuthorities().iterator().next().getAuthority())
|
||
.setIssuedAt(new Date())
|
||
.setExpiration(expiryDate)
|
||
.signWith(SignatureAlgorithm.HS512, jwtSecret)
|
||
.compact();
|
||
}
|
||
|
||
// 从JWT中解析用户ID
|
||
public Long getUserIdFromJWT(String token) {
|
||
Claims claims = Jwts.parser()
|
||
.setSigningKey(jwtSecret)
|
||
.parseClaimsJws(token)
|
||
.getBody();
|
||
return Long.parseLong(claims.getSubject());
|
||
}
|
||
|
||
// 校验JWT的有效性
|
||
public boolean validateToken(String authToken) {
|
||
try {
|
||
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
|
||
return true;
|
||
} catch (SignatureException | MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException ex) {
|
||
// 捕获所有可能的JWT校验异常
|
||
log.error("Invalid JWT token: {}", ex.getMessage());
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
```
|
||
|
||
**实现说明:**
|
||
* **配置化:** JWT的密钥(`jwtSecret`)和过期时间(`jwtExpirationInMs`)都通过`@Value`注解从`application.properties`配置文件中读取,便于不同环境的部署和管理。
|
||
* **声明(Claims):** 在生成Token时,我不仅将用户ID作为`subject`,还将用户的角色(Role)作为一个自定义的`claim`存入Token中。这样做的好处是,在后续的授权判断中,我们无需再次查询数据库,直接从Token中即可获取用户角色,提高了性能。
|
||
* **全面的异常处理:** `validateToken`方法捕获了`jjwt`库可能抛出的所有校验异常,如签名错误、格式错误、Token过期等,并记录日志,保证了校验逻辑的严谨性。
|
||
|
||
### 3. 统一出口:全局异常处理器
|
||
|
||
为了提供统一、规范的API错误响应格式,我实现了一个全局异常处理器。这避免了在每个Controller方法中都写`try-catch`块的冗余代码。
|
||
|
||
```java:ems-backend/src/main/java/com/dne/ems/exception/GlobalExceptionHandler.java
|
||
@ControllerAdvice
|
||
public class GlobalExceptionHandler {
|
||
|
||
@ExceptionHandler(value = {IllegalArgumentException.class, IllegalStateException.class})
|
||
public ResponseEntity<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天任务趋势"的折线图,另一个是"问题类型分布"的饼图。所有图表都是动态的,并支持按时间范围进行筛选。 |