Spring Boot+Vue+MySQL全栈开发实战:从架构设计到落地的核心技巧

在Web应用开发领域,Spring Boot+Vue+MySQL的技术组合凭借“后端快速开发、前端组件化高效构建、数据存储稳定可靠”的核心优势,成为企业级后台管理系统、SaaS平台及中小型业务系统的主流选型。笔者近期主导了某电商后台管理系统(涵盖商品管理、订单履约、用户运营、数据统计四大核心模块)的全栈开发,从需求拆解、架构设计、编码实现到压测部署,完整经历了项目全生命周期,期间解决了跨域适配、权限细粒度控制、数据库性能瓶颈等多个核心问题。本文将结合该项目的真实落地场景,拆解全栈开发各环节的核心技术难点与解决方案,配套可直接复用的代码实例,为开发者提供“理论+实践”的双重参考,助力提升全栈开发效率与项目质量。

一、架构设计:搭建高内聚低耦合的全栈架构

全栈开发的核心前提是架构设计合理,清晰的分层不仅能提高开发效率,还能降低后续维护成本。笔者采用的是经典的前后端分离架构,具体分层如下:

  • 前端(Vue):采用Vue 3+Vite+Element Plus,分层为视图层(页面组件)、组件层(公共组件)、请求层(Axios封装)、工具层(通用工具函数)、路由层(Vue Router)、状态管理层(Pinia);

  • 后端(Spring Boot):采用分层架构,分为控制层(Controller)、服务层(Service,含接口和实现)、数据访问层(Mapper)、实体层(Entity/VO/DTO)、工具层(Util)、配置层(Config);

  • 数据库(MySQL):按业务模块划分表结构,合理设计索引、外键,采用事务保证数据一致性。

架构设计核心要点:1. 通信规范:前后端严格遵循RESTful API设计规范,明确GET(查询)、POST(新增)、PUT(全量更新)、DELETE(删除)的语义,统一接口路径命名风格(如`/api/v1/resource/[id]`),便于团队协作与后期维护;2. 数据交互:后端统一返回Result封装格式,前端通过请求拦截器统一添加请求头(如令牌、请求ID),响应拦截器统一处理状态码与异常信息,实现“一次封装,全局复用”;3. 开发模式:采用“接口驱动开发(API-Driven Development)”,先由前后端开发者共同定义接口文档(推荐使用Swagger/OpenAPI),明确请求参数、响应数据结构及状态码,再并行开发,避免因接口变更导致的重复返工;4. 扩展性考量:预留第三方集成接口(如支付、短信、物流),采用依赖注入(DI)降低模块耦合,便于后续功能迭代与系统扩展。

二、后端核心技巧:Spring Boot高效开发实践

2.1 统一返回结果与异常处理

开发初期务必定义统一的返回结果格式,避免接口返回格式混乱,同时统一异常处理,减少重复代码。

代码实例1:统一返回结果类
import lombok.Data;

/**
 * 统一返回结果类
 */
@Data
public class Result<T> {
    // 状态码:200成功,500失败,401未授权等
    private int code;
    // 提示信息
    private String msg;
    // 数据
    private T data;

    // 成功响应(无数据)
    public static <T> Result<T> success() {
        return new Result<>(200, "操作成功", null);
    }

    // 成功响应(有数据)
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data);
    }

    // 失败响应
    public static <T> Result<T> error(String msg) {
        return new Result<>(500, msg, null);
    }

    // 自定义状态码响应
    public static <T> Result<T> error(int code, String msg) {
        return new Result<>(code, msg, null);
    }
}
代码实例2:全局异常处理器
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 处理业务异常
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        return Result.error(e.getCode(), e.getMessage());
    }

    // 处理空指针异常
    @ExceptionHandler(NullPointerException.class)
    public Result<Void> handleNullPointerException(NullPointerException e) {
        // 开发环境可打印堆栈信息,生产环境隐藏
        e.printStackTrace();
        return Result.error("系统异常:空指针异常");
    }

    // 处理其他异常
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        e.printStackTrace();
        return Result.error("系统未知异常");
    }
}

