Files
Environment-Monitoring-System/Report/系统实现.md
ChuXun 02a830145e 1
2025-10-25 19:18:43 +08:00

12 KiB
Raw Blame History

3.3 系统实现

本章节详细阐述了环境监督系统EMS的实际编码实现过程展示了如何将系统设计转化为可运行的代码。由于内容较多为了便于阅读和维护本章节分为以下三个部分

文档目录

  1. 开发环境与技术栈 & 核心功能模块实现(上)

    • 开发环境与技术栈
    • 泛型JSON存储服务
    • A*寻路算法实现
  2. 核心功能模块实现(中)

    • A*寻路算法实现要点分析
    • 反馈管理模块实现
  3. 核心功能模块实现(下) & 界面展示与成果展示

    • 任务智能分配算法
    • 界面展示与成果展示
    • 实现成果总结

请点击上述链接查看各部分的详细内容。

3.3.1 后端关键技术实现

1. 核心数据服务泛型JSON仓储

为了实现一个可复用、类型安全且与业务逻辑完全解耦的数据持久化层,我设计并实现了一个泛型的JsonStorageService。这是整个后端数据访问的基石是实现仓储模式Repository Pattern的关键底层支持。

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

设计深度解析:

  • 泛型与类型安全: writeDatareadData方法都使用了泛型<T>,使得该服务可以处理任何类型的实体列表(如List<User>List<Task>)。通过传入TypeReference<List<T>>我们解决了Java泛型在运行时被擦除的问题使得Jackson库能够准确地将JSON数组反序列化为指定类型的对象列表保证了类型安全。
  • 线程安全与并发控制: writeData方法被声明为synchronized,这是一个简单而有效的并发控制手段。它利用对象锁确保了在多线程环境下对同一文件的写操作是互斥的、串行的,从而从根本上防止了数据竞争和文件损坏的风险。
  • 健壮性与容错: 代码对存储目录不存在、文件读写IO异常、JSON解析异常等情况都进行了处理。特别是当文件不存在或内容为空/损坏时,readData会返回一个空列表而不是抛出异常这使得上层调用者Repository无需处理这些繁琐的底层细节增强了系统的整体健-壮性。

2. 安全核心JWT生成与校验

系统的认证和授权机制基于JWTJSON Web Token。我实现了一个JwtTokenProvider来封装JWT的生成和校验逻辑使其与业务代码分离。

@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块的冗余代码。

@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天任务趋势"的折线图,另一个是"问题类型分布"的饼图。所有图表都是动态的,并支持按时间范围进行筛选。