什么是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 的工作流程:
-
用户请求:用户通过浏览器发送 HTTP 请求到服务器。
-
前端控制器(DispatcherServlet):Spring MVC 的前端控制器 DispatcherServlet 拦截所有请求并进行分发。
-
处理器映射(Handler Mapping):根据请求 URL,DispatcherServlet 查找相应的控制器。
-
控制器处理:控制器处理请求,调用服务层或数据访问层以获取数据,并将数据封装到模型中。
-
视图解析器(View Resolver):控制器返回视图 名称,DispatcherServlet 使用视图解析器将视图名称解析为实际的视图对象。
-
视图渲染:视图对象负责将模型数据渲染为用户界面,通常是 HTML 页面。
-
响应返回:渲染后的视图返回给 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. 需求背景
随着垃圾分类政策的推行,社区需要一个智能垃圾分类管理系统,实现以下功能:
- 居民账户管理:居民注册/登录,记录垃圾分类行为
- 垃圾投递记录:记录每次垃圾投递的类型、重量和时间
- 环保积分系统:根据分类准确性奖励积分
- 数据可视化:展示个人和社区的垃圾分类数据
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>-->
<!-- <!– SLF4J API(logback-classic 已经包含 slf4j-api,但显式声明版本可避免冲突) –>-->
<!-- <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、配置文件

12、前端

<!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),具体表现在:
- 当控制器方法返回视图名称时(如return "home";),Spring会使用RequestDispatcher.forward()
- 视图解析器会找到对应的视图资源进行渲染
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更好。