说明:自定义BusinessException类需继承RuntimeException,便于Spring事务捕获并回滚。同时建议补充错误码枚举类(如`ErrorCodeEnum`),统一管理业务异常编码,避免硬编码导致的错误码混乱。示例如下:

/**
 * 业务异常枚举
 */
public enum ErrorCodeEnum {
    PARAM_ERROR(400, "参数校验失败"),
    USER_NOT_EXIST(404, "用户不存在"),
    STOCK_INSUFFICIENT(5001, "商品库存不足"),
    PERMISSION_DENIED(403, "权限不足");

    private final int code;
    private final String msg;

    ErrorCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    // getter方法
    public int getCode() { return code; }
    public String getMsg() { return msg; }
}

/**
 * 自定义业务异常类
 */
public class BusinessException extends RuntimeException {
    private int code;
    private String msg;

    // 直接传入错误码枚举
    public BusinessException(ErrorCodeEnum errorCodeEnum) {
        super(errorCodeEnum.getMsg());
        this.code = errorCodeEnum.getCode();
        this.msg = errorCodeEnum.getMsg();
    }

    // getter方法
    public int getCode() { return code; }
    public String getMsg() { return msg; }
}

// 使用示例
if (user == null) {
    throw new BusinessException(ErrorCodeEnum.USER_NOT_EXIST);
}

此外,全局异常处理器需注意区分开发环境与生产环境:开发环境可打印完整堆栈信息便于调试,生产环境需隐藏敏感信息,仅返回友好提示,并将异常日志存入日志系统(如ELK)便于问题排查。可通过`@Profile`注解实现环境差异化配置。

2.2 数据访问层优化:MyBatis-Plus高效使用

Spring Boot整合MyBatis-Plus可大幅减少CRUD代码量,同时提供丰富的条件构造器、分页插件等功能,提升开发效率。

代码实例3:MyBatis-Plus配置与分页实现
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis-Plus配置类
 */
@Configuration
public class MyBatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件,指定数据库类型为MySQL
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
代码实例4:Service层分页查询实现
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    /**
     * 分页查询用户列表
     * @param pageNum 页码
     * @param pageSize 每页条数
     * @param username 用户名(模糊查询)
     * @return 分页结果
     */
    @Override
    public Page<User> getUserPage(int pageNum, int pageSize, String username) {
        // 构建分页对象
        Page<User> page = new Page<>(pageNum, pageSize);
        // 构建查询条件
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNotBlank(username)) {
            queryWrapper.like(User::getUsername, username);
        }
        // 按创建时间降序排序
        queryWrapper.orderByDesc(User::getCreateTime);
        // 执行分页查询
        return userMapper.selectPage(page, queryWrapper);
    }
}

说明:通过MyBatis-Plus的Page对象和LambdaQueryWrapper可快速实现分页查询,需注意以下实操要点:1. 分页插件配置:必须指定数据库类型(如MySQL),否则分页语句可能不兼容;若项目中存在多数据源,需为每个数据源单独配置分页插件;2. LambdaQueryWrapper优势:通过Lambda表达式引用实体类字段,避免硬编码字段名导致的拼写错误,且支持编译期校验;3. 模糊查询优化:like查询若以`%`开头(如`%username`),将无法使用索引,导致全表扫描,可通过`likeRight`(右模糊,`username%`)替代,或使用全文索引(Elasticsearch)优化模糊查询性能;4. 复杂查询扩展:若需联表查询,可使用MyBatis-Plus的`@TableName`+`@TableField`配置关联关系,或结合XML编写自定义SQL,同时支持Page对象接收联表查询结果;5. 性能注意:分页查询时尽量只查询必要字段,避免使用`select *`,可通过`queryWrapper.select(User::getId, User::getUsername, User::getCreateTime)`指定查询字段,减少数据传输量。

2.3 安全配置:JWT认证与权限控制

Web应用的安全性至关重要,采用JWT令牌认证结合Spring Security实现用户认证与权限控制,是当前主流的方案。

代码实例5:JWT工具类(生成令牌、验证令牌)
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    /**
     * 分页查询用户列表
     * @param pageNum 页码
     * @param pageSize 每页条数
     * @param username 用户名(模糊查询)
     * @return 分页结果
     */
    @Override
    public Page<User> getUserPage(int pageNum, int pageSize, String username) {
        // 构建分页对象
        Page<User> page = new Page<>(pageNum, pageSize);
        // 构建查询条件
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNotBlank(username)) {
            queryWrapper.like(User::getUsername, username);
        }
        // 按创建时间降序排序
        queryWrapper.orderByDesc(User::getCreateTime);
        // 执行分页查询
        return userMapper.selectPage(page, queryWrapper);
    }
}
代码实例6:Spring Security配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

/**
 * Spring Security配置类
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启方法级权限控制
public class SecurityConfig {

    @Resource
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Resource
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF
                .csrf(csrf -> csrf.disable())
                // 关闭session(前后端分离采用无状态认证)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置授权规则
                .authorizeHttpRequests(authorize -> authorize
                        // 登录接口、注册接口放行
                        .requestMatchers("/api/auth/login", "/api/auth/register").permitAll()
                        // 静态资源放行
                        .requestMatchers("/static/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
                        // 其他接口需要认证
                        .anyRequest().authenticated()
                )
                // 配置异常处理(未认证、权限不足等)
                .exceptionHandling(exception -> exception
                        .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                )
                // 添加JWT过滤器(在用户名密码认证过滤器之前)
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    // 注入AuthenticationManager
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    // 密码加密器(BCrypt加密)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

说明:JWT认证与Spring Security集成的核心逻辑与实操要点:1. JwtAuthenticationFilter工作流程:拦截除放行接口外的所有请求,从请求头中提取Bearer令牌,通过JwtUtil验证令牌有效性,若验证通过,构建Authentication对象并存入SecurityContext,后续接口可通过`SecurityContextHolder.getContext().getAuthentication()`获取用户信息;2. 无状态认证优势:无需在服务端存储会话信息,便于服务集群部署,降低服务端存储压力;3. 安全风险规避:JWT令牌一旦签发无法主动撤销,需通过缩短过期时间(如2小时)+ 刷新令牌机制优化,刷新令牌有效期可设为7天,存储在HttpOnly Cookie中,减少XSS攻击风险;4. 权限控制细化:除了方法级权限控制(@PreAuthorize),还可实现基于URL的细粒度权限控制,通过自定义`FilterInvocationSecurityMetadataSource`和`AccessDecisionManager`,从数据库动态加载资源与权限的关联关系,实现权限的动态配置;5. 密码加密补充:BCryptPasswordEncoder是Spring Security推荐的密码加密器,其通过随机盐值加密,相同密码每次加密结果不同,可有效防止彩虹表破解,实际开发中需将加密后的密码存入数据库,登录时通过`passwordEncoder.matches(rawPassword, encodedPassword)`验证密码正确性;6. 常见问题解决:若出现“未授权却能访问接口”,需检查SecurityFilterChain配置是否正确(如放行规则顺序,具体接口放行需在anyRequest之前);若出现“Token过期后仍能访问”,需检查JwtUtil的过期时间判断逻辑是否正确。

三、前端核心技巧:Vue高效开发实践

3.1 Axios请求封装

前端统一封装Axios,处理请求拦截(添加令牌)、响应拦截(统一处理结果、异常),减少重复代码。

代码实例7:Axios封装(request.js)
import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useUserStore } from '@/stores/userStore';

// 创建Axios实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量读取基础URL
  timeout: 5000 // 请求超时时间
});

// 请求拦截器:添加JWT令牌
service.interceptors.request.use(
  (config) => {
    const userStore = useUserStore();
    // 如果存在令牌,添加到请求头
    if (userStore.token) {
      config.headers['Authorization'] = `Bearer ${userStore.token}`;
    }
    return config;
  },
  (error) => {
    // 请求错误处理
    return Promise.reject(error);
  }
);

// 响应拦截器:统一处理结果
service.interceptors.response.use(
  (response) => {
    const res = response.data;
    // 状态码不为200,视为异常
    if (res.code !== 200) {
      ElMessage.error(res.msg || '请求失败');
      // 401:未授权(令牌过期、未登录等),需要重新登录
      if (res.code === 401) {
        const userStore = useUserStore();
        ElMessageBox.confirm('登录状态已过期,请重新登录', '提示', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          // 清除用户信息,跳转登录页
          userStore.logout();
          window.location.href = '/login';
        });
      }
      return Promise.reject(res);
    }
    // 成功响应,返回数据
    return res.data;
  },
  (error) => {
    // 响应错误处理(如网络错误、服务器错误等)
    ElMessage.error(error.message || '服务器异常');
    return Promise.reject(error);
  }
);

export default service;
代码实例8:API请求封装(userApi.js)
import request from './request';

/**
 * 用户相关API
 */
// 分页查询用户列表
export const getUserPage = (pageNum, pageSize, username) => {
  return request({
    url: '/api/user/page',
    method: 'get',
    params: { pageNum, pageSize, username }
  });
};

// 新增用户
export const addUser = (userForm) => {
  return request({
    url: '/api/user',
    method: 'post',
    data: userForm
  });
};

// 修改用户
export const updateUser = (userForm) => {
  return request({
    url: '/api/user',
    method: 'put',
    data: userForm
  });
};

// 删除用户
export const deleteUser = (id) => {
  return request({
    url: `/api/user/${id}`,
    method: 'delete'
  });
};

说明:Axios封装的核心价值在于统一请求与响应处理,降低重复代码,实操中需注意以下要点:1. 环境变量配置:通过Vite的环境变量(.env.development、.env.production)区分开发/生产环境的API基础URL,避免手动切换配置,示例:.env.development中配置`VITE_API_BASE_URL=http://localhost:8080`,.env.production中配置`VITE_API_BASE_URL=https://api.example.com`;2. 请求超时处理:除了全局设置timeout,可针对特殊接口单独设置(如文件上传接口设置较长超时时间);3. 请求取消:对于频繁切换的查询接口(如表格筛选),可通过Axios的CancelToken取消上一次未完成的请求,避免重复响应,示例:在request.js中维护一个取消令牌映射表,请求前取消同类型未完成请求;4. 错误分级处理:区分网络错误(如断网)、服务器错误(500)、业务错误(400/404),给出不同的用户提示,提升用户体验;5. API模块化封装:按业务模块拆分API文件(如userApi.js、orderApi.js、productApi.js),每个文件导出对应模块的接口方法,便于维护与复用;6. TypeScript支持:若项目使用TypeScript,可为请求参数和响应数据定义接口(Interface),实现类型校验,避免数据类型错误。

3.2 状态管理:Pinia的高效使用

Vue 3推荐使用Pinia替代Vuex,Pinia简化了状态管理的写法,支持TypeScript,且无需嵌套模块。

代码实例9:用户状态管理(userStore.js)
import { defineStore } from 'pinia';
import { login as userLogin, getUserInfo } from '@/api/authApi';

/**
 * 用户状态管理
 */
export const useUserStore = defineStore('user', {
  // 状态
  state: () => ({
    token: localStorage.getItem('token') || '', // 令牌(持久化到localStorage)
    userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null') || null // 用户信息
  }),
  // 计算属性
  getters: {
    // 判断用户是否登录
    isLogin: (state) => !!state.token && !!state.userInfo
  },
  // 动作(修改状态的方法)
  actions: {
    /**
     * 登录
     * @param {Object} loginForm 登录表单(username, password)
     */
    async login(loginForm) {
      const token = await userLogin(loginForm);
      // 存储令牌到状态和localStorage
      this.token = token;
      localStorage.setItem('token', token);
      // 获取用户信息
      await this.fetchUserInfo();
    },

    /**
     * 获取用户信息
     */
    async fetchUserInfo() {
      const userInfo = await getUserInfo();
      // 存储用户信息到状态和localStorage
      this.userInfo = userInfo;
      localStorage.setItem('userInfo', JSON.stringify(userInfo));
    },

    /**
     * 退出登录
     */
    logout() {
      // 清除状态
      this.token = '';
      this.userInfo = null;
      // 清除localStorage
      localStorage.removeItem('token');
      localStorage.removeItem('userInfo');
    }
  }
});

3.3 路由守卫:实现页面权限控制

