1. 项目背景与核心需求
果蔬批发行业作为农产品流通的关键环节,长期以来面临着交易效率低、信息不对称、价格波动大等痛点。传统线下批发模式存在三个典型问题:一是买卖双方需现场看货议价,时间成本高;二是价格透明度不足,中小商户议价能力弱;三是交易记录依赖手工台账,统计分析困难。
这个基于SpringBoot+Vue的果蔬批发系统,正是针对这些行业痛点设计的B2B电商解决方案。系统核心目标是实现三大功能:
- 供应商可在线发布果蔬产品信息(包括品类、规格、产地、价格曲线等)
- 采购商能实时比价并完成在线交易
- 平台提供智能化的库存预警和销售数据分析
实际开发中发现,果蔬类商品相比普通电商商品有特殊属性:需要记录采摘日期、保鲜期、冷链运输要求等字段,这在数据库设计阶段就需要特别注意。
2. 技术架构设计
2.1 整体技术栈选型
采用前后端分离架构,技术组合经过多维度评估:
- 后端:SpringBoot 2.7 + MyBatis-Plus + Redis
- 选择SpringBoot而非传统SSM:简化配置(starter依赖)、内置Tomcat、完善的监控机制(Actuator)
- MyBatis-Plus相比原生MyBatis:提供Lambda查询、自动填充(如create_time字段)、性能分析插件
- 前端:Vue3 + Element Plus + ECharts
- Vue3组合式API更适合复杂业务逻辑开发
- 选用Element Plus而非Ant Design:表单组件更符合国内操作习惯
- 数据库:MySQL 8.0 + Redis 7.0
- MySQL配置了专门的果蔬类目扩展字段(json类型存储动态属性)
- Redis用于秒杀场景和JWT令牌管理
2.2 关键架构决策
交易流程设计:
- 采用预扣库存→支付确认→最终扣减的二级库存机制
- 引入Redis分布式锁防止超卖(Lua脚本实现原子操作)
价格浮动机制:
// 每日价格自动调整算法示例 public BigDecimal calculateDynamicPrice(LocalDate date, BigDecimal basePrice) { // 周末溢价15% if(date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) { return basePrice.multiply(new BigDecimal("1.15")); } // 凌晨时段(0-5点)折扣10% if(LocalTime.now().isBefore(LocalTime.of(5,0))) { return basePrice.multiply(new BigDecimal("0.9")); } return basePrice; }文件存储方案:
- 果蔬检测报告等大文件:MinIO对象存储
- 商品图片:阿里云OSS+CDN加速
- 小尺寸缩略图:Base64直接存数据库
3. 核心功能实现细节
3.1 商品发布模块
采用动态表单技术解决果蔬属性差异问题:
<template> <el-form :model="dynamicForm"> <template v-for="(field,index) in dynamicFields" :key="index"> <el-form-item :label="field.label"> <component :is="field.component" v-model="dynamicForm[field.prop]" v-bind="field.attrs"/> </el-form-item> </template> </el-form> </template> <script setup> // 从后端API获取字段配置 const { data } = await axios.get('/api/commodity/fields?categoryId=1') dynamicFields.value = data.fields </script>数据库设计特别注意:
CREATE TABLE `product_extra_attrs` ( `id` bigint NOT NULL AUTO_INCREMENT, `product_id` bigint NOT NULL COMMENT '关联商品ID', `attr_name` varchar(50) NOT NULL COMMENT '属性名(如甜度等级)', `attr_value` json DEFAULT NULL COMMENT '属性值(支持多种类型)', PRIMARY KEY (`id`), KEY `idx_product` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3.2 交易流程实现
订单状态机设计(使用Spring StateMachine):
@Configuration @EnableStateMachineFactory public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> { @Override public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> states) throws Exception { states.withStates() .initial(OrderState.UNPAID) .states(EnumSet.allOf(OrderState.class)); } @Override public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions) throws Exception { transitions .withExternal() .source(OrderState.UNPAID).target(OrderState.PAID) .event(OrderEvent.PAY_SUCCESS) .and() .withExternal() .source(OrderState.PAID).target(OrderState.DELIVERED) .event(OrderEvent.SHIP) // 其他状态转换... } }3.3 智能预警系统
基于Spring Schedule的库存检查:
@Scheduled(cron = "0 0 9,15 * * ?") // 每天9点和15点执行 public void checkInventory() { List<Product> products = productMapper.selectList(new QueryWrapper<Product>() .lt("stock", "min_stock")); products.forEach(product -> { String message = String.format("商品[%s]库存不足!当前:%d,最低:%d", product.getName(), product.getStock(), product.getMinStock()); // 发送企业微信通知 wechatNotifier.sendAlert(message); }); }4. 性能优化实践
4.1 缓存策略设计
采用多级缓存方案:
本地缓存:Caffeine(高频访问的基础数据)
@Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(1000)); return cacheManager; }分布式缓存:Redis(共享业务数据)
- 商品详情:30分钟过期 + 互斥锁防缓存击穿
- 价格行情:定时刷新 + 版本号控制
前端缓存:localStorage存储用户偏好设置
4.2 数据库优化
针对果蔬批发场景的特殊优化:
- 创建联合索引加速查询:
ALTER TABLE product ADD INDEX idx_category_price (category_id, current_price); - 大文本字段分表存储:
CREATE TABLE product_detail ( product_id BIGINT PRIMARY KEY, description TEXT, storage_condition TEXT, quality_report TEXT ) ENGINE=InnoDB; - 采用定时任务替代实时统计:
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 public void generateDailyReport() { // 生成前一天的交易统计报表 }
5. 典型问题排查实录
5.1 并发下单问题
现象:促销时段出现库存扣减异常排查过程:
- 日志分析发现相同商品存在负库存
- 检查代码发现直接使用:
UPDATE product SET stock=stock-1 WHERE id=? - 未做库存前置校验和并发控制
解决方案:
@Transactional public boolean placeOrder(Long productId, Integer quantity) { // 1. 前置校验(使用SELECT...FOR UPDATE) Product product = productMapper.selectByIdForUpdate(productId); if(product.getStock() < quantity) { return false; } // 2. 扣减库存(原子操作) int updated = productMapper.deductStock(productId, quantity); if(updated == 0) { throw new RuntimeException("库存不足"); } // 3. 创建订单... }5.2 文件上传失败
现象:部分用户上传的果蔬检测报告失败根本原因:
- Nginx默认限制上传大小为1MB
- 前端未做文件大小校验
完整解决方案:
- 前端增加校验:
<template> <el-upload :before-upload="beforeUpload" ...> </el-upload> </template> <script> const beforeUpload = (file) => { if(file.size > 10 * 1024 * 1024) { ElMessage.error('文件大小不能超过10MB') return false } return true } </script>- 后端配置(SpringBoot):
spring: servlet: multipart: max-file-size: 20MB max-request-size: 30MB- Nginx配置调整:
client_max_body_size 20m;6. 部署与运维实践
6.1 容器化部署方案
采用Docker Compose编排:
version: '3.8' services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} volumes: - ./mysql/data:/var/lib/mysql - ./mysql/conf:/etc/mysql/conf.d ports: - "3306:3306" redis: image: redis:7.0-alpine command: redis-server --appendonly yes volumes: - ./redis/data:/data ports: - "6379:6379" backend: build: ./backend ports: - "8080:8080" depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod frontend: build: ./frontend ports: - "80:80"6.2 监控配置
- SpringBoot Actuator配置:
management: endpoints: web: exposure: include: "*" endpoint: health: show-details: always metrics: enabled: true- Prometheus监控指标采集:
@Bean public MeterRegistryCustomizer<PrometheusMeterRegistry> configureMetrics() { return registry -> registry.config().commonTags( "application", "fruit-wholesale-system" ); }- 关键业务指标监控:
- 订单创建成功率
- 平均响应时间(按API分组)
- 商品库存预警阈值
7. 项目演进方向
- 智能定价系统:接入市场价格爬虫,结合供需关系实现动态调价
- 冷链物流跟踪:集成物联网设备数据,实时监控运输温湿度
- 区块链溯源:将农产品检测报告上链,增强可信度
- 移动端适配:开发微信小程序版本,支持现场验货签收
在实现批发核心功能后,可以考虑扩展零售模块,但需要注意批发与零售在定价策略、订单处理流程上的本质差异,建议通过微服务架构进行解耦。