去年参与某连锁餐饮企业的数字化改造时,我发现后厨每天要处理大量未售出的新鲜食材。这些食材往往因为卖相不佳或临近保质期被直接丢弃,而同一时间城市另一端可能有人正为晚餐发愁。这种矛盾促使我设计了这个食物节约盲盒系统——用技术手段搭建供需桥梁,让每一份食物都能物尽其用。
这个毕业设计级别的系统本质上是一个B2C的食品分发平台,但加入了"盲盒"玩法降低用户预期,同时提高商户参与积极性。技术上选择SpringBoot框架,不仅因为其快速开发特性适合学生项目,更因为它能轻松整合支付、地图、消息推送等第三方服务,为后续商业化预留空间。
后端采用经典的SpringBoot+MyBatis组合,数据库使用MySQL 8.0。特别说明几个关键选择:
设计时特别注意了食品行业的特殊性:
java复制// 食品条目
class FoodItem {
Long id;
String name;
String category; // 需符合国家食品分类标准
LocalDateTime productDate;
LocalDateTime expiryDate;
Integer shelfLife; // 小时为单位
String storageCondition; // 冷藏/冷冻/常温
String allergyInfo; // 过敏原信息
}
// 盲盒商品
class MysteryBox {
Long id;
Long merchantId;
BigDecimal originalPrice;
BigDecimal discountPrice;
Integer foodCount; // 包含食品种类数
LocalDateTime pickupStart;
LocalDateTime pickupEnd;
String qrCode; // 核销二维码
}
不是简单按距离排序,而是综合三个维度:
java复制public List<MysteryBox> recommendBoxes(Long userId) {
User user = userDao.selectById(userId);
List<MysteryBox> candidates = boxDao.selectAvailable();
return candidates.stream()
.map(box -> {
double freshness = calculateFreshness(box);
double ecoValue = box.getOriginalPrice().divide(box.getDiscountPrice());
double distance = geoService.getDistance(user.getAddress(), box.getAddress());
box.setRecommendScore(
0.4*freshness + 0.3*ecoValue + 0.3*(1/(distance+1))
);
return box;
})
.sorted(Comparator.comparing(MysteryBox::getRecommendScore).reversed())
.limit(20)
.collect(Collectors.toList());
}
为防止黄牛倒卖,设计双重验证:
sql复制CREATE TABLE verification_log (
id BIGINT PRIMARY KEY,
box_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
qrcode VARCHAR(64) NOT NULL,
verify_time DATETIME NOT NULL,
device_geo POINT NOT NULL,
SPATIAL INDEX(device_geo)
);
虽然毕业设计不要求区块链,但为体现技术前瞻性,使用Hyperledger Fabric简单实现关键数据上链:
yaml复制# fabric链码示例
contract FoodSafety {
async UploadCert(ctx, merchantId, certHash) {
await ctx.stub.putState(`CERT_${merchantId}`, certHash);
}
async VerifyCert(ctx, merchantId) {
return await ctx.stub.getState(`CERT_${merchantId}`);
}
}
通过定时任务检查:
java复制@Scheduled(cron = "0 0/30 * * * ?")
public void checkExpiringFood() {
List<MysteryBox> boxes = boxDao.selectExpiringSoon();
boxes.forEach(box -> {
if(box.getReserved() > 0) {
pushService.sendTemplateMsg(
box.getUserId(),
"您的盲盒即将过期",
"请在" + box.getPickupEnd() + "前领取"
);
}
});
}
初期直接用Date类型导致时区问题:
永远用LocalDateTime替代Date
MySQL字段用DATETIME而非TIMESTAMP
前端传递时间戳而非格式化字符串
测试时发现超卖问题,最终方案:
java复制public boolean reserveBox(Long boxId, Long userId) {
String lockKey = "lock:box:" + boxId;
RLock lock = redissonClient.getLock(lockKey);
try {
if(lock.tryLock(1, 10, TimeUnit.SECONDS)) {
// 真正的库存扣减逻辑
return doReserve(boxId, userId);
}
} finally {
lock.unlock();
}
return false;
}
实际部署时可考虑:
数据库表设计建议预留扩展字段:
sql复制ALTER TABLE mystery_box
ADD COLUMN extend_data JSON COMMENT '扩展字段';
这个项目让我深刻体会到,好的技术方案需要兼顾商业可行性和社会价值。在调试冷链监控模块时,凌晨三点收到温度异常报警的测试短信,那一刻突然理解了什么叫"用代码改变世界"。