SSM框架整合
一、概述
基于 JDK11、Tomcat9.0 和 MySQL8.0 环境,整合 SSM(Spring + SpringMVC + MyBatis)框架,并结合 Vue3 前端实现分离开发的完整流程。
核心技术栈与功能:
- 后端:Spring5 + SpringMVC5 + MyBatis3,使用 Druid 数据源
- 前端:Vue3(组合式 API) + Vite,不使用状态管理库
- 核心功能:用户管理 CRUD、图片上传与预览、密码加密存储
- 开发环境:JDK11 + Tomcat9.0 + MySQL8.0
二、整合原则
1.分层架构原则:
- 后端严格遵循 Controller-Service-Dao 三层架构,职责分明
- 前端采用组件化设计,分离视图与业务逻辑
- 各层通过接口交互,降低耦合度,提高可维护性
2.配置集中原则:
- 框架配置与业务配置分离管理
- 环境相关配置(数据库连接、文件路径等)通过配置文件注入
- 敏感配置信息不硬编码,通过配置文件管理
3.安全设计原则:
- 密码必须加密存储,禁止明文保存
- 上传文件进行类型验证、大小限制和安全检查
- 实现基本的输入验证,防止注入攻击
- 配置适当的跨域策略,限制访问来源
4.接口规范原则:
- 采用 RESTful 风格设计 API,使用标准 HTTP 方法
- 统一响应数据格式:
{code: 状态码, msg: 消息, data: 数据}- 明确的错误码体系,便于前后端调试
5.开发效率原则:
- 配置热部署,减少开发过程中的重启时间
- 完善的日志输出,便于问题定位
- 开发环境跨域配置,支持前后端并行开发
三、开发环境准备
- JDK 11.0.6
- Maven 3.9.10
- MySQL 8.0
- Tomcat 9.0.70
- IDE:IntelliJ IDEA 2025
四、项目结构设计
4.1 后端项目结构
ssm-backend
ssm-backend/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── sy/
│ │ │ ├── controller/ # 控制器层,处理HTTP请求
│ │ │ ├── service/ # 服务层,实现业务逻辑
│ │ │ │ └── impl/ # 服务实现类
│ │ │ ├── mapper/ # 数据访问层,数据库操作
│ │ │ ├── pojo/ # 实体类,与数据库表映射
│ │ │ ├── vo/ # 视图对象,前后端数据传输
│ │ │ └── utils/ # 工具类:加密、文件处理等
│ │ ├── resources/
│ │ │ ├── spring/
│ │ │ │ ├── spring-context.xml # Spring核心配置
│ │ │ │ ├── spring-mvc.xml # SpringMVC配置
│ │ │ │ └── spring-mybatis.xml # MyBatis整合配置
│ │ │ ├── mybatis/
│ │ │ │ ├── mybatis-config.xml # MyBatis基础配置
│ │ │ │ └── mapper/ # MyBatis映射文件
│ │ │ ├── jdbc.properties # 数据库配置
│ │ │ └── upload.properties # 文件上传配置
│ │ └── webapp/
│ │ ├── WEB-INF/
│ │ │ └── web.xml # Web应用配置
│ │ └── upload/ # 上传文件存储目录
│ └── test/ # 单元测试代码
└── pom.xml # Maven依赖配置
4.2 前端项目结构
ssm-frontend
ssm-frontend/
├── public/ # 静态资源
├── src/
│ ├── api/ # API接口封装
│ │ └── user.js # 用户相关接口
│ ├── components/ # 通用组件
│ │ └── ImageUploader.vue # 图片上传组件
│ ├── views/ # 页面视图
│ │ └── UserView.vue # 用户管理页面
│ ├── router/ # 路由配置
│ │ └── index.js # 路由定义
│ ├── utils/ # 工具函数
│ │ └── axios.js # Axios配置
│ ├── App.vue # 根组件
│ ├── index.css # 全局样式
│ └── main.js # 入口文件
├── .env.development # 开发环境变量
├── index.html # 入口HTML
├── package.json # 项目依赖
└── vite.config.js # Vite配置
五、后端实现步骤
5.1 创建数据库与表结构(user表结构)

5.2 创建 Maven 项目并配置依赖(pom.xml)
<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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sy</groupId>
<artifactId>ssm-backend</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source> <!-- JDK11 -->
<maven.compiler.target>11</maven.compiler.target>
<!-- 框架版本 -->
<spring.version>5.3.24</spring.version>
<mybatis.version>3.5.10</mybatis.version>
<mybatis-spring.version>2.0.7</mybatis-spring.version>
<mysql.version>8.0.31</mysql.version>
<servlet-api.version>4.0.1</servlet-api.version>
<jackson.version>2.13.4</jackson.version>
<!-- 其他依赖版本 -->
<druid.version>1.2.16</druid.version> <!-- Druid数据源 -->
<commons-fileupload.version>1.4</commons-fileupload.version>
<bcrypt.version>0.4</bcrypt.version> <!-- BCrypt加密 -->
<slf4j.version>1.7.36</slf4j.version>
<logback.version>1.2.11</logback.version>
</properties>
<dependencies>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Spring 事务 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet-api.version}</version>
<scope>provided</scope>
</dependency>
<!-- MyBatis -->
<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>${mybatis-spring.version}</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- 文件上传 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
<!-- 密码加密 -->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>${bcrypt.version}</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version> <!-- 请检查是否有更新的版本 -->
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
</dependency>
</dependencies>
</project>
5.3 配置文件
5.3.1 数据库配置(jdbc.properties)