通过Vue Router的路由守卫实现登录验证与页面权限控制,是前端权限管理的核心环节,可有效避免未登录用户访问受保护页面、低权限用户访问高权限页面。实操中需结合Pinia状态管理与后端权限数据,实现“动态路由+细粒度权限控制”,核心逻辑与注意事项如下:

代码实例10:路由守卫配置(router/index.js)
import { createRouter, createWebHistory, Router, RouteRecordRaw } from 'vue-router';
import { useUserStore } from '@/stores/userStore';
import { ElMessage, ElLoading } from 'element-plus';
import { getAuthRoutes } from '@/api/menuApi'; // 从后端获取当前用户的权限路由

// 导入页面组件
import Login from '@/views/Login.vue';
import Home from '@/views/Home.vue';
import NotFound from '@/views/NotFound.vue'; // 404页面

// 静态路由:无需权限即可访问的路由
const staticRoutes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/dashboard'
  },
  {
    path: '/login',
    name: 'Login',
    component: Login,
    meta: {
      requiresAuth: false // 不需要登录
    }
  },
  {
    path: '/:pathMatch(.*)*', // 404路由
    name: 'NotFound',
    component: NotFound,
    meta: {
      requiresAuth: false
    }
  }
];

// 动态路由:需要权限控制的路由(基础框架,实际路由由后端返回)
const dynamicRoutes: RouteRecordRaw[] = [
  {
    path: '/home',
    name: 'Home',
    component: Home,
    meta: {
      requiresAuth: true // 需要登录
    },
    children: [] // 子路由由后端返回后动态添加
  }
];

// 创建路由实例
const router: Router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [...staticRoutes, ...dynamicRoutes]
});

// 路由前置守卫:处理登录验证与动态路由加载
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore();
  const loading = ElLoading.service({ text: '加载中...' }); // 加载动画

  try {
    // 1. 不需要登录的路由,直接放行
    if (!to.meta.requiresAuth) {
      next();
      return;
    }

    // 2. 需要登录的路由,检查是否已登录
    if (!userStore.isLogin) {
      ElMessage.warning('请先登录');
      next('/login');
      return;
    }

    // 3. 已登录,检查是否已加载动态路由
    if (userStore.authRoutes.length === 0) {
      // 从后端获取当前用户的权限路由(含角色信息)
      const authRoutes = await getAuthRoutes();
      // 存储路由信息到Pinia
      userStore.setAuthRoutes(authRoutes);
      // 动态添加路由
      authRoutes.forEach(route => {
        // 懒加载组件(优化首屏加载速度)
        route.component = () => import(`@/views/${route.component}.vue`);
        router.addRoute('Home', route);
      });
      // 重新导航到目标路由(确保动态路由已加载)
      next({ ...to, replace: true });
      return;
    }

    // 4. 已加载动态路由,检查角色权限
    const userRole = userStore.userInfo.role;
    if (to.meta.roles && to.meta.roles.length > 0) {
      if (to.meta.roles.includes(userRole)) {
        next();
      } else {
        ElMessage.error('无权限访问该页面');
        next(from.path || '/dashboard'); // 跳回之前的页面或首页
      }
    } else {
      // 不需要角色权限,直接放行
      next();
    }
  } catch (error) {
    ElMessage.error('路由加载失败,请刷新页面重试');
    userStore.logout(); // 异常时退出登录
    next('/login');
  } finally {
    loading.close(); // 关闭加载动画
  }
});

// 路由后置守卫:可用于页面标题设置、埋点统计等
router.afterEach((to) => {
  if (to.meta.title) {
    document.title = to.meta.title as string; // 设置页面标题
  }
});

export default router;

四、数据库优化:MySQL性能提升技巧

4.1 合理设计索引

