最近在帮朋友搭建一个精品水果电商平台时,我选择了SpringBoot+Vue这套前后端分离的技术栈。这个组合在中小型电商项目中表现非常出色,既能保证开发效率,又能满足性能需求。整个系统从零开始搭建到上线部署,前后花了约三周时间,期间踩了不少坑也积累了不少经验。
为什么选择精品水果这个垂直领域?根据我的市场调研,普通水果电商已经是一片红海,但高品质、特色化的精品水果市场仍有很大发展空间。这类产品客单价高,消费者对购物体验要求也更高,正好可以发挥前后端分离架构的优势。
SpringBoot 2.7.x作为后端框架是我的首选。相比传统的SSM架构,SpringBoot的自动配置和起步依赖让项目搭建变得极其简单。我特别看重它内嵌Tomcat的特性,这让部署变得非常方便。
数据库选用MySQL 8.0,主要考虑因素是:
数据访问层使用MyBatis-Plus 3.5.x而不是原生MyBatis,因为:
Vue 3.x + Element Plus的组合让前端开发效率大幅提升。相比React,Vue的学习曲线更平缓,模板语法对后端开发者也更友好。
选择Pinia作为状态管理库而不是Vuex,因为:
用户表(user_info)的设计有几个关键点:
sql复制CREATE TABLE `user_info` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录名',
`password_hash` varchar(100) NOT NULL COMMENT 'BCrypt加密密码',
`email` varchar(100) DEFAULT NULL,
`phone_number` varchar(20) DEFAULT NULL,
`avatar_url` varchar(255) DEFAULT NULL COMMENT '头像URL',
`status` tinyint DEFAULT '1' COMMENT '0-禁用 1-正常',
`register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_login` datetime DEFAULT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`),
KEY `idx_phone` (`phone_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
商品表(product_detail)特别注意了索引设计:
sql复制CREATE TABLE `product_detail` (
`product_id` bigint NOT NULL AUTO_INCREMENT,
`product_name` varchar(100) NOT NULL,
`category_id` bigint NOT NULL,
`price` decimal(10,2) NOT NULL COMMENT '销售价',
`cost_price` decimal(10,2) DEFAULT NULL COMMENT '成本价',
`stock` int NOT NULL DEFAULT '0',
`sold_count` int DEFAULT '0' COMMENT '销量',
`description` text,
`detail_html` text COMMENT '商品详情HTML',
`main_image` varchar(255) NOT NULL COMMENT '主图URL',
`sub_images` json DEFAULT NULL COMMENT '副图JSON数组',
`specs` json DEFAULT NULL COMMENT '规格参数',
`is_hot` tinyint DEFAULT '0' COMMENT '是否热销',
`status` tinyint DEFAULT '1' COMMENT '0-下架 1-上架',
`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`),
KEY `idx_status` (`status`),
FULLTEXT KEY `ft_name_desc` (`product_name`,`description`) COMMENT '全文索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
java复制// 使用MyBatis-Plus的优化分页
Page<ProductVO> page = new Page<>(pageNum, pageSize);
page.setOptimizeCountSql(true); // 优化COUNT语句
page.setSearchCount(true); // 不查询总记录数时设为false
productMapper.selectPageVo(page, query);
java复制// Redis缓存结构设计
String cartKey = "user:cart:" + userId;
// 使用Hash存储,field为productId,value为商品数量
redisTemplate.opsForHash().put(cartKey, productId, quantity);
采用JWT + Spring Security的方案:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/product/**").permitAll()
.antMatchers("/api/cart/**").authenticated()
.antMatchers("/api/order/**").authenticated()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()));
}
}
密码加密采用BCrypt:
java复制public class PasswordEncoder {
private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
public static String encode(String rawPassword) {
return encoder.encode(rawPassword);
}
public static boolean matches(String rawPassword, String encodedPassword) {
return encoder.matches(rawPassword, encodedPassword);
}
}
商品列表API设计要点:
java复制@GetMapping("/products")
public Result<Page<ProductVO>> listProducts(
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String sortBy) {
ProductQuery query = new ProductQuery();
query.setCategoryId(categoryId);
query.setKeyword(keyword);
query.setSortBy(sortBy);
Page<ProductVO> page = productService.queryProducts(pageNum, pageSize, query);
return Result.success(page);
}
前端使用Vue实现商品筛选组件:
vue复制<template>
<div class="filter-container">
<el-select v-model="categoryId" placeholder="选择分类" clearable>
<el-option
v-for="cat in categories"
:key="cat.id"
:label="cat.name"
:value="cat.id">
</el-option>
</el-select>
<el-input
v-model="keyword"
placeholder="搜索商品"
class="search-input"
clearable>
</el-input>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</div>
</template>
下单核心逻辑:
java复制@Transactional
public OrderVO createOrder(Long userId, OrderCreateDTO dto) {
// 1. 验证商品库存
List<OrderItem> items = checkStock(dto.getItems());
// 2. 计算总金额
BigDecimal totalAmount = calculateTotal(items);
// 3. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setTotalAmount(totalAmount);
order.setStatus(OrderStatus.WAITING_PAYMENT);
orderMapper.insert(order);
// 4. 创建订单项
createOrderItems(order.getOrderId(), items);
// 5. 扣减库存
reduceStock(items);
// 6. 清空购物车
cartService.clearCart(userId);
return convertToVO(order, items);
}
支付宝支付集成关键代码:
java复制public String createAlipayOrder(Order order) {
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi.alipay.com/gateway.do",
appId,
privateKey,
"json",
"UTF-8",
alipayPublicKey,
"RSA2");
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl(returnUrl);
request.setNotifyUrl(notifyUrl);
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", order.getOrderNo());
bizContent.put("total_amount", order.getTotalAmount());
bizContent.put("subject", "精品水果订单");
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
request.setBizContent(bizContent.toString());
return alipayClient.pageExecute(request).getBody();
}
商品CRUD接口示例:
java复制@RestController
@RequestMapping("/admin/products")
public class AdminProductController {
@PostMapping
public Result addProduct(@Valid @RequestBody ProductCreateDTO dto) {
Product product = convertToEntity(dto);
productService.saveProduct(product);
return Result.success();
}
@PutMapping("/{id}")
public Result updateProduct(@PathVariable Long id,
@Valid @RequestBody ProductUpdateDTO dto) {
Product product = convertToEntity(dto);
product.setProductId(id);
productService.updateProduct(product);
return Result.success();
}
@GetMapping("/{id}")
public Result<ProductDetailVO> getDetail(@PathVariable Long id) {
ProductDetailVO vo = productService.getProductDetail(id);
return Result.success(vo);
}
}
使用ECharts实现销售数据可视化:
vue复制<template>
<div class="chart-container">
<el-card>
<div ref="salesChart" style="height:400px;"></div>
</el-card>
</div>
</template>
<script>
import * as echarts from 'echarts';
export default {
mounted() {
this.initChart();
},
methods: {
async initChart() {
const chart = echarts.init(this.$refs.salesChart);
const res = await getSalesData();
const option = {
title: { text: '近30天销售趋势' },
tooltip: { trigger: 'axis' },
xAxis: { data: res.dates },
yAxis: { type: 'value' },
series: [{
name: '销售额',
type: 'line',
data: res.amounts,
smooth: true
}]
};
chart.setOption(option);
}
}
}
</script>
使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: fruit_mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: fruit_db
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
networks:
- fruit_net
redis:
image: redis:6.2
container_name: fruit_redis
ports:
- "6379:6379"
networks:
- fruit_net
backend:
build: ./backend
container_name: fruit_backend
depends_on:
- mysql
- redis
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
networks:
- fruit_net
frontend:
build: ./frontend
container_name: fruit_frontend
ports:
- "80:80"
networks:
- fruit_net
volumes:
mysql_data:
networks:
fruit_net:
driver: bridge
Spring Boot Actuator配置:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: fruit-store-backend
配合Grafana监控面板展示关键指标:
多级缓存设计方案:
java复制@Cacheable(value = "products", key = "#productId", unless = "#result == null")
public ProductDetailVO getProductDetail(Long productId) {
return productMapper.selectDetailById(productId);
}
@CacheEvict(value = "products", key = "#productId")
public void updateProduct(Product product) {
productMapper.updateById(product);
}
在实际开发中,我特别推荐使用Swagger进行API文档管理。通过集成SpringDoc OpenAPI,可以自动生成实时更新的API文档,前后端协作效率提升明显。配置如下:
java复制@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI fruitStoreOpenAPI() {
return new OpenAPI()
.info(new Info().title("精品水果商城API")
.description("前后端分离精品水果电商平台")
.version("v1.0.0"))
.externalDocs(new ExternalDocumentation()
.description("项目Wiki")
.url("https://github.com/yourrepo/wiki"));
}
}