1. 项目背景与核心价值
去年参与了一个公益组织的数字化改造项目,他们最头疼的就是捐赠物资的流转效率问题。手工登记、电话核对、Excel表格管理的方式,在疫情这种特殊时期根本扛不住压力。当时我们用SpringBoot+Vue搭建的这套系统,上线后物资处理效率提升了3倍以上。
这种系统本质上解决的是"爱心最后一公里"的难题。当大量捐赠物资涌入时,如何快速登记、智能匹配需求、透明化分配过程,是公益机构最需要的数字化能力。我们设计的系统包含捐赠端、管理端、受赠端三个模块,实现了从物资录入到最终签收的全流程追踪。
2. 技术架构设计
2.1 为什么选择SpringBoot+Vue
后端选SpringBoot主要考虑三点:
- 快速构建RESTful API(公益项目通常开发周期紧)
- 完善的生态(整合MyBatis、Spring Security等组件方便)
- 部署简单(公益组织服务器配置通常不高)
前端用Vue.js是因为:
- 响应式开发效率高(适合动态表单多的场景)
- Element UI组件库完美匹配管理系统需求
- 前后端分离便于多端扩展(后来确实接了微信小程序)
2.2 数据库关键设计
物资表的核心字段设计:
CREATE TABLE `material` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '物资名称', `category` varchar(20) NOT NULL COMMENT '分类(食品/医疗/日用品)', `spec` varchar(100) DEFAULT NULL COMMENT '规格(如200ml/瓶)', `quantity` int NOT NULL COMMENT '数量', `donor_id` bigint NOT NULL COMMENT '捐赠方ID', `storage_location` varchar(100) DEFAULT NULL COMMENT '存放位置', `expire_date` date DEFAULT NULL COMMENT '有效期(食品药品需要)', `status` tinyint DEFAULT '0' COMMENT '状态(0待审核/1可分配/2已分配)', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;特别提醒几个设计要点:
- 物资状态必须用枚举值而非字符串(后续状态机流转方便)
- 规格字段要预留足够长度(实际遇到"50箱*24瓶/箱"这种复杂描述)
- 有效期字段对食品/药品类物资至关重要
3. 核心功能实现
3.1 智能分配算法
最核心的物资分配逻辑在MaterialAllocationService中实现:
public List<AllocationPlan> autoAllocate(List<Material> materials, List<Demand> demands) { // 1. 按物资紧急程度排序(医疗>食品>日用品) materials.sort(Comparator.comparingInt(m -> switch(m.getCategory()) { case "医疗" -> 1; case "食品" -> 2; default -> 3; })); // 2. 需求匹配(相同区域优先) return demands.stream() .filter(d -> !d.isFulfilled()) .map(demand -> { AllocationPlan plan = new AllocationPlan(); plan.setDemand(demand); // 匹配逻辑:同城>同省>全国 materials.stream() .filter(m -> m.getStatus() == MaterialStatus.AVAILABLE) .filter(m -> matchLocation(m, demand)) .forEach(m -> { int allocateQty = Math.min(m.getQuantity(), demand.getRemainQty()); plan.addMaterial(m, allocateQty); m.decreaseQuantity(allocateQty); demand.decreaseRemainQty(allocateQty); }); return plan; }) .collect(Collectors.toList()); }实际运行中发现需要增加两个优化:
- 设置单次最大分配量(避免一个需求占用全部物资)
- 加入人工审核环节(算法无法100%替代人工判断)
3.2 扫码入库实现
采用ZXing生成物资唯一二维码:
public String generateMaterialQRCode(Long materialId) throws WriterException { String content = "MATERIAL:" + materialId; QRCodeWriter writer = new QRCodeWriter(); BitMatrix matrix = writer.encode(content, BarcodeFormat.QR_CODE, 300, 300); ByteArrayOutputStream os = new ByteArrayOutputStream(); MatrixToImageWriter.writeToStream(matrix, "PNG", os); return "data:image/png;base64," + Base64.getEncoder().encodeToString(os.toByteArray()); }前端用vue-qrcode-reader实现扫码:
<template> <qrcode-stream @decode="onDecode" :camera="camera"> <div v-if="loading" class="loading-overlay"> 正在识别二维码... </div> </qrcode-stream> </template> <script> export default { methods: { onDecode(content) { if (!content.startsWith('MATERIAL:')) return; const materialId = content.split(':')[1]; this.$router.push(`/material/detail/${materialId}`); } } } </script>4. 踩坑实录与性能优化
4.1 高并发下的库存扣减
初期直接用JPA的save()方法更新库存,在捐赠高峰期出现了超发问题。最终采用乐观锁方案:
- 物资表增加version字段
ALTER TABLE `material` ADD COLUMN `version` int DEFAULT 0;- 更新时带版本检查
@Transactional public boolean allocateMaterial(Long materialId, int quantity) { Material material = materialRepository.findById(materialId) .orElseThrow(() -> new BusinessException("物资不存在")); if (material.getQuantity() < quantity) { return false; } int affected = materialRepository.reduceQuantity( materialId, quantity, material.getVersion()); return affected > 0; }对应的MyBatis XML:
<update id="reduceQuantity"> UPDATE material SET quantity = quantity - #{quantity}, version = version + 1 WHERE id = #{id} AND version = #{version} AND quantity >= #{quantity} </update>4.2 批量导入性能优化
初期使用MyBatis单条insert循环,导入1000条数据需要12秒。优化方案:
- 开启MyBatis批量模式
# application.yml mybatis: executor-type: batch- 使用 批量插入
<insert id="batchInsert"> INSERT INTO material (name, category, quantity, donor_id) VALUES <foreach collection="list" item="item" separator=","> (#{item.name}, #{item.category}, #{item.quantity}, #{item.donorId}) </foreach> </insert>- 添加rewriteBatchedStatements参数
spring: datasource: url: jdbc:mysql://localhost:3306/donation?rewriteBatchedStatements=true优化后1000条数据导入仅需1.8秒,但要注意:
- 单批次不要超过1000条
- 需要处理重复数据问题
- 失败时需要完整回滚
5. 安全防护方案
5.1 敏感操作审计日志
所有物资状态变更记录审计日志:
@Aspect @Component @RequiredArgsConstructor public class MaterialAuditAspect { private final AuditLogRepository logRepo; @AfterReturning( pointcut = "@annotation(com.example.donation.annotation.AuditLog)", returning = "result") public void afterReturning(JoinPoint jp, Object result) { MethodSignature signature = (MethodSignature) jp.getSignature(); AuditLog annotation = signature.getMethod().getAnnotation(AuditLog.class); String operation = annotation.value(); Object[] args = jp.getArgs(); Long materialId = (Long) args[0]; logRepo.save(AuditLogEntity.builder() .materialId(materialId) .operation(operation) .operator(SecurityUtils.getCurrentUserId()) .detail(JSON.toJSONString(args)) .build()); } }5.2 捐赠数据脱敏处理
在返回给前端前对捐赠人信息脱敏:
public class DonorDTO { @JsonSerialize(using = MobileSerializer.class) private String mobile; @JsonSerialize(using = NameSerializer.class) private String name; } public class MobileSerializer extends JsonSerializer<String> { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider provider) { if (StringUtils.isBlank(value)) { gen.writeString(""); return; } gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")); } }6. 部署与监控
6.1 容器化部署方案
使用Docker Compose编排服务:
version: '3' services: mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} volumes: - mysql_data:/var/lib/mysql ports: - "3306:3306" backend: build: ./backend ports: - "8080:8080" depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/donation frontend: build: ./frontend ports: - "80:80"关键配置注意点:
- MySQL需要预先创建数据库和用户
- SpringBoot应用需设置wait-for-it.sh检测MySQL就绪
- Vue应用需要配置API代理
6.2 Prometheus监控配置
采集SpringBoot应用指标:
@Configuration @EnablePrometheusEndpoint @EnableSpringBootMetricsCollector public class PrometheusConfig { @Bean public CollectorRegistry collectorRegistry() { return new CollectorRegistry(true); } }Grafana监控看板重点监测:
- 物资入库速率(counter类型)
- 分配操作耗时(histogram类型)
- 各状态物资数量(gauge类型)
7. 项目演进建议
- 增加区块链存证:将关键分配记录上链,增强公信力
- 对接物流系统:直接生成运单追踪物流信息
- 智能预测模块:基于历史数据预测未来物资需求
- 移动端适配:开发React Native跨平台应用
这套系统后来被三个地区的红十字会采用,核心经验是:公益系统要特别注重操作的便捷性和数据的透明性。我们甚至给每个受赠单位开发了扫码签收的微信小程序,捐赠人可以直接在公众号看到自己捐赠物资的最终去向。