索引是提升MySQL查询性能的核心手段,但索引设计需遵循“按需创建、避免冗余”的原则,过多索引会导致插入/更新/删除操作的IO开销增大,反而降低整体性能。结合项目实操经验,索引设计的核心技巧与注意事项如下:

  • 主键索引:每张表必须有主键,推荐使用自增INT/BIGINT作为主键(优点:有序性好,插入时可避免页分裂,提升写入性能;查询时索引定位效率高);避免使用UUID作为主键(缺点:无序性,插入时会导致频繁页分裂,增加IO开销);若需分库分表,可使用雪花算法(Snowflake)生成分布式主键;

  • 普通索引:针对查询频繁的字段(如用户名、手机号、订单号、状态字段等)创建普通索引;对于字段值重复率高的字段(如性别、是否删除),不建议创建索引(索引选择性差,查询效率提升不明显);

  • 联合索引:针对多字段查询场景(如`where role = ? and create_time > ?`)创建联合索引,遵循“最左匹配原则”设计字段顺序(高频查询字段、选择性高的字段放前面);例如联合索引`idx_role_create_time(role, create_time)`,可匹配`where role = ?`、`where role = ? and create_time = ?`、`where role = ? and create_time > ?`等查询,无法匹配`where create_time = ?`的查询;

  • 唯一索引:针对需要保证字段唯一性的场景(如用户名、手机号、订单号)创建唯一索引,既能保证数据唯一性,又能提升查询性能(效率略高于普通索引);若字段可能为NULL,需注意唯一索引允许NULL值(多个NULL值不冲突);

  • 索引维护:定期通过`EXPLAIN`分析SQL执行计划,识别未使用的冗余索引(通过`sys.schema_unused_indexes`视图查询)并删除;对于频繁更新的字段,避免创建索引(更新字段会导致索引重建,增加开销);大表添加索引时,使用`ALTER TABLE ... ADD INDEX ... ALGORITHM=INPLACE, LOCK=NONE`(MySQL 8.0+支持),避免锁表影响业务运行。

代码实例11:索引创建SQL
-- 创建用户表(含主键索引)
CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码(加密后)',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `role` varchar(20) NOT NULL COMMENT '角色(ADMIN/USER)',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) COMMENT '主键索引',
  UNIQUE KEY `idx_username` (`username`) COMMENT '用户名唯一索引',
  KEY `idx_phone` (`phone`) COMMENT '手机号普通索引',
  KEY `idx_role_create_time` (`role`,`create_time`) COMMENT '角色+创建时间联合索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';

4.2 事务与锁的合理使用

在涉及多表操作或数据一致性要求高的场景(如订单创建、转账等),需使用事务保证数据一致性。

代码实例12:Spring Boot中的事务使用
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderMapper orderMapper;

    @Resource
    private OrderItemMapper orderItemMapper;

    @Resource
    private ProductMapper productMapper;

    /**
     * 创建订单(含事务)
     * @param orderDTO 订单DTO
     */
    @Transactional(rollbackFor = Exception.class) // 发生任何异常都回滚
    @Override
    public void createOrder(OrderDTO orderDTO) {
        // 1. 插入订单主表
        Order order = new Order();
        // 赋值...
        orderMapper.insert(order);
        Long orderId = order.getId();

        // 2. 插入订单明细表(多商品)
        List<OrderItemDTO> itemDTOs = orderDTO.getOrderItemDTOs();
        for (OrderItemDTO itemDTO : itemDTOs) {
            OrderItem orderItem = new OrderItem();
            orderItem.setOrderId(orderId);
            // 其他赋值...
            orderItemMapper.insert(orderItem);

            // 3. 扣减商品库存
            Product product = productMapper.selectById(itemDTO.getProductId());
            if (product.getStock() < itemDTO.getQuantity()) {
                throw new BusinessException("商品库存不足");
            }
            product.setStock(product.getStock() - itemDTO.getQuantity());
            productMapper.updateById(product);
        }
    }
}

要点如下:1. 事务注解配置:`@Transactional(rollbackFor = Exception.class)`必须指定`rollbackFor = Exception.class`,因为Spring默认仅回滚RuntimeException及其子类,Checked Exception(如IOException)不会触发回滚;2. 事务传播机制:根据业务场景选择合适的传播机制,如嵌套事务使用`propagation = Propagation.NESTED`,跨服务事务使用`propagation = Propagation.REQUIRES_NEW`;3. 锁机制配合:在并发场景(如秒杀、多用户同时扣减库存),仅靠事务无法保证数据一致性,需结合锁机制,推荐使用乐观锁(通过版本号字段`version`实现),避免悲观锁导致的性能瓶颈;乐观锁实现示例:在Product实体类中添加`@Version`注解的version字段,MyBatis-Plus会自动在更新时添加`where ... and version = ?`条件,版本号不匹配则更新失败,需重试;4. 大事务优化:避免在事务中执行耗时操作(如文件上传、远程调用、循环插入大量数据),可将耗时操作移出事务,或拆分为多个小事务,减少事务占用资源的时间,降低死锁风险;5. 事务监控:通过Spring Boot Actuator结合监控工具(如Prometheus+Grafana)监控事务执行情况,及时发现事务超时、死锁等问题。

