Spring Boot+Vue.js超市管理系统:前后端分离项目实战
在 Java Web 开发领域前后端分离架构已经成为企业级项目的标准实践。对于需要快速交付毕业设计或希望积累完整项目经验的开发者来说一个结构清晰、技术栈主流、可扩展性强的超市管理系统是绝佳的练手项目。这类项目不仅要求前端界面友好、后端逻辑严谨还需要处理商品管理、库存跟踪、订单处理、用户权限等核心业务场景。本文将基于 Spring Boot、Vue.js 和 MyBatis 等技术栈从零开始构建一个功能完整的超市管理系统。重点不在于简单堆砌代码而是解释每个模块的设计思路、技术选型理由、前后端交互方式以及实际部署时容易遇到的坑。如果你已经掌握 Java 基础、了解 Spring Boot 和 Vue 的基本用法但缺乏完整项目经验这篇文章将带你走完从环境准备、模块开发、联调测试到生产部署的全流程。1. 技术选型与项目结构设计1.1 为什么选择前后端分离架构前后端分离的核心优势在于关注点分离。前端专注于用户界面和交互逻辑后端专注于业务规则和数据持久化。在超市管理系统这种需要频繁更新界面、支持多端访问的场景中分离架构让团队可以并行开发通过 RESTful API 进行数据交换前端可以独立部署后端服务也更易于维护和扩展。技术栈组合选择 Spring Boot Vue.js 的原因在于Spring Boot 提供了快速启动、自动配置和丰富的 starter 依赖适合快速构建 RESTful APIVue.js 学习曲线平缓组件化开发模式适合管理系统的模块化需求MyBatis 作为 ORM 框架在复杂 SQL 优化和动态查询方面比 JPA 更灵活这种组合在中小型企业中应用广泛有丰富的社区支持和问题解决方案1.2 项目目录结构规范一个规范的项目结构是团队协作和后期维护的基础。前后端分离项目应该明确区分前端和后端代码仓库或者在同一仓库下严格分离。后端项目结构Maven 标准supermarket-backend/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── supermarket/ │ │ │ ├── SupermarketApplication.java # 启动类 │ │ │ ├── config/ # 配置类 │ │ │ ├── controller/ # 控制层 │ │ │ ├── service/ # 业务层 │ │ │ ├── mapper/ # 数据访问层 │ │ │ ├── entity/ # 实体类 │ │ │ └── dto/ # 数据传输对象 │ │ └── resources/ │ │ ├── application.yml # 主配置文件 │ │ ├── mapper/ # MyBatis XML 文件 │ │ └── static/ # 静态资源 ├── pom.xml # Maven 依赖配置 └── target/ # 编译输出前端项目结构Vue CLI 标准supermarket-frontend/ ├── public/ │ ├── index.html # HTML 模板 │ └── favicon.ico # 网站图标 ├── src/ │ ├── api/ # API 接口封装 │ ├── assets/ # 静态资源 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── store/ # 状态管理 │ ├── views/ # 页面组件 │ ├── utils/ # 工具函数 │ ├── App.vue # 根组件 │ └── main.js # 入口文件 ├── package.json # 项目依赖 └── vue.config.js # Vue 配置1.3 数据库设计要点超市管理系统的核心表包括用户、商品、分类、库存、订单等。设计时要考虑数据一致性、查询效率和扩展性。关键表结构示例-- 商品表 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 商品名称, category_id BIGINT NOT NULL COMMENT 分类ID, price DECIMAL(10,2) NOT NULL COMMENT 售价, cost_price DECIMAL(10,2) COMMENT 成本价, stock INT DEFAULT 0 COMMENT 库存数量, status TINYINT DEFAULT 1 COMMENT 状态1-上架 0-下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_status (status) ) COMMENT 商品表; -- 订单表 CREATE TABLE order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL COMMENT 用户ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总额, status TINYINT DEFAULT 0 COMMENT 状态0-待支付 1-已支付 2-已发货 3-已完成 4-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_order_no (order_no) ) COMMENT 订单表;2. 后端核心功能实现2.1 Spring Boot 项目初始化首先通过 Spring Initializr 创建项目选择必要的依赖Spring Web提供 RESTful API 支持MyBatis Framework数据库操作框架MySQL Driver数据库连接驱动Lombok简化实体类编写Spring Boot DevTools开发热部署pom.xml 关键依赖配置dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml 数据库配置spring: datasource: url: jdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true2.2 商品管理模块实现商品管理是系统的核心需要实现增删改查、上下架、库存管理等功能。实体类设计使用 Lombok 简化Data TableName(product) public class Product { TableId(type IdType.AUTO) private Long id; private String name; private Long categoryId; private BigDecimal price; private BigDecimal costPrice; private Integer stock; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; // 关联查询的字段非数据库字段 TableField(exist false) private String categoryName; }Service 层接口设计public interface ProductService { PageResultProductVO getProductPage(ProductQuery query); ProductVO getProductById(Long id); boolean addProduct(ProductDTO productDTO); boolean updateProduct(ProductDTO productDTO); boolean updateStatus(Long id, Integer status); boolean updateStock(Long id, Integer stock); }Controller 层 RESTful APIRestController RequestMapping(/api/product) Api(tags 商品管理) public class ProductController { Autowired private ProductService productService; GetMapping(/page) ApiOperation(分页查询商品列表) public ResultPageResultProductVO getProductPage(ProductQuery query) { return Result.success(productService.getProductPage(query)); } PostMapping ApiOperation(添加商品) public ResultBoolean addProduct(RequestBody Valid ProductDTO productDTO) { return Result.success(productService.addProduct(productDTO)); } PutMapping(/{id}/status) ApiOperation(更新商品状态) public ResultBoolean updateStatus(PathVariable Long id, RequestParam Integer status) { return Result.success(productService.updateStatus(id, status)); } }2.3 订单业务逻辑处理订单处理涉及库存扣减、金额计算、状态流转等复杂逻辑需要保证事务一致性。订单创建服务实现Service Transactional public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private ProductMapper productMapper; Override public String createOrder(OrderCreateDTO orderDTO) { // 1. 验证商品库存 for (OrderItemDTO item : orderDTO.getItems()) { Product product productMapper.selectById(item.getProductId()); if (product null) { throw new BusinessException(商品不存在: item.getProductId()); } if (product.getStock() item.getQuantity()) { throw new BusinessException(商品库存不足: product.getName()); } } // 2. 生成订单号 String orderNo generateOrderNo(); // 3. 计算订单金额 BigDecimal totalAmount BigDecimal.ZERO; for (OrderItemDTO item : orderDTO.getItems()) { Product product productMapper.selectById(item.getProductId()); BigDecimal itemTotal product.getPrice().multiply(new BigDecimal(item.getQuantity())); totalAmount totalAmount.add(itemTotal); } // 4. 创建订单主记录 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(orderDTO.getUserId()); order.setTotalAmount(totalAmount); order.setStatus(OrderStatus.PENDING_PAYMENT.getCode()); orderMapper.insert(order); // 5. 创建订单明细并扣减库存 for (OrderItemDTO item : orderDTO.getItems()) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setQuantity(item.getQuantity()); // 设置其他字段... orderItemMapper.insert(orderItem); // 扣减库存 productMapper.updateStock(item.getProductId(), -item.getQuantity()); } return orderNo; } private String generateOrderNo() { // 时间戳 随机数确保唯一性 return ORD System.currentTimeMillis() String.format(%06d, new Random().nextInt(999999)); } }3. 前端 Vue.js 开发实战3.1 Vue 项目初始化与配置使用 Vue CLI 创建项目并安装必要依赖vue create supermarket-frontend cd supermarket-frontend npm install axios vue-router vuex element-ui配置开发环境代理解决跨域问题vue.config.jsmodule.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }3.2 商品管理页面组件开发商品列表页面需要实现查询、分页、操作等功能。商品列表组件ProductList.vuetemplate div classproduct-container el-card div slotheader classclearfix span商品管理/span el-button typeprimary clickhandleAdd stylefloat: right; 添加商品 /el-button /div !-- 查询条件 -- el-form :modelqueryParams inline el-form-item label商品名称 el-input v-modelqueryParams.name placeholder请输入商品名称 clearable / /el-form-item el-form-item label商品状态 el-select v-modelqueryParams.status placeholder请选择状态 clearable el-option label已上架 value1 / el-option label已下架 value0 / /el-select /el-form-item el-form-item el-button typeprimary clickloadData查询/el-button el-button clickresetQuery重置/el-button /el-form-item /el-form !-- 商品表格 -- el-table :dataproductList v-loadingloading el-table-column propid labelID width80 / el-table-column propname label商品名称 min-width150 / el-table-column propcategoryName label分类 width120 / el-table-column propprice label价格 width100 template slot-scopescope ¥{{ scope.row.price }} /template /el-table-column el-table-column propstock label库存 width100 / el-table-column propstatus label状态 width100 template slot-scopescope el-tag :typescope.row.status 1 ? success : info {{ scope.row.status 1 ? 已上架 : 已下架 }} /el-tag /template /el-table-column el-table-column label操作 width200 fixedright template slot-scopescope el-button sizemini clickhandleEdit(scope.row)编辑/el-button el-button sizemini :typescope.row.status 1 ? warning : success clickhandleStatusChange(scope.row) {{ scope.row.status 1 ? 下架 : 上架 }} /el-button el-button sizemini typedanger clickhandleDelete(scope.row) 删除 /el-button /template /el-table-column /el-table !-- 分页 -- el-pagination size-changehandleSizeChange current-changehandleCurrentChange :current-pagequeryParams.pageNum :page-sizes[10, 20, 50, 100] :page-sizequeryParams.pageSize layouttotal, sizes, prev, pager, next, jumper :totaltotal /el-pagination /el-card /div /template script import { getProductPage, updateProductStatus } from /api/product export default { name: ProductList, data() { return { loading: false, productList: [], total: 0, queryParams: { pageNum: 1, pageSize: 10, name: , status: } } }, mounted() { this.loadData() }, methods: { async loadData() { this.loading true try { const response await getProductPage(this.queryParams) if (response.code 200) { this.productList response.data.list this.total response.data.total } } catch (error) { this.$message.error(获取商品列表失败) } finally { this.loading false } }, async handleStatusChange(row) { try { const newStatus row.status 1 ? 0 : 1 await updateProductStatus(row.id, newStatus) this.$message.success(操作成功) this.loadData() } catch (error) { this.$message.error(操作失败) } }, handleSizeChange(val) { this.queryParams.pageSize val this.loadData() }, handleCurrentChange(val) { this.queryParams.pageNum val this.loadData() }, resetQuery() { this.queryParams { pageNum: 1, pageSize: 10, name: , status: } this.loadData() } } } /script3.3 API 接口统一管理创建 API 层统一管理后端接口调用// src/api/product.js import request from /utils/request export function getProductPage(params) { return request({ url: /api/product/page, method: get, params }) } export function addProduct(data) { return request({ url: /api/product, method: post, data }) } export function updateProductStatus(id, status) { return request({ url: /api/product/${id}/status, method: put, params: { status } }) }请求拦截器配置utils/request.jsimport axios from axios import { Message } from element-ui import router from /router // 创建 axios 实例 const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) // 请求拦截器 service.interceptors.request.use( config { // 添加 token const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message({ message: res.message || Error, type: error, duration: 5 * 1000 }) // token 过期处理 if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message || Error)) } else { return res } }, error { Message({ message: error.message, type: error, duration: 5 * 1000 }) return Promise.reject(error) } ) export default service4. 前后端联调与部署4.1 跨域问题解决方案开发环境使用 Vue CLI 代理解决跨域生产环境需要通过 Nginx 配置或后端处理。Spring Boot 跨域配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }4.2 接口文档与测试使用 Swagger 生成 API 文档方便前后端协作Configuration EnableSwagger2 public class SwaggerConfig { Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage(com.supermarket.controller)) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(超市管理系统 API 文档) .description(前后端分离项目接口文档) .version(1.0) .build(); } }4.3 生产环境部署配置后端部署配置application-prod.ymlspring: datasource: url: jdbc:mysql://生产数据库IP:3306/supermarket?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: 生产用户名 password: 生产密码 server: port: 8080 servlet: context-path: /api logging: level: com.supermarket: debug file: name: logs/supermarket.log前端生产构建npm run buildNginx 配置示例server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } # 后端 API 代理 location /api/ { proxy_pass http://后端服务器IP:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }5. 常见问题排查与优化建议5.1 开发阶段常见问题问题1前端请求后端接口出现 404 错误排查步骤检查后端服务是否正常启动端口是否正确确认请求 URL 路径是否与 Controller 中 RequestMapping 配置一致查看浏览器 Network 面板确认请求是否发送成功检查后端日志是否有相关请求记录问题2MyBatis 查询结果映射失败解决方案!-- 在 mybatis-config.xml 中开启驼峰命名转换 -- settings setting namemapUnderscoreToCamelCase valuetrue/ /settings !-- 或者检查 resultMap 配置 -- resultMap idProductResult typecom.supermarket.entity.Product id propertyid columnid/ result propertycategoryName columncategory_name/ /resultMap问题3Vue 页面刷新后路由丢失解决方案使用 history 模式并配置 Nginx fallback// router/index.js const router new VueRouter({ mode: history, routes })5.2 性能优化建议数据库优化为常用查询字段添加索引避免 SELECT *只查询需要的字段大数据量分页查询使用游标分页而非 LIMIT offset前端优化组件按需加载const ProductList () import(/views/product/List)图片资源压缩和懒加载API 请求合理使用防抖和节流后端优化频繁查询的数据使用 Redis 缓存数据库连接池配置优化启用 Gzip 压缩减少网络传输5.3 安全注意事项SQL 注入防护使用 MyBatis 参数绑定避免字符串拼接XSS 攻击防护前端对用户输入进行转义后端进行参数校验CSRF 防护使用 Token 验证机制权限控制实现基于角色的访问控制RBAC敏感信息保护密码加密存储日志脱敏处理5.4 项目扩展方向完成基础功能后可以考虑扩展以下模块会员积分管理系统供应商管理和采购流程财务报表和统计分析移动端小程序支持第三方支付集成消息推送和邮件通知这个超市管理系统项目涵盖了企业级应用的核心技术点通过实际编码和问题解决能够显著提升全栈开发能力。建议在理解本文示例的基础上根据自己的需求调整功能设计逐步完善项目细节。