SpringMVC核心原理与实战指南

什么是MVC?

MVC英文是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计规范。

MVC是用一种业务逻辑、数据、界面显示分离的方法,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

  • Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。
  • View(视图)是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的。
  • Controller(控制器)是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

什么是SpringMVC?

Spring MVC是Spring在Spring Container Core和AOP等技术基础上,遵循上述Web MVC的规范推出的web开发框架,目的是为了简化Java栈的web开发。

Spring Web MVC 是一种基于Java 的实现了Web MVC 设计模式的请求驱动类型的轻量级Web 框架,即使用了MVC 架 构模式的思想,将 web 层进行职责解耦,基于请求驱动指的就是使用请求-响应模型,框架的目的就是帮助我们简化开发,Spring Web MVC 也是要简化我们日常Web 开发的。

模型(Model):

模型代表应用程序的数据和业务逻辑。它通常包含数据对象(如 POJO)和服务层(如 Spring 服务)来处理业务逻辑。模型负责从数据库或其他数据源获取数据,并将数据传递给视图以显示给用户。

视图(View):

视图负责展示数据,通常是 HTML 页面或其他类型的用户界面。Spring MVC 支持多种视图技术,包括 JSP、Thymeleaf、FreeMarker 等。视图从模型获取数据并将其呈现给用户。

控制器(Controller):

控制器处理用户请求并决定将数据传递给哪个视图。它接收用户输入,调用模型进行处理,并选择合适的视图来显示结果。控制器通常使用 @Controller 注解来标识,并使用 @RequestMapping 注解来映射 URL 请求。

SpringMVC的组件

Spring MVC框架的核心组件:

1、DispatcherServlet(前端控制器) 这是Spring MVC的核心组件,作为整个框架的入口点。它接收所有的HTTP请求,然后根据配置将请求分发给相应的处理器。DispatcherServlet实现了前端控制器模式,统一处理请求的分发、异常处理、视图解析等工作。它在web.xml中配置,或通过Java配置类进行设置。

2、HandlerMapping(处理器映射器) 负责根据请求的URL找到对应的处理器(Controller)。Spring MVC提供了多种HandlerMapping实现,如RequestMappingHandlerMapping用于处理@RequestMapping注解,SimpleUrlHandlerMapping用于简单的URL映射。它会返回一个HandlerExecutionChain对象,包含处理器和相关的拦截器。

3、HandlerAdapter(处理器适配器) 由于处理器的类型多样(可能是Controller接口实现类、带@RequestMapping注解的方法等),HandlerAdapter负责调用具体的处理器方法。不同类型的处理器需要不同的适配器,如RequestMappingHandlerAdapter用于处理注解式控制器,SimpleControllerHandlerAdapter用于处理Controller接口的实现类。

4、Controller(控制器) 处理具体的业务逻辑,接收用户请求,调用服务层处理业务,然后返回ModelAndView对象。控制器可以通过实现Controller接口或使用@Controller注解来定义。现代Spring MVC主要使用注解式控制器,通过@RequestMapping等注解来映射请求。

5、ModelAndView 封装了模型数据和视图信息的对象。Model包含要传递给视图的数据,View指定要渲染的视图名称。控制器处理完请求后返回ModelAndView对象,或者分别返回模型数据和视图名称。

6、ViewResolver(视图解析器) 根据逻辑视图名解析出具体的视图对象。Spring MVC支持多种视图技术,如JSP、Thymeleaf、Freemarker等。常用的视图解析器包括InternalResourceViewResolver用于JSP视图,ThymeleafViewResolver用于Thymeleaf模板引擎。

7、View(视图) 负责渲染模型数据,生成响应内容返回给客户端。视图可以是JSP页面、Thymeleaf模板、JSON数据等。不同的视图技术有不同的View实现类。

8、HandlerInterceptor(拦截器) 提供请求处理的前置和后置处理功能。拦截器可以在请求到达控制器之前、控制器处理完成后、视图渲染完成后进行相应的处理。常用于日志记录、权限检查、性能监控等横切关注点。

9、HandlerExceptionResolver(异常解析器) 处理请求过程中抛出的异常,将异常转换为相应的视图或响应。Spring MVC提供了多种异常解析器,如SimpleMappingExceptionResolver用于简单的异常映射,ExceptionHandlerExceptionResolver用于处理@ExceptionHandler注解的方法。

10、MultipartResolver(文件上传解析器) 处理multipart类型的HTTP请求,主要用于文件上传功能。它将multipart请求解析为MultipartHttpServletRequest对象,使得控制器可以方便地处理上传的文件。

11、LocaleResolver(国际化解析器) 确定用户的区域设置,支持应用程序的国际化功能。它可以从HTTP请求头、Session、Cookie等地方获取用户的语言偏好。

12、ThemeResolver(主题解析器) 支持Web应用程序的主题功能,允许用户选择不同的界面主题。虽然在现代Web开发中使用较少,但在某些场景下仍然有用。

SpringMVC的工作原理

SpringMVC的执行流程:

步骤1:用户发送请求 用户通过浏览器向服务器发送HTTP请求(如GET /user/list),请求首先到达Web服务器,然后被转发到Spring MVC的DispatcherServlet。

步骤2:DispatcherServlet查找Handler DispatcherServlet作为前端控制器,接收到请求后,会调用HandlerMapping组件来查找能够处理该请求的Handler(通常是Controller中的某个方法)。HandlerMapping会根据请求的URL、HTTP方法等信息,通过注解映射或配置文件找到对应的处理器。

步骤3:返回执行链 HandlerMapping找到匹配的Handler后,不是直接返回Handler,而是返回一个HandlerExecutionChain对象。这个执行链包含了目标Handler以及与该Handler相关的所有拦截器(HandlerInterceptor1、HandlerInterceptor2等)。拦截器可以在Handler执行前后进行额外的处理。

步骤4:请求适配器执行 DispatcherServlet获得HandlerExecutionChain后,需要找到合适的HandlerAdapter来执行Handler。因为Handler的类型可能多样(实现Controller接口的类、带@RequestMapping注解的方法等),所以需要适配器模式来统一调用接口。DispatcherServlet会遍历所有注册的HandlerAdapter,找到支持当前Handler类型的适配器。

步骤5:执行Handler HandlerAdapter调用具体的Handler方法(通常是Controller中的业务方法)。在执行Handler之前,会先执行拦截器链的preHandle方法。Handler方法执行时会处理业务逻辑,可能调用Service层、DAO层等组件,处理完成后准备返回结果。

步骤6:返回ModelAndView Handler执行完毕后,返回处理结果。这个结果通常是ModelAndView对象,包含了模型数据(Model)和逻辑视图名(View name)。Model包含要传递给前端页面的数据,View name是一个字符串,表示要渲染的视图的逻辑名称。

步骤7:HandlerAdapter处理返回值 HandlerAdapter接收Handler的返回值,将其封装成标准的ModelAndView对象返回给DispatcherServlet。如果Handler返回的是其他类型(如字符串、@ResponseBody注解的对象等),HandlerAdapter会进行相应的转换处理。

步骤8:请求视图解析 DispatcherServlet拿到ModelAndView后,调用ViewResolver(视图解析器)来解析逻辑视图名。ViewResolver根据配置的规则(如前缀、后缀等),将逻辑视图名转换为具体的视图对象(View)。例如,逻辑视图名"userList"可能被解析为"/WEB-INF/views/userList.jsp"。

步骤9:返回View对象 ViewResolver将解析后的View对象返回给DispatcherServlet。View对象知道如何渲染特定类型的视图,比如JSP视图、Thymeleaf视图、JSON视图等。

步骤10:视图渲染 DispatcherServlet调用View对象的render方法,将Model中的数据填充到视图模板中,生成最终的HTML、JSON或其他格式的响应内容。在这个过程中,模型数据会被放到request域中,供视图模板使用。

步骤11:响应用户 视图渲染完成后,DispatcherServlet将最终的响应内容返回给用户的浏览器。在返回响应之前,还会执行拦截器链的postHandle和afterCompletion方法,完成一些清理工作。

简单描述:

Spring MVC 的工作流程:

  1. 用户请求:用户通过浏览器发送 HTTP 请求到服务器。

  2. 前端控制器(DispatcherServlet):Spring MVC 的前端控制器 DispatcherServlet 拦截所有请求并进行分发。

  3. 处理器映射(Handler Mapping):根据请求 URL,DispatcherServlet 查找相应的控制器。

  4. 控制器处理:控制器处理请求,调用服务层或数据访问层以获取数据,并将数据封装到模型中。

  5. 视图解析器(View Resolver):控制器返回视图 名称,DispatcherServlet 使用视图解析器将视图名称解析为实际的视图对象。

  6. 视图渲染:视图对象负责将模型数据渲染为用户界面,通常是 HTML 页面。

  7. 响应返回:渲染后的视图返回给 DispatcherServlet,DispatcherServlet 将最终的响应发送回用户浏览器。

SpringMVC项目实战

1、 创建Maven Web项目

2、 完善项目结构

3、配置pom文件

