1. 项目背景与核心需求
2020年以来的全球公共卫生事件让应急物资管理系统的重要性凸显。传统物资管理多依赖Excel表格和人工记录,存在数据孤岛、响应滞后、统计困难等问题。我们团队为某省级红十字会开发的这套系统,正是为了解决以下痛点:
- 实时库存可视化:医疗物资的采购、调拨、消耗需要分钟级更新
- 多维度溯源:需追踪每批口罩/防护服的捐赠方、接收方、效期等完整链路
- 智能预警:对临期物资、库存阈值、分配异常等情况自动触发提醒
- 权限隔离:不同层级机构(省-市-县)需有差异化的数据操作权限
关键设计指标:系统需支持500+并发请求,核心接口响应时间<300ms,支持10万级物资条目管理,所有操作留痕可审计。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选型与架构设计
2.1 后端技术组合解析
选择SpringBoot 2.7作为基础框架,主要考虑:
- 内嵌Tomcat简化部署,与Jenkins流水线天然契合
- Starter机制快速集成MyBatis+MySQL(对比JPA更适应复杂查询场景)
- Actuator端点便于监控系统健康状态
数据库采用MySQL 8.0而非PostgreSQL的原因:
- 团队更熟悉MySQL的优化方案(索引策略、分表方案等)
- 物资数据关系明确,不需要复杂的地理空间查询
- 利用Window函数实现物资消耗趋势分析足够高效
2.2 前端技术决策
Vue 3 + TypeScript组合的价值点:
- Composition API更适合处理物资表单的复杂状态逻辑
- 腾讯地图SDK的集成成本低于高德(捐赠物资运输轨迹展示需求)
- Vite构建速度比Webpack快60%,这对需要频繁调试的报表模块至关重要
2.3 前后端交互设计
采用RESTful风格接口,但针对特殊场景做了优化:
java复制// 物资批量导入接口
@PostMapping("/materials/batch")
public R<String> batchImport(
@RequestParam MultipartFile file,
@RequestHeader("X-Org-Id") String orgId) {
// 使用POI流式解析Excel避免OOM
}
前端对应采用axios拦截器统一处理:
javascript复制service.interceptors.request.use(config => {
if (config.url.includes('/batch')) {
config.headers['Content-Type'] = 'multipart/form-data'
}
return config
})
3. 核心业务模块实现
3.1 物资生命周期管理
数据库关键表设计:
sql复制CREATE TABLE `t_material` (
`id` BIGINT PRIMARY KEY,
`name` VARCHAR(100) NOT NULL COMMENT 'N95口罩/防护服',
`spec` VARCHAR(50) COMMENT '规格如175/92',
`batch_no` VARCHAR(30) NOT NULL COMMENT '生产批号',
`expire_date` DATE NOT NULL,
`stock` INT UNSIGNED DEFAULT 0,
`safety_stock` INT UNSIGNED COMMENT '安全库存阈值',
`org_id` INT NOT NULL COMMENT '所属机构'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
业务逻辑中的重点难点:
- 效期预警:通过Spring Scheduler每天凌晨扫描临期物资
java复制@Scheduled(cron = "0 0 2 * * ?")
public void checkExpiration() {
LocalDate warnDate = LocalDate.now().plusDays(30);
List<Material> materials = materialMapper.selectExpiring(warnDate);
materials.forEach(m -> {
String msg = String.format("物资%s将在%s过期", m.getName(), m.getExpireDate());
alertService.send(m.getOrgId(), msg);
});
}
- 分布式锁控制:防止超发物资
java复制public boolean deductStock(Long materialId, int num) {
String lockKey = "material:" + materialId;
try {
// 使用Redisson客户端
RLock lock = redissonClient.getLock(lockKey);
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
Material m = materialMapper.selectForUpdate(materialId);
if (m.getStock() >= num) {
return materialMapper.updateStock(materialId, -num) > 0;
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
3.2 智能报表模块
利用MySQL 8.0的CTE(Common Table Expressions)实现复杂统计:
sql复制WITH regional_stats AS (
SELECT
r.name AS region,
SUM(CASE WHEN m.type='mask' THEN m.stock ELSE 0 END) AS mask_total,
SUM(CASE WHEN m.type='protective' THEN m.stock ELSE 0 END) AS protective_total
FROM t_material m
JOIN t_organization o ON m.org_id = o.id
JOIN t_region r ON o.region_id = r.id
GROUP BY r.id
)
SELECT * FROM regional_stats
WHERE mask_total < 10000 OR protective_total < 5000;
前端使用ECharts实现动态可视化:
javascript复制<template>
<div ref="chart" style="width:600px;height:400px"></div>
</template>
<script setup>
import * as echarts from 'echarts'
const chart = ref(null)
onMounted(() => {
const instance = echarts.init(chart.value)
instance.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: regions },
yAxis: { type: 'value' },
series: [
{ name: '口罩库存', type: 'bar', data: maskData },
{ name: '防护服库存', type: 'bar', data: protectiveData }
]
})
})
</script>
4. 部署与性能优化实战
4.1 基于Docker的部署方案
后端Dockerfile关键配置:
dockerfile复制FROM openjdk:17-jdk-slim
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar",
"-XX:+UseG1GC",
"-Xms512m",
"-Xmx1024m",
"-Dspring.profiles.active=prod",
"/app.jar"]
前端Nginx配置要点:
nginx复制server {
listen 80;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header X-Real-IP $remote_addr;
}
}
4.2 性能调优记录
问题现象:物资查询接口在500并发时RT达到1.2s
排查过程:
- 使用Arthas的trace命令发现MyBatis查询耗时占比85%
- 检查SQL发现没有利用到复合索引
- 使用EXPLAIN确认全表扫描
优化方案:
sql复制-- 原始查询
SELECT * FROM t_material WHERE org_id = ? AND status = 1;
-- 优化后
ALTER TABLE t_material ADD INDEX idx_org_status (org_id, status);
效果对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均RT | 1200ms | 230ms |
| 99线 | 2500ms | 500ms |
| CPU使用率 | 85% | 45% |
5. 典型问题解决方案
5.1 内存泄漏排查案例
现象:服务运行24小时后出现OutOfMemoryError: Java heap space
诊断工具:
- 在启动参数添加
-XX:+HeapDumpOnOutOfMemoryError自动生成dump文件 - 使用MAT分析发现MyBatis的SqlSession对象未释放
根本原因:
java复制// 错误写法
public List<Material> getMaterials(Long orgId) {
SqlSession session = sqlSessionFactory.openSession();
return session.selectList("selectMaterials", orgId); // 未关闭session
}
// 正确写法
@Transactional
public List<Material> getMaterials(Long orgId) {
return materialMapper.selectByOrg(orgId);
}
5.2 前后端跨域问题
开发环境遇到CORS错误时的完整解决方案:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST")
.allowCredentials(false)
.maxAge(3600);
}
}
配合前端axios配置:
javascript复制const service = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
withCredentials: false
})
6. 项目扩展方向
- 物联网集成:通过MQTT协议接入智能货柜的实时库存数据
java复制@Configuration
public class MqttConfig {
@Bean
public MqttPahoClientFactory factory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setServerURIs("tcp://iot.example.com:1883");
return factory;
}
}
-
区块链存证:将关键物资流转记录上链(Hyperledger Fabric方案调研中)
-
智能预测:基于历史消耗数据训练LSTM模型预测需求峰值
这套系统在试运行阶段已管理超过50万件防疫物资,期间处理了2000+次调拨操作。最大的收获是认识到:在高并发场景下,乐观锁(version机制)比悲观锁(select for update)更适合物资扣减场景,后者在分布式环境下容易引发死锁。
