用Vue 2.7 + SpringBoot 3.1 + MySQL 8 从零搭建一个超市商品管理系统(附完整源码和数据库脚本)
Vue 2.7 + SpringBoot 3.1 + MySQL 8 全栈开发超市商品管理系统实战
超市商品管理系统是零售行业数字化转型的典型应用场景。对于计算机专业学生和全栈开发初学者而言,通过一个完整的项目实践来掌握前后端技术栈的协同开发,是提升工程能力的有效途径。本文将基于Vue 2.7、SpringBoot 3.1和MySQL 8技术组合,从零开始构建一个功能完备的超市商品管理系统,重点解决实际开发中的技术难点和常见陷阱。
1. 开发环境准备与项目初始化
1.1 开发工具配置
工欲善其事,必先利其器。在开始编码前,我们需要确保开发环境配置正确:
- Java开发环境:JDK 17(SpringBoot 3.1的最低要求)
- Node.js环境:建议安装LTS版本(如16.x)
- IDE选择:
- IntelliJ IDEA(后端开发)
- VS Code(前端开发)
- 数据库工具:MySQL Workbench或Navicat
注意:SpringBoot 3.1要求JDK 17及以上版本,与SpringBoot 2.x系列有显著差异,这是初学者容易忽略的兼容性问题。
1.2 项目骨架搭建
我们采用前后端分离的架构,需要分别初始化两个项目:
后端项目初始化(SpringBoot 3.1):
# 使用Spring Initializr创建项目 curl https://start.spring.io/starter.zip \ -d dependencies=web,mybatis-plus,mysql,lombok \ -d javaVersion=17 \ -d artifactId=supermarket-backend \ -d bootVersion=3.1.0 \ -o supermarket-backend.zip前端项目初始化(Vue 2.7):
# 使用Vue CLI创建项目 npm install -g @vue/cli vue create supermarket-frontend # 选择Vue 2模板,手动配置添加Router和Vuex1.3 数据库环境准备
MySQL 8相比5.7版本有显著的性能提升和安全增强,我们需要特别注意认证方式的配置:
-- 创建数据库和用户 CREATE DATABASE supermarket CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'superuser'@'%' IDENTIFIED WITH mysql_native_password BY 'SecurePass123!'; GRANT ALL PRIVILEGES ON supermarket.* TO 'superuser'@'%'; FLUSH PRIVILEGES;2. 数据库设计与MyBatis Plus集成
2.1 数据库表结构设计
超市管理系统的核心实体包括商品、商品类型、超市区域和货架。以下是优化后的DDL:
CREATE TABLE `product_category` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '分类名称', `sort_order` int DEFAULT '0' COMMENT '排序字段', `status` tinyint DEFAULT '1' COMMENT '状态:1-启用,0-禁用', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表'; CREATE TABLE `store_area` ( `id` bigint NOT NULL AUTO_INCREMENT, `area_code` varchar(20) NOT NULL COMMENT '区域编码', `area_name` varchar(50) NOT NULL COMMENT '区域名称', `description` varchar(200) DEFAULT NULL, `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_area_code` (`area_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='超市区域表';2.2 MyBatis Plus 3.5.3.1配置技巧
MyBatis Plus能极大简化DAO层开发,但在SpringBoot 3.1中需要特别注意以下配置:
# application.yml配置 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto logic-delete-field: deleted # 逻辑删除字段 logic-not-delete-value: 0 logic-delete-value: 1实体类示例:
@Data @TableName("product") public class Product { @TableId(type = IdType.AUTO) private Long id; private String name; private String barcode; private BigDecimal price; private Integer stock; private Long categoryId; private Long shelfId; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }3. 后端API开发实战
3.1 RESTful API设计规范
我们遵循行业通用的RESTful设计原则:
- 资源命名:使用复数名词(如/products)
- HTTP方法:
- GET:获取资源
- POST:创建资源
- PUT:全量更新
- PATCH:部分更新
- DELETE:删除资源
- 状态码:
- 200:成功
- 201:创建成功
- 400:客户端错误
- 404:资源不存在
- 500:服务器错误
3.2 商品管理API实现
商品管理是系统的核心模块,我们采用三层架构:
- Controller层:处理HTTP请求
@RestController @RequestMapping("/api/products") @RequiredArgsConstructor public class ProductController { private final ProductService productService; @GetMapping public ResponseEntity<PageResult<ProductVO>> listProducts( @RequestParam(required = false) String name, @RequestParam(required = false) Long categoryId, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { return ResponseEntity.ok(productService.listProducts(name, categoryId, page, size)); } }- Service层:业务逻辑处理
@Service @RequiredArgsConstructor public class ProductServiceImpl implements ProductService { private final ProductMapper productMapper; @Override @Transactional(readOnly = true) public PageResult<ProductVO> listProducts(String name, Long categoryId, Integer page, Integer size) { Page<Product> pageInfo = new Page<>(page, size); LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.like(StringUtils.isNotBlank(name), Product::getName, name) .eq(categoryId != null, Product::getCategoryId, categoryId); Page<Product> productPage = productMapper.selectPage(pageInfo, queryWrapper); return new PageResult<>(productPage.getTotal(), productPage.getRecords().stream().map(this::convertToVO).collect(Collectors.toList())); } }- Mapper层:数据持久化
public interface ProductMapper extends BaseMapper<Product> { @Select("SELECT p.*, c.name as category_name FROM product p LEFT JOIN product_category c ON p.category_id = c.id WHERE p.id = #{id}") ProductDetailVO selectProductDetailById(@Param("id") Long id); }3.3 全局异常处理
统一的异常处理能提升API的健壮性:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) { return ResponseEntity.status(ex.getStatus()) .body(new ErrorResponse(ex.getCode(), ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) { String message = ex.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); return ResponseEntity.badRequest() .body(new ErrorResponse("VALIDATION_FAILED", message)); } }4. 前端开发与前后端联调
4.1 Vue项目结构优化
合理的项目结构能提高代码可维护性:
src/ ├── api/ # API请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── router/ # 路由配置 ├── store/ # Vuex状态管理 ├── utils/ # 工具函数 ├── views/ # 页面组件 │ ├── product/ # 商品管理相关页面 │ ├── category/ # 分类管理 │ └── layout/ # 布局组件 └── main.js # 应用入口4.2 Axios封装与API调用
对Axios进行二次封装,统一处理请求和响应:
// api/request.js import axios from 'axios' const service = axios.create({ baseURL: process.env.VUE_APP_API_BASE_URL, timeout: 10000 }) // 请求拦截器 service.interceptors.request.use( config => { 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.error(res.message || 'Error') return Promise.reject(new Error(res.message || 'Error')) } return res.data }, error => { Message.error(error.message || '请求失败') return Promise.reject(error) } ) export default service4.3 商品列表页面实现
使用Element UI实现商品管理界面:
<template> <div class="product-container"> <el-card shadow="never"> <div class="filter-container"> <el-input v-model="listQuery.name" placeholder="商品名称" style="width: 200px" @keyup.enter.native="handleFilter" /> <el-select v-model="listQuery.categoryId" placeholder="商品分类" clearable style="width: 200px" > <el-option v-for="item in categories" :key="item.id" :label="item.name" :value="item.id" /> </el-select> <el-button type="primary" @click="handleFilter">查询</el-button> </div> <el-table v-loading="listLoading" :data="list" border fit highlight-current-row > <el-table-column label="ID" prop="id" width="80" /> <el-table-column label="商品名称" prop="name" /> <el-table-column label="分类" prop="categoryName" /> <el-table-column label="价格" prop="price" /> <el-table-column label="库存" prop="stock" /> <el-table-column label="操作" width="200"> <template #default="{row}"> <el-button size="mini" @click="handleEdit(row)">编辑</el-button> <el-button size="mini" type="danger" @click="handleDelete(row)" >删除</el-button > </template> </el-table-column> </el-table> <pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit" @pagination="getList" /> </el-card> </div> </template> <script> import { fetchList, deleteProduct } from '@/api/product' import Pagination from '@/components/Pagination' export default { name: 'ProductList', components: { Pagination }, data() { return { list: null, total: 0, listLoading: true, listQuery: { page: 1, limit: 10, name: undefined, categoryId: undefined }, categories: [] } }, created() { this.getList() this.getCategories() }, methods: { getList() { this.listLoading = true fetchList(this.listQuery).then(response => { this.list = response.items this.total = response.total this.listLoading = false }) }, handleFilter() { this.listQuery.page = 1 this.getList() }, handleEdit(row) { this.$router.push(`/product/edit/${row.id}`) }, handleDelete(row) { this.$confirm('确认删除该商品?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { deleteProduct(row.id).then(() => { this.$message.success('删除成功') this.getList() }) }) } } } </script>5. 系统部署与性能优化
5.1 后端部署配置
SpringBoot应用的生产环境配置要点:
# application-prod.yml server: port: 8080 compression: enabled: true mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json min-response-size: 1024 spring: datasource: url: jdbc:mysql://prod-db:3306/supermarket?useSSL=false&characterEncoding=utf8 username: ${DB_USER} password: ${DB_PASSWORD} hikari: maximum-pool-size: 20 connection-timeout: 30000 jpa: open-in-view: false cache: type: redis redis: time-to-live: 3600000 management: endpoints: web: exposure: include: health,info,metrics5.2 前端性能优化
通过以下手段提升Vue应用性能:
- 路由懒加载:
const ProductList = () => import('./views/product/List.vue') const ProductEdit = () => import('./views/product/Edit.vue')- 代码分割:在vue.config.js中配置
module.exports = { configureWebpack: { optimization: { splitChunks: { chunks: 'all', maxSize: 244 * 1024 // 244KB } } } }- CDN引入常用库:
// vue.config.js module.exports = { chainWebpack: config => { config.externals({ vue: 'Vue', 'vue-router': 'VueRouter', axios: 'axios', 'element-ui': 'ELEMENT' }) } }5.3 数据库性能调优
针对超市管理系统的高频查询场景,我们需要优化数据库:
- 索引优化:
-- 为高频查询字段添加索引 ALTER TABLE product ADD INDEX idx_category_status (category_id, status); ALTER TABLE product ADD INDEX idx_name (name);- 查询优化:
// 使用MyBatis Plus的selectPage优化分页查询 public Page<Product> listProducts(int page, int size) { return productMapper.selectPage(new Page<>(page, size), Wrappers.<Product>lambdaQuery() .select(Product.class, info -> !info.getColumn().equals("description") && !info.getColumn().equals("specification")) ); }- 缓存策略:
// 使用Spring Cache注解实现缓存 @Cacheable(value = "product", key = "#id", unless = "#result == null") public Product getProductById(Long id) { return productMapper.selectById(id); } @CacheEvict(value = "product", key = "#product.id") public void updateProduct(Product product) { productMapper.updateById(product); }