更新ASIN库新需求
This commit is contained in:
@@ -44,8 +44,10 @@ public class DedupeTotalDataController {
|
||||
public ApiResponse<DedupeTotalDataPageVo> page(
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize,
|
||||
@Parameter(description = "模糊搜索关键字") @RequestParam(required = false) String keyword) {
|
||||
return ApiResponse.success(dedupeTotalDataService.page(page, pageSize, keyword));
|
||||
@Parameter(description = "数据值模糊搜索关键字") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "用户名模糊搜索关键字") @RequestParam(required = false) String username,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId) {
|
||||
return ApiResponse.success(dedupeTotalDataService.page(page, pageSize, keyword, username, operatorId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@@ -54,8 +56,10 @@ public class DedupeTotalDataController {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "创建成功", content = @Content(schema = @Schema(implementation = DedupeTotalDataItemVo.class))),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "参数不合法或数据重复")
|
||||
})
|
||||
public ApiResponse<DedupeTotalDataItemVo> create(@Valid @RequestBody DedupeTotalDataCreateRequest request) {
|
||||
return ApiResponse.success("创建成功", dedupeTotalDataService.create(request));
|
||||
public ApiResponse<DedupeTotalDataItemVo> create(
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
||||
@Valid @RequestBody DedupeTotalDataCreateRequest request) {
|
||||
return ApiResponse.success("创建成功", dedupeTotalDataService.create(request, operatorId));
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
@@ -65,14 +69,17 @@ public class DedupeTotalDataController {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "文件不合法或缺少 ASIN 列")
|
||||
})
|
||||
public ApiResponse<DedupeTotalDataImportStartVo> importExcel(
|
||||
@Parameter(description = "xlsx 文件", required = true) @RequestParam("file") MultipartFile file) {
|
||||
return ApiResponse.success("开始导入", dedupeTotalDataService.startImport(file));
|
||||
@Parameter(description = "xlsx 文件", required = true) @RequestParam("file") MultipartFile file,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId) {
|
||||
return ApiResponse.success("开始导入", dedupeTotalDataService.startImport(file, operatorId));
|
||||
}
|
||||
|
||||
@GetMapping("/import/{importId}")
|
||||
@Operation(summary = "查询导入进度", description = "根据导入任务 ID 查询当前进度。")
|
||||
public ApiResponse<DedupeTotalDataImportProgressVo> importProgress(@PathVariable String importId) {
|
||||
return ApiResponse.success(dedupeTotalDataService.getImportProgress(importId));
|
||||
public ApiResponse<DedupeTotalDataImportProgressVo> importProgress(
|
||||
@PathVariable String importId,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId) {
|
||||
return ApiResponse.success(dedupeTotalDataService.getImportProgress(importId, operatorId));
|
||||
}
|
||||
|
||||
@PostMapping("/delete-import")
|
||||
@@ -82,14 +89,17 @@ public class DedupeTotalDataController {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "文件不合法或缺少 ASIN 列")
|
||||
})
|
||||
public ApiResponse<DedupeTotalDataImportStartVo> deleteImportExcel(
|
||||
@Parameter(description = "xlsx 文件", required = true) @RequestParam("file") MultipartFile file) {
|
||||
return ApiResponse.success("开始删除", dedupeTotalDataService.startDeleteImport(file));
|
||||
@Parameter(description = "xlsx 文件", required = true) @RequestParam("file") MultipartFile file,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId) {
|
||||
return ApiResponse.success("开始删除", dedupeTotalDataService.startDeleteImport(file, operatorId));
|
||||
}
|
||||
|
||||
@GetMapping("/delete-import/{importId}")
|
||||
@Operation(summary = "查询删除导入进度", description = "根据删除导入任务 ID 查询当前进度。")
|
||||
public ApiResponse<DedupeTotalDataImportProgressVo> deleteImportProgress(@PathVariable String importId) {
|
||||
return ApiResponse.success(dedupeTotalDataService.getDeleteImportProgress(importId));
|
||||
public ApiResponse<DedupeTotalDataImportProgressVo> deleteImportProgress(
|
||||
@PathVariable String importId,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId) {
|
||||
return ApiResponse.success(dedupeTotalDataService.getDeleteImportProgress(importId, operatorId));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@@ -101,8 +111,9 @@ public class DedupeTotalDataController {
|
||||
})
|
||||
public ApiResponse<DedupeTotalDataItemVo> update(
|
||||
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
||||
@Valid @RequestBody DedupeTotalDataUpdateRequest request) {
|
||||
return ApiResponse.success("更新成功", dedupeTotalDataService.update(id, request));
|
||||
return ApiResponse.success("更新成功", dedupeTotalDataService.update(id, request, operatorId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@@ -111,8 +122,10 @@ public class DedupeTotalDataController {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "数据不存在")
|
||||
})
|
||||
public ApiResponse<Void> delete(@Parameter(description = "主键ID", required = true) @PathVariable Long id) {
|
||||
dedupeTotalDataService.delete(id);
|
||||
public ApiResponse<Void> delete(
|
||||
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId) {
|
||||
dedupeTotalDataService.delete(id, operatorId);
|
||||
return ApiResponse.success("删除成功", null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ public class DedupeTotalDataEntity {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String dataValue;
|
||||
private Long uploaderUserId;
|
||||
private String uploaderUsername;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ public class DedupeTotalDataItemVo {
|
||||
@Schema(description = "总数据值")
|
||||
private String dataValue;
|
||||
|
||||
@Schema(description = "上传用户ID")
|
||||
private Long uploaderUserId;
|
||||
|
||||
@Schema(description = "上传用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportStartVo;
|
||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataImportVo;
|
||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo;
|
||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo;
|
||||
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||
import com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
@@ -24,15 +27,18 @@ import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -44,9 +50,13 @@ public class DedupeTotalDataService {
|
||||
private static final int COMPARE_BATCH_SIZE = 5000;
|
||||
|
||||
private final DedupeTotalDataMapper dedupeTotalDataMapper;
|
||||
private final AdminUserMapper adminUserMapper;
|
||||
private final ShopManageGroupMapper shopManageGroupMapper;
|
||||
private final PlatformTransactionManager transactionManager;
|
||||
private final Map<String, DedupeTotalDataImportProgressVo> importProgressMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, DedupeTotalDataImportProgressVo> deleteImportProgressMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, Long> importOwnerMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, Long> deleteImportOwnerMap = new ConcurrentHashMap<>();
|
||||
|
||||
private TransactionTemplate newRequiresNewTemplate() {
|
||||
TransactionTemplate template = new TransactionTemplate(transactionManager);
|
||||
@@ -54,12 +64,22 @@ public class DedupeTotalDataService {
|
||||
return template;
|
||||
}
|
||||
|
||||
public DedupeTotalDataPageVo page(long page, long pageSize, String keyword) {
|
||||
public DedupeTotalDataPageVo page(long page, long pageSize, String keyword, String username, Long operatorId) {
|
||||
long safePage = Math.max(page, 1);
|
||||
long safePageSize = Math.min(Math.max(pageSize, 1), 100);
|
||||
String safeKeyword = keyword == null ? "" : keyword.trim();
|
||||
String safeUsername = username == null ? "" : username.trim();
|
||||
AccessScope scope = resolveAccessScope(operatorId);
|
||||
Set<Long> usernameUserIds = safeUsername.isEmpty()
|
||||
? Set.of()
|
||||
: findUserIdsByUsername(safeUsername, scope);
|
||||
if (!safeUsername.isEmpty() && usernameUserIds.isEmpty()) {
|
||||
return emptyPage(safePage, safePageSize);
|
||||
}
|
||||
LambdaQueryWrapper<DedupeTotalDataEntity> query = new LambdaQueryWrapper<DedupeTotalDataEntity>()
|
||||
.like(!safeKeyword.isEmpty(), DedupeTotalDataEntity::getDataValue, safeKeyword)
|
||||
.in(!scope.allUsers(), DedupeTotalDataEntity::getUploaderUserId, scope.userIds())
|
||||
.in(!safeUsername.isEmpty(), DedupeTotalDataEntity::getUploaderUserId, usernameUserIds)
|
||||
.orderByDesc(DedupeTotalDataEntity::getId);
|
||||
Long total = dedupeTotalDataMapper.selectCount(query);
|
||||
List<DedupeTotalDataItemVo> items = dedupeTotalDataMapper.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize))
|
||||
@@ -75,11 +95,14 @@ public class DedupeTotalDataService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DedupeTotalDataItemVo create(DedupeTotalDataCreateRequest request) {
|
||||
public DedupeTotalDataItemVo create(DedupeTotalDataCreateRequest request, Long operatorId) {
|
||||
AdminUserEntity uploader = getOperator(operatorId);
|
||||
String dataValue = normalizeComparableValue(request.getDataValue());
|
||||
ensureUnique(dataValue, null);
|
||||
DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
|
||||
entity.setDataValue(dataValue);
|
||||
entity.setUploaderUserId(uploader.getId());
|
||||
entity.setUploaderUsername(uploader.getUsername());
|
||||
dedupeTotalDataMapper.insert(entity);
|
||||
return toItemVo(getById(entity.getId()));
|
||||
}
|
||||
@@ -111,10 +134,11 @@ public class DedupeTotalDataService {
|
||||
return existingValues;
|
||||
}
|
||||
|
||||
public DedupeTotalDataImportStartVo startImport(MultipartFile file) {
|
||||
public DedupeTotalDataImportStartVo startImport(MultipartFile file, Long operatorId) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("请上传 xlsx 文件");
|
||||
}
|
||||
AdminUserEntity uploader = getOperator(operatorId);
|
||||
String importId = IdUtil.fastSimpleUUID();
|
||||
DedupeTotalDataImportProgressVo progress = new DedupeTotalDataImportProgressVo();
|
||||
progress.setStatus("pending");
|
||||
@@ -124,13 +148,16 @@ public class DedupeTotalDataService {
|
||||
progress.setInsertedCount(0);
|
||||
progress.setSkippedCount(0);
|
||||
importProgressMap.put(importId, progress);
|
||||
importOwnerMap.put(importId, uploader.getId());
|
||||
|
||||
try {
|
||||
File tempFile = saveMultipartToTempFile(file);
|
||||
String filename = file.getOriginalFilename();
|
||||
Thread.ofVirtual().start(() -> runImportTask(importId, tempFile, filename));
|
||||
Thread.ofVirtual().start(() -> runImportTask(
|
||||
importId, tempFile, filename, uploader.getId(), uploader.getUsername()));
|
||||
} catch (Exception e) {
|
||||
importProgressMap.remove(importId);
|
||||
importOwnerMap.remove(importId);
|
||||
throw new BusinessException("读取上传文件失败");
|
||||
}
|
||||
|
||||
@@ -139,18 +166,21 @@ public class DedupeTotalDataService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
public DedupeTotalDataImportProgressVo getImportProgress(String importId) {
|
||||
public DedupeTotalDataImportProgressVo getImportProgress(String importId, Long operatorId) {
|
||||
DedupeTotalDataImportProgressVo progress = importProgressMap.get(importId);
|
||||
if (progress == null) {
|
||||
Long ownerId = importOwnerMap.get(importId);
|
||||
if (progress == null || ownerId == null) {
|
||||
throw new BusinessException("导入任务不存在");
|
||||
}
|
||||
ensureUploaderAccess(ownerId, resolveAccessScope(operatorId));
|
||||
return progress;
|
||||
}
|
||||
|
||||
public DedupeTotalDataImportStartVo startDeleteImport(MultipartFile file) {
|
||||
public DedupeTotalDataImportStartVo startDeleteImport(MultipartFile file, Long operatorId) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("请上传 xlsx 文件");
|
||||
}
|
||||
AdminUserEntity operator = getOperator(operatorId);
|
||||
String importId = IdUtil.fastSimpleUUID();
|
||||
DedupeTotalDataImportProgressVo progress = new DedupeTotalDataImportProgressVo();
|
||||
progress.setStatus("pending");
|
||||
@@ -160,13 +190,15 @@ public class DedupeTotalDataService {
|
||||
progress.setInsertedCount(0);
|
||||
progress.setSkippedCount(0);
|
||||
deleteImportProgressMap.put(importId, progress);
|
||||
deleteImportOwnerMap.put(importId, operator.getId());
|
||||
|
||||
try {
|
||||
File tempFile = saveMultipartToTempFile(file);
|
||||
String filename = file.getOriginalFilename();
|
||||
Thread.ofVirtual().start(() -> runDeleteImportTask(importId, tempFile, filename));
|
||||
Thread.ofVirtual().start(() -> runDeleteImportTask(importId, tempFile, filename, operator.getId()));
|
||||
} catch (Exception e) {
|
||||
deleteImportProgressMap.remove(importId);
|
||||
deleteImportOwnerMap.remove(importId);
|
||||
throw new BusinessException("读取上传文件失败");
|
||||
}
|
||||
|
||||
@@ -175,62 +207,25 @@ public class DedupeTotalDataService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
public DedupeTotalDataImportProgressVo getDeleteImportProgress(String importId) {
|
||||
public DedupeTotalDataImportProgressVo getDeleteImportProgress(String importId, Long operatorId) {
|
||||
DedupeTotalDataImportProgressVo progress = deleteImportProgressMap.get(importId);
|
||||
if (progress == null) {
|
||||
Long ownerId = deleteImportOwnerMap.get(importId);
|
||||
if (progress == null || ownerId == null) {
|
||||
throw new BusinessException("删除任务不存在");
|
||||
}
|
||||
ensureUploaderAccess(ownerId, resolveAccessScope(operatorId));
|
||||
return progress;
|
||||
}
|
||||
|
||||
private void runDeleteImportTask(String importId, byte[] fileBytes, String filename) {
|
||||
DedupeTotalDataImportProgressVo progress = deleteImportProgressMap.get(importId);
|
||||
if (progress == null) {
|
||||
return;
|
||||
}
|
||||
progress.setStatus("running");
|
||||
try (InputStream inputStream = new ByteArrayInputStream(fileBytes)) {
|
||||
DedupeTotalDataImportVo result = deleteFromExcelInternal(inputStream, filename, progress);
|
||||
progress.setTotalRows(result.getTotalRows());
|
||||
progress.setAsinCount(result.getAsinCount());
|
||||
progress.setInsertedCount(result.getInsertedCount());
|
||||
progress.setSkippedCount(result.getSkippedCount());
|
||||
progress.setProcessedRows(result.getTotalRows());
|
||||
progress.setStatus("success");
|
||||
} catch (Exception e) {
|
||||
progress.setStatus("failed");
|
||||
progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "删除 Excel 匹配数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void runImportTask(String importId, byte[] fileBytes, String filename) {
|
||||
DedupeTotalDataImportProgressVo progress = importProgressMap.get(importId);
|
||||
if (progress == null) {
|
||||
return;
|
||||
}
|
||||
progress.setStatus("running");
|
||||
try (InputStream inputStream = new ByteArrayInputStream(fileBytes)) {
|
||||
DedupeTotalDataImportVo result = importFromExcelInternal(inputStream, filename, progress);
|
||||
progress.setTotalRows(result.getTotalRows());
|
||||
progress.setAsinCount(result.getAsinCount());
|
||||
progress.setInsertedCount(result.getInsertedCount());
|
||||
progress.setSkippedCount(result.getSkippedCount());
|
||||
progress.setProcessedRows(result.getTotalRows());
|
||||
progress.setStatus("success");
|
||||
} catch (Exception e) {
|
||||
progress.setStatus("failed");
|
||||
progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "导入 Excel 失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void runDeleteImportTask(String importId, File tempFile, String filename) {
|
||||
private void runDeleteImportTask(String importId, File tempFile, String filename, Long operatorId) {
|
||||
DedupeTotalDataImportProgressVo progress = deleteImportProgressMap.get(importId);
|
||||
if (progress == null) {
|
||||
return;
|
||||
}
|
||||
progress.setStatus("running");
|
||||
try (InputStream inputStream = Files.newInputStream(tempFile.toPath())) {
|
||||
DedupeTotalDataImportVo result = deleteFromExcelInternal(inputStream, filename, progress);
|
||||
AccessScope scope = resolveAccessScope(operatorId);
|
||||
DedupeTotalDataImportVo result = deleteFromExcelInternal(inputStream, filename, progress, scope);
|
||||
progress.setTotalRows(result.getTotalRows());
|
||||
progress.setAsinCount(result.getAsinCount());
|
||||
progress.setInsertedCount(result.getInsertedCount());
|
||||
@@ -245,14 +240,16 @@ public class DedupeTotalDataService {
|
||||
}
|
||||
}
|
||||
|
||||
private void runImportTask(String importId, File tempFile, String filename) {
|
||||
private void runImportTask(String importId, File tempFile, String filename,
|
||||
Long uploaderUserId, String uploaderUsername) {
|
||||
DedupeTotalDataImportProgressVo progress = importProgressMap.get(importId);
|
||||
if (progress == null) {
|
||||
return;
|
||||
}
|
||||
progress.setStatus("running");
|
||||
try (InputStream inputStream = Files.newInputStream(tempFile.toPath())) {
|
||||
DedupeTotalDataImportVo result = importFromExcelInternal(inputStream, filename, progress);
|
||||
DedupeTotalDataImportVo result = importFromExcelInternal(
|
||||
inputStream, filename, progress, uploaderUserId, uploaderUsername);
|
||||
progress.setTotalRows(result.getTotalRows());
|
||||
progress.setAsinCount(result.getAsinCount());
|
||||
progress.setInsertedCount(result.getInsertedCount());
|
||||
@@ -291,12 +288,14 @@ public class DedupeTotalDataService {
|
||||
}
|
||||
}
|
||||
|
||||
public DedupeTotalDataImportVo importFromExcel(MultipartFile file) {
|
||||
public DedupeTotalDataImportVo importFromExcel(MultipartFile file, Long operatorId) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("请上传 xlsx 文件");
|
||||
}
|
||||
AdminUserEntity uploader = getOperator(operatorId);
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
return importFromExcelInternal(inputStream, file.getOriginalFilename(), null);
|
||||
return importFromExcelInternal(
|
||||
inputStream, file.getOriginalFilename(), null, uploader.getId(), uploader.getUsername());
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -304,12 +303,13 @@ public class DedupeTotalDataService {
|
||||
}
|
||||
}
|
||||
|
||||
public DedupeTotalDataImportVo deleteFromExcel(MultipartFile file) {
|
||||
public DedupeTotalDataImportVo deleteFromExcel(MultipartFile file, Long operatorId) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("请上传 xlsx 文件");
|
||||
}
|
||||
AccessScope scope = resolveAccessScope(operatorId);
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
return deleteFromExcelInternal(inputStream, file.getOriginalFilename(), null);
|
||||
return deleteFromExcelInternal(inputStream, file.getOriginalFilename(), null, scope);
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -317,7 +317,9 @@ public class DedupeTotalDataService {
|
||||
}
|
||||
}
|
||||
|
||||
private DedupeTotalDataImportVo importFromExcelInternal(InputStream inputStream, String filename, DedupeTotalDataImportProgressVo progress) {
|
||||
private DedupeTotalDataImportVo importFromExcelInternal(InputStream inputStream, String filename,
|
||||
DedupeTotalDataImportProgressVo progress,
|
||||
Long uploaderUserId, String uploaderUsername) {
|
||||
String lowerFilename = filename == null ? "" : filename.toLowerCase();
|
||||
if (!(lowerFilename.endsWith(".xlsx") || lowerFilename.endsWith(".xls"))) {
|
||||
throw new BusinessException("仅支持 .xlsx 或 .xls 文件");
|
||||
@@ -411,6 +413,8 @@ public class DedupeTotalDataService {
|
||||
}
|
||||
DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
|
||||
entity.setDataValue(pendingDataValue);
|
||||
entity.setUploaderUserId(uploaderUserId);
|
||||
entity.setUploaderUsername(uploaderUsername);
|
||||
dedupeTotalDataMapper.insert(entity);
|
||||
return true;
|
||||
});
|
||||
@@ -440,7 +444,9 @@ public class DedupeTotalDataService {
|
||||
}
|
||||
}
|
||||
|
||||
private DedupeTotalDataImportVo deleteFromExcelInternal(InputStream inputStream, String filename, DedupeTotalDataImportProgressVo progress) {
|
||||
private DedupeTotalDataImportVo deleteFromExcelInternal(InputStream inputStream, String filename,
|
||||
DedupeTotalDataImportProgressVo progress,
|
||||
AccessScope scope) {
|
||||
String lowerFilename = filename == null ? "" : filename.toLowerCase();
|
||||
if (!(lowerFilename.endsWith(".xlsx") || lowerFilename.endsWith(".xls"))) {
|
||||
throw new BusinessException("仅支持 .xlsx 或 .xls 文件");
|
||||
@@ -518,7 +524,7 @@ public class DedupeTotalDataService {
|
||||
continue;
|
||||
}
|
||||
|
||||
int deletedThisRow = newRequiresNewTemplate().execute(status -> deleteByDataValue(dataValue));
|
||||
int deletedThisRow = newRequiresNewTemplate().execute(status -> deleteByDataValue(dataValue, scope));
|
||||
if (deletedThisRow > 0) {
|
||||
deletedCount += deletedThisRow;
|
||||
} else {
|
||||
@@ -546,8 +552,8 @@ public class DedupeTotalDataService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DedupeTotalDataItemVo update(Long id, DedupeTotalDataUpdateRequest request) {
|
||||
DedupeTotalDataEntity entity = getById(id);
|
||||
public DedupeTotalDataItemVo update(Long id, DedupeTotalDataUpdateRequest request, Long operatorId) {
|
||||
DedupeTotalDataEntity entity = getAccessibleById(id, operatorId);
|
||||
String dataValue = normalizeComparableValue(request.getDataValue());
|
||||
ensureUnique(dataValue, id);
|
||||
entity.setDataValue(dataValue);
|
||||
@@ -556,11 +562,17 @@ public class DedupeTotalDataService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
DedupeTotalDataEntity entity = getById(id);
|
||||
public void delete(Long id, Long operatorId) {
|
||||
DedupeTotalDataEntity entity = getAccessibleById(id, operatorId);
|
||||
dedupeTotalDataMapper.deleteById(entity.getId());
|
||||
}
|
||||
|
||||
private DedupeTotalDataEntity getAccessibleById(Long id, Long operatorId) {
|
||||
DedupeTotalDataEntity entity = getById(id);
|
||||
ensureUploaderAccess(entity.getUploaderUserId(), resolveAccessScope(operatorId));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private DedupeTotalDataEntity getById(Long id) {
|
||||
DedupeTotalDataEntity entity = dedupeTotalDataMapper.selectById(id);
|
||||
if (entity == null) {
|
||||
@@ -598,9 +610,70 @@ public class DedupeTotalDataService {
|
||||
return dedupeTotalDataMapper.selectOne(query) != null;
|
||||
}
|
||||
|
||||
private int deleteByDataValue(String dataValue) {
|
||||
private int deleteByDataValue(String dataValue, AccessScope scope) {
|
||||
return dedupeTotalDataMapper.delete(new LambdaQueryWrapper<DedupeTotalDataEntity>()
|
||||
.eq(DedupeTotalDataEntity::getDataValue, dataValue));
|
||||
.eq(DedupeTotalDataEntity::getDataValue, dataValue)
|
||||
.in(!scope.allUsers(), DedupeTotalDataEntity::getUploaderUserId, scope.userIds()));
|
||||
}
|
||||
|
||||
private AccessScope resolveAccessScope(Long operatorId) {
|
||||
AdminUserEntity operator = getOperator(operatorId);
|
||||
if (isSuperAdmin(operator)) {
|
||||
return new AccessScope(true, Set.of());
|
||||
}
|
||||
LinkedHashSet<Long> visibleUserIds = new LinkedHashSet<>();
|
||||
visibleUserIds.add(operator.getId());
|
||||
List<Long> memberUserIds = shopManageGroupMapper.selectManagedMemberUserIds(operator.getId());
|
||||
if (memberUserIds != null) {
|
||||
memberUserIds.stream()
|
||||
.filter(id -> id != null && id > 0)
|
||||
.forEach(visibleUserIds::add);
|
||||
}
|
||||
return new AccessScope(false, Set.copyOf(visibleUserIds));
|
||||
}
|
||||
|
||||
private AdminUserEntity getOperator(Long operatorId) {
|
||||
if (operatorId == null || operatorId <= 0) {
|
||||
throw new BusinessException("当前操作用户不能为空");
|
||||
}
|
||||
AdminUserEntity operator = adminUserMapper.selectById(operatorId);
|
||||
if (operator == null) {
|
||||
throw new BusinessException("当前操作用户不存在");
|
||||
}
|
||||
return operator;
|
||||
}
|
||||
|
||||
private boolean isSuperAdmin(AdminUserEntity user) {
|
||||
String role = user.getRole() == null ? "" : user.getRole().trim().toLowerCase(Locale.ROOT);
|
||||
return "super_admin".equals(role)
|
||||
|| (role.isEmpty() && Integer.valueOf(1).equals(user.getIsAdmin()) && user.getCreatedById() == null);
|
||||
}
|
||||
|
||||
private Set<Long> findUserIdsByUsername(String username, AccessScope scope) {
|
||||
Set<Long> matched = adminUserMapper.selectIdsByUsernameLike(username)
|
||||
.stream()
|
||||
.filter(id -> id != null && id > 0)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (scope.allUsers()) {
|
||||
return matched;
|
||||
}
|
||||
matched.retainAll(scope.userIds());
|
||||
return matched;
|
||||
}
|
||||
|
||||
private void ensureUploaderAccess(Long uploaderUserId, AccessScope scope) {
|
||||
if (!scope.allUsers() && (uploaderUserId == null || !scope.userIds().contains(uploaderUserId))) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "无权操作该总数据");
|
||||
}
|
||||
}
|
||||
|
||||
private DedupeTotalDataPageVo emptyPage(long page, long pageSize) {
|
||||
DedupeTotalDataPageVo vo = new DedupeTotalDataPageVo();
|
||||
vo.setItems(List.of());
|
||||
vo.setTotal(0L);
|
||||
vo.setPage(page);
|
||||
vo.setPageSize(pageSize);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private String normalizeExcelText(String value) {
|
||||
@@ -621,7 +694,12 @@ public class DedupeTotalDataService {
|
||||
DedupeTotalDataItemVo vo = new DedupeTotalDataItemVo();
|
||||
vo.setId(entity.getId());
|
||||
vo.setDataValue(entity.getDataValue());
|
||||
vo.setUploaderUserId(entity.getUploaderUserId());
|
||||
vo.setUsername(entity.getUploaderUsername());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private record AccessScope(boolean allUsers, Set<Long> userIds) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,14 @@ package com.nanri.aiimage.modules.permission.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface AdminUserMapper extends BaseMapper<AdminUserEntity> {
|
||||
|
||||
@Select("SELECT id FROM users WHERE username LIKE CONCAT('%', #{username}, '%') ORDER BY id ASC")
|
||||
List<Long> selectIdsByUsernameLike(@Param("username") String username);
|
||||
}
|
||||
|
||||
@@ -67,4 +67,15 @@ public interface ShopManageGroupMapper extends BaseMapper<ShopManageGroupEntity>
|
||||
ORDER BY visible_user_id ASC
|
||||
""")
|
||||
List<Long> selectAccessibleCreatorUserIds(@Param("operatorId") Long operatorId);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT gm.user_id
|
||||
FROM biz_shop_manage_group g
|
||||
INNER JOIN biz_shop_manage_group_member gm ON gm.group_id = g.id
|
||||
INNER JOIN users u ON u.id = gm.user_id
|
||||
WHERE g.created_by_id = #{operatorId}
|
||||
OR g.user_id = #{operatorId}
|
||||
ORDER BY gm.user_id ASC
|
||||
""")
|
||||
List<Long> selectManagedMemberUserIds(@Param("operatorId") Long operatorId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
SET @uploader_user_id_col_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'biz_dedupe_total_data'
|
||||
AND COLUMN_NAME = 'uploader_user_id'
|
||||
);
|
||||
|
||||
SET @sql_add_uploader_user_id := IF(
|
||||
@uploader_user_id_col_exists = 0,
|
||||
'ALTER TABLE biz_dedupe_total_data ADD COLUMN uploader_user_id BIGINT NULL COMMENT ''上传用户ID'' AFTER data_value',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt_add_uploader_user_id FROM @sql_add_uploader_user_id;
|
||||
EXECUTE stmt_add_uploader_user_id;
|
||||
DEALLOCATE PREPARE stmt_add_uploader_user_id;
|
||||
|
||||
SET @uploader_username_col_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'biz_dedupe_total_data'
|
||||
AND COLUMN_NAME = 'uploader_username'
|
||||
);
|
||||
|
||||
SET @sql_add_uploader_username := IF(
|
||||
@uploader_username_col_exists = 0,
|
||||
'ALTER TABLE biz_dedupe_total_data ADD COLUMN uploader_username VARCHAR(64) NULL COMMENT ''上传用户名'' AFTER uploader_user_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt_add_uploader_username FROM @sql_add_uploader_username;
|
||||
EXECUTE stmt_add_uploader_username;
|
||||
DEALLOCATE PREPARE stmt_add_uploader_username;
|
||||
|
||||
SET @dedupe_root_user_id := (
|
||||
SELECT id
|
||||
FROM users
|
||||
WHERE LOWER(COALESCE(role, '')) = 'super_admin'
|
||||
OR (COALESCE(role, '') = '' AND is_admin = 1 AND created_by_id IS NULL)
|
||||
ORDER BY CASE WHEN username = 'root' THEN 0 ELSE 1 END, id ASC
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SET @dedupe_root_username := (
|
||||
SELECT username
|
||||
FROM users
|
||||
WHERE id = @dedupe_root_user_id
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
DROP TEMPORARY TABLE IF EXISTS dedupe_root_user_guard;
|
||||
CREATE TEMPORARY TABLE dedupe_root_user_guard (
|
||||
user_id BIGINT NOT NULL,
|
||||
username VARCHAR(64) NOT NULL
|
||||
);
|
||||
INSERT INTO dedupe_root_user_guard (user_id, username)
|
||||
VALUES (@dedupe_root_user_id, @dedupe_root_username);
|
||||
DROP TEMPORARY TABLE dedupe_root_user_guard;
|
||||
|
||||
UPDATE biz_dedupe_total_data
|
||||
SET uploader_user_id = @dedupe_root_user_id,
|
||||
uploader_username = @dedupe_root_username
|
||||
WHERE uploader_user_id IS NULL
|
||||
OR uploader_username IS NULL
|
||||
OR uploader_username = '';
|
||||
|
||||
ALTER TABLE biz_dedupe_total_data
|
||||
MODIFY COLUMN uploader_user_id BIGINT NOT NULL COMMENT '上传用户ID',
|
||||
MODIFY COLUMN uploader_username VARCHAR(64) NOT NULL COMMENT '上传用户名';
|
||||
|
||||
SET @uploader_user_id_idx_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'biz_dedupe_total_data'
|
||||
AND INDEX_NAME = 'idx_uploader_user_id_id'
|
||||
);
|
||||
|
||||
SET @sql_add_uploader_user_id_idx := IF(
|
||||
@uploader_user_id_idx_exists = 0,
|
||||
'ALTER TABLE biz_dedupe_total_data ADD INDEX idx_uploader_user_id_id (uploader_user_id, id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt_add_uploader_user_id_idx FROM @sql_add_uploader_user_id_idx;
|
||||
EXECUTE stmt_add_uploader_user_id_idx;
|
||||
DEALLOCATE PREPARE stmt_add_uploader_user_id_idx;
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.nanri.aiimage.modules.dedupe.service;
|
||||
|
||||
import com.nanri.aiimage.modules.dedupe.mapper.DedupeTotalDataMapper;
|
||||
import com.nanri.aiimage.modules.dedupe.model.dto.DedupeTotalDataCreateRequest;
|
||||
import com.nanri.aiimage.modules.dedupe.model.entity.DedupeTotalDataEntity;
|
||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataItemVo;
|
||||
import com.nanri.aiimage.modules.dedupe.model.vo.DedupeTotalDataPageVo;
|
||||
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||
import com.nanri.aiimage.modules.shopkey.mapper.ShopManageGroupMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DedupeTotalDataServiceTest {
|
||||
|
||||
@Mock
|
||||
private DedupeTotalDataMapper dedupeTotalDataMapper;
|
||||
@Mock
|
||||
private AdminUserMapper adminUserMapper;
|
||||
@Mock
|
||||
private ShopManageGroupMapper shopManageGroupMapper;
|
||||
@Mock
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@InjectMocks
|
||||
private DedupeTotalDataService service;
|
||||
|
||||
@Test
|
||||
void createRecordsUploaderIdentity() {
|
||||
AdminUserEntity uploader = user(23L, "normal", "member-a");
|
||||
when(adminUserMapper.selectById(23L)).thenReturn(uploader);
|
||||
when(dedupeTotalDataMapper.selectOne(any())).thenReturn(null);
|
||||
when(dedupeTotalDataMapper.insert(any(DedupeTotalDataEntity.class))).thenAnswer(invocation -> {
|
||||
DedupeTotalDataEntity entity = invocation.getArgument(0);
|
||||
entity.setId(91L);
|
||||
return 1;
|
||||
});
|
||||
when(dedupeTotalDataMapper.selectById(91L)).thenAnswer(invocation -> {
|
||||
DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
|
||||
entity.setId(91L);
|
||||
entity.setDataValue("B012345678");
|
||||
entity.setUploaderUserId(23L);
|
||||
entity.setUploaderUsername("member-a");
|
||||
return entity;
|
||||
});
|
||||
|
||||
DedupeTotalDataCreateRequest request = new DedupeTotalDataCreateRequest();
|
||||
request.setDataValue(" B012345678 ");
|
||||
DedupeTotalDataItemVo item = service.create(request, 23L);
|
||||
|
||||
ArgumentCaptor<DedupeTotalDataEntity> captor = ArgumentCaptor.forClass(DedupeTotalDataEntity.class);
|
||||
verify(dedupeTotalDataMapper).insert(captor.capture());
|
||||
assertEquals(23L, captor.getValue().getUploaderUserId());
|
||||
assertEquals("member-a", captor.getValue().getUploaderUsername());
|
||||
assertEquals("member-a", item.getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
void leaderCanDeleteManagedMemberData() {
|
||||
when(adminUserMapper.selectById(10L)).thenReturn(user(10L, "admin", "leader"));
|
||||
when(shopManageGroupMapper.selectManagedMemberUserIds(10L)).thenReturn(List.of(23L));
|
||||
when(dedupeTotalDataMapper.selectById(91L)).thenReturn(data(91L, 23L));
|
||||
|
||||
service.delete(91L, 10L);
|
||||
|
||||
verify(dedupeTotalDataMapper).deleteById(91L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void memberCannotDeleteAnotherUsersData() {
|
||||
when(adminUserMapper.selectById(23L)).thenReturn(user(23L, "normal", "member-a"));
|
||||
when(shopManageGroupMapper.selectManagedMemberUserIds(23L)).thenReturn(List.of());
|
||||
when(dedupeTotalDataMapper.selectById(91L)).thenReturn(data(91L, 24L));
|
||||
|
||||
ResponseStatusException exception = assertThrows(
|
||||
ResponseStatusException.class,
|
||||
() -> service.delete(91L, 23L));
|
||||
|
||||
assertEquals(403, exception.getStatusCode().value());
|
||||
verify(dedupeTotalDataMapper, never()).deleteById(any(Long.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void superAdminCanDeleteAnyUsersData() {
|
||||
when(adminUserMapper.selectById(1L)).thenReturn(user(1L, "super_admin", "root"));
|
||||
when(dedupeTotalDataMapper.selectById(91L)).thenReturn(data(91L, 99L));
|
||||
|
||||
service.delete(91L, 1L);
|
||||
|
||||
verify(dedupeTotalDataMapper).deleteById(91L);
|
||||
verify(shopManageGroupMapper, never()).selectManagedMemberUserIds(any(Long.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateExcelValueKeepsOriginalUploader() throws Exception {
|
||||
when(adminUserMapper.selectById(23L)).thenReturn(user(23L, "normal", "member-a"));
|
||||
when(dedupeTotalDataMapper.selectOne(any())).thenReturn(data(91L, 99L));
|
||||
MockMultipartFile file = asinWorkbook("B012345678");
|
||||
|
||||
var result = service.importFromExcel(file, 23L);
|
||||
|
||||
assertEquals(0, result.getInsertedCount());
|
||||
assertEquals(1, result.getSkippedCount());
|
||||
verify(dedupeTotalDataMapper, never()).insert(any(DedupeTotalDataEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void usernameSearchCannotExpandLeaderScope() {
|
||||
when(adminUserMapper.selectById(10L)).thenReturn(user(10L, "admin", "leader"));
|
||||
when(shopManageGroupMapper.selectManagedMemberUserIds(10L)).thenReturn(List.of(23L));
|
||||
when(adminUserMapper.selectIdsByUsernameLike("other")).thenReturn(List.of(99L));
|
||||
|
||||
DedupeTotalDataPageVo page = service.page(1, 15, "", "other", 10L);
|
||||
|
||||
assertEquals(0L, page.getTotal());
|
||||
assertTrue(page.getItems().isEmpty());
|
||||
verify(dedupeTotalDataMapper, never()).selectCount(any());
|
||||
verify(dedupeTotalDataMapper, never()).selectList(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void comparableValueLookupRemainsGlobal() {
|
||||
when(dedupeTotalDataMapper.selectExistingDataValues(List.of("B012345678")))
|
||||
.thenReturn(List.of("B012345678"));
|
||||
|
||||
Set<String> values = service.findExistingComparableValues(List.of(" B012345678 "));
|
||||
|
||||
assertEquals(Set.of("B012345678"), values);
|
||||
verify(adminUserMapper, never()).selectById(any(Long.class));
|
||||
verify(shopManageGroupMapper, never()).selectManagedMemberUserIds(any(Long.class));
|
||||
}
|
||||
|
||||
private AdminUserEntity user(Long id, String role, String username) {
|
||||
AdminUserEntity user = new AdminUserEntity();
|
||||
user.setId(id);
|
||||
user.setRole(role);
|
||||
user.setUsername(username);
|
||||
user.setIsAdmin("normal".equals(role) ? 0 : 1);
|
||||
return user;
|
||||
}
|
||||
|
||||
private DedupeTotalDataEntity data(Long id, Long uploaderUserId) {
|
||||
DedupeTotalDataEntity entity = new DedupeTotalDataEntity();
|
||||
entity.setId(id);
|
||||
entity.setDataValue("B012345678");
|
||||
entity.setUploaderUserId(uploaderUserId);
|
||||
entity.setUploaderUsername("uploader-" + uploaderUserId);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private MockMultipartFile asinWorkbook(String asin) throws Exception {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
var sheet = workbook.createSheet("data");
|
||||
sheet.createRow(0).createCell(0).setCellValue("ASIN");
|
||||
sheet.createRow(1).createCell(0).setCellValue(asin);
|
||||
workbook.write(output);
|
||||
return new MockMultipartFile(
|
||||
"file",
|
||||
"data.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
output.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user