SSM整合

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)

  • 功能:使用生成的盐值对明文密码进行哈希计算。
  • 过程:
  1. 将明文密码与盐值结合;
  2. 通过 BCrypt 算法进行多轮哈希计算(次数由工作因子决定);
  3. 最终生成包含盐值的哈希字符串(格式:$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接收请求,调用UserServicegetAllUsers()方法
    • 服务层调用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 前端的分离开发实现。

    通过本文档,你可以掌握:

    1. 不使用 Spring Boot 的 SSM 框架手动整合方法
    2. Druid 数据源的配置与使用
    3. 密码加密的常用方式及 BCrypt 算法的实现
    4. 图片上传与预览功能的完整实现
    5. Vue3 组合式 API 与 SSM 后端的对接方式

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值