五、全栈开发避坑指南

  1. 前后端数据类型不一致:核心痛点是日期类型、数字类型的适配问题。解决方案:日期类型:后端使用`@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")`注解格式化Date类型字段,前端使用dayjs工具库统一解析日期字符串;数字类型:后端避免返回BigDecimal类型的小数(前端可能出现精度丢失),可转为String类型或保留固定小数位后转为Double;ID字段:若使用雪花算法生成的Long类型ID(超过JavaScript的Number最大值),后端需转为String类型返回,避免前端精度丢失;

  2. 跨域问题:开发环境常见问题,核心是浏览器的同源策略限制。解决方案:前端:Vite配置代理,在vite.config.js中添加proxy配置,将/api请求代理到后端服务地址;后端:推荐通过CorsFilter全局配置跨域,比@CrossOrigin注解更灵活,支持配置允许的Origin、Method、Header等,避免重复配置;CorsFilter配置示例:

  3. JWT令牌过期处理:核心痛点是用户操作过程中令牌过期导致请求失败。优化方案:实现令牌自动刷新机制,前端在响应拦截器中捕获401错误后,检查是否存在刷新令牌,若存在则调用刷新令牌接口获取新令牌,更新本地存储的令牌后重新发起原请求;若刷新令牌也过期,则提示用户重新登录;

  4. SQL注入风险:核心是避免直接拼接SQL字符串。解决方案:优先使用MyBatis-Plus的LambdaQueryWrapper或XML中的#{}参数绑定(MyBatis会自动进行SQL转义);禁止使用${}拼接SQL,若必须使用(如动态表名、排序字段),需对参数进行严格校验(如排序字段只能是指定的实体类字段),避免恶意参数注入;

  5. 前端表单校验:核心是“前端校验减少无效请求,后端校验保证数据安全”。前端:使用Element Plus的Form组件配合async-validator实现表单校验(如必填项、手机号格式、密码强度);后端:使用JSR-380规范的校验注解(如@NotNull、@NotBlank、@Pattern、@Min)对DTO参数进行校验,配合全局异常处理器捕获MethodArgumentNotValidException,返回统一的参数校验错误信息;后端校验示例:

六、总结

Spring Boot+Vue+MySQL全栈开发的核心是“架构分层清晰、技术选型适配场景、细节处理到位”。本文结合真实电商后台项目,从架构设计、后端开发、前端开发、数据库优化四个核心环节,拆解了全栈开发的关键技术难点与落地技巧,配套的代码实例均经过项目验证可直接复用。需要强调的是,全栈开发不仅是技术的堆砌,更需要具备“全局思维”——后端开发需考虑前端交互体验,前端开发需理解后端数据逻辑,数据库设计需适配业务查询场景,只有各环节协同优化,才能开发出高效、稳定、易维护的Web应用。

后续可深入探索的方向:1. 性能优化:引入Redis实现热点数据缓存(如商品信息、用户权限)、分布式锁,结合MySQL读写分离提升数据库并发能力;2. 实时通信:集成WebSocket实现订单状态推送、消息通知等功能;3. 容器化部署:使用Docker封装应用,通过Docker Compose管理多服务(应用服务、数据库、Redis、Nginx),实现环境一致性与快速部署;4. 自动化测试:后端使用JUnit5+Mockito实现单元测试与集成测试,前端使用Vitest+Testing Library实现组件测试,结合Jenkins实现CI/CD流水线,提升代码质量与迭代效率。若本文内容对你有帮助,欢迎点赞收藏;若有技术疑问或不同见解,欢迎在评论区交流探讨!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值