1. 项目概述:SpringBoot+Vue网上超市系统全栈实践
去年接手的一个社区超市数字化转型项目,让我对SpringBoot+Vue的全栈组合有了全新认识。这个看似普通的网上超市系统,实际开发中涉及的技术栈深度远超预期——从商品秒杀的压力测试到会员体系的积分规则设计,每个环节都需要前后端的精密配合。本文将基于实际交付的商用系统,拆解从零搭建一个高可用网上超市的核心技术实现,包含源码结构解析、数据库设计陷阱规避以及前后端联调的真实经验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计
2.1 为什么选择SpringBoot+Vue组合
在对比了PHP+Laravel和Python+Django方案后,最终选择Java+SpringBoot作为后端核心。三个关键决策因素:
- 商品库存的并发控制需要强事务支持(@Transactional注解的隔离级别调优)
- 订单模块与第三方支付(微信/支付宝)的SDK集成成熟度
- 后期扩展会员营销系统时的多线程处理能力
前端选用Vue 2.x而非React的考量:
- 超市运营人员使用的后台管理系统存在大量表单操作,v-model双向绑定开发效率更高
- 需要快速实现商品分类的多级联动组件(递归组件+keep-alive优化)
- 移动端H5页面与小程序共享API层(axios实例封装)
2.2 系统分层架构图解
code复制[用户层]
├── 微信小程序
├── H5移动端
└── PC管理后台
[表现层]
├── Vue CLI构建的SPA
└── Thymeleaf服务端模板(用于SEO商品页)
[业务层]
├── SpringBoot RESTful API
│ ├── 商品服务(SKU/SPU分离设计)
│ ├── 订单服务(状态机设计模式)
│ └── 会员服务(分级积分体系)
└── Spring Security OAuth2
[数据层]
├── MySQL 8.0(主从分离)
├── Redis(购物车+秒杀缓存)
└── Elasticsearch(商品检索)
3. 核心业务模块实现
3.1 商品系统的防超卖设计
在实现"限时秒杀"功能时,遇到过典型的库存超卖问题。最终采用分布式锁+Redis原子操作的混合方案:
java复制// 商品服务中的扣减库存方法
@Transactional
public boolean reduceStock(Long skuId, Integer quantity) {
// 第一层:Redis原子递减
Long remain = redisTemplate.opsForValue()
.increment("stock:"+skuId, -quantity.longValue());
if (remain < 0) {
redisTemplate.opsForValue()
.increment("stock:"+skuId, quantity.longValue());
throw new BusinessException("库存不足");
}
// 第二层:数据库乐观锁
int affected = productMapper.updateStock(
skuId,
quantity,
LocalDateTime.now() // 版本号替代方案
);
if (affected == 0) {
// 补偿Redis操作
redisTemplate.opsForValue()
.increment("stock:"+skuId, quantity.longValue());
throw new ConcurrentUpdateException("请重试");
}
return true;
}
3.2 订单状态机的实践陷阱
初期使用简单的if-else处理订单流转,导致后期新增"部分退货"状态时出现逻辑混乱。重构后采用状态机模式:
java复制// 订单状态枚举定义
public enum OrderStatus {
UNPAID(1, "待支付") {
@Override
public boolean canChangeTo(OrderStatus next) {
return next == PAID || next == CANCELLED;
}
},
PAID(2, "已支付") {
@Override
public boolean canChangeTo(OrderStatus next) {
return next == SHIPPED || next == REFUNDING;
}
};
// 其他状态省略...
}
// 状态变更服务
public class OrderStateMachine {
public void changeStatus(Order order, OrderStatus newStatus) {
if (!order.getStatus().canChangeTo(newStatus)) {
throw new IllegalStateException("非法状态变更");
}
// 触发关联操作(如支付完成时生成发货单)
handleStateChangeHook(order, newStatus);
}
}
4. 前后端协同开发要点
4.1 接口契约管理
使用Swagger UI + YApi的双重保障:
- 后端通过SpringFox生成Swagger文档
- 前端根据YApi的Mock数据并行开发
- 接口变更时通过YApi的变更通知机制同步
特别要注意枚举值的协同:
typescript复制// 前端定义与后端一致的枚举
export const OrderStatus = {
UNPAID: { value: 1, text: '待支付' },
PAID: { value: 2, text: '已支付' }
} as const;
// 类型守卫函数
export function isOrderStatus(value: number): value is OrderStatus {
return Object.values(OrderStatus)
.map(item => item.value)
.includes(value);
}
4.2 文件上传的坑与解决方案
商品图片上传时遇到三个典型问题:
- 大文件分片上传(使用vue-simple-uploader)
- 图片格式转换(后端使用Thumbnailator)
- OSS直传的安全策略(临时Token生成)
前端关键代码:
vue复制<template>
<uploader
:options="options"
:file-status-text="statusText"
@file-complete="handleComplete"
>
<uploader-unsupport></uploader-unsupport>
<uploader-drop>
<p>拖拽文件到此处</p>
<uploader-btn>选择文件</uploader-btn>
</uploader-drop>
</uploader>
</template>
<script>
export default {
data() {
return {
options: {
target: '/api/upload/chunk',
chunkSize: 2 * 1024 * 1024,
testChunks: false,
headers: {
'X-Auth-Token': this.getSTSToken()
}
}
}
}
}
</script>
5. 性能优化实战记录
5.1 数据库查询优化案例
商品列表页的N+1查询问题:
sql复制-- 反例:执行1次查询商品 + N次查询分类
SELECT * FROM product WHERE status = 1;
-- 正例:单次联合查询
SELECT p.*, c.name as category_name
FROM product p
LEFT JOIN category c ON p.category_id = c.id
WHERE p.status = 1;
配合MyBatis二级缓存配置:
xml复制<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<mapper namespace="com.supermarket.mapper.ProductMapper">
<cache eviction="LRU" flushInterval="60000"/>
</mapper>
5.2 前端懒加载的临界点计算
通过Chrome DevTools的Coverage工具分析发现:
- 首屏加载的JS利用率仅43%
- 商品详情页的富文本编辑器占用200KB+
优化方案:
- 路由级代码分割
javascript复制const ProductDetail = () => import('./views/ProductDetail.vue');
- 组件异步加载
vue复制<template>
<div>
<async-rich-text-editor v-if="showEditor"/>
</div>
</template>
<script>
export default {
components: {
AsyncRichTextEditor: () => import('./RichTextEditor.vue')
}
}
</script>
6. 部署与监控体系建设
6.1 Docker Compose编排实践
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PWD}
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
redis:
image: redis:6-alpine
command: redis-server --appendonly yes
volumes:
- ./redis/data:/data
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_PROFILES_ACTIVE: prod
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
6.2 Prometheus监控指标暴露
SpringBoot Actuator配置:
properties复制management.endpoints.web.exposure.include=*
management.metrics.tags.application=${spring.application.name}
自定义业务指标:
java复制@RestController
public class OrderController {
private final Counter orderCounter;
public OrderController(MeterRegistry registry) {
this.orderCounter = registry.counter("order.create.total");
}
@PostMapping("/orders")
public Order createOrder() {
orderCounter.increment();
// 创建订单逻辑
}
}
7. 典型问题排查手册
7.1 Vuex状态丢失之谜
现象:用户刷新页面后购物车数据清空
根因:Vuex默认内存存储,刷新即丢失
解决方案组合:
- 持久化插件vuex-persistedstate
- 服务端渲染SSR方案
- 本地缓存兜底策略
javascript复制// store/index.js
import createPersistedState from 'vuex-persistedstate'
export default new Vuex.Store({
plugins: [
createPersistedState({
storage: window.sessionStorage,
paths: ['cart']
})
]
})
7.2 SpringBoot跨域的血泪史
开发阶段遇到的CORS问题排查流程:
- 确认是简单请求还是预检请求(OPTIONS)
- 检查@CrossOrigin注解的位置(类级/方法级)
- 测试全局配置是否生效
最终采用的全局配置:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.maxAge(3600)
.allowedHeaders("*")
.exposedHeaders("Authorization");
}
}
8. 源码结构与开发规范
8.1 后端项目布局
code复制src/main/java
├── com.supermarket
│ ├── config # 配置类
│ ├── controller # 分层结构
│ ├── service
│ │ ├── impl # 实现隔离
│ ├── dao
│ ├── entity
│ ├── dto # 数据传输对象
│ ├── exception # 异常体系
│ └── util # 工具类
resources
├── mapper # MyBatis映射文件
├── static # 静态资源
└── application.yml # 多环境配置
8.2 前端项目规范
- 组件命名采用PascalCase
- API请求层统一管理
- 样式使用Scoped CSS
- TypeScript接口定义与后端同步
code复制src/
├── api/ # API模块化
│ ├── product.ts
│ └── order.ts
├── types/ # 类型定义
│ ├── response.ts
│ └── enum.ts
└── views/
├── product/ # 业务模块
│ ├── components/
│ │ ├── SkuSelector.vue
│ ├── ProductList.vue
9. 数据库设计精要
9.1 商品核心表关系
sql复制CREATE TABLE `product` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`spu_id` BIGINT COMMENT '标准产品单位',
`name` VARCHAR(100) NOT NULL,
`price` DECIMAL(10,2) UNSIGNED NOT NULL,
`stock` INT UNSIGNED DEFAULT 0,
`status` TINYINT DEFAULT 1 COMMENT '1-上架 0-下架',
PRIMARY KEY (`id`),
INDEX `idx_spu` (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `product_sku` (
`id` BIGINT NOT NULL,
`product_id` BIGINT NOT NULL,
`specs` JSON COMMENT '规格键值对',
PRIMARY KEY (`id`),
FOREIGN KEY (`product_id`) REFERENCES `product` (`id`)
) ENGINE=InnoDB;
9.2 订单分表策略
按用户ID哈希分表(16张物理表):
java复制// 分表路由组件
public class OrderTableRouter {
public static String getTableSuffix(Long userId) {
return String.format("%02x", userId % 16);
}
}
// MyBatis拦截器实现
@Intercepts({
@Signature(type= StatementHandler.class,
method="prepare",
args={Connection.class, Integer.class})
})
public class TableSplitInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) {
// 解析SQL替换表名
String newSql = sql.replace("order",
"order_" + getTableSuffix(userId));
// 修改BoundSql
}
}
10. 文档体系构建经验
10.1 接口文档自动化
Swagger增强配置:
java复制@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.supermarket"))
.paths(PathSelectors.any())
.build()
.securitySchemes(securitySchemes());
}
}
10.2 数据库变更管理
Liquibase的使用规范:
xml复制<databaseChangeLog>
<changeSet id="1" author="dev">
<createTable tableName="product">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
在项目根目录建立changelog目录,按日期组织变更集:
code复制changelog/
├── 2023/
│ ├── 01-product-init.xml
│ └── 02-order-table.xml
└── master.xml
