这个基于SpringBoot+Vue的手机销售管理平台,是我在指导本科生毕业设计时反复打磨的一个电商类教学项目。相比市面上那些只给代码不给思路的"资源包",我更想带大家完整走一遍从数据库设计到前后端联调的实战流程。
为什么选择手机销售这个垂直领域?从教学角度考虑,3C品类具有商品属性明确(品牌、型号、配置参数标准化)、业务逻辑典型(购物车、订单、支付全流程)的特点。一个2000行左右的代码量就能覆盖CRUD、权限控制、前后端分离等企业级开发的核心技能点,非常适合作为Java全栈的入门项目。
技术选型上,后端采用SpringBoot 2.7 + MyBatis-Plus的组合。SpringBoot的自动配置特性能让初学者快速搭建Web服务,而MyBatis-Plus的ActiveRecord模式比传统MyBatis更符合新手直觉。前端选用Vue 3 + Element Plus,其组合式API和丰富的UI组件能大幅降低前端开发门槛。数据库使用MySQL 8.0,主要考虑其在校企环境中的普及度。
用户表(user)的密码字段采用VARCHAR(100)而非CHAR,这是为BCrypt加密预留空间。实测表明,BCrypt加密后的字符串长度通常在60-80字符之间。建议增加salt字段(虽然BCrypt自带salt),这是为了应对某些特殊场景下的二次加密需求。
sql复制CREATE TABLE `user` (
`user_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '雪花算法ID',
`username` VARCHAR(50) NOT NULL COMMENT '登录账号',
`password` VARCHAR(100) NOT NULL COMMENT 'BCrypt加密密码',
`salt` VARCHAR(20) DEFAULT NULL COMMENT '预留盐值',
`email` VARCHAR(100) UNIQUE COMMENT '邮箱',
`phone` VARCHAR(20) UNIQUE COMMENT '手机号',
`avatar` VARCHAR(255) DEFAULT 'default.jpg' COMMENT '头像路径',
`status` TINYINT DEFAULT 1 COMMENT '0-禁用 1-正常',
`register_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`last_login` DATETIME ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
踩坑提醒:手机号字段切忌使用INT/BIGINT类型。国际号码包含+号,且数值类型会丢失前导零。建议用VARCHAR(20)并添加前缀索引。
商品表(product)的price字段使用DECIMAL(10,2)而非FLOAT,这是为了避免浮点数精度问题。库存字段设计为无符号INT,配合MySQL的CHECK约束(8.0.16+版本支持)防止负数库存:
sql复制CREATE TABLE `product` (
`product_id` BIGINT NOT NULL AUTO_INCREMENT,
`product_name` VARCHAR(100) NOT NULL,
`price` DECIMAL(10,2) CHECK (price > 0),
`stock` INT UNSIGNED DEFAULT 0,
`category_id` INT COMMENT '关联分类表',
`brand_id` INT COMMENT '关联品牌表',
`spec_json` JSON COMMENT '规格参数',
`main_image` VARCHAR(255) COMMENT '主图URL',
`sub_images` TEXT COMMENT '子图URLs,逗号分隔',
`detail_html` TEXT COMMENT '商品详情HTML',
`sales` INT DEFAULT 0 COMMENT '销量',
`publish_status` TINYINT DEFAULT 0 COMMENT '0-下架 1-上架',
`publish_time` DATETIME COMMENT '上架时间',
PRIMARY KEY (`product_id`),
FULLTEXT KEY `ft_idx_name_desc` (`product_name`,`description`)
) ENGINE=InnoDB;
实战技巧:对于频繁查询的字段组合(如商品名+描述),建议添加FULLTEXT索引支持模糊搜索。JSON类型字段适合存储动态规格参数(如手机配置:CPU/内存/颜色等)。
订单表(order)采用主表+明细表的分拆设计。主表记录订单概要,明细表存储商品条目,这种设计能有效避免数据冗余:
java复制// Order.java
public class Order {
private Long orderId;
private Long userId;
private BigDecimal totalAmount;
private Integer payType;
private Integer status;
private String shippingAddress;
private LocalDateTime createTime;
private LocalDateTime payTime;
// getters/setters
}
// OrderItem.java
public class OrderItem {
private Long itemId;
private Long orderId;
private Long productId;
private String productName;
private BigDecimal currentPrice;
private Integer quantity;
private BigDecimal totalPrice;
// getters/setters
}
事务控制要点:创建订单时需要原子性地执行:1)扣减库存 2)生成订单主表 3)插入订单明细。建议使用Spring的@Transactional注解,并设置隔离级别为REPEATABLE_READ。
采用经典的三层架构,但针对教学项目做了适度简化:
code复制src/main/java
├── config # 配置类
├── controller # 表现层
├── service # 业务逻辑层
│ ├── impl # 实现类
├── dao # 数据访问层
├── entity # 实体类
├── dto # 数据传输对象
├── util # 工具类
└── exception # 异常处理
采用MyBatis-Plus的动态SQL构建查询条件,支持多字段组合搜索:
java复制public Page<Product> searchProducts(ProductQuery query, Pageable pageable) {
return lambdaQuery()
.like(StringUtils.isNotBlank(query.getKeyword()),
Product::getProductName, query.getKeyword())
.eq(query.getCategoryId() != null,
Product::getCategoryId, query.getCategoryId())
.between(query.getMinPrice() != null && query.getMaxPrice() != null,
Product::getPrice, query.getMinPrice(), query.getMaxPrice())
.orderByAsc(query.getSortBy() == 1, Product::getPrice)
.orderByDesc(query.getSortBy() == 2, Product::getPrice)
.orderByDesc(query.getSortBy() == 3, Product::getSales)
.page(new Page<>(pageable.getPageNumber(), pageable.getPageSize()));
}
性能优化:对于高频访问的热门商品列表,建议添加Redis缓存:
java复制@Cacheable(value = "product", key = "#categoryId + '_' + #page + '_' + #size")
public List<Product> getHotProducts(Long categoryId, int page, int size) {
// 数据库查询逻辑
}
采用Redis Hash结构存储用户购物车,键格式:cart:{userId},字段:productId,值:商品数量。相比数据库存储,Redis方案能承受更高并发:
java复制public void addToCart(Long userId, Long productId, int quantity) {
String key = "cart:" + userId;
// 检查商品是否存在及库存
Product product = productService.getById(productId);
if (product == null || product.getStock() < quantity) {
throw new BusinessException("商品不存在或库存不足");
}
// Redis原子操作
redisTemplate.opsForHash().increment(key, productId.toString(), quantity);
}
并发控制:使用Redis的WATCH+MULTI实现乐观锁,防止超卖。实际项目中建议用Lua脚本保证原子性。
针对购物车状态,设计如下store模块:
javascript复制// store/modules/cart.js
const actions = {
async addToCart({ commit }, { productId, quantity }) {
const { data } = await api.addCartItem(productId, quantity)
commit('UPDATE_CART', data)
}
}
const mutations = {
UPDATE_CART(state, cartItems) {
state.items = cartItems
state.total = cartItems.reduce((sum, item) => sum + item.quantity, 0)
}
}
使用Vuelidate实现商品添加表单验证:
javascript复制validations: {
product: {
productName: { required },
price: { required, numeric, minValue: minValue(0.01) },
stock: { required, integer, minValue: minValue(0) }
}
}
利用Element Plus的栅格系统实现移动端适配:
html复制<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="product in products">
<product-card :product="product"/>
</el-col>
</el-row>
使用SpringBoot的profile功能管理环境配置:
yaml复制# application-dev.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/phone_shop?useSSL=false
username: dev
password: dev123
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db:3306/phone_shop?useSSL=true
username: ${DB_USER}
password: ${DB_PASS}
配置vue.config.js实现分块打包:
javascript复制configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
elementUI: {
name: 'chunk-elementUI',
test: /[\\/]node_modules[\\/]_?element-ui(.*)/,
priority: 20
}
}
}
}
}
在指导课程设计时,我通常建议学生分阶段实现:
调试技巧:前后端联调时,建议先用Postman测试接口,再对接前端。遇到跨域问题时,可通过@CrossOrigin注解临时解决,生产环境建议配置Nginx反向代理。