5.3.2 文件上传配置(upload.properties)

5.3.3 Web 配置(web.xml)
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!-- Spring监听器,加载Spring配置 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 配置Spring配置文件位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-context.xml,classpath:spring/spring-mybatis.xml</param-value>
</context-param>
<!-- SpringMVC前端控制器 -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<!-- 支持异步请求 -->
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/*</url-pattern> <!-- API请求路径前缀 -->
</servlet-mapping>
<!-- 字符编码过滤器 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<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>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
5.3.4 Spring 核心配置(spring-context.xml)

5.3.5 SpringMVC 配置(spring-mvc.xml)
<?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: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">
<!--spring-mvc.xml文件是==>springmvc配置-->
<!-- 只扫描Controller层组件 -->
<context:component-scan base-package="com.sy" use-default-filters="false">
<!--只扫描Controller层注解-->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 加载上传配置 -->
<context:property-placeholder location="classpath:upload.properties"/>
<!-- 文件上传解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 最大上传文件大小 -->
<property name="maxUploadSize" value="${upload.maxFileSize}"/>
<!-- 默认编码 -->
<property name="defaultEncoding" value="UTF-8"/>
</bean>
<!-- 注解驱动,支持@RequestMapping、@RequestBody等 -->
<mvc:annotation-driven>
<!-- 配置JSON消息转换器 处理responseBody 里面日期类型 -->
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<!-- 配置日期格式化
可替换为
// 单个字段指定日期格式
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-->
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg type="java.lang.String" value="yyyy-MM-dd HH:mm:ss" />
</bean>
</property>
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 静态资源处理 -->
<mvc:default-servlet-handler/>
<!-- 配置资源映射,用于访问上传的图片 -->
<mvc:resources mapping="/upload/**" location="/upload/"/>
</beans>
5.3.6 MyBatis 整合配置(spring-mybatis.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置Druid数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="initialSize" value="${jdbc.initialSize}"/>
<property name="minIdle" value="${jdbc.minIdle}"/>
<property name="maxActive" value="${jdbc.maxActive}"/>
<property name="maxWait" value="${jdbc.maxWait}"/>
<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}"/>
<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}"/>
<property name="validationQuery" value="${jdbc.validationQuery}"/>
<property name="testWhileIdle" value="${jdbc.testWhileIdle}"/>
<property name="testOnBorrow" value="${jdbc.testOnBorrow}"/>
<property name="testOnReturn" value="${jdbc.testOnReturn}"/>
</bean>
<!-- 配置MyBatis的SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"/>
<!-- 指定MyBatis配置文件位置 -->
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
<!-- 指定Mapper映射文件位置 -->
<property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/>
<!-- 配置别名 -->
<property name="typeAliasesPackage" value="com.sy.pojo"/>
</bean>
<!-- 配置Mapper扫描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 指定扫描的包路径 -->
<property name="basePackage" value="com.sy.mapper"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
</beans>
5.3.7 MyBatis 配置(mybatis-config.xml)

5.4 工具类实现
5.4.1 跨域配置工具类
package com.sy.utils;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author: zx_dong
* @CreateTime: 2025-12-20
*/
// 跨域过滤器
@WebFilter("/*")
public class CrosFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
//设置post请求中文乱码
request.setCharacterEncoding("utf-8");
//返回中文乱码处理
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
// 不使用*,自动适配跨域域名,避免携带Cookie时失效
String origin = request.getHeader("Origin");
if(StringUtils.isNotBlank(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
}
// 自适应所有自定义头
String headers = request.getHeader("Access-Control-Request-Headers");
if(StringUtils.isNotBlank(headers)) {
response.setHeader("Access-Control-Allow-Headers", headers);
response.setHeader("Access-Control-Expose-Headers", headers);
}
// 允许跨域的请求方法类型
response.setHeader("Access-Control-Allow-Methods", "*");
// 预检命令(OPTIONS)缓存时间,单位:秒
response.setHeader("Access-Control-Max-Age", "3600");
// 明确许可客户端发送Cookie,不允许删除字段即可
response.setHeader("Access-Control-Allow-Credentials", "true");
filterChain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
5.4.2 密码加密工具类
package com.sy.utils;
import org.mindrot.jbcrypt.BCrypt;
/**
* 密码加密工具类
*/
public class PasswordUtils {
//1.对密码进行加密处理
public static String encryptPassword(String plainPassword){
//判断密码是否为空
if (plainPassword == null || plainPassword == "" || plainPassword.isEmpty()){
throw new IllegalCallerException("密码不能为空");
}
/**
* plainPassword:明文密码
* BCrypt.gensalt():生成盐(固定的字符串) 12:迭代次数(加载因子)
* BCrypt加密方式: 不可逆 加密的次数范围4-31(值越大, 安全性越高, 加密越慢 耗费内存和性能) 取值为:21
* 如何加密的: 明文密码 + 盐(固定字符串) + 加载因子(加密的次数)
*/
try {
return BCrypt.hashpw(plainPassword, BCrypt.gensalt(12));
} catch (Exception e) {
throw new IllegalCallerException("密码加载失败");
}
}
/**
* 2.校验密码
* @param plainPassword 明文密码
* @param encryptedPassword 加密后的密码
* @return 验证结果 true:表示匹配 ,false:表示不匹配
*/
public static boolean checkPassword(String plainPassword,String encryptedPassword){
//判断密码是否为空
if (
plainPassword == null || plainPassword == "" || plainPassword.isEmpty()
|| encryptedPassword == null || encryptedPassword == "" || encryptedPassword.isEmpty()
){
return false;
}
try {
//验证密码
return BCrypt.checkpw(plainPassword,encryptedPassword);
} catch (Exception e) {
return false;
}
}
}
加密代码说明:
对用户输入的明文密码进行加密处理,生成不可逆的哈希值,用于安全存储(避免数据库中保存明文密码)。
1.各部分详解
(1)
BCrypt.gensalt(WORK_FACTOR)
- 功能:生成一个随机的「盐值(salt)」,并指定加密强度。
WORK_FACTOR(工作因子):是一个整数(通常 4-31),表示加密算法的迭代次数(2^WORK_FACTOR次)。值越大,加密过程越慢(计算成本越高),破解难度越大(安全性越高);
实际开发中常用
10-12(平衡安全性和性能)。
- 示例:
gensalt(12)会生成类似$2a$12$xxxxxxxxxxxxxxxxxxxxxx的盐值(包含算法标识、工作因子和随机字符串)。(2)
BCrypt.hashpw(plainPassword, salt)
- 功能:使用生成的盐值对明文密码进行哈希计算。
- 过程:
- 将明文密码与盐值结合;
- 通过 BCrypt 算法进行多轮哈希计算(次数由工作因子决定);
- 最终生成包含盐值的哈希字符串(格式:
$2a$12$xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy)。
- 特点:
相同明文密码 + 不同盐值 → 结果完全不同(避免彩虹表破解,彩虹表攻击是一种利用预计算的哈希值来破解密码的攻击方式);
哈希结果中已包含盐值,验证时无需单独存储盐值。
2.为什么安全?
- 不可逆:无法从哈希值反推明文密码;
- 抗暴力破解:工作因子可调整,提高破解成本;
- 自带盐值:每次加密自动生成随机盐值,相同密码加密结果不同。
3.配套验证方法
加密后,验证密码时需使用
BCrypt.checkpw(plainPassword,encryptedPassword)
5.4.3 文件上传工具类
package com.sy.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
/**
* 图片上传工具类
*/
@Component
@PropertySource("classPath:upload.properties")
public class UploadUtils {
//从配置文件中获取上传图片的根路径
@Value("${upload.basePath}")
private String basePath;
//从配置文件中获取允许上传图片的类型
@Value("${upload.allowedTypes}")
private String allowedTypes;
//将允许的图片类型存储到集合中
/**
* Set集合:无序,不重复
*/
private Set<String> allowedTypeSet;
//定义一个方法 ==> 初始化允许文件类型的集合
public void init(){
//创建Set集合
allowedTypeSet = new HashSet<>();
//将允许上传的图片类型字符串拆分成数组
String[] types = allowedTypes.split(",");
//遍历数组,将遍历出来的每个图片类型添加到Set集合中
for (String type : types) {
allowedTypeSet.add(type.trim());//trim()去空格
}
}
/**
* 文件上传
* @param file 上传的文件对象
* @return 上传成功后的文件地址(访问URL路径)
*/
public String upload(MultipartFile file, HttpServletRequest request) throws IOException {
// 验证文件是否为空
if (file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
// 初始化允许的文件类型
if (allowedTypeSet == null) {
init();
}
// 验证文件类型
String contentType = file.getContentType();
if (!allowedTypeSet.contains(contentType)) {
throw new IllegalArgumentException("不支持的文件类型: " + contentType
+ ",支持的类型: " + allowedTypes);
}
// 验证文件大小
if (file.getSize() <= 0) {
throw new IllegalArgumentException("文件大小不能为0");
}
// 获取服务器真实路径
String realPath = request.getServletContext().getRealPath(basePath);
// 创建日期目录,按日期分类存储
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String dateDir = sdf.format(new Date());
File dateDirFile = new File(realPath + File.separator + dateDir);//File.separator表示为\ 或 "/"
if (!dateDirFile.exists()) {
dateDirFile.mkdirs(); // 递归创建目录
}
// 生成新的文件名,避免冲突
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFileName = System.currentTimeMillis() + "_" + new Random().nextInt(1000) + suffix;
// 保存文件
File destFile = new File(dateDirFile, newFileName);
//上传操作
file.transferTo(destFile);
// 构建访问URL
String contextPath = request.getContextPath();//获取项目名称
return contextPath + basePath + dateDir + File.separator + newFileName;
}
}
5.5 数据模型与实体类
5.5.1 用户实体类(User.java)

5.5.2 视图对象(UserVO.java)

5.5.3 统一响应对象(Result.java)

5.6 Mapper 层实现
5.6.1 用户 mapper 接口(UserMapper.java)
package com.sy.mapper;
import com.sy.pojo.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户数据访问接口
* 定义与数据库交互的方法
*/
public interface UserMapper {
/**
* 根据ID查询用户
* @param id 用户ID
* @return 用户对象
*/
User findUserById(Integer id);
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户对象
*/
User findUserByUsername(String username);
/**
* 查询所有用户
* @return 用户列表
*/
List<User> findAllUsers();
/**
* 新增用户
* @param user 用户对象
* @return 影响的行数
*/
Integer insertUser(User user);
/**
* 更新用户信息
* @param user 用户对象
* @return 影响的行数
*/
Integer updateUser(User user);
/**
* 更新用户头像
* @param id 用户ID
* @param avatar 头像URL
* @return 影响的行数
*/
Integer updateAvatar(@Param("id") Integer id, @Param("avatar") String avatar);
/**
* 根据ID删除用户
* @param id 用户ID
* @return 影响的行数
*/
Integer deleteUserById(Integer id);
}
5.6.2 MyBatis 映射文件(UserMapper.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">
<!-- namespace:命名空间 -->
<mapper namespace="com.sy.mapper.UserMapper">
<!-- 定义通用查询字段 -->
<sql id="base_column_list">
id, username, password, nickname, avatar, email, create_time, update_time
</sql>
<!-- 根据ID查询用户 -->
<select id="findUserById" parameterType="integer" resultType="user">
select <include refid="base_column_list"/> from user where id = #{id}
</select>
<!-- 根据用户名查询用户 -->
<select id="findUserByUsername" parameterType="string" resultType="user">
select <include refid="base_column_list"/> from user where username = #{username}
</select>
<!-- 查询所有用户 -->
<select id="findAllUsers" resultType="user">
select <include refid="base_column_list"/> from user order by create_time desc
</select>
<!-- 新增用户 -->
<insert id="insertUser" parameterType="user" useGeneratedKeys="true" keyProperty="id">
insert into user (username, password, nickname, avatar, email, create_time, update_time)
values (#{username}, #{password}, #{nickname}, #{avatar}, #{email}, now(), now())
</insert>
<!-- 更新用户信息 -->
<update id="updateUser" parameterType="user">
update user
<set>
<if test="password != null"> password = #{password}, </if>
<if test="nickname != null"> nickname = #{nickname}, </if>
<if test="avatar != null"> avatar = #{avatar}, </if>
<if test="email != null"> email = #{email}, </if>
update_time = now()
</set>
where id = #{id}
</update>
<!-- 更新用户头像 -->
<update id="updateAvatar">
update user set avatar = #{avatar}, update_time = now() where id = #{id}
</update>
<!-- 根据ID删除用户 -->
<delete id="deleteUserById" parameterType="java.lang.Long">
delete from user where id = #{id}
</delete>
</mapper>
5.7 Service 层实现
5.7.1 用户服务接口(UserService.java)
package com.sy.service;
import com.sy.pojo.User;
import com.sy.vo.UserVo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户数据访问接口
* 定义与数据库交互的方法
*/
public interface UserService {
/**
* 根据ID查询用户
* @param id 用户ID
* @return 用户视图对象
*/
UserVo findUserById(Integer id);
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户视图对象
*/
/*UserVo findUserByUsername(String username);*/
/**
* 查询所有用户
* @return 用户视图对象列表
*/
List<UserVo> findAllUsers();
/**
* 新增用户
* @param user 用户对象
* @return 创建结果,true表示成功,false表示失败
*/
boolean insertUser(User user);
/**
* 更新用户信息
* @param user 用户对象
* @return 创建结果,true表示成功,false表示失败
*/
boolean updateUser(User user);
/**
* 更新用户头像
* @param id 用户ID
* @param avatar 头像URL
* @return 创建结果,true表示成功,false表示失败
*/
boolean updateAvatar(@Param("id") Integer id, @Param("avatar") String avatar);
/**
* 根据ID删除用户
* @param id 用户ID
* @return 创建结果,true表示成功,false表示失败
*/
boolean deleteUserById(Integer id);
}
5.7.2 用户服务实现类(UserServiceImpl.java)
package com.sy.service.impl;
import com.sy.mapper.UserMapper;
import com.sy.pojo.User;
import com.sy.service.UserService;
import com.sy.utils.PasswordUtils;
import com.sy.vo.UserVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
/**
* 根据ID查询用户
* @param id 用户ID
* @return 用户视图对象
*/
@Override
public UserVo findUserById(Integer id) {
User user = userMapper.findUserById(id);
//判断
if (user == null){
return null;
}
//创建用户视图对象
UserVo userVo = new UserVo();
//将用户对象中的数据复制一份给用户视图对象
BeanUtils.copyProperties(user,userVo);
return userVo;
}
/**
* 查询所有用户
* @return
*/
@Override
public List<UserVo> findAllUsers() {
// 查询所有用户
List<User> users = userMapper.findAllUsers();
List<UserVo> userVOs = new ArrayList<>();
// 转换为VO对象列表
for (User user : users) {
UserVo userVO = new UserVo();
BeanUtils.copyProperties(user, userVO);
userVOs.add(userVO);
}
return userVOs;
}
/**
* 新增用户信息
* @param user 用户对象
* @return
*/
@Override
public boolean insertUser(User user) {
//根据用户名查询当前用户是否存在
User existUser = userMapper.findUserByUsername(user.getUsername());
//判断
if (existUser != null){
//证明当前用户已存在
return false;
}
//首先对密码进行加密处理
String encryptPassword = PasswordUtils.encryptPassword(user.getPassword());
//将加密后的密码设置给用户对象
user.setPassword(encryptPassword);
//实现添加操作
return userMapper.insertUser(user)>0 ? true:false;
}
/**
* 更新用户信息
* @param user 用户对象
* @return 更新结果,true表示成功,false表示失败
*/
@Override
public boolean updateUser(User user) {
// 如果更新密码,则需要加密处理
if (user.getPassword() != null && !user.getPassword().isEmpty()) {
String encryptedPassword = PasswordUtils.encryptPassword(user.getPassword());
user.setPassword(encryptedPassword);
}
// 更新用户信息
return userMapper.updateUser(user) > 0 ? true : false;
}
/**
* 更新用户头像
* @param id 用户ID
* @param avatarUrl 头像URL
* @return 更新结果,true表示成功,false表示失败
*/
@Override
public boolean updateAvatar(Integer id, String avatarUrl) {
// 更新用户头像
return userMapper.updateAvatar(id, avatarUrl) > 0 ? true : false;
}
/**
* 删除用户
* @param id 用户ID
* @return 删除结果,true表示成功,false表示失败
*/
@Override
public boolean deleteUserById(Integer id) {
// 删除用户
return userMapper.deleteUserById(id) > 0 ? true : false;
}
}
5.8 Controller 层实现

测试:



package com.sy.controller;
import com.sy.pojo.User;
import com.sy.service.UserService;
import com.sy.utils.UploadUtils;
import com.sy.vo.Result;
import com.sy.vo.UserVo;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;
/**
* @author Mr·Zhai
* @version 1.0
* @description: 用户控制器
* @date 2026/3/25 14:43
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private UploadUtils uploadUtils;
/**
* 查询所有用户信息
* @return
*/
@GetMapping("/list")
public Result<List<UserVo>> findAllUsers() {
try {
List<UserVo> userVoList = userService.findAllUsers();
return Result.success(userVoList);
} catch (Exception e) {
return Result.error(500, "查询用户列表失败, " + e.getMessage());
}
}
/**
* 根据ID查询用户
* @param id
* @return
*/
@GetMapping("/{id}")
public Result<UserVo> findUserById(@PathVariable Integer id) {
UserVo userVo = userService.findUserById(id);
return userVo != null ? Result.success(userVo) : Result.error(400, "用户不存在");
}
/**
* 新增用户信息
* @param user
* @return
*/
@PostMapping
public Result<Boolean> insertUser(@RequestBody User user) {
try {
boolean flag = userService.insertUser(user);
return flag ? Result.success(flag) : Result.error(400, "用户名已存在");
} catch (Exception e) {
return Result.error(500, "创建用户失败, " + e.getMessage());
}
}
/**
* 根据用户id修改用户头像
* @param id
* @param file
* @param request
* @return
*/
@PostMapping("/{id}/avatar")
public Result<String> uploadAvatar(@PathVariable Integer id,
@RequestParam("file") MultipartFile file,
HttpServletRequest request){
// 通过上传工具类实现头像的上传操作
try {
// 获取上传成功后的头像地址
String avatarUrl = uploadUtils.upload(file, request);
// 更新用户头像(根据用户id更新用户的头像)
boolean flag = userService.updateAvatar(id, avatarUrl);
return flag ? Result.success(avatarUrl) : Result.error(400, "更新用户头像失败");
} catch (IOException e) {
return Result.error(500, "上传失败" + e.getMessage());
}
}
/**
* 修改用户信息
* @param id
* @param user
* @return
*/
@PutMapping("/{id}")
public Result<Boolean> updateUser(@PathVariable Integer id,@RequestBody User user){
try {
// 将用户的id值设置到用户对象中
user.setId(id);
// 根据id修改用户信息
boolean flag = userService.updateUser(user);
return flag ? Result.success(flag) : Result.error(400, "更新用户信息失败");
} catch (Exception e) {
return Result.error(500, "更新失败, " + e.getMessage());
}
}
/**
* 上传用户头像
* @param file
* @param request
* @return
*/
/*@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file,
HttpServletRequest request){
try {
// 通过上传工具类实现头像的上传操作
// 获取上传成功后的头像地址
String avatarUrl = uploadUtils.upload(file, request);
return Result.success(avatarUrl);
} catch (IOException e) {
return Result.error(500, "上传失败" + e.getMessage());
}
}*/
/**
* 上传用户头像
* @param file
* @param request
* @return
*/
@SneakyThrows
@PostMapping("/upload")
public Result<String> uploadAvatar(
@RequestParam("file") MultipartFile file,
HttpServletRequest request) {
// 上传文件并获取URL
String avatarUrl = uploadUtils.upload(file, request);
if (avatarUrl != null){
return Result.success(avatarUrl);
}
return Result.error(500, "上传文件失败");
}
/**
* 修改用户信息
* @param id
* @return
*/
@DeleteMapping("/{id}")
public Result<Boolean> deleteUserById(@PathVariable Integer id){
try {
// 根据id删除用户信息
boolean flag = userService.deleteUserById(id);
return flag ? Result.success(flag) : Result.error(400, "删除用户信息失败");
} catch (Exception e) {
return Result.error(500, "删除失败, " + e.getMessage());
}
}
}
六、前端实现步骤
6.1 创建前端项目并安装依赖
# 创建Vue项目
npm create vue@latest# 进入项目目录
cd ssm-frontend# 进入项目目录
cd ssm-frontend# 安装依赖
npm install axios element-plus @element-plus/icons-vue vue-router# 安装其它依赖
npm i
6.2 Vite 配置(vite.config.js)(前端解决跨域可省略)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 3000, // 前端服务端口
open: true, // 自动打开浏览器
proxy: {
// 代理API请求到后端服务
'/api': {
target: 'http://localhost:8080/ssm-backend',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
// 代理上传文件的访问
'/ssm-backend/upload': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
6.3 Axios 配置(src/utils/axios.js)
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 创建axios实例
const request = axios.create({
baseURL: "http://localhost:8080",
timeout: 5000, // 超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
// 请求拦截器
request.interceptors.request.use(
config => {
// 可以在这里添加请求头,如Token等
return config
},
error => {
// 请求错误处理
ElMessage.error('请求发送失败')
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
response => {
const res = response.data
// 处理后端返回的结果
if (res.code !== 200) {
ElMessage.error(res.msg || '操作失败')
return Promise.reject(res.msg || '操作失败')
}
return res.data
},
error => {
// 处理HTTP错误
let message = '网络错误'
if (error.response) {
switch (error.response.status) {
case 404:
message = '请求地址不存在'
break
case 500:
message = '服务器内部错误'
break
default:
message = `请求错误: ${error.response.status}`
}
}
ElMessage.error(message)
return Promise.reject(error)
}
)
export default request
6.4 API 封装(src/api/user.js)
import request from '../utils/axios'
/**
* 获取用户列表
* @returns 用户列表 http://localhost:8080/ssm_backend/user/list 提交方式是get请求
*/
export const getAllUsers = () => {
return request({
// 请求地址
url: '/user/list',
// 提交方式
method: 'get'
})
}
/**
* 根据ID获取用户
* @param {number} id 用户ID
* @returns 用户信息
*/
export const getUserById = (id) => {
return request({
url: `/user/${id}`,
method: 'get'
})
}
/**
* 创建用户
* @param {Object} user 用户信息
* @returns 创建结果
*/
export const createUser = (user) => {
return request({
url: '/user',
method: 'post',
data: user
})
}
/**
* 更新用户
* @param {number} id 用户ID
* @param {Object} user 用户信息
* @returns 更新结果
*/
export const updateUser = (id, user) => {
return request({
url: `/user/${id}`,
method: 'put',
data: user
})
}
/**
* 删除用户
* @param {number} id 用户ID
* @returns 删除结果
*/
export const deleteUser = (id) => {
return request({
url: `/user/${id}`,
method: 'delete'
})
}
/**
* 上传用户头像
* @param {number} id 用户ID
* @param {FormData} formData 包含图片的FormData
* @returns 头像URL
*/
export const uploadAvatar = (id, formData) => {
return request({
url: `/user/${id}/avatar`,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 上传头像操作
export const upload = (formData) => {
return request({
url: `/user/upload`,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
6.5 路由配置(src/router/index.js)

6.6 图片上传组件(src/components/ImageUploader.vue)
<template>
<div class="image-uploader">
<!-- 图片预览区域 -->
<div v-if="imageUrl" class="preview-container">
<img :src="imageUrl" class="preview-image" :alt="altText">
<el-button
type="text"
class="delete-btn"
@click="handleDeleteImage"
:disabled="disabled"
>
<el-icon><Delete /></el-icon> 移除
</el-button>
</div>
<!-- 上传区域 -->
<div v-else class="upload-area" @click="handleClickUpload" :class="{ disabled: disabled }">
<el-icon class="upload-icon"><Plus /></el-icon>
<div class="upload-text">点击上传图片</div>
<div class="upload-hint">支持JPG、PNG、GIF格式,不超过10MB</div>
<!-- 隐藏的文件输入框 -->
<input
type="file"
ref="fileInput"
class="file-input"
accept="image/jpeg,image/png,image/gif,image/bmp"
@change="handleFileChange"
:disabled="disabled"
>
</div>
<!-- 上传进度条 -->
<el-progress
v-if="uploading"
:percentage="progress"
stroke-width="2"
style="margin-top: 10px;"
></el-progress>
</div>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits, defineExpose } from 'vue'
import { Plus, Delete } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// 接收的props
const props = defineProps({
// 图片URL,用于回显
modelValue: {
type: String,
default: ''
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 图片alt文本
alt: {
type: String,
default: '图片预览'
}
})
// 发出的事件
const emit = defineEmits(['update:modelValue', 'upload-success', 'upload-error'])
// 内部状态
const imageUrl = ref(props.modelValue)
const fileInput = ref(null)
const uploading = ref(false)
const progress = ref(0)
const selectedFile = ref(null) // 存储选中的文件
const altText = ref(props.alt)
// 监听props变化,更新内部状态
watch(
() => props.modelValue,
(newVal) => {
imageUrl.value = newVal
}
)
watch(
() => props.alt,
(newVal) => {
altText.value = newVal
}
)
// 点击上传区域,触发文件选择
const handleClickUpload = () => {
if (!props.disabled) {
fileInput.value.click()
}
}
// 处理文件选择
const handleFileChange = (e) => {
const file = e.target.files[0]
if (file) {
// 验证文件大小(10MB)
if (file.size > 10 * 1024 * 1024) {
ElMessage.error('文件大小不能超过10MB')
return
}
// 显示预览
const reader = new FileReader()
reader.onload = (event) => {
imageUrl.value = event.target.result
selectedFile.value = file
}
reader.readAsDataURL(file)
// 发出文件选择事件
emit('upload-success', file)
}
// 重置input,允许重复选择同一文件
e.target.value = ''
}
// 删除图片
const handleDeleteImage = () => {
if (!props.disabled) {
imageUrl.value = ''
selectedFile.value = null
emit('update:modelValue', '')
}
}
// 提供给父组件的上传方法
const upload = (apiFunc) => {
return new Promise((resolve, reject) => {
if (!selectedFile.value) {
ElMessage.error('请先选择图片')
reject(new Error('请先选择图片'))
return
}
uploading.value = true
progress.value = 0
// 模拟进度(实际项目中可以根据axios的onUploadProgress实现)
const timer = setInterval(() => {
progress.value += 10
if (progress.value >= 90) {
clearInterval(timer)
}
}, 200)
// 调用API上传
apiFunc()
.then(url => {
clearInterval(timer)
progress.value = 100
imageUrl.value = url
emit('update:modelValue', url)
emit('upload-success', url)
ElMessage.success('上传成功')
resolve(url)
})
.catch(error => {
clearInterval(timer)
emit('upload-error', error)
ElMessage.error('上传失败: ' + (error.message || '未知错误'))
reject(error)
})
.finally(() => {
// 延迟隐藏进度条,让用户看到100%的状态
setTimeout(() => {
uploading.value = false
progress.value = 0
}, 500)
})
})
}
// 暴露方法给父组件
defineExpose({
upload,
selectedFile
})
</script>
<style scoped>
.upload-area {
width: 100%;
height: 150px;
border: 2px dashed #ccc;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.3s;
}
.upload-area:hover:not(.disabled) {
border-color: #409eff;
}
.upload-area.disabled {
border-color: #e0e0e0;
cursor: not-allowed;
background-color: #f5f5f5;
}
.upload-icon {
font-size: 24px;
color: #409eff;
margin-bottom: 10px;
}
.upload-text {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.upload-hint {
font-size: 12px;
color: #999;
}
.file-input {
display: none;
}
.preview-container {
width: 100%;
border-radius: 6px;
overflow: hidden;
position: relative;
}
.preview-image {
width: 100%;
height: 150px;
object-fit: contain;
border: 1px solid #eee;
}
.delete-btn {
position: absolute;
bottom: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 2px 8px;
}
.delete-btn:hover {
background-color: rgba(0, 0, 0, 0.7);
color: white;
}
</style>
6.7 用户管理页面(src/views/UserView.vue)
<template>
<div class="user-container">
<el-page-header @back="handleBack" content="用户管理"></el-page-header>
<el-card class="user-card">
<div class="card-header">
<el-button type="primary" @click="openAddDialog">
<el-icon><Plus /></el-icon>
新增用户
</el-button>
</div>
<!-- 用户列表 -->
<el-table
v-loading="loading"
:data="userList"
border
style="width: 100%; margin-top: 15px"
>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column label="头像" width="100">
<template #default="scope">
<el-avatar v-if="scope.row.avatar" size="large">
<img :src="scope.row.avatar" :alt="scope.row.username + '的头像'">
</el-avatar>
<el-avatar v-else size="large">{{ scope.row.username.charAt(0) }}</el-avatar>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名"></el-table-column>
<el-table-column prop="nickname" label="昵称"></el-table-column>
<el-table-column prop="email" label="邮箱"></el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="openEditDialog(scope.row.id)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 新增/编辑用户对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="600px"
>
<el-form
:model="form"
label-width="100px"
:rules="rules"
ref="formRef"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" :disabled="isEdit"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="form.password" type="password"></el-input>
<template #help>
<span v-if="isEdit" class="text-xs text-gray-500">
不修改密码请留空
</span>
</template>
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="form.nickname"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email"></el-input>
</el-form-item>
<el-form-item label="头像">
<image-uploader
v-model="form.avatar"
ref="avatarUploader"
:alt="form.username + '的头像'"
@upload-success="handleAvatarSelected"
></image-uploader>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { Plus, Delete } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import ImageUploader from '../components/ImageUploader.vue'
import {
getAllUsers,
createUser,
updateUser,
deleteUser,
getUserById,
uploadAvatar,
upload
} from '../api/user'
// 状态管理
const userList = ref([])
const loading = ref(false)
const selectedFile = ref(null)
// 对话框状态
const dialogVisible = ref(false)
const dialogTitle = ref('新增用户')
const isEdit = ref(false)
const currentUserId = ref(null)
// 表单数据
const form = reactive({
username: '',
password: '',
nickname: '',
email: '',
avatar: ''
})
// 表单验证规则
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: !isEdit, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于 6 个字符', trigger: 'blur' }
],
nickname: [
{ required: true, message: '请输入昵称', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] }
]
}
// 表单和组件引用
const formRef = ref(null)
const avatarUploader = ref(null)
// 监听isEdit变化,动态修改密码验证规则
watch(
() => isEdit.value,
(newVal) => {
rules.password[0].required = !newVal
}
)
// 页面加载时获取用户列表
onMounted(() => {
// 查询所有用户信息
fetchAllUsers()
})
/**
* 获取所有用户
*/
const fetchAllUsers = async () => {
loading.value = true
try {
// 向服务端发送请求 查询所有用户信息的请求 找user.js文件中查询所有用户的请求
userList.value = await getAllUsers()
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
/**
* 获取单个用户
*/
const fetchUserById = async (id) => {
loading.value = true
try {
// 向服务端发送请求
return await getUserById(id)
} catch (error) {
console.error('获取用户失败:', error)
return null
} finally {
loading.value = false
}
}
/**
* 打开新增对话框
*/
const openAddDialog = () => {
dialogTitle.value = '新增用户'
isEdit.value = false
currentUserId.value = null
resetForm()
dialogVisible.value = true
}
/**
* 打开编辑对话框
*/
const openEditDialog = async (id) => {
dialogTitle.value = '编辑用户'
isEdit.value = true
currentUserId.value = id
const user = await fetchUserById(id)
if (user) {
form.username = user.username
form.nickname = user.nickname
form.email = user.email
form.avatar = user.avatar || ''
form.password = '' // 不显示密码
dialogVisible.value = true
}
}
/**
* 处理头像选择
*/
const handleAvatarSelected = (file) => {
if (file instanceof File) {
selectedFile.value = file
}
}
/**
* 提交表单
*/
const handleSubmit = async () => {
// 表单验证
await formRef.value.validate()
try {
// 如果有选中的图片且是编辑状态,先上传图片
if (selectedFile.value && currentUserId.value) {
await uploadSelectedAvatar()
}
if (isEdit.value) {
// 编辑用户
await updateUser(currentUserId.value, {
nickname: form.nickname,
email: form.email,
password: form.password || undefined, // 密码为空则不更新
avatar: form.avatar
})
ElMessage.success('更新成功')
} else {
// 头像上传
// 创建FormData
const formData = new FormData()
formData.append('file', selectedFile.value)
// 向服务端发送请求进行头像上传
const avatarUrl = await upload(formData);
// 将头像的url地址赋值个from对象的头像
form.avatar = avatarUrl;
// 新增用户
await createUser(form)
ElMessage.success('添加成功')
}
dialogVisible.value = false
fetchAllUsers()
resetForm()
} catch (error) {
console.error('提交失败:', error)
}
}
/**
* 上传选中的头像
*/
const uploadSelectedAvatar = async () => {
if (!selectedFile.value || !currentUserId.value) return
// 创建FormData
const formData = new FormData()
formData.append('file', selectedFile.value)
// 调用上传API
const avatarUrl = await avatarUploader.value.upload(() => {
return uploadAvatar(currentUserId.value, formData)
})
form.avatar = avatarUrl
selectedFile.value = null
}
/**
* 删除用户
*/
const handleDelete = async (id) => {
try {
// 确认对话框
await ElMessageBox.confirm(
'确定要删除这个用户吗?',
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 执行删除
await deleteUser(id)
ElMessage.success('删除成功')
fetchAllUsers()
} catch (error) {
// 如果是取消操作,不显示错误信息
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
}
}
/**
* 重置表单
*/
const resetForm = () => {
formRef.value?.resetFields()
form.username = ''
form.password = ''
form.nickname = ''
form.email = ''
form.avatar = ''
selectedFile.value = null
}
/**
* 返回操作
*/
const handleBack = () => {
history.back()
}
</script>
<style scoped>
.user-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: flex-end;
margin-bottom: 15px;
}
</style>
6.8 入口文件配置(src/main.js)
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
// 引入element-plus
import ElementPlus from 'element-plus'
// 引入element-plus的样式
import 'element-plus/dist/index.css'
const app = createApp(App)
// 使用
app.use(router)
app.use(ElementPlus)
app.mount('#app')
6.9 根组件(src/App.vue)

新增用户时上传头像失败优化:
后端实现:

前端实现:


七、业务流程说明
7.1 用户管理流程
1.用户列表加载流程:
- 前端页面加载时调用
getAllUsers()接口- 后端
UserController接收请求,调用UserService的getAllUsers()方法- 服务层调用
UserDao查询所有用户- DAO 层通过 MyBatis 执行 SQL 查询
- 服务层将查询结果转换为 VO 对象(过滤敏感字段)
- 控制器将结果封装为统一响应格式返回给前端
- 前端接收数据并渲染到表格中
2.用户创建流程:
- 前端填写用户表单,点击提交按钮
- 表单验证通过后调用
createUser()接口- 后端接收用户数据,服务层检查用户名是否已存在
- 密码通过
PasswordUtils加密处理- 调用 DAO 层插入用户数据
- 返回创建结果,前端显示提示信息并刷新列表
3.用户编辑流程:
- 点击编辑按钮,前端获取用户 ID 并调用
getUserById()接口- 后端返回用户信息,前端填充表单
- 用户修改信息后提交,调用
updateUser()接口- 如果修改了密码,服务层会重新加密
- 服务层调用 DAO 层更新用户数据
- 返回更新结果,前端显示提示信息并刷新列表
4.用户删除流程:
- 点击删除按钮,前端显示确认对话框
- 用户确认后调用
deleteUser()接口- 后端接收请求,服务层调用 DAO 层删除用户
- 返回删除结果,前端显示提示信息并刷新列表
7.2 图片上传流程
1.图片选择与预览流程:
- 用户点击上传区域,触发文件选择对话框
- 选择图片后,前端通过
FileReader读取图片并显示预览- 图片文件暂存在前端,未上传到服务器
2.图片上传与保存流程:
- 用户提交表单时,前端检查是否有选中的图片
- 创建
FormData对象,添加图片文件- 调用
uploadAvatar()接口上传图片- 后端
UserController接收文件,调用UploadUtils处理UploadUtils验证文件类型和大小,生成新文件名- 保存文件到服务器指定目录,返回访问 URL
- 服务层调用 DAO 层更新用户头像 URL
- 前端接收头像 URL,更新表单并显示成功提示
八、开发环境运行与测试
8.1 后端运行
# 进入后端项目目录
cd ssm_backend# 使用外部Tomcat启动项目
8.2 前端运行
# 进入前端项目目录
cd vue3-front
# 启动开发服务器
npm run dev
8.3 功能测试
1.用户管理测试:
- 访问用户管理页面,验证用户列表是否正常显示
- 测试新增用户功能,检查密码是否加密存储
- 测试编辑用户功能,验证信息是否正确更新
- 测试删除用户功能,确认用户是否被正确删除
2.图片上传测试:
- 新增或编辑用户时上传头像图片
- 验证图片预览功能是否正常
- 提交后检查图片是否成功上传到服务器
- 确认用户列表中是否正确显示头像
九、总结
本指南详细介绍了基于 JDK11、Tomcat9.0 和 MySQL8.0 环境的 SSM 框架整合方案,以及与 Vue3 前端的分离开发实现。
通过本文档,你可以掌握:
- 不使用 Spring Boot 的 SSM 框架手动整合方法
- Druid 数据源的配置与使用
- 密码加密的常用方式及 BCrypt 算法的实现
- 图片上传与预览功能的完整实现
- Vue3 组合式 API 与 SSM 后端的对接方式

2436

被折叠的 条评论
为什么被折叠?



