在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)监控事务执行情况,及时发现事务超时、死锁等问题。
五、全栈开发避坑指南
-
前后端数据类型不一致:核心痛点是日期类型、数字类型的适配问题。解决方案:日期类型:后端使用`@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")`注解格式化Date类型字段,前端使用dayjs工具库统一解析日期字符串;数字类型:后端避免返回BigDecimal类型的小数(前端可能出现精度丢失),可转为String类型或保留固定小数位后转为Double;ID字段:若使用雪花算法生成的Long类型ID(超过JavaScript的Number最大值),后端需转为String类型返回,避免前端精度丢失;
-
跨域问题:开发环境常见问题,核心是浏览器的同源策略限制。解决方案:前端:Vite配置代理,在vite.config.js中添加proxy配置,将/api请求代理到后端服务地址;后端:推荐通过CorsFilter全局配置跨域,比@CrossOrigin注解更灵活,支持配置允许的Origin、Method、Header等,避免重复配置;CorsFilter配置示例:
-
JWT令牌过期处理:核心痛点是用户操作过程中令牌过期导致请求失败。优化方案:实现令牌自动刷新机制,前端在响应拦截器中捕获401错误后,检查是否存在刷新令牌,若存在则调用刷新令牌接口获取新令牌,更新本地存储的令牌后重新发起原请求;若刷新令牌也过期,则提示用户重新登录;
-
SQL注入风险:核心是避免直接拼接SQL字符串。解决方案:优先使用MyBatis-Plus的LambdaQueryWrapper或XML中的#{}参数绑定(MyBatis会自动进行SQL转义);禁止使用${}拼接SQL,若必须使用(如动态表名、排序字段),需对参数进行严格校验(如排序字段只能是指定的实体类字段),避免恶意参数注入;
-
前端表单校验:核心是“前端校验减少无效请求,后端校验保证数据安全”。前端:使用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流水线,提升代码质量与迭代效率。若本文内容对你有帮助,欢迎点赞收藏;若有技术疑问或不同见解,欢迎在评论区交流探讨!
2103

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



