作为一个在电商系统开发领域摸爬滚打多年的老码农,今天想和大家分享一个基于SpringBoot+Vue的网上购物商城系统实现方案。这个项目特别适合作为计算机相关专业的毕业设计或课程设计,也适合想学习前后端分离开发的朋友练手。
这个系统采用了经典的前后端分离架构,后端使用SpringBoot框架提供RESTful API接口,前端使用Vue.js进行页面渲染,数据库采用MySQL存储数据。系统实现了完整的电商功能闭环,包括用户管理、商品展示、购物车、订单处理等核心模块。
后端选择SpringBoot框架主要基于以下几个考虑:
数据库选用MySQL的原因:
前端选择Vue.js框架的优势:
系统采用前后端分离架构,主要分为以下几层:
这种架构的优势在于:
用户表(user_info)的设计考虑了以下几个要点:
sql复制CREATE TABLE `user_info` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password_hash` varchar(64) NOT NULL,
`email` varchar(50) NOT NULL,
`phone` varchar(20) DEFAULT NULL,
`register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_login` datetime DEFAULT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
商品表(product_detail)的设计要点:
sql复制CREATE TABLE `product_detail` (
`product_id` bigint NOT NULL AUTO_INCREMENT,
`product_name` varchar(100) NOT NULL,
`category_id` int NOT NULL,
`price` decimal(10,2) NOT NULL,
`stock` int NOT NULL DEFAULT '0',
`description` text,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`product_id`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
订单表(order_history)的设计考虑:
sql复制CREATE TABLE `order_history` (
`order_id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`total_amount` decimal(10,2) NOT NULL,
`payment_status` tinyint NOT NULL DEFAULT '0',
`delivery_info` varchar(200) DEFAULT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`),
KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
系统采用JWT(JSON Web Token)实现用户认证,主要流程:
java复制@Service
public class AuthService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
public String generateToken(UserDetails userDetails) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
Date expiration = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getExpiration();
return expiration.before(new Date());
}
}
商品管理实现了CRUD操作和分页查询功能:
java复制@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping
public ResponseEntity<Page<Product>> getAllProducts(
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "10") int size) {
Page<Product> products = productService.findAll(page, size);
return ResponseEntity.ok(products);
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.findById(id);
return ResponseEntity.ok(product);
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
Product savedProduct = productService.save(product);
return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct);
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id, @RequestBody Product product) {
Product updatedProduct = productService.update(id, product);
return ResponseEntity.ok(updatedProduct);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.delete(id);
return ResponseEntity.noContent().build();
}
}
订单处理的核心逻辑包括:
java复制@Service
@Transactional
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductRepository productRepository;
@Autowired
private UserRepository userRepository;
public Order createOrder(OrderRequest orderRequest, String username) {
// 验证用户
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
// 创建订单
Order order = new Order();
order.setUser(user);
order.setPaymentStatus(PaymentStatus.UNPAID);
// 处理订单项
BigDecimal totalAmount = BigDecimal.ZERO;
for (OrderItemRequest itemRequest : orderRequest.getItems()) {
Product product = productRepository.findById(itemRequest.getProductId())
.orElseThrow(() -> new RuntimeException("Product not found"));
// 检查库存
if (product.getStock() < itemRequest.getQuantity()) {
throw new RuntimeException("Insufficient stock for product: " + product.getProductName());
}
// 扣减库存
product.setStock(product.getStock() - itemRequest.getQuantity());
productRepository.save(product);
// 计算金额
BigDecimal itemAmount = product.getPrice().multiply(new BigDecimal(itemRequest.getQuantity()));
totalAmount = totalAmount.add(itemAmount);
// 添加订单项
OrderItem orderItem = new OrderItem();
orderItem.setProduct(product);
orderItem.setQuantity(itemRequest.getQuantity());
orderItem.setPrice(product.getPrice());
order.addOrderItem(orderItem);
}
order.setTotalAmount(totalAmount);
return orderRepository.save(order);
}
}
前端使用Vue.js和Vue Router实现用户认证流程:
javascript复制// src/store/modules/auth.js
const state = {
token: localStorage.getItem('token') || '',
status: '',
user: null
}
const mutations = {
AUTH_REQUEST: state => {
state.status = 'loading'
},
AUTH_SUCCESS: (state, { token, user }) => {
state.status = 'success'
state.token = token
state.user = user
},
AUTH_ERROR: state => {
state.status = 'error'
},
LOGOUT: state => {
state.token = ''
state.user = null
}
}
const actions = {
login({ commit }, userData) {
return new Promise((resolve, reject) => {
commit('AUTH_REQUEST')
axios.post('/api/auth/login', userData)
.then(resp => {
const token = resp.data.token
const user = resp.data.user
localStorage.setItem('token', token)
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token
commit('AUTH_SUCCESS', { token, user })
resolve(resp)
})
.catch(err => {
commit('AUTH_ERROR')
localStorage.removeItem('token')
reject(err)
})
})
},
register({ commit }, userData) {
return new Promise((resolve, reject) => {
commit('AUTH_REQUEST')
axios.post('/api/auth/register', userData)
.then(resp => {
resolve(resp)
})
.catch(err => {
commit('AUTH_ERROR')
reject(err)
})
})
},
logout({ commit }) {
return new Promise((resolve) => {
commit('LOGOUT')
localStorage.removeItem('token')
delete axios.defaults.headers.common['Authorization']
resolve()
})
}
}
商品列表页实现分页和搜索功能:
javascript复制<template>
<div class="product-list">
<el-row :gutter="20">
<el-col :span="6" v-for="product in products" :key="product.id">
<el-card :body-style="{ padding: '0px' }">
<img :src="product.image" class="image">
<div style="padding: 14px;">
<span>{{ product.name }}</span>
<div class="bottom clearfix">
<span class="price">¥{{ product.price }}</span>
<el-button type="text" class="button" @click="addToCart(product)">加入购物车</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.current"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.size"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</div>
</template>
<script>
export default {
data() {
return {
products: [],
pagination: {
current: 1,
size: 10,
total: 0
},
searchQuery: ''
}
},
created() {
this.fetchProducts()
},
methods: {
fetchProducts() {
const params = {
page: this.pagination.current - 1,
size: this.pagination.size,
q: this.searchQuery
}
axios.get('/api/products', { params })
.then(response => {
this.products = response.data.content
this.pagination.total = response.data.totalElements
})
},
handleSizeChange(val) {
this.pagination.size = val
this.fetchProducts()
},
handleCurrentChange(val) {
this.pagination.current = val
this.fetchProducts()
},
addToCart(product) {
this.$store.dispatch('cart/addToCart', product)
}
}
}
</script>
购物车使用Vuex进行状态管理:
javascript复制// src/store/modules/cart.js
const state = {
items: JSON.parse(localStorage.getItem('cart')) || []
}
const mutations = {
ADD_ITEM(state, product) {
const existingItem = state.items.find(item => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
state.items.push({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity: 1
})
}
localStorage.setItem('cart', JSON.stringify(state.items))
},
REMOVE_ITEM(state, productId) {
state.items = state.items.filter(item => item.id !== productId)
localStorage.setItem('cart', JSON.stringify(state.items))
},
UPDATE_QUANTITY(state, { productId, quantity }) {
const item = state.items.find(item => item.id === productId)
if (item) {
item.quantity = quantity
localStorage.setItem('cart', JSON.stringify(state.items))
}
},
CLEAR_CART(state) {
state.items = []
localStorage.removeItem('cart')
}
}
const actions = {
addToCart({ commit }, product) {
commit('ADD_ITEM', product)
},
removeFromCart({ commit }, productId) {
commit('REMOVE_ITEM', productId)
},
updateQuantity({ commit }, payload) {
commit('UPDATE_QUANTITY', payload)
},
clearCart({ commit }) {
commit('CLEAR_CART')
}
}
const getters = {
cartItems: state => state.items,
cartTotal: state => {
return state.items.reduce((total, item) => {
return total + (item.price * item.quantity)
}, 0)
},
cartItemCount: state => {
return state.items.reduce((count, item) => {
return count + item.quantity
}, 0)
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
SpringBoot应用可以使用以下方式部署:
打包命令:
bash复制mvn clean package
Dockerfile示例:
dockerfile复制FROM openjdk:11-jre-slim
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Vue.js应用部署步骤:
构建命令:
bash复制npm run build
Nginx配置示例:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /path/to/dist;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
MySQL数据库部署建议:
Docker运行MySQL命令:
bash复制docker run --name mysql \
-e MYSQL_ROOT_PASSWORD=yourpassword \
-e MYSQL_DATABASE=ecommerce \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppassword \
-p 3306:3306 \
-d mysql:8.0
前后端分离开发常见的跨域问题解决方案:
SpringBoot配置CORS示例:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(false)
.maxAge(3600);
}
}
系统性能优化方向:
添加数据库索引示例:
sql复制ALTER TABLE product_detail ADD INDEX idx_name (product_name);
ALTER TABLE order_history ADD INDEX idx_user_status (user_id, payment_status);
系统安全防护措施:
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/**").authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
可以考虑添加的功能:
可以尝试的技术升级:
深入学习相关技术的资源:
在实际开发这个电商系统的过程中,我发现有几个关键点特别值得注意: