1. 项目概述
作为一个从业多年的Java全栈开发者,我最近完成了一个网上宠物店系统的开发项目。这个项目采用了当前主流的技术栈:SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0,实现了完整的电商功能闭环。在开发过程中,我积累了不少实战经验,今天就来详细分享这个项目的技术实现细节。
宠物电商行业近年来发展迅猛,根据我的市场调研,线上宠物用品销售额年增长率保持在30%以上。但很多现有的宠物电商系统要么功能单一,要么技术架构陈旧。我们这个项目就是为了解决这些问题而设计的,它不仅包含了完整的电商功能模块,还特别针对宠物行业的特性做了优化。
2. 技术选型与架构设计
2.1 后端技术栈
选择SpringBoot2作为后端框架主要基于以下几个考虑:
- 快速开发:SpringBoot的自动配置和起步依赖大大减少了配置工作量
- 生态丰富:Spring生态拥有完善的解决方案
- 性能稳定:经过大量生产环境验证
MyBatis-Plus的选择理由:
- 简化了MyBatis的使用,提供了很多开箱即用的功能
- 强大的条件构造器,可以避免手写复杂SQL
- 内置分页插件,简化分页查询实现
数据库选用MySQL8.0是因为:
- 新版本在JSON支持、窗口函数等方面有显著改进
- 性能比5.7版本提升明显
- 对事务的支持更加完善
2.2 前端技术栈
Vue3相比Vue2的主要优势:
- Composition API提供了更好的代码组织方式
- 更好的TypeScript支持
- 性能提升,特别是虚拟DOM的优化
- 更小的打包体积
Element Plus作为UI框架的选择理由:
- 专为Vue3设计
- 组件丰富,满足电商系统需求
- 主题定制方便
- 社区活跃,文档完善
2.3 系统架构设计
我们采用前后端分离架构,具体分工如下:
后端职责:
- 提供RESTful API接口
- 业务逻辑处理
- 数据持久化
- 权限控制
- 支付对接
前端职责:
- 用户界面展示
- 用户交互处理
- 数据可视化
- 本地状态管理
这种架构的优势在于:
- 前后端可以并行开发
- 职责分离,代码更易维护
- 可以针对不同平台(Web/App)提供统一API
- 更利于团队协作
3. 数据库设计与实现
3.1 核心表结构
用户表(user)
sql复制CREATE TABLE `user` (
`user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_account` varchar(50) NOT NULL COMMENT '登录账号',
`user_password` varchar(100) NOT NULL COMMENT '登录密码',
`user_nickname` varchar(50) DEFAULT NULL COMMENT '用户昵称',
`user_phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`user_email` varchar(50) DEFAULT NULL COMMENT '邮箱',
`register_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_account` (`user_account`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
商品表(product)
sql复制CREATE TABLE `product` (
`product_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`product_name` varchar(100) NOT NULL COMMENT '商品名称',
`product_price` decimal(10,2) NOT NULL COMMENT '商品价格',
`product_stock` int NOT NULL COMMENT '商品库存',
`product_desc` text COMMENT '商品描述',
`category_id` bigint NOT NULL COMMENT '分类ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`product_id`),
KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';
订单表(order)
sql复制CREATE TABLE `order` (
`order_id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_number` varchar(50) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`order_quantity` int NOT NULL COMMENT '购买数量',
`order_amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`order_status` varchar(20) NOT NULL COMMENT '订单状态',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
PRIMARY KEY (`order_id`),
UNIQUE KEY `idx_order_number` (`order_number`),
KEY `idx_user` (`user_id`),
KEY `idx_product` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单信息表';
3.2 数据库优化实践
- 索引设计:
- 为所有外键字段添加索引
- 为高频查询条件字段添加索引
- 使用联合索引减少回表操作
- 字段类型选择:
- 金额使用DECIMAL而不是FLOAT,避免精度问题
- 文本内容根据长度选择VARCHAR或TEXT
- 时间字段使用DATETIME而不是TIMESTAMP
- 分表策略:
- 用户表按user_id范围分表
- 订单表按创建时间分表
- 商品表按分类分表
4. 后端核心功能实现
4.1 用户模块
注册接口实现
java复制@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(@RequestBody UserRegisterDTO dto) {
// 参数校验
if (StringUtils.isEmpty(dto.getAccount()) ||
StringUtils.isEmpty(dto.getPassword())) {
return Result.fail("账号密码不能为空");
}
// 密码加密
String encryptedPwd = PasswordUtil.encrypt(dto.getPassword());
// 构建用户实体
User user = new User();
user.setUserAccount(dto.getAccount());
user.setUserPassword(encryptedPwd);
user.setUserNickname(dto.getNickname());
user.setUserPhone(dto.getPhone());
user.setUserEmail(dto.getEmail());
// 保存用户
boolean success = userService.save(user);
return success ? Result.success() : Result.fail("注册失败");
}
}
登录接口实现
java复制@PostMapping("/login")
public Result login(@RequestBody UserLoginDTO dto) {
// 参数校验
if (StringUtils.isEmpty(dto.getAccount()) ||
StringUtils.isEmpty(dto.getPassword())) {
return Result.fail("账号密码不能为空");
}
// 查询用户
User user = userService.lambdaQuery()
.eq(User::getUserAccount, dto.getAccount())
.one();
// 用户不存在
if (user == null) {
return Result.fail("用户不存在");
}
// 密码校验
if (!PasswordUtil.matches(dto.getPassword(), user.getUserPassword())) {
return Result.fail("密码错误");
}
// 生成token
String token = JwtUtil.generateToken(user.getUserId());
// 更新最后登录时间
user.setLastLoginTime(LocalDateTime.now());
userService.updateById(user);
// 返回结果
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("userInfo", user);
return Result.success(data);
}
4.2 商品模块
商品分页查询
java复制@GetMapping("/page")
public Result page(ProductQueryDTO query) {
// 构建查询条件
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(query.getKeyword()),
Product::getProductName, query.getKeyword())
.eq(query.getCategoryId() != null,
Product::getCategoryId, query.getCategoryId())
.orderByDesc(Product::getCreateTime);
// 执行分页查询
Page<Product> page = new Page<>(query.getPageNum(), query.getPageSize());
productService.page(page, wrapper);
// 返回结果
return Result.success(page);
}
商品详情缓存实现
java复制@Cacheable(value = "product", key = "#id")
public Product getByIdWithCache(Long id) {
return getById(id);
}
@CachePut(value = "product", key = "#product.productId")
public Product updateWithCache(Product product) {
updateById(product);
return product;
}
@CacheEvict(value = "product", key = "#id")
public void deleteWithCache(Long id) {
removeById(id);
}
5. 前端核心功能实现
5.1 商品列表页
vue复制<template>
<div class="product-list">
<el-row :gutter="20">
<el-col :span="6" v-for="product in products" :key="product.productId">
<el-card :body-style="{ padding: '0px' }">
<img :src="product.image" class="product-image" />
<div style="padding: 14px;">
<span>{{ product.productName }}</span>
<div class="bottom">
<span class="price">¥{{ product.productPrice }}</span>
<el-button
type="text"
class="button"
@click="addToCart(product)"
>加入购物车</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-pagination
@current-change="handlePageChange"
:current-page="query.pageNum"
:page-size="query.pageSize"
layout="total, prev, pager, next"
:total="total">
</el-pagination>
</div>
</template>
<script>
import { getProductPage } from '@/api/product'
export default {
data() {
return {
products: [],
total: 0,
query: {
pageNum: 1,
pageSize: 8,
keyword: '',
categoryId: null
}
}
},
created() {
this.loadProducts()
},
methods: {
async loadProducts() {
const res = await getProductPage(this.query)
this.products = res.data.records
this.total = res.data.total
},
handlePageChange(page) {
this.query.pageNum = page
this.loadProducts()
},
addToCart(product) {
this.$store.dispatch('cart/addItem', product)
this.$message.success('已加入购物车')
}
}
}
</script>
5.2 购物车功能实现
vue复制<template>
<div class="cart-container">
<el-table
:data="cartItems"
style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
label="商品"
width="400">
<template #default="{row}">
<div class="product-info">
<img :src="row.image" class="product-image" />
<span>{{ row.productName }}</span>
</div>
</template>
</el-table-column>
<el-table-column
prop="price"
label="单价"
width="120">
</el-table-column>
<el-table-column
label="数量"
width="150">
<template #default="{row}">
<el-input-number
v-model="row.quantity"
:min="1"
:max="row.stock"
@change="updateQuantity(row)">
</el-input-number>
</template>
</el-table-column>
<el-table-column
label="小计"
width="120">
<template #default="{row}">
¥{{ (row.price * row.quantity).toFixed(2) }}
</template>
</el-table-column>
<el-table-column
label="操作"
width="100">
<template #default="{row}">
<el-button
type="text"
@click="removeItem(row.productId)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="cart-footer">
<div class="total">
总计: ¥{{ totalAmount.toFixed(2) }}
</div>
<el-button
type="primary"
:disabled="selectedItems.length === 0"
@click="checkout">
结算({{ selectedItems.length }})
</el-button>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapGetters('cart', ['cartItems', 'selectedItems', 'totalAmount'])
},
methods: {
...mapActions('cart', [
'updateItemQuantity',
'removeItem',
'toggleItemSelection',
'checkout'
]),
handleSelectionChange(selection) {
this.toggleItemSelection(selection)
},
updateQuantity(item) {
this.updateItemQuantity({
productId: item.productId,
quantity: item.quantity
})
}
}
}
</script>
6. 系统部署与运维
6.1 后端部署
使用Docker部署SpringBoot应用:
dockerfile复制# 基础镜像
FROM openjdk:11-jre-slim
# 维护者信息
LABEL maintainer="yourname@example.com"
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 创建应用目录
RUN mkdir -p /app
WORKDIR /app
# 复制JAR文件
COPY target/pet-store-0.0.1-SNAPSHOT.jar /app/app.jar
# 暴露端口
EXPOSE 8080
# 启动命令
ENTRYPOINT ["java", "-jar", "app.jar"]
构建并运行容器:
bash复制docker build -t pet-store .
docker run -d -p 8080:8080 --name pet-store pet-store
6.2 前端部署
Nginx配置示例:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
index index.html;
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;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# 静态资源缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}
6.3 数据库部署
MySQL容器化部署:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=yourpassword \
-e MYSQL_DATABASE=pet_store \
-v /data/mysql:/var/lib/mysql \
mysql:8.0 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci
7. 项目开发经验总结
7.1 技术难点与解决方案
- 购物车并发问题:
- 问题:高并发下商品库存可能出现超卖
- 解决方案:使用Redis分布式锁 + 乐观锁
java复制public boolean decreaseStock(Long productId, int quantity) {
String lockKey = "product_stock_lock:" + productId;
String requestId = UUID.randomUUID().toString();
try {
// 获取分布式锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);
if (!locked) {
return false;
}
// 查询商品
Product product = productMapper.selectById(productId);
if (product == null || product.getProductStock() < quantity) {
return false;
}
// 更新库存
int rows = productMapper.updateStock(productId, quantity, product.getVersion());
return rows > 0;
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
- 订单支付超时处理:
- 问题:用户下单后未支付占用库存
- 解决方案:使用延迟队列自动取消超时订单
java复制@RabbitListener(queues = "order.delay.queue")
public void processExpiredOrder(Order order) {
// 检查订单状态
Order current = orderService.getById(order.getOrderId());
if (current != null && "待支付".equals(current.getOrderStatus())) {
// 取消订单
orderService.cancelOrder(order.getOrderId(), "超时未支付");
}
}
7.2 性能优化实践
- 接口响应优化:
- 启用Gzip压缩
- 使用@JsonView控制返回字段
- 分页查询优化
- 数据库查询优化:
- 合理使用索引
- 避免SELECT *
- 使用JOIN替代多次查询
- 前端性能优化:
- 路由懒加载
- 组件按需引入
- 图片懒加载
7.3 项目扩展方向
- 移动端适配:开发微信小程序版本
- 推荐系统:基于用户行为实现个性化推荐
- 社交功能:增加宠物社区和用户互动
- 智能客服:集成AI客服解答常见问题
这个项目从技术选型到最终上线历时3个月,期间遇到了不少挑战,但也收获了很多宝贵的经验。特别是对于电商系统的高并发场景处理,有了更深入的理解。希望我的分享能对正在开发类似项目的同学有所帮助。