1. 项目背景与需求分析
本庄村果园预售系统是一个典型的农产品电商平台,旨在解决当地果农面临的销售渠道单一、库存管理混乱、订单处理效率低下等问题。系统采用SpringBoot+Vue的前后端分离架构,结合MySQL数据库和MyBatis持久层框架,为果园管理者提供从产品上架到订单履约的全流程数字化解决方案。
1.1 农业电商的特殊性
农产品电商与传统电商相比具有显著差异:
- 季节性波动:需要支持产品预售和定期配送功能
- 库存动态性:需实时反映果园实际产量变化
- 物流敏感性:需考虑生鲜产品的配送时效要求
- 用户地域性:主要服务周边社区,需强化本地化运营功能
1.2 核心功能需求
系统需要实现以下核心模块:
- 果园管理:果树种植批次、产量预测、采摘计划
- 预售管理:产品预售、定金支付、尾款结算
- 订单处理:订单自动分配、采摘任务下发
- 配送管理:配送路线规划、配送状态跟踪
- 数据分析:销售趋势分析、用户购买行为分析
2. 技术架构设计
2.1 整体架构方案
采用前后端分离架构:
code复制前端:Vue 3 + Element Plus + Axios
后端:SpringBoot 2.7 + MyBatis Plus
数据库:MySQL 8.0
部署:Nginx + Docker
2.1.1 技术选型理由
- Vue 3:组合式API更适合复杂业务逻辑管理
- Element Plus:提供丰富的农业电商UI组件(如日历选择器用于采摘日期选择)
- SpringBoot:快速构建微服务,内置Tomcat简化部署
- MyBatis Plus:增强的CRUD操作减少基础代码量
2.2 数据库设计要点
2.2.1 关键表结构
sql复制-- 产品预售表
CREATE TABLE `product_pre_sale` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` bigint NOT NULL COMMENT '关联果园产品',
`pre_sale_price` decimal(10,2) NOT NULL COMMENT '预售单价',
`start_time` datetime NOT NULL COMMENT '预售开始时间',
`end_time` datetime NOT NULL COMMENT '预售结束时间',
`estimated_delivery` date NOT NULL COMMENT '预计配送日期',
`max_quantity` int DEFAULT NULL COMMENT '最大可预售量',
`deposit_rate` decimal(5,2) DEFAULT '0.30' COMMENT '定金比例',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 订单状态流转表
CREATE TABLE `order_status_flow` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_id` bigint NOT NULL,
`from_status` varchar(20) NOT NULL,
`to_status` varchar(20) NOT NULL,
`operator` varchar(50) DEFAULT NULL COMMENT '操作人',
`operate_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
INDEX `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.2.2 农业数据特殊处理
- 产品表增加
ripeness_degree字段记录成熟度 - 订单表增加
preferred_delivery_window字段记录配送时段偏好 - 使用空间数据类型存储果园地块位置信息
3. 核心功能实现
3.1 预售业务流程实现
3.1.1 前端关键代码
vue复制<template>
<el-form :model="preSaleForm" label-width="120px">
<el-form-item label="选择产品">
<el-select
v-model="preSaleForm.productId"
filterable
@change="handleProductChange"
>
<el-option
v-for="item in productOptions"
:key="item.id"
:label="`${item.name} (剩余:${item.availableStock}${item.unit})`"
:value="item.id"
:disabled="item.availableStock <= 0"
/>
</el-select>
</el-form-item>
<el-form-item label="预计配送日">
<el-date-picker
v-model="preSaleForm.deliveryDate"
type="date"
:disabled-date="disabledDate"
placeholder="选择配送日期"
/>
</el-form-item>
<el-form-item label="预售数量">
<el-input-number
v-model="preSaleForm.quantity"
:min="1"
:max="maxAvailable"
/>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
preSaleForm: {
productId: '',
deliveryDate: '',
quantity: 1
},
productOptions: [],
maxAvailable: 0
}
},
methods: {
// 禁用非采摘季的日期
disabledDate(time) {
return time.getTime() < Date.now() ||
!this.isHarvestSeason(time);
},
// 获取产品可售库存
async handleProductChange(productId) {
const res = await getProductStock(productId);
this.maxAvailable = res.data.availableStock;
}
}
}
</script>
3.1.2 后端支付处理逻辑
java复制@RestController
@RequestMapping("/api/order")
public class OrderController {
@PostMapping("/createPreOrder")
public Result createPreOrder(@Valid @RequestBody PreOrderDTO dto) {
// 1. 库存预占检查
Product product = productService.checkStock(dto.getProductId(), dto.getQuantity());
// 2. 计算定金金额(按比例或固定值)
BigDecimal depositAmount = calculateDeposit(product, dto);
// 3. 创建预订单(状态为待支付定金)
PreOrder order = new PreOrder();
order.setOrderNo(generateOrderNo());
order.setProductId(dto.getProductId());
order.setQuantity(dto.getQuantity());
order.setDepositAmount(depositAmount);
order.setStatus(OrderStatus.WAIT_DEPOSIT);
// 4. 保存订单并返回支付信息
orderService.save(order);
return Result.success(paymentService.createPaymentRequest(order));
}
private BigDecimal calculateDeposit(Product product, PreOrderDTO dto) {
// 农产品特殊逻辑:早鸟优惠
if (isEarlyBirdPeriod()) {
return product.getPreSalePrice()
.multiply(new BigDecimal(dto.getQuantity()))
.multiply(new BigDecimal("0.2")); // 早鸟只需20%定金
}
return product.getPreSalePrice()
.multiply(new BigDecimal(dto.getQuantity()))
.multiply(product.getDepositRate());
}
}
3.2 采摘任务分配算法
java复制public class HarvestTaskAllocator {
/**
* 智能分配采摘任务
* @param orderIds 待分配订单ID集合
* @return 分配结果(工人ID -> 采摘任务列表)
*/
public Map<Long, List<HarvestTask>> allocate(Set<Long> orderIds) {
// 1. 获取订单对应的产品位置信息
List<OrderProductLocation> locations =
orderDao.selectProductLocations(orderIds);
// 2. 按果园区块分组
Map<String, List<OrderProductLocation>> areaMap = locations.stream()
.collect(Collectors.groupingBy(OrderProductLocation::getOrchardArea));
// 3. 获取可用工人及其技能评级
List<Worker> availableWorkers =
workerDao.selectAvailableWorkers();
// 4. 基于贪心算法分配任务
Map<Long, List<HarvestTask>> allocation = new HashMap<>();
for (Map.Entry<String, List<OrderProductLocation>> entry : areaMap.entrySet()) {
String area = entry.getKey();
List<OrderProductLocation> areaOrders = entry.getValue();
// 按工人到区块的距离+技能评分排序
availableWorkers.sort(Comparator.comparingInt(
w -> calculateSuitabilityScore(w, area)));
// 轮询分配任务
int workerIndex = 0;
for (OrderProductLocation opl : areaOrders) {
Worker worker = availableWorkers.get(
workerIndex % availableWorkers.size());
allocation.computeIfAbsent(worker.getId(), k -> new ArrayList<>())
.add(createHarvestTask(opl));
workerIndex++;
}
}
return allocation;
}
private int calculateSuitabilityScore(Worker worker, String area) {
// 综合考量:距离系数(40%) + 技能评分(30%) + 当前负载(30%)
int distanceScore = calculateDistanceScore(worker, area);
int skillScore = worker.getSkillLevel(area);
int loadScore = 100 - worker.getCurrentLoad();
return (int)(distanceScore*0.4 + skillScore*0.3 + loadScore*0.3);
}
}
4. 系统特色功能实现
4.1 农产品溯源模块
4.1.1 区块链存证设计
java复制public class ProductTraceService {
@Autowired
private BlockchainClient blockchainClient;
/**
* 记录种植关键事件
*/
public void recordPlantingEvent(PlantingEvent event) {
String txHash = blockchainClient.sendTransaction(
"Planting",
Map.of(
"productBatch", event.getBatchNo(),
"plantingDate", event.getPlantingDate(),
"location", event.getLocation(),
"fertilizer", event.getFertilizerType()
));
traceDao.save(
new ProductTrace(
event.getBatchNo(),
"PLANTING",
txHash,
LocalDateTime.now()
));
}
/**
* 生成溯源二维码内容
*/
public String generateTraceQrContent(String batchNo) {
List<TraceRecord> records = traceDao.selectByBatch(batchNo);
return records.stream()
.map(r -> {
String txUrl = blockchainClient.getTxExplorerUrl(r.getTxHash());
return String.format("%s: %s | 验证: %s",
r.getEventType(),
r.getEventTime(),
txUrl);
})
.collect(Collectors.joining("\n"));
}
}
4.1.2 前端溯源展示
vue复制<template>
<el-card class="trace-card">
<div slot="header" class="clearfix">
<span>产品溯源信息</span>
<el-button
style="float: right; padding: 3px 0"
type="text"
@click="refreshTrace"
>
刷新数据
</el-button>
</div>
<el-timeline>
<el-timeline-item
v-for="(event, index) in traceEvents"
:key="index"
:timestamp="formatDate(event.eventTime)"
placement="top"
>
<el-card>
<h4>{{ eventTypeMap[event.eventType] }}</h4>
<p>{{ event.eventDescription }}</p>
<el-link
type="primary"
:href="event.txExplorerUrl"
target="_blank"
>
区块链验证
</el-link>
</el-card>
</el-timeline-item>
</el-timeline>
</el-card>
</template>
4.2 智能配送路线规划
4.2.1 基于GIS的路线算法
java复制public class DeliveryRoutePlanner {
/**
* 生成最优配送路线
* @param deliveryTasks 待配送任务列表
* @param warehouseLocation 仓库位置
* @return 排序后的配送任务序列
*/
public List<DeliveryTask> planRoute(
List<DeliveryTask> deliveryTasks,
Point warehouseLocation) {
// 1. 构建地点距离矩阵
double[][] distanceMatrix = buildDistanceMatrix(
warehouseLocation, deliveryTasks);
// 2. 使用遗传算法求解TSP问题
GeneticAlgorithm ga = new GeneticAlgorithm(50, 0.01, 1000);
int[] bestRoute = ga.solveTSP(distanceMatrix);
// 3. 按最优序列重排任务
List<DeliveryTask> optimizedRoute = new ArrayList<>();
for (int i = 1; i < bestRoute.length; i++) { // 跳过0(仓库)
optimizedRoute.add(deliveryTasks.get(bestRoute[i]-1));
}
return optimizedRoute;
}
private double[][] buildDistanceMatrix(
Point warehouse,
List<DeliveryTask> tasks) {
int size = tasks.size() + 1;
double[][] matrix = new double[size][size];
// 填充仓库到各点的距离
for (int i = 0; i < tasks.size(); i++) {
matrix[0][i+1] = calculateDistance(
warehouse, tasks.get(i).getDeliveryLocation());
matrix[i+1][0] = matrix[0][i+1];
}
// 填充点与点之间的距离
for (int i = 0; i < tasks.size(); i++) {
for (int j = i+1; j < tasks.size(); j++) {
double dist = calculateDistance(
tasks.get(i).getDeliveryLocation(),
tasks.get(j).getDeliveryLocation());
matrix[i+1][j+1] = dist;
matrix[j+1][i+1] = dist;
}
}
return matrix;
}
private double calculateDistance(Point p1, Point p2) {
// 使用Haversine公式计算球面距离
double lat1 = p1.getLat();
double lon1 = p1.getLon();
double lat2 = p2.getLat();
double lon2 = p2.getLon();
final int R = 6371; // 地球半径(km)
double latDistance = Math.toRadians(lat2 - lat1);
double lonDistance = Math.toRadians(lon2 - lon1);
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
* Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
}
4.2.2 配送状态实时更新
java复制@RestController
@RequestMapping("/api/delivery")
public class DeliveryController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@PostMapping("/updateStatus")
public Result updateDeliveryStatus(
@RequestBody DeliveryStatusUpdateDTO updateDTO) {
// 1. 更新数据库状态
deliveryService.updateStatus(
updateDTO.getDeliveryId(),
updateDTO.getStatus(),
updateDTO.getLocation());
// 2. 推送WebSocket通知
DeliveryStatusMessage msg = new DeliveryStatusMessage();
msg.setDeliveryId(updateDTO.getDeliveryId());
msg.setStatus(updateDTO.getStatus());
msg.setTimestamp(LocalDateTime.now());
msg.setLocation(updateDTO.getLocation());
messagingTemplate.convertAndSend(
"/topic/delivery/" + updateDTO.getOrderId(),
msg);
return Result.success();
}
}
5. 部署与性能优化
5.1 生产环境部署方案
5.1.1 容器化部署配置
dockerfile复制# SpringBoot服务Dockerfile
FROM openjdk:17-jdk-slim
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
# Nginx配置片段
upstream backend {
server springboot:8080;
keepalive 32;
}
server {
listen 80;
server_name orchard.example.com;
location /api {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
5.1.2 数据库优化措施
- 分区表设计:
sql复制-- 按年份分区的订单表
CREATE TABLE `order_info` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL,
`user_id` bigint NOT NULL,
`total_amount` decimal(10,2) NOT NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`, `create_time`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (YEAR(create_time)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
- 缓存策略:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return cacheManager;
}
@Bean
public KeyGenerator productKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append("product_");
if (params.length > 0 && params[0] instanceof Long) {
sb.append(params[0]);
}
return sb.toString();
};
}
}
5.2 高并发场景应对
5.2.1 秒杀功能实现
java复制@Service
public class FlashSaleService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private ProductStockDao stockDao;
@Transactional
public Result handleFlashSale(Long userId, Long productId) {
// 1. 分布式锁防止超卖
RLock lock = redissonClient.getLock(
"flash_sale:" + productId);
try {
boolean locked = lock.tryLock(1, 10, TimeUnit.SECONDS);
if (!locked) {
return Result.fail("系统繁忙,请重试");
}
// 2. 校验库存(使用Redis原子操作)
String stockKey = "product_stock:" + productId;
long stock = redissonClient.getAtomicLong(stockKey).decrementAndGet();
if (stock < 0) {
// 库存不足回滚
redissonClient.getAtomicLong(stockKey).incrementAndGet();
return Result.fail("商品已售罄");
}
// 3. 创建订单(异步处理)
createFlashSaleOrder(userId, productId);
return Result.success();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Result.fail("系统异常");
} finally {
lock.unlock();
}
}
@Async
public void createFlashSaleOrder(Long userId, Long productId) {
// 异步订单处理逻辑
}
}
5.2.2 接口限流配置
java复制@Configuration
public class RateLimitConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor())
.addPathPatterns("/api/flashsale/**");
}
@Bean
public RateLimitInterceptor rateLimitInterceptor() {
return new RateLimitInterceptor(
RateLimiter.create(500)); // 每秒500个请求
}
@Bean
public FilterRegistrationBean<RateLimitFilter> rateLimitFilter() {
FilterRegistrationBean<RateLimitFilter> registration =
new FilterRegistrationBean<>();
registration.setFilter(new RateLimitFilter());
registration.addUrlPatterns("/api/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}
6. 项目总结与扩展方向
在实际开发中,我们发现农业电商系统有几个需要特别注意的关键点:
-
库存准确性:农产品库存需要区分"在田库存"和"可售库存",我们通过引入库存预测模型来解决这个问题:
- 基于历史数据的产量预测
- 天气因素加权计算
- 病虫害影响评估
-
配送时效性:生鲜产品对配送时效要求极高,我们实现了:
- 动态配送区域划分
- 实时交通状况考量
- 温控配送车辆调度
-
系统扩展性建议:
- 增加物联网设备接入(果园传感器数据采集)
- 开发微信小程序扩大用户覆盖面
- 引入机器学习预测最佳采摘时间
一个特别实用的调试技巧:在开发支付模块时,我们使用SpringBoot的@Profile功能创建了模拟支付实现,这样在开发环境可以跳过真实支付流程:
java复制public interface PaymentService {
PaymentResult createPayment(Order order);
}
@Service
@Profile("!prod")
public class MockPaymentService implements PaymentService {
@Override
public PaymentResult createPayment(Order order) {
PaymentResult result = new PaymentResult();
result.setSuccess(true);
result.setPaymentNo("MOCK_" + System.currentTimeMillis());
return result;
}
}
@Service
@Profile("prod")
public class AlipayPaymentService implements PaymentService {
// 真实支付宝实现
}
