fix(app): 修复 AppController 安全与查询问题

- 加强异常处理、类型安全和图片上传校验
- 优化设备相关查询,避免重复访问数据源
- 补充并记录并发测试与审查修复实施计划
This commit is contained in:
yuhaiming
2026-08-13 11:01:57 +08:00
parent 215d6ba167
commit 2f7597ed53
48 changed files with 112 additions and 1368 deletions

View File

@@ -10,6 +10,8 @@ import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.domain.vo.DashboardStatsVo;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.utils.ExcelUtil;
@@ -22,6 +24,8 @@ import org.dromara.common.web.core.BaseController;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
@@ -122,6 +126,26 @@ public class AppDeviceController extends BaseController {
return toAjax(appDeviceService.generateQrCode(List.of(ids)));
}
/**
* 批量下载设备二维码 ZIP。
*
* @param deviceNos 逗号分隔的设备编号
*/
@SaCheckPermission("app:device:edit")
@Log(title = "设备二维码", businessType = BusinessType.EXPORT)
@PostMapping("/downloadQrCodes")
public void downloadQrCodes(HttpServletResponse response,
@NotEmpty(message = "设备编号不能为空") @RequestParam("deviceNos") String deviceNos) throws IOException {
List<String> ids = Arrays.stream(deviceNos.split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.toList();
response.reset();
FileUtils.setAttachmentResponseHeader(response, "device_qrcodes.zip");
response.setContentType("application/zip");
appDeviceService.downloadQrCodes(ids, response.getOutputStream());
}
/**
* 删除设备信息

View File

@@ -6,6 +6,8 @@ import org.dromara.app.domain.vo.DashboardStatsVo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -104,6 +106,15 @@ public interface IAppDeviceService {
*/
Boolean generateQrCode(Collection<String> deviceNos);
/**
* 从 OSS 批量下载设备二维码并打包为 ZIP。
*
* @param deviceNos 设备编号集合
* @param outputStream ZIP 输出流
* @throws IOException 写入 ZIP 失败
*/
void downloadQrCodes(Collection<String> deviceNos, OutputStream outputStream) throws IOException;
/**
* 校验并批量删除设备信息
信息

View File

@@ -30,6 +30,7 @@ import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.entity.UploadResult;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.satoken.utils.LoginHelper;
@@ -41,11 +42,13 @@ import org.springframework.transaction.annotation.Transactional;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Slf4j
@RequiredArgsConstructor
@@ -379,6 +382,53 @@ public class AppDeviceServiceImpl implements IAppDeviceService {
return true;
}
@Override
public void downloadQrCodes(Collection<String> deviceNos, OutputStream outputStream) throws IOException {
if (deviceNos == null || deviceNos.isEmpty()) {
throw new ServiceException("设备编号不能为空");
}
if (outputStream == null) {
throw new ServiceException("下载输出流不能为空");
}
List<String> normalizedDeviceNos = deviceNos.stream()
.map(deviceNo -> deviceNo == null ? "" : deviceNo.trim())
.toList();
if (normalizedDeviceNos.stream().anyMatch(StringUtils::isBlank)) {
throw new ServiceException("设备编号不能为空");
}
List<String> distinctDeviceNos = normalizedDeviceNos.stream().distinct().toList();
List<AppDevice> devices = baseMapper.selectByIds(distinctDeviceNos);
if (devices.size() != distinctDeviceNos.size()) {
throw new ServiceException("存在无效的设备编号");
}
if (devices.stream().anyMatch(device -> StringUtils.isBlank(device.getQrcode()))) {
throw new ServiceException("所选设备存在未生成二维码的设备");
}
OssClient storage = OssFactory.instance();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
for (AppDevice device : devices) {
Path tempFile = storage.fileDownload(device.getQrcode());
try {
zipOutputStream.putNextEntry(new ZipEntry(buildQrCodeFileName(device.getDeviceNo())));
try (InputStream inputStream = Files.newInputStream(tempFile)) {
inputStream.transferTo(zipOutputStream);
}
zipOutputStream.closeEntry();
} finally {
Files.deleteIfExists(tempFile);
}
}
}
}
private String buildQrCodeFileName(String deviceNo) {
String safeDeviceNo = deviceNo.replaceAll("[\\\\/:*?\"<>|]", "_");
return safeDeviceNo + "_qrcode.png";
}
private String buildQrCodeContent(AppDevice device) {
if (StringUtils.isBlank(device.getMacAddress())) {
throw new ServiceException("设备MAC地址不能为空无法生成二维码");