作为一个常年混迹在Java和前端开发圈的老码农,最近帮学弟学妹们做了不少毕业设计指导。发现网上超市系统这个选题特别受欢迎——既贴近生活,又能完整覆盖主流技术栈。今天我就把去年指导的一个SpringBoot+Vue网上超市项目做个完整复盘,这个项目特别适合作为Java全栈开发的练手项目。
这个系统采用经典的前后端分离架构,前端用Vue3+Element Plus实现响应式界面,后端基于SpringBoot 2.7整合MyBatis-Plus,数据库选用MySQL 8.0。整个项目从需求分析到部署上线共耗时3周,最终实现的系统包含完整的商品管理、购物车、订单支付等核心电商功能模块。
提示:项目源码已去除所有敏感信息并开源在Gitee,文末会给出获取方式。建议跟着文章边看边实操,遇到问题可以直接参考源码。
前端技术栈:
后端技术栈:
数据库:
典型的B/S三层架构:
code复制┌───────────────────────────────────────┐
│ 客户端层 │
│ ┌───────────┐ ┌─────────────┐ │
│ │ PC浏览器 │ │ 移动端H5页面 │ │
│ └───────────┘ └─────────────┘ │
└───────────────────┬───────────────────┘
│ HTTP/HTTPS
▼
┌───────────────────────────────────────┐
│ 表现层 │
│ ┌───────────┐ ┌─────────────┐ │
│ │ Vue前端 │ │ 管理后台Vue │ │
│ └───────────┘ └─────────────┘ │
└───────────────────┬───────────────────┘
│ RESTful API
▼
┌───────────────────────────────────────┐
│ 业务逻辑层 │
│ ┌─────────────────────────────────┐ │
│ │ SpringBoot应用 │ │
│ │ ┌───────┐ ┌──────────┐ │ │
│ │ │控制层 │ │ 服务层 │ │ │
│ │ └───────┘ └──────────┘ │ │
│ └─────────────────────────────────┘ │
└───────────────────┬───────────────────┘
│ JDBC/MyBatis
▼
┌───────────────────────────────────────┐
│ 数据持久层 │
│ ┌───────────┐ ┌─────────────┐ │
│ │ MySQL 8.0 │ │ Redis 7 │ │
│ └───────────┘ └─────────────┘ │
└───────────────────────────────────────┘
用户表(user)优化方案:
sql复制CREATE TABLE `user` (
`user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '登录账号',
`nickname` varchar(50) DEFAULT NULL COMMENT '用户昵称',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`gender` tinyint DEFAULT '0' COMMENT '性别(0-未知 1-男 2-女)',
`status` tinyint DEFAULT '0' COMMENT '状态(0-正常 1-禁用)',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
`register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`),
KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
商品表(product)设计要点:
订单表(order)的拆分策略:
在电商系统中,查询性能至关重要。我们为以下字段建立了索引:
注意:索引不是越多越好,需要根据实际查询场景来设计。我们通过EXPLAIN分析慢查询,逐步优化索引策略。
application.yml关键配置:
yaml复制server:
port: 8080
servlet:
context-path: /api
spring:
datasource:
url: jdbc:mysql://localhost:3306/online_mall?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
redis:
host: localhost
port: 6379
password:
database: 0
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDeleted # 全局逻辑删除字段
logic-delete-value: 1 # 逻辑已删除值
logic-not-delete-value: 0 # 逻辑未删除值
JWT工具类核心代码:
java复制public class JwtUtil {
private static final String SECRET_KEY = "your-256-bit-secret";
private static final long EXPIRATION_TIME = 86400000; // 24小时
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public static Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 其他工具方法...
}
Spring Security配置:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/product/list").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
// 其他配置...
}
商品列表页示例:
vue复制<script setup>
import { ref, onMounted } from 'vue'
import { getProductList } from '@/api/product'
const products = ref([])
const loading = ref(true)
const pagination = ref({
current: 1,
pageSize: 10,
total: 0
})
const fetchProducts = async () => {
try {
loading.value = true
const res = await getProductList({
page: pagination.value.current,
size: pagination.value.pageSize
})
products.value = res.data.list
pagination.value.total = res.data.total
} finally {
loading.value = false
}
}
onMounted(() => {
fetchProducts()
})
</script>
使用Pinia管理购物车状态:
javascript复制// stores/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
actions: {
async addItem(product, quantity = 1) {
const existingItem = this.items.find(item => item.id === product.id)
if (existingItem) {
existingItem.quantity += quantity
} else {
this.items.push({
...product,
quantity,
selected: true
})
}
this.calculateTotal()
},
calculateTotal() {
this.total = this.items
.filter(item => item.selected)
.reduce((sum, item) => sum + (item.price * item.quantity), 0)
}
},
persist: true // 启用本地持久化
})
Docker Compose部署文件:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mall-mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: online_mall
ports:
- "3306:3306"
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
restart: always
redis:
image: redis:7.0
container_name: mall-redis
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
restart: always
backend:
build: ./backend
container_name: mall-backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_PROFILES_ACTIVE: prod
restart: always
frontend:
build: ./frontend
container_name: mall-frontend
ports:
- "80:80"
restart: always
缓存策略:
SQL优化:
前端优化:
后端解决方案(SpringBoot):
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.maxAge(3600);
}
}
前端解决方案(Axios配置):
javascript复制const service = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
withCredentials: true // 允许携带cookie
})
// 请求拦截器
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
使用乐观锁解决超卖问题:
java复制@Transactional
public OrderResult createOrder(OrderRequest request) {
// 1. 校验商品库存(带版本号)
Product product = productMapper.selectByIdForUpdate(request.getProductId());
if (product.getStock() < request.getQuantity()) {
throw new BusinessException("库存不足");
}
// 2. 扣减库存(带版本号检查)
int updateCount = productMapper.reduceStock(
request.getProductId(),
request.getQuantity(),
product.getVersion()
);
if (updateCount == 0) {
throw new ConcurrentOrderException("订单并发冲突,请重试");
}
// 3. 创建订单
Order order = new Order();
// 设置订单属性...
orderMapper.insert(order);
return OrderResult.success(order.getId());
}
微服务化改造:
支付系统集成:
大数据分析:
这个项目从技术选型到最终实现,完整覆盖了Java全栈开发的各项技能点。我在实现过程中最大的体会是:电商系统看似简单,但要处理好各种边界条件和并发场景,需要扎实的编程基础和系统设计能力。特别是订单和库存模块,一个小小的并发问题就可能导致严重的超卖事故。
项目完整源码和数据库脚本已上传Gitee,包含详细的中文注释和部署文档。建议初学者可以按照"用户模块->商品模块->购物车->订单模块"的顺序逐步实现,遇到问题随时查看源码参考。