复制代码
<!-- Maven项目核心配置文件,定义项目结构、依赖、构建信息等 -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <!-- Maven模型版本,固定为4.0.0 -->
  <modelVersion>4.0.0</modelVersion>

  <!-- 项目组织唯一标识,通常是公司或组织域名倒写 -->
  <groupId>com.example.springmvcdemo</groupId>
  <!-- 项目模块名称 -->
  <artifactId>springmvc-demo</artifactId>
  <!-- 打包方式,war表示Web应用 -->
  <packaging>war</packaging>
  <!-- 项目版本号,SNAPSHOT表示开发中版本 -->
  <version>1.0-SNAPSHOT</version>
  <!-- 项目名称,用于描述 -->
  <name>springmvc-demo</name>

  <!-- 自定义属性,便于统一管理版本号和编码等信息 -->
  <properties>
    <!-- JDK编译版本为1.8 -->
    <maven.compiler.source>8</maven.compiler.source>
    <!-- JDK目标运行版本为1.8 -->
    <maven.compiler.target>8</maven.compiler.target>
    <!-- 项目构建时源码编码为UTF-8 -->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- Spring框架版本 -->
    <spring.version>5.3.21</spring.version>
    <!-- MyBatis版本 -->
    <mybatis.version>3.5.10</mybatis.version>
    <!-- MySQL驱动版本 -->
    <mysql.version>8.0.29</mysql.version>
    <!-- Jackson JSON处理库版本 -->
    <jackson.version>2.13.3</jackson.version>
  </properties>

  <!-- 项目依赖管理 -->
  <dependencies>
    <!-- ========== Spring 核心依赖 ========== -->
    <!-- Spring 框架核心工具类 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Spring 上下文,提供框架式Bean访问方式 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Spring Bean管理 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Spring 表达式语言支持 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-expression</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- ========== Spring MVC 相关依赖 ========== -->
    <!-- Spring Web MVC 核心,用于构建Web应用 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Spring Web基础支持,包含HTTP相关功能 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- ========== Spring JDBC 与事务管理 ========== -->
    <!-- Spring JDBC 支持,简化数据库操作 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Spring 事务管理 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- ========== MyBatis 相关依赖 ========== -->
    <!-- MyBatis ORM框架核心 -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>${mybatis.version}</version>
    </dependency>
    <!-- MyBatis与Spring整合支持 -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>2.0.7</version>
    </dependency>

    <!-- ========== 数据库相关依赖 ========== -->
    <!-- MySQL JDBC驱动 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>${mysql.version}</version>
    </dependency>
    <!-- 数据库连接池Druid -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.2.11</version>
    </dependency>

    <!-- ========== JSON处理相关依赖 ========== -->
    <!-- Jackson核心库 -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>${jackson.version}</version>
    </dependency>
    <!-- Jackson数据绑定,用于对象与JSON互转 -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>${jackson.version}</version>
    </dependency>
    <!-- Jackson注解支持 -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-annotations</artifactId>
      <version>${jackson.version}</version>
    </dependency>

    <!-- ========== Servlet 相关依赖 ========== -->
    <!-- Servlet API,编译期需要,运行时由容器提供 -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
    <!-- JSP API,编译期使用,运行时由容器提供 -->
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.2</version>
      <scope>provided</scope>
    </dependency>
    <!-- JSTL标签库,用于JSP页面 -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>

    <!-- ========== 日志相关依赖 ========== -->
    <!-- Log4j日志框架 -->
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.23.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
      <version>2.23.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j2-impl</artifactId>
      <version>2.23.1</version>
    </dependency>

    <!-- ========== 测试相关依赖 ========== -->
    <!-- JUnit单元测试框架 -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>
    <!-- Spring测试支持,用于集成测试 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>${spring.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <!-- 构建配置,如插件配置 -->
  <build>
    <plugins>
      <!-- Maven编译插件,配置JDK版本与编码 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>8</source>  <!-- 源码编译使用JDK 8 -->
          <target>8</target>  <!-- 目标字节码为JDK 8 -->
          <encoding>UTF-8</encoding>  <!-- 源码和编译编码均为UTF-8 -->
        </configuration>
      </plugin>

      <!-- Tomcat Maven插件,用于本地启动嵌入式Tomcat进行测试 -->
      <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
        <configuration>
          <port>8080</port>  <!-- Tomcat服务端口为8080 -->
          <path>/ssm</path>  <!-- 应用上下文路径为 /ssm -->
        </configuration>
      </plugin>

    </plugins>
  </build>
</project>

4、配置文件

1、applicationContext.xml(Spring核心配置)
复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">

      <!-- 扫描com.example.springmvcdemo包下的Spring组件(如@Service, @Repository等),但不包括@Controller -->
      <context:component-scan base-package="com.example.springmvcdemo">
            <context:exclude-filter type="annotation"
                                    expression="org.springframework.stereotype.Controller"/>
      </context:component-scan>

      <!-- 加载classpath下的jdbc.properties文件,用于读取数据库连接等配置信息 -->
      <context:property-placeholder location="classpath:jdbc.properties"/>

      <!-- 配置Druid数据源,使用属性占位符从jdbc.properties中读取数据库连接信息 -->
      <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
            destroy-method="close">
            <property name="driverClassName" value="${jdbc.driver}"/>      <!-- 数据库驱动类名 -->
            <property name="url" value="${jdbc.url}"/>                    <!-- 数据库连接URL -->
            <property name="username" value="${jdbc.username}"/>          <!-- 数据库用户名 -->
            <property name="password" value="${jdbc.password}"/>          <!-- 数据库密码 -->
            <property name="maxActive" value="20"/>                       <!-- 最大活跃连接数 -->
            <property name="initialSize" value="1"/>                      <!-- 初始化连接数 -->
            <property name="maxWait" value="60000"/>                      <!-- 获取连接的最大等待时间(毫秒) -->
            <property name="minIdle" value="1"/>                          <!-- 最小空闲连接数 -->
      </bean>

      <!-- 配置MyBatis的SqlSessionFactory,指定数据源、MyBatis全局配置文件及Mapper XML文件位置 -->
      <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            <property name="dataSource" ref="dataSource"/>               <!-- 引用上面配置的数据源 -->
            <property name="configLocation" value="classpath:mybatis-config.xml"/>  <!-- MyBatis全局配置文件 -->
            <property name="mapperLocations" value="classpath:mapper/*.xml"/>       <!-- Mapper XML文件位置 -->
      </bean>

      <!-- 配置MyBatis Mapper接口扫描器,自动将指定包下的Mapper接口与XML映射绑定 -->
      <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
            <property name="basePackage" value="com.example.ssm.dao"/>    <!-- Mapper接口所在的包 -->
            <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>  <!-- 引用SqlSessionFactory的Bean名称 -->
      </bean>

      <!-- 配置Spring JDBC事务管理器,用于管理数据库事务 -->
      <bean id="transactionManager"
            class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>               <!-- 引用数据源 -->
      </bean>

      <!-- 开启基于注解的声明式事务管理,如使用@Transactional注解 -->
      <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>
2、spring-mvc.xml(SpringMVC配置)
复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!-- Spring MVC 配置文件的根节点,定义使用的 XML Schema -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 扫描指定包(com.example.springmvcdemo.controller)下的组件,如@Controller等注解的类 -->
    <context:component-scan base-package="com.example.springmvcdemo.controller"/>

    <!-- 启用Spring MVC的注解驱动功能,支持如@RequestMapping、@ResponseBody等注解 -->
    <mvc:annotation-driven>
        <!-- 配置消息转换器,用于处理请求与响应体的格式转换 -->
        <mvc:message-converters>
            <!-- 配置Jackson库的JSON消息转换器,用于将Java对象转为JSON并响应给前端 -->
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <!-- 支持的响应类型:application/json;charset=UTF-8 -->
                        <value>application/json;charset=UTF-8</value>
                        <!-- 支持的响应类型:text/json;charset=UTF-8 -->
                        <value>text/json;charset=UTF-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <!-- 配置静态资源(如JS、CSS、图片等)的访问路径,无需经过Controller -->
    <!-- 请求路径以 /static/ 开头的资源,将从 /static/ 目录下查找 -->
    <mvc:resources mapping="/static/**" location="/static/"/>

    <!-- 配置JSP视图解析器,用于将逻辑视图名解析为实际的JSP页面路径 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- JSP文件所在的目录前缀,如:/ -->
        <property name="prefix" value="/"/>
        <!-- JSP文件的后缀,如:.jsp -->
        <property name="suffix" value=".jsp"/>
        <!-- 使用JSTL视图 -->
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    </bean>

    <!-- 配置文件上传解析器,支持用户通过表单上传文件 -->
    <bean id="multipartResolver"
          class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- 最大上传文件大小限制为10MB(10 * 1024 * 1024 = 10485760字节) -->
        <property name="maxUploadSize" value="10485760"/>
        <!-- 默认编码为UTF-8 -->
        <property name="defaultEncoding" value="UTF-8"/>
    </bean>

</beans>
3、 mybatis-config.xml (mybatis的配置文件)
复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <settings>
        <!-- 开启延迟加载 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 开启驼峰命名转换 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 打印SQL语句 - 使用Log4j -->
        <setting name="logImpl" value="LOG4J"/>
        <!-- 如果使用logback,改为下面这行 -->
        <!-- <setting name="logImpl" value="SLF4J"/> -->
    </settings>

    <!-- 类型别名 -->
    <typeAliases>
        <package name="com.example.springmvcdemo.entity"/>
    </typeAliases>

</configuration>
4、 jdbc.properties (数据库配置文件)
复制代码
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/sql_springmvc_demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=root

5、 日志配置文件

1、log4j2.xml
复制代码
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
    <Properties>
        <Property name="LOG_HOME">./logs</Property>
        <Property name="APP_NAME">ssm-demo</Property>
        <Property name="LOG_PATTERN">[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%t] %-5level [%logger{36}:%L] - %msg%n</Property>
        <Property name="SQL_PATTERN">%d{HH:mm:ss.SSS} [%t] %logger{36} - %msg%n</Property>
    </Properties>

    <Appenders>
        <!-- 控制台输出 -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
        </Console>

        <!-- 应用日志文件 -->
        <RollingFile name="ApplicationFile" fileName="${LOG_HOME}/${APP_NAME}.log"
                     filePattern="${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log">
            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="30" fileIndex="min">
                <Delete basePath="${LOG_HOME}" maxDepth="1">
                    <IfFileName glob="${APP_NAME}*.log" />
                    <IfLastModified age="30d" />
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>

        <!-- 错误日志单独输出 -->
        <RollingFile name="ErrorFile" fileName="${LOG_HOME}/error.log"
                     filePattern="${LOG_HOME}/error.%d{yyyy-MM-dd}.%i.log">
            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
            <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="50 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="60"/>
        </RollingFile>

        <!-- SQL日志单独输出 -->
        <RollingFile name="SqlFile" fileName="${LOG_HOME}/sql.log"
                     filePattern="${LOG_HOME}/sql.%d{yyyy-MM-dd}.log">
            <PatternLayout pattern="${SQL_PATTERN}" charset="UTF-8"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
            </Policies>
            <DefaultRolloverStrategy max="7"/>
        </RollingFile>

        <!-- 异步Appender -->
        <Async name="AsyncApplication" bufferSize="1024">
            <AppenderRef ref="ApplicationFile"/>
        </Async>
    </Appenders>

    <Loggers>
        <!-- MyBatis SQL日志 -->
        <Logger name="com.example.ssm.dao" level="debug" additivity="false">
            <AppenderRef ref="SqlFile"/>
            <AppenderRef ref="Console"/>
        </Logger>

        <Logger name="org.apache.ibatis" level="info" additivity="false">
            <AppenderRef ref="SqlFile"/>
        </Logger>

        <Logger name="java.sql.Connection" level="info" additivity="false">
            <AppenderRef ref="SqlFile"/>
        </Logger>

        <Logger name="java.sql.Statement" level="debug" additivity="false">
            <AppenderRef ref="SqlFile"/>
        </Logger>

        <Logger name="java.sql.PreparedStatement" level="debug" additivity="false">
            <AppenderRef ref="SqlFile"/>
        </Logger>

        <!-- 项目包日志 -->
        <Logger name="com.example.ssm" level="debug"/>

        <!-- Spring框架日志 -->
        <Logger name="org.springframework" level="warn"/>

        <!-- 数据源日志 -->
        <Logger name="com.alibaba.druid" level="warn"/>

        <!-- Root Logger -->
        <Root level="info">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="AsyncApplication"/>
            <AppenderRef ref="ErrorFile"/>
        </Root>
    </Loggers>
</Configuration>
2、logback.xml
复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">

    <!-- 定义日志文件输出目录 -->
    <property name="LOG_HOME" value="./logs" />
    <property name="APP_NAME" value="ssm-demo" />
    <property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level [%logger{36}:%line] - %msg%n" />
    <property name="SQL_PATTERN" value="[%d{HH:mm:ss.SSS}] [%thread] %logger{36} - %msg%n" />

    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 生产环境可以注释掉控制台输出 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
    </appender>

    <!-- 应用日志文件输出 -->
    <appender name="APPLICATION_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/${APP_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>5GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 错误日志单独输出 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- SQL日志单独输出 -->
    <appender name="SQL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/sql.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/sql.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${SQL_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 异步日志 -->
    <appender name="ASYNC_APPLICATION" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>1024</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <includeCallerData>true</includeCallerData>
        <appender-ref ref="APPLICATION_FILE" />
    </appender>

    <!-- MyBatis SQL日志配置 -->
    <logger name="com.example.ssm.dao" level="DEBUG" additivity="false">
        <appender-ref ref="SQL_FILE"/>
        <appender-ref ref="CONSOLE"/>
    </logger>

    <logger name="org.apache.ibatis" level="INFO" additivity="false">
        <appender-ref ref="SQL_FILE"/>
    </logger>

    <logger name="java.sql.Connection" level="INFO" additivity="false">
        <appender-ref ref="SQL_FILE"/>
    </logger>

    <logger name="java.sql.Statement" level="DEBUG" additivity="false">
        <appender-ref ref="SQL_FILE"/>
    </logger>

    <logger name="java.sql.PreparedStatement" level="DEBUG" additivity="false">
        <appender-ref ref="SQL_FILE"/>
    </logger>

    <!-- 项目包日志 -->
    <logger name="com.example.ssm" level="DEBUG"/>

    <!-- Spring框架日志 -->
    <logger name="org.springframework" level="WARN"/>

    <!-- 数据源日志 -->
    <logger name="com.alibaba.druid" level="WARN"/>

    <!-- Root Logger -->
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="ASYNC_APPLICATION"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>

</configuration>

6、 web.xml配置

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">  <!-- 声明这是一个 Java EE 3.0 版本的 Web 应用配置文件 -->

    <display-name>SpringMvcDemo</display-name>  <!-- 当前 Web 应用的显示名称,通常用于管理工具中显示 -->

    <!-- ===================== 字符编码过滤器 ===================== -->
    <!-- 用于统一处理请求和响应的字符编码,防止中文乱码等问题 -->
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>  <!-- 过滤器名称 -->
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>  <!-- 使用 Spring 提供的字符编码过滤器 -->
        <init-param>
            <param-name>encoding</param-name>  <!-- 设置编码为 UTF-8 -->
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>  <!-- 强制请求和响应都使用指定编码 -->
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>  <!-- 对所有 URL 请求生效 -->
    </filter-mapping>

    <!-- ===================== Spring 上下文监听器 ===================== -->
    <!-- 用于在 Web 应用启动时加载 Spring 的根应用上下文(通常是业务层、数据层等 Bean) -->
    <context-param>
        <param-name>contextConfigLocation</param-name>  <!-- 指定 Spring 配置文件的位置 -->
        <param-value>classpath:applicationContext.xml</param-value>  <!-- 类路径下的 applicationContext.xml -->
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  <!-- Spring 提供的上下文加载监听器 -->
    </listener>

    <!-- ===================== Spring MVC 前端控制器 ===================== -->
    <!-- Spring MVC 的核心 Servlet,负责接收所有请求并分发给对应的 Controller 处理 -->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>  <!-- Servlet 名称 -->
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  <!-- 使用 Spring 的 DispatcherServlet -->
        <init-param>
            <param-name>contextConfigLocation</param-name>  <!-- 指定 Spring MVC 的配置文件位置 -->
            <param-value>classpath:spring-mvc.xml</param-value>  <!-- 类路径下的 spring-mvc.xml -->
        </init-param>
        <load-on-startup>1</load-on-startup>  <!-- Web 应用启动时立即加载该 Servlet,优先级为 1 -->
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>  <!-- 拦截所有请求,交由 Spring MVC 处理 -->
    </servlet-mapping>

    <!-- ===================== 静态资源处理 ===================== -->
    <!-- 让默认 Servlet 处理静态资源请求,如 CSS、JS、PNG 等,避免被 Spring MVC 拦截 -->
    <servlet-mapping>
        <servlet-name>default</servlet-name>  <!-- 使用容器(如 Tomcat)提供的默认 Servlet -->
        <url-pattern>*.css</url-pattern>  <!-- 处理所有 CSS 文件请求 -->
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.js</url-pattern>  <!-- 处理所有 JS 文件请求 -->
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.png</url-pattern>  <!-- 处理所有 PNG 图片文件请求 -->
    </servlet-mapping>
</web-app>

7、配置tomcat(以tomcat8.5为例)

8、编写测试接口进行测试

复制代码
package com.example.springmvcdemo.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * 测试类
 * @author liuwei
 * @version JDK 8
 * @className TestController
 * @date 2025/7/28
 * @description 测试类
 */
@Controller
@RequestMapping("/test")
@Slf4j
public class TestController {


    /**
     * 测试方法
     * @return
     */
    @GetMapping("/hello")
    public String test(){
        log.info("这是测试类:{}","ok");
        return "hello";
    }
}

<%--
  Created by IntelliJ IDEA.
  User: 24193
  Date: 2025/7/28
  Time: 18:09
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>hello</title>
</head>
<body>
ok
</body>
</html>

SSM项目案例:智能垃圾分类管理系统

一、项目需求分析

1. 需求背景

随着垃圾分类政策的推行,社区需要一个智能垃圾分类管理系统,实现以下功能:

  1. 居民账户管理:居民注册/登录,记录垃圾分类行为
  2. 垃圾投递记录:记录每次垃圾投递的类型、重量和时间
  3. 环保积分系统:根据分类准确性奖励积分
  4. 数据可视化:展示个人和社区的垃圾分类数据

2. 用户角色

  • 普通居民:记录投递、查看积分
  • 社区管理员:管理居民账户、查看社区数据

二、数据库设计(2张表)

1. 用户表(user)

复制代码
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码(加密存储)',
  `phone` varchar(20) NOT NULL COMMENT '手机号',
  `address` varchar(200) DEFAULT NULL COMMENT '住址',
  `role` tinyint(1) DEFAULT '0' COMMENT '0-居民 1-管理员',
  `points` int(11) DEFAULT '0' COMMENT '环保积分',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_username` (`username`),
  UNIQUE KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 垃圾投递记录表(garbage_record)

复制代码
CREATE TABLE `garbage_record` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL COMMENT '投递用户ID',
  `type` tinyint(1) NOT NULL COMMENT '1-可回收 2-有害 3-厨余 4-其他',
  `weight` decimal(10,2) NOT NULL COMMENT '重量(kg)',
  `accuracy` tinyint(1) DEFAULT '100' COMMENT '分类准确率(%)',
  `points_earned` int(11) DEFAULT '0' COMMENT '获得积分',
  `image_url` varchar(255) DEFAULT NULL COMMENT '投递照片',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三、SSM项目代码完成

1、pom配置文件

复制代码
<!-- Maven项目核心配置文件,定义项目结构、依赖、构建信息等 -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <!-- Maven模型版本,固定为4.0.0 -->
  <modelVersion>4.0.0</modelVersion>

  <!-- 项目组织唯一标识,通常是公司或组织域名倒写 -->
  <groupId>com.example.springmvcdemo</groupId>
  <!-- 项目模块名称 -->
  <artifactId>springmvc-demo</artifactId>
  <!-- 打包方式,war表示Web应用 -->
  <packaging>war</packaging>
  <!-- 项目版本号,SNAPSHOT表示开发中版本 -->
  <version>1.0-SNAPSHOT</version>
  <!-- 项目名称,用于描述 -->
  <name>springmvc-demo</name>

  <!-- 自定义属性,便于统一管理版本号和编码等信息 -->
  <properties>
    <!-- JDK编译版本为1.8 -->
    <maven.compiler.source>8</maven.compiler.source>
    <!-- JDK目标运行版本为1.8 -->
    <maven.compiler.target>8</maven.compiler.target>
    <!-- 项目构建时源码编码为UTF-8 -->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- Spring框架版本 -->
    <spring.version>5.3.21</spring.version>
    <!-- MyBatis版本 -->
    <mybatis.version>3.5.10</mybatis.version>
    <!-- MySQL驱动版本 -->
    <mysql.version>8.0.29</mysql.version>
    <!-- Jackson JSON处理库版本 -->
    <jackson.version>2.13.3</jackson.version>
  </properties>

  <!-- 项目依赖管理 -->
  <dependencies>
    <!-- ========== Spring 核心依赖 ========== -->
    <!-- Spring 框架核心工具类 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Spring 上下文,提供框架式Bean访问方式 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Spring Bean管理 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Spring 表达式语言支持 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-expression</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- ========== Spring MVC 相关依赖 ========== -->
    <!-- Spring Web MVC 核心,用于构建Web应用 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <!-- Spring Web基础支持,包含HTTP相关功能 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- ========== Spring JDBC 与事务管理 ========== -->
    <!-- Spring JDBC 支持,简化数据库操作 -->
    <dependency>
      <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- Spring 事务管理 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- ========== MyBatis 相关依赖 ========== -->
        <!-- MyBatis ORM框架核心 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>${mybatis.version}</version>
        </dependency>
        <!-- MyBatis与Spring整合支持 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.7</version>
        </dependency>

        <!-- ========== 数据库相关依赖 ========== -->
        <!-- MySQL JDBC驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <!-- 数据库连接池Druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.11</version>
        </dependency>

        <!-- ========== JSON处理相关依赖 ========== -->
        <!-- Jackson核心库 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <!-- Jackson数据绑定,用于对象与JSON互转 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <!-- Jackson注解支持 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <!-- ========== Servlet 相关依赖 ========== -->
        <!-- Servlet API,编译期需要,运行时由容器提供 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
        <!-- JSP API,编译期使用,运行时由容器提供 -->
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.2</version>
            <scope>provided</scope>
        </dependency>
        <!-- JSTL标签库,用于JSP页面 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.32</version>
            <scope>provided</scope>
        </dependency>

        <!-- ========== 日志相关依赖 ========== -->
        <!-- 日志配置 - 方案一:使用Log4j -->
        <!-- Log4j2核心依赖 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.23.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.23.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.23.1</version>
        </dependency>
        <!-- 异步日志支持 -->
        <dependency>
            <groupId>com.lmax</groupId>
            <artifactId>disruptor</artifactId>
            <version>3.4.4</version>
        </dependency>

        <!-- 日志配置 - 方案二:使用Logback(推荐) -->
        <!-- Logback 日志实现 -->
        <!--        <dependency>-->
        <!--            <groupId>ch.qos.logback</groupId>-->
        <!--            <artifactId>logback-classic</artifactId>-->
        <!--            <version>1.2.5</version>-->
        <!--        </dependency>-->

        <!--        &lt;!&ndash; SLF4J API(logback-classic 已经包含 slf4j-api,但显式声明版本可避免冲突) &ndash;&gt;-->
        <!--        <dependency>-->
        <!--            <groupId>org.slf4j</groupId>-->
        <!--            <artifactId>slf4j-api</artifactId>-->
        <!--            <version>1.7.36</version>-->
        <!--        </dependency>-->


        <!-- ========== 测试相关依赖 ========== -->
        <!-- JUnit单元测试框架 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <!-- Spring测试支持,用于集成测试 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- PageHelper 分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.3.1</version> <!-- 请根据需要选择合适的版本 -->
        </dependency>
        <!-- Thymeleaf 核心库 -->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf</artifactId>
            <version>3.1.2.RELEASE</version>
        </dependency>

        <!-- Thymeleaf 与 Spring 集成 -->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
            <version>3.1.2.RELEASE</version>
        </dependency>

        <!-- fastjson   -->
        <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>2.0.21</version>
        </dependency>

        <!-- 文件上传支持 -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>

    </dependencies>

    <!-- 构建配置,如插件配置 -->
    <build>
        <plugins>
            <!-- Maven编译插件,配置JDK版本与编码 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>8</source>  <!-- 源码编译使用JDK 8 -->
                    <target>8</target>  <!-- 目标字节码为JDK 8 -->
                    <encoding>UTF-8</encoding>  <!-- 源码和编译编码均为UTF-8 -->
                </configuration>
            </plugin>

            <!-- Tomcat Maven插件,用于本地启动嵌入式Tomcat进行测试 -->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <port>8080</port>  <!-- Tomcat服务端口为8080 -->
                    <path>/</path>  <!-- 应用上下文路径为 /ssm -->
                </configuration>
            </plugin>

        </plugins>
    </build>
</project>

2、mapper接口

复制代码
package com.example.springmvcdemo.mapper;

import com.example.springmvcdemo.entity.po.GarbageRecordPO;
import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;

/**
 * 垃圾记录数据访问接口
 * 定义了垃圾记录相关的数据库操作方法
 */
@Mapper
public interface GarbageMapper {

    /**
     * 插入一条垃圾记录
     * @param record 垃圾记录实体对象
     * @return 插入成功的记录数
     */
    int insert(GarbageRecordPO record);

    /**
     * 根据用户ID查询垃圾记录列表
     * @param userId 用户ID
     * @return 该用户的所有垃圾记录列表
     */
    List<GarbageRecordPO> selectByUserId(@Param("userId") Integer userId);

    /**
     * 统计指定用户的垃圾记录总数
     * @param userId 用户ID
     * @return 该用户的垃圾记录总数量
     */
    int countByUserId(@Param("userId") Integer userId);

    /**
     * 计算指定用户获得的总积分
     * @param userId 用户ID
     * @return 该用户获得的积分总和
     */
    int sumPointsByUserId(@Param("userId") Integer userId);

    /**
     * 按垃圾类型统计用户记录数量
     * @param userId 用户ID
     * @return 包含垃圾类型和对应数量的映射列表
     */
    List<Map<String, Object>> countByType(@Param("userId") Integer userId);
}

package com.example.springmvcdemo.mapper;

import java.util.List;

import com.example.springmvcdemo.entity.po.UserPO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * (user)表数据库访问层
 */
@Mapper
public interface UserPOMapper {
    /**
     * 通过ID查询单条数据
     *
     * @param id 主键
     * @return 实例对象
     */
    UserPO queryById(Integer id);

    /**
     * 分页查询指定行数据
     *
     * @param user 查询条件
     * @return 对象列表
     */
    List<UserPO> queryAllByLimit(UserPO user);

    /**
     * 统计总行数
     *
     * @param user 查询条件
     * @return 总行数
     */
    long count(UserPO user);

    /**
     * 新增数据
     *
     * @param user 实例对象
     * @return 影响行数
     */
    int insert(UserPO user);

    /**
     * 批量新增数据
     *
     * @param entities List<UserPO> 实例对象列表
     * @return 影响行数
     */
    int insertBatch(@Param("entities") List<UserPO> entities);

    /**
     * 批量新增或按主键更新数据
     *
     * @param entities List<UserPO> 实例对象列表
     * @return 影响行数
     */
    int insertOrUpdateBatch(@Param("entities") List<UserPO> entities);

    /**
     * 更新数据
     *
     * @param user 实例对象
     * @return 影响行数
     */
    int update(UserPO user);

    /**
     * 通过主键删除数据
     *
     * @param id 主键
     * @return 影响行数
     */
    int deleteById(Integer id);

    /**
     * 通过用户名查询用户信息
     * @param username
     * @return
     */
    UserPO selectByUsername(String username);


    /**
     * 添加用户积分
     * @param id 用户ID
     * @param points 要添加的积分数量
     */
    void addPoints(@Param("id") int id, @Param("points") int points);


    /**
     * 根据用户ID查询用户信息
     * @param userId 用户ID
     * @return 用户信息对象
     */
    UserPO selectById(Integer userId);


}

3、mapper接口对印的XML

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.springmvcdemo.mapper.GarbageMapper">

  <resultMap id="BaseResultMap" type="com.example.springmvcdemo.entity.po.GarbageRecordPO">
    <id column="id" property="id"/>
    <result column="user_id" property="userId"/>
    <result column="type" property="type"/>
    <result column="weight" property="weight"/>
    <result column="accuracy" property="accuracy"/>
    <result column="points_earned" property="pointsEarned"/>
    <result column="image_url" property="imageUrl"/>
    <result column="create_time" property="createTime"/>
  </resultMap>

  <insert id="insert" parameterType="com.example.springmvcdemo.entity.po.GarbageRecordPO"
    useGeneratedKeys="true" keyProperty="id">
    INSERT INTO garbage_record
    (user_id, type, weight, accuracy, points_earned, image_url)
    VALUES
    (#{userId}, #{type}, #{weight}, #{accuracy}, #{pointsEarned}, #{imageUrl})
  </insert>

  <select id="selectByUserId" resultMap="BaseResultMap">
    SELECT * FROM garbage_record
    WHERE user_id = #{userId}
    ORDER BY create_time DESC
  </select>

  <select id="countByUserId" resultType="int">
    SELECT COUNT(*) FROM garbage_record
    WHERE user_id = #{userId}
  </select>

  <select id="sumPointsByUserId" resultType="int">
    SELECT IFNULL(SUM(points_earned), 0) FROM garbage_record
    WHERE user_id = #{userId}
  </select>

  <select id="countByType" resultType="map">
    SELECT
    `type` as type,
    COUNT(*) as count,
    SUM(weight) as total_weight,
    SUM(points_earned) as total_points
    FROM garbage_record
    WHERE user_id = #{userId}
    GROUP BY `type`
  </select>
</mapper>

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springmvcdemo.mapper.UserPOMapper">
    <resultMap type="com.example.springmvcdemo.entity.po.UserPO" id="UserPOMap">
        <result property="id" column="id" jdbcType="INTEGER"/>
        <result property="username" column="username" jdbcType="VARCHAR"/>
        <result property="password" column="password" jdbcType="VARCHAR"/>
        <result property="phone" column="phone" jdbcType="VARCHAR"/>
        <result property="address" column="address" jdbcType="VARCHAR"/>
        <result property="role" column="role" jdbcType="TINYINT"/>
        <result property="points" column="points" jdbcType="INTEGER"/>
        <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
    </resultMap>

    <!-- 通过ID查询单条数据 -->
    <select id="queryById" resultMap="UserPOMap">
        select
            id,username,password,phone,address,role,points,create_time
        from user
        where id = #{id}
    </select>

    <!--分页查询指定行数据-->
    <select id="queryAllByLimit" resultMap="UserPOMap">
        select
        id,username,password,phone,address,role,points,create_time
        from user
        <where>
            <if test="id != null">
                and id = #{id}
            </if>
            <if test="username != null and username != ''">
                and username = #{username}
            </if>
            <if test="password != null and password != ''">
                and password = #{password}
            </if>
            <if test="phone != null and phone != ''">
                and phone = #{phone}
            </if>
            <if test="address != null and address != ''">
                and address = #{address}
            </if>
            <if test="role != null">
                and role = #{role}
            </if>
            <if test="points != null">
                and points = #{points}
            </if>
            <if test="createTime != null">
                and create_time = #{createTime}
            </if>
        </where>
    </select>

    <!--统计总行数-->
    <select id="count" resultType="java.lang.Long">
        select count(1)
        from user
        <where>
            <if test="id != null and id != ''">
                and id = #{id}
            </if>
            <if test="username != null and username != ''">
                and username = #{username}
            </if>
            <if test="password != null and password != ''">
                and password = #{password}
            </if>
            <if test="phone != null and phone != ''">
                and phone = #{phone}
            </if>
            <if test="address != null and address != ''">
                and address = #{address}
            </if>
            <if test="role != null">
                and role = #{role}
            </if>
            <if test="points != null">
                and points = #{points}
            </if>
            <if test="createTime != null">
                and create_time = #{createTime}
            </if>
        </where>
    </select>
    <select id="selectByUsername" resultType="com.example.springmvcdemo.entity.po.UserPO">
        select * from user where username = #{username}
    </select>
    <select id="selectById" resultType="com.example.springmvcdemo.entity.po.UserPO">
        select * from user where id = #{userId}
    </select>

    <!--新增数据-->
    <insert id="insert" keyProperty="id" useGeneratedKeys="true">
        insert into user(id,username,password,phone,address,role,points,create_time)
        values (#{id},#{username},#{password},#{phone},#{address},#{role},#{points},#{createTime})
    </insert>

    <!-- 批量新增数据 -->
    <insert id="insertBatch" keyProperty="id" useGeneratedKeys="true">
        insert into user(id,username,password,phone,address,role,points,create_time)
        values
        <foreach collection="entities" item="entity" separator=",">
            (#{entity.id},#{entity.username},#{entity.password},#{entity.phone},#{entity.address},#{entity.role},#{entity.points},#{entity.createTime})
        </foreach>
    </insert>

    <!-- 批量新增或按主键更新数据 -->
    <insert id="insertOrUpdateBatch" keyProperty="id" useGeneratedKeys="true">
        insert into user(id,username,password,phone,address,role,points,create_time)
        values
        <foreach collection="entities" item="entity" separator=",">
            (#{entity.id},#{entity.username},#{entity.password},#{entity.phone},#{entity.address},#{entity.role},#{entity.points},#{entity.createTime})
        </foreach>
        on duplicate key update
        id=values(id),
        username=values(username),
        password=values(password),
        phone=values(phone),
        address=values(address),
        role=values(role),
        points=values(points),
        create_time=values(create_time)
    </insert>
    <insert id="addPoints">
        update user set points = points + #{points} where id = #{id}
    </insert>

    <!-- 更新数据 -->
    <update id="update">
        update user
        <set>
            <if test="id != null">
                id = #{id},
            </if>
            <if test="username != null and username != ''">
                username = #{username},
            </if>
            <if test="password != null and password != ''">
                password = #{password},
            </if>
            <if test="phone != null and phone != ''">
                phone = #{phone},
            </if>
            <if test="address != null and address != ''">
                address = #{address},
            </if>
            <if test="role != null">
                role = #{role},
            </if>
            <if test="points != null">
                points = #{points},
            </if>
            <if test="createTime != null">
                create_time = #{createTime},
            </if>
        </set>
        where id = #{id}
    </update>

    <!--通过主键删除-->
    <delete id="deleteById">
        delete from user where id = #{id}
    </delete>
</mapper>

4、service接口

复制代码
package com.example.springmvcdemo.service;

import com.example.springmvcdemo.entity.dto.UserDTO;
import com.example.springmvcdemo.entity.po.UserPO;

/**
 * @className UserService
 * @date 2025/7/28
 * @description 用户服务层
 */
public interface UserService {
    /**
     * 用户登录功能
     * 根据用户名和密码验证用户身份,返回对应的用户信息
     *
     * @param username 用户名
     * @param password 密码
     * @return UserPO 用户信息对象,如果登录失败返回null
     */
    UserPO login(String username, String password);

    /**
     * 用户注册功能
     * 根据用户提供的注册信息创建新用户
     *
     * @param userDTO 用户注册信息数据传输对象
     * @return UserPO 新创建的用户信息对象
     */
    UserPO register(UserDTO userDTO);

    /**
     * 为指定用户添加积分
     * 根据用户ID为用户账户增加指定数量的积分
     *
     * @param id 用户ID
     * @param points 要添加的积分数量
     */
    void addPoints(int id, int points);
}

package com.example.springmvcdemo.service;

import com.example.springmvcdemo.entity.dto.GarbageRecordDTO;
import com.example.springmvcdemo.entity.vo.GarbageRecordVO;
import com.github.pagehelper.PageInfo;
import java.util.Map;

/**
 * 垃圾回收服务接口
 * 提供垃圾记录管理、用户记录查询和用户统计信息获取功能
 */
public interface GarbageService {

    /**
     * 添加垃圾回收记录
     * @param record 垃圾记录数据传输对象,包含记录的详细信息
     * @return 添加成功返回true,失败返回false
     */
    boolean addRecord(GarbageRecordDTO record);

    /**
     * 根据用户ID分页获取垃圾回收记录
     * @param userId 用户ID,用于筛选指定用户的记录
     * @param page 页码,从1开始计数
     * @param size 每页记录数量
     * @return 分页包装的垃圾记录视图对象列表
     */
    PageInfo<GarbageRecordVO> getRecordsByUser(Integer userId, Integer page, Integer size);

    /**
     * 获取指定用户的垃圾回收统计信息
     * @param userId 用户ID,用于获取对应用户的统计数据
     * @return 包含用户统计信息的键值对映射,key为统计项名称,value为统计值
     */
    Map<String, Object> getUserStats(Integer userId);

}

5、service的实现

复制代码
package com.example.springmvcdemo.service.impl;

import com.example.springmvcdemo.entity.dto.UserDTO;
import com.example.springmvcdemo.entity.po.UserPO;
import com.example.springmvcdemo.mapper.UserPOMapper;
import com.example.springmvcdemo.service.UserService;
import com.example.springmvcdemo.utils.AESUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

/**
 * @className UserServiceImpl
 * @date 2025/7/28
 * @description 用户服务层实现
 */
@Slf4j
@Service
public class UserServiceImpl implements UserService {

    // 从配置文件中读取 AES 加密使用的密钥
    @Value("${aes.secret.key}")
    private String aesSecretKey;

    // 从配置文件中读取 AES 加密使用的初始化向量(IV)
    @Value("${aes.iv}")
    private String aesIV;

    // 自动注入 MyBatis 的 UserPOMapper 接口,用于操作用户数据
    @Autowired
    private UserPOMapper userMapper;

    /**
     * 用户登录方法
     *
     * @param username 用户名
     * @param password 密码(前端传明文,后端加密后与数据库比对)
     * @return 登录成功返回用户对象,失败返回 null
     */
    @Override
    public UserPO login(String username, String password) {

        // 1. 先根据用户名查询用户
        UserPO user = userMapper.selectByUsername(username);
        if (user == null) {
            log.info("登录失败,用户名不存在: {}", username);
            return null;
        }

        // 2. 验证密码:从数据库取出已加密的密码,和前端传来的明文密码加密后比对
        String passwordFromDB = user.getPassword();
        if (passwordFromDB == null) {
            log.info("登录失败,密码为空");
            return null;
        }
        if (!passwordFromDB.equals(AESUtil.encrypt(password, aesSecretKey, aesIV))) {
            log.info("登录失败,密码错误");
            return null;
        }
        return user;
    }

    /**
     * 用户注册方法
     *
     * @param userDTO 前端传入的用户注册信息数据传输对象
     * @return 注册成功返回用户对象,失败返回 null
     */
    @Override
    public UserPO register(UserDTO userDTO) {
        if (userDTO == null) {
            log.warn("注册失败,用户信息为空");
            return null;
        }
        String username = userDTO.getUsername();
        String password = userDTO.getPassword();
        if (username == null || password == null) {
            log.warn("注册失败,用户名或密码为空");
        }
        UserPO user = new UserPO();
        user.setUsername(username);
        // 对用户密码进行 AES 加密后存入数据库
        user.setPassword(AESUtil.encrypt(password, aesSecretKey, aesIV));
        user.setPhone(userDTO.getPhone());
        user.setAddress(userDTO.getAddress());
        user.setRole((byte) 1); // 默认角色,例如普通用户
        user.setPoints(0); // 初始积分为 0
        user.setCreateTime(LocalDateTime.now()); // 设置创建时间为当前时间
        int insert = userMapper.insert(user);
        if (insert > 0) {
            log.info("注册成功,用户信息:{}", user);
            return user;
        }
        return null;
    }

    /**
     * 为用户增加积分
     *
     * @param id     用户 ID
     * @param points 要增加的积分数值
     */
    @Override
    public void addPoints(int id, int points) {
        userMapper.addPoints(id, points);
    }

}

package com.example.springmvcdemo.service.impl;

import com.alibaba.fastjson2.JSON;
import com.example.springmvcdemo.entity.dto.GarbageRecordDTO;
import com.example.springmvcdemo.entity.po.GarbageRecordPO;
import com.example.springmvcdemo.entity.po.UserPO;
import com.example.springmvcdemo.entity.vo.GarbageRecordVO;
import com.example.springmvcdemo.enums.GarbageType;
import com.example.springmvcdemo.mapper.GarbageMapper;
import com.example.springmvcdemo.mapper.UserPOMapper;
import com.example.springmvcdemo.service.GarbageService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 垃圾服务实现类
 */
@Slf4j
@Service
public class GarbageServiceImpl implements GarbageService {

    @Autowired
    private GarbageMapper garbageMapper;


    /**
     * 添加垃圾记录
     * @param record 垃圾记录数据传输对象
     * @return 是否添加成功
     */
    @Override
    @Transactional
    public boolean addRecord(GarbageRecordDTO record) {
        GarbageRecordPO recordPO = new GarbageRecordPO();
        BeanUtils.copyProperties(record, recordPO); // 将DTO属性拷贝到PO
        return garbageMapper.insert(recordPO) > 0; // 插入数据库并判断是否成功
    }

    /**
     * 根据用户ID分页获取垃圾记录,并转换为VO对象
     * @param userId 用户ID
     * @param page 页码
     * @param size 每页大小
     * @return 分页后的垃圾记录视图对象
     */
    @Override
    public PageInfo<GarbageRecordVO> getRecordsByUser(Integer userId, Integer page, Integer size) {
        PageHelper.startPage(page, size); // 开启分页
        List<GarbageRecordPO> records = garbageMapper.selectByUserId(userId); // 查询用户垃圾记录
        PageInfo<GarbageRecordPO> pageInfo = PageInfo.of(records); // 构造PO分页信息
        // 转换为VO并补充垃圾类型名称
        List<GarbageRecordVO> recordVOs = records.stream().map(po -> {
            GarbageRecordVO vo = new GarbageRecordVO();
            BeanUtils.copyProperties(po, vo); // 拷贝PO属性到VO
            vo.setTypeName(GarbageType.fromCode(po.getType()).getName()); // 设置垃圾类型名称
            return vo;
        }).collect(Collectors.toList());
        // 构造并返回VO分页信息
        PageInfo<GarbageRecordVO> pageInfoList = getGarbageRecordVOPageInfo(recordVOs, pageInfo);
        log.info("pageInfoList: {}", pageInfoList); // 日志记录分页信息
        return pageInfoList;
    }

    /**
     * 将PO分页信息转换为VO分页信息(手动拷贝分页参数)
     * @param recordVOs 垃圾记录视图对象列表
     * @param pageInfo 原始PO分页信息
     * @return 转换后的VO分页信息
     */
    private PageInfo<GarbageRecordVO> getGarbageRecordVOPageInfo(List<GarbageRecordVO> recordVOs, PageInfo<GarbageRecordPO> pageInfo) {
        PageInfo<GarbageRecordVO> pageInfoList = PageInfo.of(recordVOs);
        pageInfoList.setTotal(pageInfo.getTotal()); // 总记录数
        pageInfoList.setPages(pageInfo.getPages()); // 总页数
        pageInfoList.setPageNum(pageInfo.getPageNum()); // 当前页码
        pageInfoList.setPageSize(pageInfo.getPageSize()); // 每页大小
        pageInfoList.setList(recordVOs); // 当前页数据列表
        pageInfoList.setNavigatePages(pageInfo.getNavigatePages()); // 导航页码数
        pageInfoList.setNavigatepageNums(pageInfo.getNavigatepageNums()); // 导航页码列表
        pageInfoList.setPrePage(pageInfo.getPrePage()); // 上一页页码
        pageInfoList.setNextPage(pageInfo.getNextPage()); // 下一页页码
        pageInfoList.setIsFirstPage(pageInfo.isIsFirstPage()); // 是否第一页
        pageInfoList.setIsLastPage(pageInfo.isIsLastPage()); // 是否最后一页
        pageInfoList.setHasPreviousPage(pageInfo.isHasPreviousPage()); // 是否有上一页
        return pageInfoList;
    }

    /**
     * 获取用户垃圾投放统计数据
     * @param userId 用户ID
     * @return 统计信息,包括总次数、总积分、各类型投放统计
     */
    @Override
    public Map<String, Object> getUserStats(Integer userId) {
        Map<String, Object> stats = new HashMap<>();
        stats.put("totalCount", garbageMapper.countByUserId(userId)); // 总投放次数
        stats.put("totalPoints", garbageMapper.sumPointsByUserId(userId)); // 总获得积分
        List<Map<String, Object>> maps = garbageMapper.countByType(userId); // 按类型统计投放次数
        log.info("maps: {}", JSON.toJSONString(maps)); // 打印类型统计日志
        stats.put("typeStats", maps); // 各类型统计数据
        return stats;
    }


}

6、Controller控制器的实现

复制代码
package com.example.springmvcdemo.controller;

import com.example.springmvcdemo.entity.dto.UserDTO;
import com.example.springmvcdemo.entity.po.UserPO;
import com.example.springmvcdemo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpSession;


/**
 * 用户控制器
 */
// 使用 Lombok 提供的日志注解,自动注入 Logger 对象(变量名为 log)
@Slf4j
// 声明该类为一个 Spring MVC 的控制器组件
@Controller
// 为该控制器类下的所有请求映射添加统一的前缀 /user
@RequestMapping("/user")
public class UserController {

    // 自动注入 UserService 组件,用于处理用户相关的业务逻辑
    @Autowired
    private UserService userService;

    /**
     * 处理用户登录的 POST 请求,路径为 /user/login
     */
    @PostMapping("/login")
    public String login(String username, String password, HttpSession session) {
        try {
            log.info("用户登录:{},登录密码:{}", username, password);

            // 如果用户名或密码为空,则重定向回登录页并附加错误参数 error=1,用于前端展示提示信息
            if (username == null || password == null) {
                return "redirect:/login.html?error=1";
            }
            // 调用服务层方法,根据用户名和密码进行登录验证,返回用户对象
            UserPO user = userService.login(username, password);
            if (user != null) {
                // 登录成功,将用户对象存入 Session 中,便于后续请求识别用户身份
                session.setAttribute("user", user);
                // 重定向到主页 /index.html
                return "redirect:/garbage/list"; // 重定向到垃圾列表 // 重定向到主页
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("登录失败:{}", e.getMessage());
        }
        // 登录失败,重定向回登录页并附加错误参数 error=1,用于前端展示提示信息
        return "redirect:/login.html?error=1";
    }


    /**
     * 用户注册
     */
    @PostMapping("/register")
    public String register(UserDTO userDTO, HttpSession session) {
        try {
            if (userDTO == null) {
                return "redirect:/register.html?error=1";
            }
            String username = userDTO.getUsername();
            String password = userDTO.getPassword();
            if (username == null || password == null) {
                return "redirect:/register.html?error=2";
            }
            // 调用服务层方法,根据用户名和密码进行注册
            UserPO user = userService.register(userDTO);
            if (user != null) {
                // 注册成功,将用户对象存入 Session 中,便于后续请求识别用户身份
                session.setAttribute("user", user);
                // 重定向到主页 /index.html
                return "redirect:/login.html"; // 重定向到主页
            }
            // 注册失败,重定向回注册页并附加错误参数 error=1,用于前端展示提示信息
            return "redirect:/register.html?error=3";
        } catch (Exception e) {
            e.printStackTrace();
            log.error("注册失败:{}", e.getMessage());
            return "redirect:/register.html?error=4";
        }

    }


    /**
     * 处理用户登出的 GET 请求,路径为 /user/logout
     *
     * @param session
     * @return
     */
    @GetMapping("/logout")
    public String logout(HttpSession session) {
        // 使当前 Session 失效,清除用户登录状态
        session.invalidate();
        // 重定向回登录页
        return "redirect:/login.html";
    }
}

package com.example.springmvcdemo.controller;

import com.alibaba.fastjson.JSON;
import com.example.springmvcdemo.entity.dto.GarbageRecordDTO;
import com.example.springmvcdemo.entity.po.UserPO;
import com.example.springmvcdemo.entity.vo.GarbageRecordVO;
import com.example.springmvcdemo.service.GarbageService;
import com.example.springmvcdemo.service.UserService;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;


import javax.servlet.http.HttpSession;
import java.io.File;
import java.util.Map;
import java.util.UUID;

@Slf4j
@Controller
@RequestMapping("/garbage")
public class GarbageController {

    @Autowired
        private GarbageService garbageService; // 自动注入垃圾记录服务,用于处理垃圾投递相关业务逻辑
    
        @Autowired
        private UserService userService; // 自动注入用户服务,用于处理用户相关操作,如积分管理
    
        @Value("${file.upload-dir}") 
        private String uploadDir; // 从配置文件中读取文件上传目录路径
    
    
        @GetMapping("/add")
        public String showAddPage(HttpSession session) {
            log.info("新增 showAddPage"); // 打印日志:进入添加垃圾记录页面
            UserPO user = (UserPO) session.getAttribute("user"); // 从会话中获取当前登录用户
            if (user == null) {
                return "redirect:/login.html"; // 如果用户未登录,重定向到登录页
            }
            return "garbage/add"; // 返回添加垃圾记录的视图页面
        }
    
        @PostMapping("/add")
        public String addRecord(@RequestParam("type") Integer type,
                                @RequestParam("weight") Double weight,
                                @RequestParam("accuracy") Integer accuracy,
                                @RequestParam("image") MultipartFile image,
                                HttpSession session, Model model) {
            try {
                log.info("新增 addRecord: type:{}, weight:{}, accuracy:{}", type, weight, accuracy); // 打印日志:接收到新增记录请求参数
                UserPO user = (UserPO) session.getAttribute("user"); // 获取当前用户
                if (user == null) {
                    return "redirect:/login.html"; // 未登录则跳转到登录页
                }
    
                GarbageRecordDTO record = new GarbageRecordDTO(); // 创建垃圾记录数据传输对象
                record.setType(type); // 设置垃圾类型
                record.setWeight(weight); // 设置垃圾重量
                record.setAccuracy(accuracy); // 设置识别准确度
                record.setUserId(user.getId()); // 设置当前用户ID
    
                // 处理文件上传
                if (!image.isEmpty()) {  // 如果上传的图片文件不为空
                    File dir = new File(uploadDir);  // 根据配置的目录路径创建File对象
                    if (!dir.exists()) {  // 如果目录不存在
                        dir.mkdirs();  // 创建目录(包括多级目录)
                    }
                    // 获取用户上传文件的原始名称,如 "photo.jpg"
                    String originalFilename = image.getOriginalFilename();
                    // 提取文件后缀,例如 ".jpg"
                    String fileExt = originalFilename.substring(originalFilename.lastIndexOf("."));
                    // 使用UUID生成唯一文件名,避免文件覆盖,保留原扩展名
                    String newFilename = UUID.randomUUID() + fileExt;
    
                    // 构造目标文件对象,即上传目录 + 新文件名
                    File dest = new File(dir, newFilename);
                    // 将上传的临时文件保存到目标位置
                    image.transferTo(dest);
    
                    // 打印日志:文件上传成功及存储路径
                    log.info("文件上传成功,保存路径为:{}", dest.getAbsolutePath());
                    // 设置记录中的图片URL(此处应使用相对路径或网络访问路径,目前直接拼接了绝对路径,建议优化)
                    record.setImageUrl(dest.getAbsolutePath() + newFilename); 
                }
    
                // 根据重量和准确度计算获得的积分
                int points = (int) (record.getWeight() * 10 * record.getAccuracy() / 100);
                record.setPointsEarned(points); // 设置该记录获得的积分
                log.info("新增2 addRecord: {}", record); // 打印当前记录信息
                boolean success = garbageService.addRecord(record); // 调用服务保存垃圾记录
                if (success) {
                    log.info("新增成功 addRecord: {}", record); // 保存成功,打印日志
                    userService.addPoints(user.getId(), points); // 给用户增加对应积分
                    model.addAttribute("message", "投递记录添加成功!获得" + points + "积分"); // 设置成功提示信息
                    return "redirect:/garbage/list"; // 重定向到垃圾记录列表页
                }
            } catch (Exception e) {
                log.error("新增失败 addRecord", e); // 捕获并打印异常日志
            }
            model.addAttribute("error", "添加记录失败"); // 设置错误提示信息
            return "garbage/add"; // 返回添加页面并显示错误
        }
    
        @GetMapping("/list")
        public String listRecords(
                @RequestParam(defaultValue = "1") int page,
                @RequestParam(defaultValue = "5") int size,
                HttpSession session, Model model) {
            UserPO user = (UserPO) session.getAttribute("user"); // 获取当前登录用户
            if (user == null) {
                log.info("获取用户信息失败"); // 用户未登录,打印日志
                return "redirect:/login.html"; // 重定向到登录页
            }
            log.info("分页查询参数:page:{}, size:{}", page, size); // 打印分页查询的参数
            PageInfo<GarbageRecordVO> pageInfo = garbageService.getRecordsByUser(user.getId(), page, size); // 查询当前用户垃圾记录(分页)
            model.addAttribute("pageInfo", pageInfo); // 将分页结果放入模型,供视图展示
            log.info("分页查询结果:{}", JSON.toJSONString(pageInfo)); // 打印分页查询结果日志
            return "garbage/list"; // 返回垃圾记录列表视图页面
        }
    
        @GetMapping("/stats")
        public String getStats(HttpSession session, Model model) {
            UserPO user = (UserPO) session.getAttribute("user"); // 获取当前登录用户
            if (user == null) {
                return "redirect:/login.html"; // 未登录,重定向到登录页
            }
            Map<String, Object> stats = garbageService.getUserStats(user.getId()); // 查询该用户的统计信息,如总积分、投递次数等
            log.info("获取统计信息: {}", stats); // 打印统计信息日志
            model.addAttribute("stats", stats); // 将统计信息加入模型
            model.addAttribute("user", user); // 将用户信息也传入视图
            return "garbage/stats"; // 返回用户统计信息视图页面
        }
    
}

7、entity的实现

复制代码
package com.example.springmvcdemo.entity.po;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;

/**
 * 垃圾记录持久化对象(PO,Persistent Object),
 * 表示垃圾记录相关的数据实体,
 * 实现了 Serializable 接口以支持对象序列化,
 * 实现了 Cloneable 接口以支持对象克隆。
 */
public class GarbageRecordPO implements Serializable, Cloneable {

    /**
     * ID
     */
    private int id;
    /**
     * 投递用户ID,;
     */
    private int userId;
    /**
     * 1-可回收 2-有害 3-厨余 4-其他,;
     */
    private Integer type;
    /**
     * 重量(kg),;
     */
    private Double weight;
    /**
     * 分类准确率(%),;
     */
    private byte accuracy;
    /**
     * 获得积分,;
     */
    private int pointsEarned;
    /**
     * 投递照片,;
     */
    private String imageUrl;
    /**
     * 创建时间,;
     */
    private LocalDateTime createTime;

    /**
     * ;
     */
    public int getId() {
        return this.id;
    }

    /**
     * ;
     */
    public void setId(int id) {
        this.id = id;
    }

    /**
     * 投递用户ID,;
     */
    public int getUserId() {
        return this.userId;
    }

    /**
     * 投递用户ID,;
     */
    public void setUserId(int userId) {
        this.userId = userId;
    }

    /**
     * 1-可回收 2-有害 3-厨余 4-其他,;
     */
    public Integer getType() {
        return this.type;
    }

    /**
     * 1-可回收 2-有害 3-厨余 4-其他,;
     */
    public void setType(Integer type) {
        this.type = type;
    }

    /**
     * 重量(kg),;
     */
    public Double getWeight() {
        return this.weight;
    }

    /**
     * 重量(kg),;
     */
    public void setWeight(Double weight) {
        this.weight = weight;
    }

    /**
     * 分类准确率(%),;
     */
    public byte getAccuracy() {
        return this.accuracy;
    }

    /**
     * 分类准确率(%),;
     */
    public void setAccuracy(byte accuracy) {
        this.accuracy = accuracy;
    }

    /**
     * 获得积分,;
     */
    public int getPointsEarned() {
        return this.pointsEarned;
    }

    /**
     * 获得积分,;
     */
    public void setPointsEarned(int pointsEarned) {
        this.pointsEarned = pointsEarned;
    }

    /**
     * 投递照片,;
     */
    public String getImageUrl() {
        return this.imageUrl;
    }

    /**
     * 投递照片,;
     */
    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }

    /**
     * 创建时间,;
     */
    public LocalDateTime getCreateTime() {
        return this.createTime;
    }

    /**
     * 创建时间,;
     */
    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }
}

package com.example.springmvcdemo.entity.po;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;

/**
 * 用户持久化对象(PO,Persistent Object),用于表示数据库中的用户数据
 * 实现了 Serializable 接口,支持对象的序列化(如网络传输、持久化存储)
 * 实现了 Cloneable 接口,支持对象的浅拷贝
 */
public class UserPO implements Serializable, Cloneable {
    /** ID; */
    private int id ;
    /** 用户名,; */
    private String username ;
    /** 密码(加密存储),; */
    private String password ;
    /** 手机号,; */
    private String phone ;
    /** 住址,; */
    private String address ;
    /** 0-居民 1-管理员,; */
    private byte role ;
    /** 环保积分,; */
    private int points ;
    /** 创建时间,; */
    private LocalDateTime createTime ;

    /** ID,; */
    public int getId(){
        return this.id;
    }
    /** ID,; */
    public void setId(int id){
        this.id=id;
    }
    /** 用户名,; */
    public String getUsername(){
        return this.username;
    }
    /** 用户名,; */
    public void setUsername(String username){
        this.username=username;
    }
    /** 密码(加密存储),; */
    public String getPassword(){
        return this.password;
    }
    /** 密码(加密存储),; */
    public void setPassword(String password){
        this.password=password;
    }
    /** 手机号,; */
    public String getPhone(){
        return this.phone;
    }
    /** 手机号,; */
    public void setPhone(String phone){
        this.phone=phone;
    }
    /** 住址,; */
    public String getAddress(){
        return this.address;
    }
    /** 住址,; */
    public void setAddress(String address){
        this.address=address;
    }
    /** 0-居民 1-管理员,; */
    public byte getRole(){
        return this.role;
    }
    /** 0-居民 1-管理员,; */
    public void setRole(byte role){
        this.role=role;
    }
    /** 环保积分,; */
    public int getPoints(){
        return this.points;
    }
    /** 环保积分,; */
    public void setPoints(int points){
        this.points=points;
    }
    /** 创建时间,; */
    public LocalDateTime getCreateTime(){
        return this.createTime;
    }
    /** 创建时间,; */
    public void setCreateTime(LocalDateTime createTime){
        this.createTime=createTime;
    }
}

package com.example.springmvcdemo.entity.dto;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 垃圾记录数据传输对象(DTO,Data Transfer Object),
 * 表示垃圾记录相关的数据实体,
 * 实现了 Serializable 接口以支持对象序列化,
 * 实现了 Cloneable 接口以支持对象克隆。
 */
public class GarbageRecordDTO implements Serializable, Cloneable {

    /**
     * ID
     */
    private int id;
    /**
     * 投递用户ID,;
     */
    private int userId;
    /**
     * 1-可回收 2-有害 3-厨余 4-其他,;
     */
    private Integer type;
    /**
     * 重量(kg),;
     */
    private Double weight;
    /**
     * 分类准确率(%),;
     */
    private Integer accuracy;
    /**
     * 获得积分,;
     */
    private Integer pointsEarned;
    /**
     * 投递照片,;
     */
    private String imageUrl;
    /**
     * 创建时间,;
     */
    private LocalDateTime createTime;

    /**
     * ;
     */
    public int getId() {
        return this.id;
    }

    /**
     * ;
     */
    public void setId(int id) {
        this.id = id;
    }

    /**
     * 投递用户ID,;
     */
    public int getUserId() {
        return this.userId;
    }

    /**
     * 投递用户ID,;
     */
    public void setUserId(int userId) {
        this.userId = userId;
    }

    /**
     * 1-可回收 2-有害 3-厨余 4-其他,;
     */
    public Integer getType() {
        return this.type;
    }

    /**
     * 1-可回收 2-有害 3-厨余 4-其他,;
     */
    public void setType(Integer type) {
        this.type = type;
    }

    /**
     * 重量(kg),;
     */
    public Double getWeight() {
        return this.weight;
    }

    /**
     * 重量(kg),;
     */
    public void setWeight(Double weight) {
        this.weight = weight;
    }

    /**
     * 分类准确率(%),;
     */
    public Integer getAccuracy() {
        return this.accuracy;
    }

    /**
     * 分类准确率(%),;
     */
    public void setAccuracy(Integer accuracy) {
        this.accuracy = accuracy;
    }

    /**
     * 获得积分,;
     */
    public Integer getPointsEarned() {
        return this.pointsEarned;
    }

    /**
     * 获得积分,;
     */
    public void setPointsEarned(Integer pointsEarned) {
        this.pointsEarned = pointsEarned;
    }

    /**
     * 投递照片,;
     */
    public String getImageUrl() {
        return this.imageUrl;
    }

    /**
     * 投递照片,;
     */
    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }

    /**
     * 创建时间,;
     */
    public LocalDateTime getCreateTime() {
        return this.createTime;
    }

    /**
     * 创建时间,;
     */
    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }
}

package com.example.springmvcdemo.entity.dto;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 用户数据传输对象(DTO,Persistent Object),用于表示数据库中的用户数据
 * 实现了 Serializable 接口,支持对象的序列化(如网络传输、持久化存储)
 * 实现了 Cloneable 接口,支持对象的浅拷贝
 */
public class UserDTO implements Serializable, Cloneable {
    /** 用户名,; */
    private String username ;
    /** 密码(加密存储),; */
    private String password ;
    /** 手机号,; */
    private String phone ;
    /** 住址,; */
    private String address ;
    /** 创建时间,; */
    private LocalDateTime createTime ;

    /** 用户名,; */
    public String getUsername(){
        return this.username;
    }
    /** 用户名,; */
    public void setUsername(String username){
        this.username=username;
    }
    /** 密码(加密存储),; */
    public String getPassword(){
        return this.password;
    }
    /** 密码(加密存储),; */
    public void setPassword(String password){
        this.password=password;
    }
    /** 手机号,; */
    public String getPhone(){
        return this.phone;
    }
    /** 手机号,; */
    public void setPhone(String phone){
        this.phone=phone;
    }
    /** 住址,; */
    public String getAddress(){
        return this.address;
    }
    /** 住址,; */
    public void setAddress(String address){
        this.address=address;
    }

    /** 创建时间,; */
    public LocalDateTime getCreateTime(){
        return this.createTime;
    }
    /** 创建时间,; */
    public void setCreateTime(LocalDateTime createTime){
        this.createTime=createTime;
    }
}

package com.example.springmvcdemo.entity.vo;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 *  垃圾记录视图对象(VO,View Object),
 * 表示垃圾记录相关的数据实体,
 * 实现了 Serializable 接口以支持对象序列化,
 * 实现了 Cloneable 接口以支持对象克隆。
 */
public class GarbageRecordVO implements Serializable, Cloneable {

    /**
     * ID
     */
    private int id;
    /**
     * 投递用户ID,;
     */
    private int userId;
    /**
     * 1-可回收 2-有害 3-厨余 4-其他,;
     */
    private byte type;

    private String typeName;
    /**
     * 重量(kg),;
     */
    private Double weight;
    /**
     * 分类准确率(%),;
     */
    private byte accuracy;
    /**
     * 获得积分,;
     */
    private int pointsEarned;
    /**
     * 投递照片,;
     */
    private String imageUrl;
    /**
     * 创建时间,;
     */
    private LocalDateTime createTime;

    /**
     * ;
     */
    public int getId() {
        return this.id;
    }

    /**
     * ;
     */
    public void setId(int id) {
        this.id = id;
    }

    /**
     * 投递用户ID,;
     */
    public int getUserId() {
        return this.userId;
    }

    /**
     * 投递用户ID,;
     */
    public void setUserId(int userId) {
        this.userId = userId;
    }

    /**
     * 1-可回收 2-有害 3-厨余 4-其他,;
     */
    public byte getType() {
        return this.type;
    }

    /**
     * 1-可回收 2-有害 3-厨余 4-其他,;
     */
    public void setType(byte type) {
        this.type = type;
    }

    public String getTypeName() {
        return typeName;
    }

    public void setTypeName(String typeName) {
        this.typeName = typeName;
    }

    /**
     * 重量(kg),;
     */
    public Double getWeight() {
        return this.weight;
    }

    /**
     * 重量(kg),;
     */
    public void setWeight(Double weight) {
        this.weight = weight;
    }

    /**
     * 分类准确率(%),;
     */
    public byte getAccuracy() {
        return this.accuracy;
    }

    /**
     * 分类准确率(%),;
     */
    public void setAccuracy(byte accuracy) {
        this.accuracy = accuracy;
    }

    /**
     * 获得积分,;
     */
    public int getPointsEarned() {
        return this.pointsEarned;
    }

    /**
     * 获得积分,;
     */
    public void setPointsEarned(int pointsEarned) {
        this.pointsEarned = pointsEarned;
    }

    /**
     * 投递照片,;
     */
    public String getImageUrl() {
        return this.imageUrl;
    }

    /**
     * 投递照片,;
     */
    public void setImageUrl(String imageUrl) {
        this.imageUrl = imageUrl;
    }

    /**
     * 创建时间,;
     */
    public LocalDateTime getCreateTime() {
        return this.createTime;
    }

    /**
     * 创建时间,;
     */
    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }
}

8、拦截器AuthInterceptor

复制代码
package com.example.springmvcdemo.interceptor;

import com.example.springmvcdemo.entity.po.UserPO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

// 使用 Lombok 提供的日志注解,自动生成日志对象 log
@Slf4j
// 标记该类为 Spring 组件,由 Spring 容器管理
@Component
// 实现 Spring MVC 的拦截器接口,用于拦截请求并进行前置、后置处理
public class AuthInterceptor implements HandlerInterceptor {

    /**
     * 在控制器方法执行前调用,用于做权限校验、登录检查等操作
     * 返回 true 表示放行,继续执行后续的处理器和拦截器;
     * 返回 false 则中断请求,不会继续执行后续逻辑。
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取当前请求的URI,用于判断请求路径
        String uri = request.getRequestURI();
        log.info("拦截器获取拦截请求:{}", uri);

        // 放行公开路径,比如登录、注册页面,不需要登录即可访问
        if (uri.contains("/login") || uri.contains("/register")) {
            return true;
        }

        // 获取当前用户的 Session
        HttpSession session = request.getSession();
        // 从 session 中尝试获取用户信息,通常登录成功后会将用户对象存入 session
        UserPO user = (UserPO) session.getAttribute("user");

        // 如果 session 中没有用户信息,说明用户未登录
        if (user == null) {
            log.info("用户未登录,跳转到登录页面");
            // 重定向到登录页面,阻止当前请求继续执行
            response.sendRedirect(request.getContextPath() + "/login.html");
            return false; // 中断请求
        }

        // 用户已登录,允许访问当前请求
        return true;
    }

    /**
     * 在控制器方法执行后,视图渲染前调用
     * 可用于对 ModelAndView 对象进行修改,添加公共模型数据等
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        log.info("拦截器postHandle获取拦截请求:{}", request.getRequestURI());
        // 此处可以添加一些需要在渲染视图前处理的逻辑,比如统一添加数据到模型
    }

    /**
     * 在整个请求完成之后调用,即视图已经渲染完毕
     * 一般用于资源清理、日志记录等收尾工作
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) throws Exception {
        // 请求完成后执行的逻辑,可用于记录请求耗时、异常处理等
        log.info("拦截器afterCompletion获取拦截请求:{}", request.getRequestURI());
    }
}
拦截器配置方式
1、XML配置
复制代码
<!-- spring-mvc.xml -->
<mvc:interceptors>

  <!-- 或指定拦截路径 -->
  <mvc:interceptor>
    <mvc:mapping path="/**"/>
    <mvc:exclude-mapping path="/login.html"/>
    <mvc:exclude-mapping path="/register.html"/>
    <mvc:exclude-mapping path="/static/**"/>
    <mvc:exclude-mapping path="/error"/>
    <bean class="com.example.springmvcdemo.interceptor.AuthInterceptor"/>
  </mvc:interceptor>
</mvc:interceptors>
2、java配置
复制代码
@Slf4j
@ComponentScan(basePackages = "com.example.springmvcdemo")  // 扫描指定包及其子包中的组件,自动注册为Spring容器中的Bean
@EnableWebMvc   // 启用 Spring MVC,简化配置并激活注解驱动的开发模式,使用注解必须将xml配置文件注释掉<mvc:annotation-driven/>
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        log.info("注册拦截器");
        // 认证拦截器 - 拦截特定路径
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login")
                .excludePathPatterns("/register"); // 排除登录页面
    }
}

9、枚举类(垃圾分类枚举定义)

复制代码
package com.example.springmvcdemo.enums;

/**
 * 垃圾类型枚举类
 * 定义了四种垃圾分类类型及其对应的代码和名称
 */
public enum GarbageType {
    RECYCLABLE(1, "可回收垃圾"),
    HAZARDOUS(2, "有害垃圾"),
    KITCHEN(3, "厨余垃圾"),
    OTHER(4, "其他垃圾");

    private final int code;
    private final String name;

    /**
     * 构造函数
     * @param code 垃圾类型代码
     * @param name 垃圾类型名称
     */
    GarbageType(int code, String name) {
        this.code = code;
        this.name = name;
    }

    /**
     * 获取垃圾类型代码
     * @return 垃圾类型代码
     */
    public int getCode() {
        return code;
    }

    /**
     * 获取垃圾类型名称
     * @return 垃圾类型名称
     */
    public String getName() {
        return name;
    }

    /**
     * 根据代码获取对应的垃圾类型枚举值
     * @param code 垃圾类型代码
     * @return 对应的垃圾类型枚举值
     * @throws IllegalArgumentException 当代码无效时抛出异常
     */
    public static GarbageType fromCode(int code) {
        // 遍历所有枚举值,查找匹配的代码
        for (GarbageType type : values()) {
            if (type.code == code) {
                return type;
            }
        }
        throw new IllegalArgumentException("无效的垃圾类型代码: " + code);
    }
}

10、AES加密解密工具类

复制代码
package com.example.springmvcdemo.utils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * AES加密解密工具类
 * 支持AES/CBC/PKCS5Padding模式
 */
public class AESUtil {

    private static final String ALGORITHM = "AES";
    private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
    private static final int KEY_SIZE = 128; // 128, 192 or 256
    
    /**
     * 生成AES密钥
     * @return 返回Base64编码的密钥字符串
     */
    public static String generateKey() {
        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
            keyGenerator.init(KEY_SIZE);
            SecretKey secretKey = keyGenerator.generateKey();
            return Base64.getEncoder().encodeToString(secretKey.getEncoded());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("生成AES密钥失败", e);
        }
    }

    /**
     * 加密
     * @param data 待加密数据
     * @param key Base64编码的密钥
     * @param iv Base64编码的初始化向量
     * @return 返回Base64编码的加密结果
     */
    public static String encrypt(String data, String key, String iv) {
        try {
            SecretKeySpec secretKeySpec = new SecretKeySpec(Base64.getDecoder().decode(key), ALGORITHM);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv));
            
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
            
            byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encryptedBytes);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("AES加密失败", e);
        }
    }

    /**
     * 解密
     * @param encryptedData Base64编码的加密数据
     * @param key Base64编码的密钥
     * @param iv Base64编码的初始化向量
     * @return 返回解密后的原始字符串
     */
    public static String decrypt(String encryptedData, String key, String iv) {
        try {
            SecretKeySpec secretKeySpec = new SecretKeySpec(Base64.getDecoder().decode(key), ALGORITHM);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(Base64.getDecoder().decode(iv));
            
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
            
            byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);
            byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
            return new String(decryptedBytes, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("AES解密失败", e);
        }
    }

    /**
     * 生成随机初始化向量(IV)
     * @return 返回Base64编码的IV
     */
    public static String generateIV() {
        byte[] iv = new byte[16]; // AES块大小是128位(16字节)
        new SecureRandom().nextBytes(iv);
        return Base64.getEncoder().encodeToString(iv);
    }


    public static void main(String[] args) {
        String key = AESUtil.generateKey();
        String iv = AESUtil.generateIV();
        System.out.println("Key: " + key);
        System.out.println("IV: " + iv);
    }
}

11、配置文件

applicationContext.xml

log4j2.xml

logback.xml

mybatis-config.xml

spring-mvc.xml

web.xml

12、前端

bootstrap.min.css

bootstrap.bundle.min.js

chart.js

复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>智能垃圾分类系统 - 登录</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      body {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        min-height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .container {
        background: rgba(255, 255, 255, 0.95);
        padding: 40px;
        border-radius: 20px;
        box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
        width: 100%;
        max-width: 400px;
        backdrop-filter: blur(10px);
      }

      .logo {
        text-align: center;
        margin-bottom: 30px;
      }

      .logo h1 {
        color: #4CAF50;
        font-size: 28px;
        margin-bottom: 10px;
      }

      .logo p {
        color: #666;
        font-size: 14px;
      }

      .form-group {
        margin-bottom: 20px;
        position: relative;
      }

      .form-group label {
        display: block;
        color: #333;
        font-weight: 500;
        margin-bottom: 8px;
      }

      .form-group input {
        width: 100%;
        padding: 12px 40px 12px 15px;
        border: 2px solid #e1e5e9;
        border-radius: 10px;
        font-size: 16px;
        transition: all 0.3s ease;
        background: #f8f9fa;
      }

      .form-group input:focus {
        outline: none;
        border-color: #4CAF50;
        background: white;
        box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
      }

      .form-group .icon {
        position: absolute;
        right: 15px;
        top: 70%;
        transform: translateY(-50%);
        color: #999;
        font-size: 18px;
      }

      .btn {
        width: 100%;
        padding: 15px;
        background: linear-gradient(135deg, #4CAF50, #45a049);
        color: white;
        border: none;
        border-radius: 10px;
        font-size: 16px;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.3s ease;
        margin-bottom: 20px;
      }

      .btn:hover {
        transform: translateY(-2px);
        box-shadow: 0 10px 20px rgba(76, 175, 80, 0.3);
      }

      .btn:active {
        transform: translateY(0);
      }

      .links {
        text-align: center;
      }

      .links a {
        color: #667eea;
        text-decoration: none;
        font-weight: 500;
        transition: color 0.3s ease;
      }

      .links a:hover {
        color: #4CAF50;
        }

        .error-message {
            background: #ffebee;
            color: #c62828;
            padding: 12px;
            border-radius: 8px;
            margin-bottom: 20px;
            border-left: 4px solid #c62828;
            display: none;
        }

        .features {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 15px;
            margin-top: 30px;
        }

        .feature {
            text-align: center;
            padding: 15px;
            background: #f8f9fa;
            border-radius: 10px;
            transition: transform 0.3s ease;
        }

        .feature:hover {
            transform: translateY(-2px);
        }

        .feature-icon {
            font-size: 24px;
            margin-bottom: 8px;
        }

        .feature-text {
            font-size: 12px;
            color: #666;
        }

        @media (max-width: 480px) {
            .container {
                padding: 30px 20px;
                margin: 20px;
            }

            .features {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
<div class="container">
    <div class="logo">
        <h1>🌱 智能垃圾分类</h1>
        <p>共建绿色家园,从正确分类开始</p>
    </div>

    <div class="error-message" id="errorMessage">
        登录失败,请检查用户名和密码
    </div>

    <form action="/user/login" method="post" id="loginForm">
        <div class="form-group">
            <label for="username">用户名</label>
            <input type="text" id="username" name="username" required>
            <span class="icon">👤</span>
        </div>

        <div class="form-group">
            <label for="password">密码</label>
            <input type="password" id="password" name="password" required>
            <span class="icon">🔒</span>
        </div>

        <button type="submit" class="btn">登录</button>
    </form>

    <div class="links">
        <p>还没有账号?<a href="register.html">立即注册</a></p>
    </div>

    <div class="features">
        <div class="feature">
            <div class="feature-icon">♻️</div>
            <div class="feature-text">智能识别</div>
        </div>
        <div class="feature">
            <div class="feature-icon">🏆</div>
            <div class="feature-text">积分奖励</div>
        </div>
        <div class="feature">
            <div class="feature-icon">📊</div>
            <div class="feature-text">数据统计</div>
        </div>
    </div>
</div>

<script>
    // 检查URL参数中是否有错误信息
    const urlParams = new URLSearchParams(window.location.search);
    const error = urlParams.get('error');

    if (error === '1') {
        document.getElementById('errorMessage').style.display = 'block';
    }

    // 表单提交处理
    document.getElementById('loginForm').addEventListener('submit', function(e) {
        const username = document.getElementById('username').value.trim();
        const password = document.getElementById('password').value.trim();

        if (!username || !password) {
            e.preventDefault();
            alert('请填写完整的登录信息');
            return;
        }
    });

    // 输入框动画效果
    const inputs = document.querySelectorAll('input');
    inputs.forEach(input => {
        input.addEventListener('focus', function() {
            this.parentElement.classList.add('focused');
        });

        input.addEventListener('blur', function() {
            if (!this.value) {
                this.parentElement.classList.remove('focused');
            }
        });
    });
</script>
</body>
</html>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>智能垃圾分类系统 - 注册</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
        }

        .container {
            background: rgba(255, 255, 255, 0.95);
            padding: 40px;
            border-radius: 20px;
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
            width: 100%;
            max-width: 500px;
            backdrop-filter: blur(10px);
        }

        .logo {
            text-align: center;
            margin-bottom: 30px;
        }

        .logo h1 {
            color: #4CAF50;
            font-size: 28px;
            margin-bottom: 10px;
        }

        .logo p {
            color: #666;
            font-size: 14px;
        }

        .form-group {
            margin-bottom: 20px;
            position: relative;
        }

        .form-group label {
            display: block;
            color: #333;
            font-weight: 500;
            margin-bottom: 8px;
        }

        .form-group input, .form-group textarea {
            width: 100%;
            padding: 12px 40px 12px 15px;
            border: 2px solid #e1e5e9;
            border-radius: 10px;
            font-size: 16px;
            transition: all 0.3s ease;
            background: #f8f9fa;
            font-family: inherit;
        }

        .form-group textarea {
            height: 80px;
            resize: vertical;
        }

        .form-group input:focus, .form-group textarea:focus {
            outline: none;
            border-color: #4CAF50;
            background: white;
            box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
        }

        .form-group .icon {
            position: absolute;
            right: 15px;
            top: 70%;
            transform: translateY(-50%);
            color: #999;
            font-size: 18px;
        }

        .form-group.textarea-group .icon {
            top: 40px;
        }

        .btn {
            width: 100%;
            padding: 15px;
            background: linear-gradient(135deg, #4CAF50, #45a049);
            color: white;
            border: none;
            border-radius: 10px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            margin-bottom: 20px;
        }

        .btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 20px rgba(76, 175, 80, 0.3);
        }

        .btn:active {
            transform: translateY(0);
        }

        .links {
            text-align: center;
        }

        .links a {
            color: #667eea;
            text-decoration: none;
            font-weight: 500;
            transition: color 0.3s ease;
        }

        .links a:hover {
            color: #4CAF50;
        }

        .error-message {
            background: #ffebee;
            color: #c62828;
            padding: 12px;
            border-radius: 8px;
            margin-bottom: 20px;
            border-left: 4px solid #c62828;
            display: none;
        }

        .password-strength {
            height: 4px;
            background: #e1e5e9;
            border-radius: 2px;
            margin-top: 5px;
            overflow: hidden;
        }

        .password-strength-bar {
            height: 100%;
            width: 0%;
            transition: all 0.3s ease;
            border-radius: 2px;
        }

        .strength-weak { background: #ff4444; width: 33%; }
        .strength-medium { background: #ffaa00; width: 66%; }
        .strength-strong { background: #4CAF50; width: 100%; }

        .form-row {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 15px;
        }

        @media (max-width: 580px) {
            .container {
                padding: 30px 20px;
                margin: 10px;
            }

            .form-row {
                grid-template-columns: 1fr;
                gap: 0;
            }
        }
    </style>
</head>
<body>
<div class="container">
    <div class="logo">
        <h1>🌱 智能垃圾分类</h1>
        <p>注册账号,开启环保之旅</p>
    </div>

    <div class="error-message" id="errorMessage">
        注册失败,请检查填写信息
    </div>

    <form action="/user/register" method="post" id="registerForm">
        <div class="form-group">
            <label for="username">用户名 *</label>
            <input type="text" id="username" name="username" required>
            <span class="icon">👤</span>
        </div>

        <div class="form-group">
            <label for="password">密码 *</label>
            <input type="password" id="password" name="password" required>
            <span class="icon">🔒</span>
            <div class="password-strength">
                <div class="password-strength-bar" id="passwordStrengthBar"></div>
            </div>
        </div>

        <div class="form-group">
            <label for="confirmPassword">确认密码 *</label>
            <input type="password" id="confirmPassword" name="confirmPassword" required>
            <span class="icon">🔐</span>
        </div>

        <div class="form-row">
            <div class="form-group">
                <label for="phone">手机号</label>
                <input type="tel" id="phone" name="phone" placeholder="请输入手机号">
                <span class="icon">📱</span>
            </div>
        </div>

        <div class="form-group textarea-group">
            <label for="address">住址</label>
            <textarea id="address" name="address" placeholder="请输入详细地址"></textarea>
            <span class="icon">🏠</span>
        </div>

        <button type="submit" class="btn">注册</button>
    </form>

    <div class="links">
        <p>已有账号?<a href="/login.html">立即登录</a></p>
    </div>
</div>

<script>
    // 检查URL参数中是否有错误信息
    const urlParams = new URLSearchParams(window.location.search);
    const error = urlParams.get('error');

    const errorMessages = {
        '1': '注册失败,请检查填写信息',
        '2': '用户名或密码不能为空',
        '3': '注册失败,用户名可能已存在',
        '4': '系统错误,请稍后重试'
    };

    if (error && errorMessages[error]) {
        const errorEl = document.getElementById('errorMessage');
        errorEl.textContent = errorMessages[error];
        errorEl.style.display = 'block';
    }

    // 密码强度检测
    document.getElementById('password').addEventListener('input', function() {
        const password = this.value;
        const strengthBar = document.getElementById('passwordStrengthBar');

        let strength = 0;
        if (password.length >= 6) strength++;
        if (/[A-Z]/.test(password)) strength++;
        if (/[0-9]/.test(password)) strength++;
        if (/[^A-Za-z0-9]/.test(password)) strength++;

        strengthBar.className = 'password-strength-bar';
        if (strength >= 1) strengthBar.classList.add('strength-weak');
        if (strength >= 2) strengthBar.classList.add('strength-medium');
        if (strength >= 3) strengthBar.classList.add('strength-strong');
    });

    // 表单验证
    document.getElementById('registerForm').addEventListener('submit', function(e) {
        const username = document.getElementById('username').value.trim();
        const password = document.getElementById('password').value;
        const confirmPassword = document.getElementById('confirmPassword').value;
        const phone = document.getElementById('phone').value.trim();

        if (!username || !password) {
            e.preventDefault();
            alert('用户名和密码不能为空');
            return;
        }

        if (password !== confirmPassword) {
            e.preventDefault();
            alert('两次输入的密码不一致');
            return;
        }

        if (password.length < 6) {
            e.preventDefault();
            alert('密码长度至少6位');
            return;
        }

        if (phone && !/^1[3-9]\d{9}$/.test(phone)) {
            e.preventDefault();
            alert('请输入正确的手机号码');
            return;
        }
    });

    // 输入框动画效果
    const inputs = document.querySelectorAll('input, textarea');
    inputs.forEach(input => {
        input.addEventListener('focus', function() {
            this.parentElement.classList.add('focused');
        });

        input.addEventListener('blur', function() {
            if (!this.value) {
                this.parentElement.classList.remove('focused');
            }
        });
    });
</script>
</body>
</html>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>垃圾投递</title>
    <link rel="stylesheet" href="/css/style.css">
    <link href="../../../static/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
    <div class="card shadow">
        <div class="card-header bg-primary text-white">
            <h2 class="mb-0">垃圾投递</h2>
        </div>
        <div class="card-body">
            <form th:action="@{/garbage/add}" method="post" enctype="multipart/form-data">
                <div class="mb-3">
                    <label class="form-label">垃圾类型</label>
                    <select name="type"  class="form-select" required>
                        <option value="" disabled selected>请选择垃圾类型</option>
                        <option value="1">可回收垃圾</option>
                        <option value="2">有害垃圾</option>
                        <option value="3">厨余垃圾</option>
                        <option value="4">其他垃圾</option>
                    </select>
                </div>

                <div class="mb-3">
                    <label class="form-label">重量(kg)</label>
                    <input type="number" name="weight" class="form-control" step="0.1" min="0.1" placeholder="例如: 2.5" required>
                </div>

                <div class="mb-3">
                    <label class="form-label">分类准确率(%)</label>
                    <input type="number" name="accuracy" class="form-control" min="0" max="100" value="100">
                    <div class="form-text">请根据实际情况估计分类准确率</div>
                </div>

                <div class="mb-3">
                    <label class="form-label">上传照片</label>
                    <input type="file" name="image" class="form-control" accept="image/*">
                    <div class="form-text">上传垃圾照片(可选)</div>
                </div>

                <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                    <a href="/garbage/list" class="btn btn-outline-secondary me-md-2">查看记录</a>
                    <button type="submit" class="btn btn-primary">提交投递</button>
                </div>

                <div th:if="${error}" class="alert alert-danger mt-3" th:text="${error}"></div>
            </form>
        </div>
    </div>
</div>
<script src="../../../static/js/bootstrap.bundle.min.js"></script>
</body>
</html>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的投递记录</title>
    <link href="../../../static/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h2>我的投递记录</h2>
        <div>
            <a href="/garbage/add" class="btn btn-primary me-2">新增投递</a>
            <a href="/garbage/stats" class="btn btn-info">查看统计</a>
        </div>
    </div>

    <div class="card shadow">
        <div class="card-body">
            <div class="table-responsive">
                <table class="table table-striped table-hover">
                    <thead class="table-light">
                    <tr>
                        <th>类型</th>
                        <th>重量(kg)</th>
                        <th>获得积分</th>
                        <th>投递时间</th>
                    </tr>
                    </thead>
                    <tbody>
                    <tr th:each="record : ${pageInfo.list}">
                        <td th:text="${record.typeName}"></td>
                        <td th:text="${record.weight}"></td>
                        <td><span class="badge bg-success" th:text="${record.pointsEarned}"></span></td>
                        <td th:text="${#temporals.format(record.createTime, 'yyyy-MM-dd HH:mm')}"></td>
                    </tr>
                    </tbody>
                </table>
            </div>

            <!-- 分页信息显示 -->
            <div class="d-flex justify-content-between align-items-center mt-3">
                <div class="text-muted">
                    共 <span th:text="${pageInfo.total}"></span> 条记录,
                    第 <span th:text="${pageInfo.pageNum}"></span> / <span th:text="${pageInfo.pages}"></span> 页
                </div>
                <div class="text-muted">
                    每页显示 <span th:text="${pageInfo.pageSize}"></span> 条
                </div>
            </div>

            <!-- 分页导航 - 只有超过1页时才显示 -->
            <nav aria-label="Page navigation" class="mt-4" th:if="${pageInfo.pages > 1}">
                <ul class="pagination justify-content-center">
                    <!-- 首页 -->
                    <li class="page-item" th:classappend="${pageInfo.pageNum == 1} ? 'disabled'">
                        <a class="page-link"
                           th:href="${pageInfo.pageNum == 1} ? '#' : @{/garbage/list(page=1,size=${pageInfo.pageSize})}"
                           th:onclick="${pageInfo.pageNum == 1} ? 'return false;' : 'showLoading()'"
                           style="cursor: pointer;">首页</a>
                    </li>

                    <!-- 上一页 -->
                    <li class="page-item" th:classappend="${!pageInfo.hasPreviousPage} ? 'disabled'">
                        <a class="page-link"
                           th:href="${!pageInfo.hasPreviousPage} ? '#' : @{/garbage/list(page=${pageInfo.prePage},size=${pageInfo.pageSize})}"
                           th:onclick="${!pageInfo.hasPreviousPage} ? 'return false;' : 'showLoading()'"
                           style="cursor: pointer;">上一页</a>
                    </li>

                    <!-- 页码 -->
                    <li class="page-item" th:each="num : ${pageInfo.navigatepageNums}"
                        th:classappend="${num == pageInfo.pageNum} ? 'active'">
                        <a class="page-link"
                           th:href="${num == pageInfo.pageNum} ? '#' : @{/garbage/list(page=${num},size=${pageInfo.pageSize})}"
                           th:onclick="${num == pageInfo.pageNum} ? 'return false;' : 'showLoading()'"
                           th:text="${num}"
                           style="cursor: pointer;"></a>
                    </li>

                    <!-- 下一页 -->
                    <li class="page-item" th:classappend="${!pageInfo.hasNextPage} ? 'disabled'">
                        <a class="page-link"
                           th:href="${!pageInfo.hasNextPage} ? '#' : @{/garbage/list(page=${pageInfo.nextPage},size=${pageInfo.pageSize})}"
                           th:onclick="${!pageInfo.hasNextPage} ? 'return false;' : 'showLoading()'"
                           style="cursor: pointer;">下一页</a>
                    </li>

                    <!-- 尾页 -->
                    <li class="page-item" th:classappend="${pageInfo.pageNum == pageInfo.pages} ? 'disabled'">
                        <a class="page-link"
                           th:href="${pageInfo.pageNum == pageInfo.pages} ? '#' : @{/garbage/list(page=${pageInfo.pages},size=${pageInfo.pageSize})}"
                           th:onclick="${pageInfo.pageNum == pageInfo.pages} ? 'return false;' : 'showLoading()'"
                           style="cursor: pointer;">尾页</a>
                    </li>
                </ul>
            </nav>

            <!-- 每页显示条数选择 -->
            <div class="d-flex justify-content-center mt-3" th:if="${pageInfo.total > 0}">
                <div class="d-flex align-items-center">
                    <span class="me-2">每页显示:</span>
                    <select class="form-select form-select-sm" style="width: auto;" onchange="changePageSize(this.value)">
                        <option value="5" th:selected="${pageInfo.pageSize == 5}">5条</option>
                        <option value="10" th:selected="${pageInfo.pageSize == 10}">10条</option>
                        <option value="20" th:selected="${pageInfo.pageSize == 20}">20条</option>
                        <option value="50" th:selected="${pageInfo.pageSize == 50}">50条</option>
                    </select>
                </div>
            </div>

            <!-- 当只有一页或没有数据时的提示 -->
            <div class="text-center mt-4" th:if="${pageInfo.pages <= 1}">
                <span class="text-muted" th:if="${pageInfo.total == 0}">暂无投递记录</span>
                <span class="text-muted" th:if="${pageInfo.total > 0 && pageInfo.pages == 1}">所有记录已显示完毕</span>
            </div>
        </div>
    </div>
</div>

<script src="../../../static/js/bootstrap.bundle.min.js"></script>

<script>
    // 显示加载状态
    function showLoading() {
        // 显示加载中的提示
        const loadingToast = document.createElement('div');
        loadingToast.className = 'toast position-fixed top-0 start-50 translate-middle-x mt-3';
        loadingToast.style.zIndex = '9999';
        loadingToast.innerHTML = `
        <div class="toast-header bg-primary text-white">
            <strong class="me-auto">系统提示</strong>
        </div>
        <div class="toast-body">
            <div class="d-flex align-items-center">
                <div class="spinner-border spinner-border-sm me-2" role="status">
                    <span class="visually-hidden">Loading...</span>
                </div>
                正在加载数据...
            </div>
        </div>
    `;
        document.body.appendChild(loadingToast);
        const toast = new bootstrap.Toast(loadingToast, { delay: 1000 });
        toast.show();
    }

    // 改变每页显示条数
    function changePageSize(newSize) {
        showLoading();
        // 跳转到第一页并改变页面大小
        window.location.href = `/garbage/list?page=1&size=${newSize}`;
    }

    // 页面加载完成后的处理
    document.addEventListener('DOMContentLoaded', function() {
        // 为所有分页链接添加点击事件监听(如果没有onclick的话)
        const paginationLinks = document.querySelectorAll('.pagination .page-link');
        paginationLinks.forEach(link => {
            if (!link.onclick && link.href) {
                link.addEventListener('click', function(e) {
                    showLoading();
                });
            }
        });
    });
</script>
</body>
</html>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的统计</title>
    <link rel="stylesheet" href="/css/style.css">
    <link href="../../../static/css/bootstrap.min.css" rel="stylesheet">
    <script src="../../../static/js/chart.js"></script>
</head>
<body>
<div class="container mt-5">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h2>我的环保统计</h2>
        <div>
            <a href="/garbage/add" class="btn btn-primary me-2">新增投递</a>
            <a href="/garbage/list" class="btn btn-secondary">查看记录</a>
        </div>
    </div>

    <div class="row mb-4">
        <div class="col-md-4">
            <div class="card text-white bg-primary mb-3">
                <div class="card-body text-center">
                    <h5 class="card-title">总投递次数</h5>
                    <p class="card-text display-6" th:text="${stats?.totalCount ?: 0}">0</p>
                </div>
            </div>
        </div>
        <div class="col-md-4">
            <div class="card text-white bg-success mb-3">
                <div class="card-body text-center">
                    <h5 class="card-title">总获得积分</h5>
                    <p class="card-text display-6" th:text="${stats?.totalPoints ?: 0}">0</p>
                </div>
            </div>
        </div>
        <div class="col-md-4">
            <div class="card text-white bg-info mb-3">
                <div class="card-body text-center">
                    <h5 class="card-title">当前积分</h5>
                    <p class="card-text display-6" th:text="${user?.points ?: 0}">0</p>
                </div>
            </div>
        </div>
    </div>

    <div class="card shadow mb-4">
        <div class="card-header">
            <h5 class="mb-0">垃圾类型分布</h5>
        </div>
        <div class="card-body">
            <div class="chart-container" style="position: relative; height:400px;">
                <canvas id="typeChart"></canvas>
            </div>
        </div>
    </div>

    <script th:inline="javascript">
        // 安全地获取数据,如果没有数据则使用默认值0
        var typeStats = /*[[${stats?.typeStats}]]*/ [];
        var type1Weight = 0;
        var type2Weight = 0;
        var type3Weight = 0;
        var type4Weight = 0;

        if (typeStats && typeStats.length > 0) {
            typeStats.forEach(function(item) {
                console.log('item:', item);
                switch(item.type) {
                    case 1: type1Weight = item.total_weight || 0; break;
                    case 2: type2Weight = item.total_weight || 0; break;
                    case 3: type3Weight = item.total_weight || 0; break;
                    case 4: type4Weight = item.total_weight || 0; break;
                }
            });
            // 打印 数据
            console.log('type1Weight:', type1Weight);
            console.log('type2Weight:', type2Weight);
            console.log('type3Weight:', type3Weight);
            console.log('type4Weight:', type4Weight);
        }

        const typeData = {
            labels: ['可回收', '有害', '厨余', '其他'],
            datasets: [{
                data: [type1Weight, type2Weight, type3Weight, type4Weight],
                backgroundColor: [
                    '#4CAF50', '#F44336', '#FFC107', '#9E9E9E'
                ],
                borderWidth: 1
            }]
        };

        // 创建图表
        document.addEventListener('DOMContentLoaded', function() {
            const ctx = document.getElementById('typeChart');
            if (ctx) {
                new Chart(ctx, {
                    type: 'pie',
                    data: typeData,
                    options: {
                        responsive: true,
                        maintainAspectRatio: false,
                        plugins: {
                            legend: {
                                position: 'right',
                            },
                            tooltip: {
                                callbacks: {
                                    label: function(context) {
                                        const label = context.label || '';
                                        const value = context.raw || 0;
                                        const total = context.dataset.data.reduce((a, b) => a + b, 0);
                                        const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
                                        return `${label}: ${value}kg (${percentage}%)`;
                                    }
                                }
                            }
                        }
                    }
                });
            }
        });
    </script>
</div>
<script src="../../../static/js/bootstrap.bundle.min.js"></script>
</body>
</html>

13、项目演示截图

重定向和转发的区别

**转发(Forward):**是通过服务器内部的转发机制实现的。当服务器接收到一个请求后,根据请求的URL地址找到对应的资源,然后将该请求转发给另一个资源进行处理,最终将该资源的处理结果返回给客户端。在这个过程中,客户端只知道自己访问了一个资源,而不知道这个资源是被转发到的。

**重定向(Redirect):**是通过HTTP响应头中的Location字段实现的。当服务器接收到一个请求后,发现该请求需要访问另一个资源才能得到响应,于是在HTTP响应头中设置Location字段,告诉客户端重新发送一个请求,访问另一个资源。当客户端收到这个响应后,会重新发送一个请求,访问另一个资源,因此客户端会知道自己访问了两个资源。

客户端和服务器端的处理不同:

重定向:服务器告诉客户端一个新的URL,客户端再发送新的请求。

转发:服务器内部直接调用资源处理请求,客户端并不知道发生了转发。

URL的变化:

重定向:浏览器的URL会变成新地址。

转发:浏览器的URL不会改变,仍然显示的是最初的地址。

请求次数:

重定向:会产生两次请求,第一次请求服务器,服务器返回新的URL,浏览器再次请求新URL。

转发:只有一次请求,服务器内部直接处理。

数据传递:

重定向:由于是两次请求,无法在请求间传递数据(除非使用Session或其他持久化手段)。

转发:可以在转发过程中共享Request对象中的数据。

Spring MVC默认使用的是转发(Forward),具体表现在:

  1. 当控制器方法返回视图名称时(如return "home";),Spring会使用RequestDispatcher.forward()
  2. 视图解析器会找到对应的视图资源进行渲染

Cookie和Session的区别

Cookie:

Cookie是一种机制,客户端保存服务端数据。当客户端(比如浏览器)访问网页时,服务器端可以把一些状态数据以k-v的形式写入Cookie,存储到客户端。如果客户端再次访问服务器端,就会携带Cookie发送到服务器端,服务器端根据Cookie携带的内容识别使用者,如果不关闭浏览器,那么Cookie变量一直是有效的,所以能够保证长时间不掉线。不过这里有个问题,如果你能够获取某个用户的Cookie,将这个Cookie发送到服务器,那么服务器还是认为你是合法的。所以,使用cookie被攻击的可能性比较大。

Session:

Session是一种会话,是服务器的一个容器对象。Servlet容器分配一个Session对象,用于存储当前会话产生的一些状态数据。当客户端初次发送请求到服务器端,服务器端会创建一个Session,同时会创建一个特殊的Cookie,然后将该Cookie发送至客户端。HTTP协议是无状态协议,服务器端不知道客户端发送的多次请求属于同一个用户,Session就用来弥补这个不足。通过服务器端的Session存储机制结合客户端的Cookie机制,实现一个"有状态"的HTTP协议。客户端第一次访问服务端时,服务器端会针对这次请求创建一个会话,生成唯一的SessionId标注会话,随后服务器端把SessionId写入到客户端的Cookie,保存客户端状态,后续的请求都携带SessionId,服务器端根据SessionId识别当前会话状态,以确定用户是否登录或具有某种权限。数据是存储在服务器上面,不能伪造,这里会比Cookie更好。

相关推荐
搜狐技术产品小编20232 分钟前
浅析责任链模式在视频审核场景中的应用
java·开发语言·责任链模式
泥泞开出花朵27 分钟前
LRU缓存淘汰算法的详细介绍与具体实现
java·数据结构·后端·算法·缓存
zc.z1 小时前
Tomcat线程池、业务线程池与数据库连接池的层级约束关系解析及配置优化
服务器·数据库·tomcat
七七软件开发1 小时前
团购商城 app 系统架构分析
java·python·小程序·eclipse·系统架构·php
七七软件开发1 小时前
打车小程序 app 系统架构分析
java·python·小程序·系统架构·交友
_祝你今天愉快1 小时前
Java-JVM探析
android·java·jvm
学编程的司马光1 小时前
Idea集成Jenkins Control插件,在IDEA中触发Jenkins中项目的构建
java·jenkins·intellij-idea
孟君的编程札记1 小时前
别只知道 Redis,真正用好缓存你得懂这些
java·后端
幻雨様1 小时前
UE5多人MOBA+GAS 番外篇:同时造成多种类型伤害,以各种属性值的百分比来应用伤害(版本二)
java·前端·ue5
cleble2 小时前
(转)mybatis和hibernate的 缓存区别?
mybatis·hibernate