在基于RuoYi框架开发的"帝可得"智能售货机管理系统中,文件存储和设备管理是两个核心功能模块。本文将详细解析这两个模块的技术实现方案,包括从本地存储到云存储的迁移过程,以及设备管理全流程的开发实践。
在项目初期,系统采用本地文件存储方案,将上传的图片直接保存在服务器磁盘上。这种方案虽然实现简单,但随着业务发展暴露出几个关键问题:
性能瓶颈:当并发上传量增大时,本地磁盘I/O会成为系统瓶颈。我们实测发现,在100并发上传场景下,响应时间会从平均200ms陡增至1.5s以上。
存储容量限制:随着业务扩展,图片存储需求快速增长。一个中等规模的售货机网络每月会产生约50GB的新图片数据,本地磁盘很快面临扩容压力。
单点故障风险:服务器磁盘损坏会导致所有存储图片丢失,缺乏数据冗余保障。我们曾经历过一次磁盘故障导致一周的运营数据图片无法恢复。
针对上述问题,我们评估了三种替代方案:
| 方案类型 | 代表产品 | 优点 | 缺点 |
|---|---|---|---|
| 自建存储服务 | FastDFS/MinIO | 可控性强,成本较低 | 维护复杂,需自行保障可用性 |
| 第三方云存储 | 阿里云OSS | 开箱即用,弹性扩展 | 长期使用成本较高 |
| 混合存储方案 | 本地+云备份 | 兼顾性能与可靠性 | 架构复杂,一致性难保证 |
经过综合评估,我们选择阿里云OSS作为最终方案,主要基于以下考虑:
创建Bucket:在阿里云控制台创建名为"dkd-project-app"的Bucket,选择华东1(杭州)区域,设置ACL为私有读写。
访问控制:使用RAM账号生成AccessKey,遵循最小权限原则,仅授予该账号对特定Bucket的读写权限。
域名配置:绑定自定义域名并启用HTTPS,提升安全性和品牌一致性。我们使用static.dkd.com作为CDN加速域名。
在项目中引入官方SDK依赖:
xml复制<!-- 阿里云OSS SDK -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.16.1</version>
</dependency>
配置客户端实例:
java复制@Bean
public OSS ossClient() {
return new OSSClientBuilder().build(
"oss-cn-hangzhou.aliyuncs.com",
"your-access-key",
"your-secret-key");
}
针对大文件上传,我们实现了分片上传方案:
java复制public String uploadLargeFile(MultipartFile file, String path) {
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(
bucketName, path);
InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request);
// 分片上传逻辑
List<PartETag> partETags = new ArrayList<>();
long contentLength = file.getSize();
long partSize = 5 * 1024 * 1024; // 5MB
try (InputStream inputStream = file.getInputStream()) {
for (int i = 0; i < contentLength / partSize; i++) {
// 上传分片...
}
// 完成上传
} catch (Exception e) {
// 异常处理
}
return getFileUrl(path);
}
为简化不同存储平台的切换,我们引入了x-file-storage组件:
xml复制<dependency>
<groupId>org.dromara.x-file-storage</groupId>
<artifactId>x-file-storage-spring</artifactId>
<version>2.3.0</version>
</dependency>
yaml复制dromara:
x-file-storage:
default-platform: aliyun-oss-1
thumbnail-suffix: ".min.jpg"
aliyun-oss:
- platform: aliyun-oss-1
enable-storage: true
access-key: ${oss.access-key}
secret-key: ${oss.secret-key}
end-point: oss-cn-hangzhou.aliyuncs.com
bucket-name: dkd-project-app
domain: https://static.dkd.com/
base-path: dkd-images/
改造原上传接口:
java复制@PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file) {
try {
String objectName = LocalDate.now()
.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + "/";
FileInfo fileInfo = fileStorageService.of(file)
.setPath(objectName)
.upload();
return AjaxResult.success()
.put("url", fileInfo.getUrl())
.put("fileName", fileInfo.getFilename());
} catch (Exception e) {
return AjaxResult.error("上传失败: " + e.getMessage());
}
}
修改前端上传组件,直接使用返回的URL:
javascript复制// 原代码
// const url = serverConfig.url + fileName;
// 新代码
const url = response.data.url;
设备管理模块主要实现以下功能:
业务流程如下:
核心表结构设计:
sql复制CREATE TABLE `tb_vm_type` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) COMMENT '型号名称',
`model` varchar(50) COMMENT '型号编码',
`image` varchar(255) COMMENT '设备图片',
`vm_row` int COMMENT '货道行数',
`vm_col` int COMMENT '货道列数',
`channel_max_capacity` int COMMENT '货道最大容量',
PRIMARY KEY (`id`)
);
sql复制CREATE TABLE `tb_vending_machine` (
`id` bigint NOT NULL AUTO_INCREMENT,
`inner_code` varchar(50) COMMENT '设备编号',
`vm_type_id` bigint COMMENT '设备类型ID',
`node_id` bigint COMMENT '点位ID',
`addr` varchar(255) COMMENT '详细地址',
`vm_status` tinyint DEFAULT 0 COMMENT '状态(0未投放,1运营,3撤机)',
`partner_id` bigint COMMENT '合作商ID',
`region_id` bigint COMMENT '区域ID',
`business_type` varchar(20) COMMENT '商圈类型',
PRIMARY KEY (`id`)
);
sql复制CREATE TABLE `tb_channel` (
`id` bigint NOT NULL AUTO_INCREMENT,
`channel_code` varchar(20) COMMENT '货道编号',
`vm_id` bigint COMMENT '设备ID',
`inner_code` varchar(50) COMMENT '设备编号',
`max_capacity` int COMMENT '最大容量',
PRIMARY KEY (`id`)
);
使用RuoYi代码生成器快速生成CRUD代码:
配置生成器:
基础功能验证:
java复制@Override
public int insertVendingMachine(VendingMachine vendingMachine) {
// 1. 生成设备编号
String innerCode = "DK" + System.currentTimeMillis();
vendingMachine.setInnerCode(innerCode);
// 2. 补充设备信息
VmType vmType = vmTypeService.getById(vendingMachine.getVmTypeId());
vendingMachine.setChannelMaxCapacity(vmType.getChannelMaxCapacity());
// 3. 补充点位信息
Node node = nodeService.getById(vendingMachine.getNodeId());
vendingMachine.setAddr(node.getAddress());
vendingMachine.setPartnerId(node.getPartnerId());
vendingMachine.setRegionId(node.getRegionId());
// 4. 保存设备
int result = vendingMachineMapper.insert(vendingMachine);
// 5. 生成货道
if(result > 0) {
generateChannels(vendingMachine, vmType);
}
return result;
}
private void generateChannels(VendingMachine vm, VmType vmType) {
List<Channel> channels = new ArrayList<>();
for(int row = 1; row <= vmType.getVmRow(); row++) {
for(int col = 1; col <= vmType.getVmCol(); col++) {
Channel channel = new Channel();
channel.setChannelCode(row + "-" + col);
channel.setVmId(vm.getId());
channel.setInnerCode(vm.getInnerCode());
channel.setMaxCapacity(vmType.getChannelMaxCapacity());
channels.add(channel);
}
}
channelService.saveBatch(channels);
}
实现设备状态流转控制:
java复制public enum VmStatus {
UN_DEPLOYED(0, "未投放"),
OPERATING(1, "运营中"),
WITHDRAWN(3, "已撤机");
// 省略构造函数和getter
}
public AjaxResult changeStatus(Long vmId, VmStatus targetStatus) {
VendingMachine vm = getById(vmId);
if(vm == null) {
return AjaxResult.error("设备不存在");
}
// 状态流转校验
if(vm.getVmStatus() == VmStatus.UN_DEPLOYED.code &&
targetStatus != VmStatus.OPERATING) {
return AjaxResult.error("未投放设备只能变更为运营状态");
}
// 更新状态
vm.setVmStatus(targetStatus.getCode());
updateById(vm);
// 记录状态变更日志
logStatusChange(vmId, targetStatus);
return AjaxResult.success();
}
优化表单验证和展示:
vue复制<el-form-item label="货道数" prop="vmRow">
<el-input-number
v-model="form.vmRow"
:min="1" :max="10"
@change="validateChannelConfig"/>
行
<el-input-number
v-model="form.vmCol"
:min="1" :max="10"
@change="validateChannelConfig"/>
列
</el-form-item>
<script>
function validateChannelConfig() {
if(this.form.vmRow * this.form.vmCol > 50) {
this.$message.warning('货道总数不能超过50个');
this.form.vmRow = 5;
this.form.vmCol = 5;
}
}
</script>
实现状态筛选和可视化展示:
vue复制<el-table-column label="运营状态" align="center">
<template #default="{row}">
<el-tag :type="getStatusTagType(row)">
{{ getStatusText(row) }}
</el-tag>
</template>
</el-table-column>
<script>
function getStatusTagType(row) {
const status = row.runningStatus ?
JSON.parse(row.runningStatus).status : false;
return status ? 'success' : 'danger';
}
</script>
迁移策略:
成本优化:
性能优化:
事务处理:
java复制@Transactional
public int addDevice(VendingMachine vm) {
// 设备入库
int result = vmMapper.insert(vm);
// 生成货道
if(result > 0) {
channelService.generateChannels(vm);
}
return result;
}
批量操作优化:
前端性能优化:
OSS上传超时:
货道生成不全:
设备状态不同步:
实现实时监控可视化:
java复制@GetMapping("/dashboard")
public AjaxResult getDeviceDashboard() {
// 获取各状态设备数量
Map<String, Integer> statusCount = vmService.getStatusCount();
// 获取区域分布
List<RegionDistribution> regionData = vmService.getRegionDistribution();
// 获取最近告警
List<Alert> recentAlerts = alertService.getRecentAlerts(10);
return AjaxResult.success()
.put("statusCount", statusCount)
.put("regionData", regionData)
.put("recentAlerts", recentAlerts);
}
实现折扣策略功能:
java复制public AjaxResult applyDiscountPolicy(Long vmId, DiscountPolicy policy) {
// 验证策略有效性
if(policy.getStartTime().after(policy.getEndTime())) {
return AjaxResult.error("开始时间不能晚于结束时间");
}
// 应用策略
VendingMachine vm = getById(vmId);
vm.setPolicyId(policy.getId());
updateById(vm);
// 同步到设备端
mqttService.publish(
"/device/" + vm.getInnerCode() + "/policy",
policy.toString());
return AjaxResult.success();
}
实现基础控制指令:
java复制@PostMapping("/control")
public AjaxResult sendControlCommand(
@RequestParam String innerCode,
@RequestParam String command) {
// 验证设备状态
VendingMachine vm = vmService.getByInnerCode(innerCode);
if(vm.getVmStatus() != VmStatus.OPERATING.code) {
return AjaxResult.error("设备未在运营状态");
}
// 发送指令
try {
String topic = "/device/" + innerCode + "/control";
mqttService.publish(topic, command);
return AjaxResult.success("指令发送成功");
} catch (Exception e) {
return AjaxResult.error("指令发送失败: " + e.getMessage());
}
}
通过本项目的实施,我们成功将系统文件存储从本地迁移到阿里云OSS,解决了存储扩展性和可靠性的问题。在设备管理模块,我们实现了完整的生命周期管理,包括:
技术方案上,我们采用以下策略确保系统质量:
在实际部署中,系统成功支持了5000+台设备的管理需求,日均处理文件上传约3000次,验证了架构的可靠性和扩展性。