视频任务管理更新

This commit is contained in:
super
2026-07-19 22:23:26 +08:00
parent 69784b8d32
commit cbde22ec79
62 changed files with 2164 additions and 280 deletions

View File

@@ -149,4 +149,19 @@ class AppearancePatentCozeClientTest {
assertThat(merged.get(0).getAppearanceRisk()).isNullOrEmpty();
verify(brandCheckClient, never()).checkTitleText(anyString(), anyString());
}
@Test
void preserveSubmittedPriceWhenMergingCozeResult() throws Exception {
AppearancePatentResultRowDto row = new AppearancePatentResultRowDto();
row.setId("1");
row.setPrice("12.99");
String dataText = """
{"data":[{"row_id":"1","title":"","appearance":"无侵权","result":"无侵权"}]}
""";
List<AppearancePatentResultRowDto> merged = client.mergeRowsFromDataText(List.of(row), dataText);
assertThat(merged).hasSize(1);
assertThat(merged.get(0).getPrice()).isEqualTo("12.99");
}
}

View File

@@ -34,4 +34,16 @@ class AppearancePatentTaskServiceTest {
parsedRow.getValues().clear();
assertEquals("", AppearancePatentTaskService.resolveBrand(resultRow, parsedRow));
}
@Test
void resultPriceUsesOnlyPythonSubmittedValue() {
assertEquals("", AppearancePatentTaskService.resolvePrice(null));
AppearancePatentResultRowDto resultRow = new AppearancePatentResultRowDto();
resultRow.setPrice(" 12.99 ");
assertEquals("12.99", AppearancePatentTaskService.resolvePrice(resultRow));
resultRow.setPrice(null);
assertEquals("", AppearancePatentTaskService.resolvePrice(resultRow));
}
}

View File

@@ -13,10 +13,53 @@ import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class ImageVideoArchiveServiceTest {
@Test
void deletesExpiredWorkflowTasksAndArchivedObjects() {
ImageVideoAsyncTaskMapper taskMapper = mock(ImageVideoAsyncTaskMapper.class);
OssStorageService ossStorageService = mock(OssStorageService.class);
ImageVideoArchiveService archiveService = new ImageVideoArchiveService(
taskMapper, ossStorageService, new ImageVideoProperties(), new ObjectMapper());
ImageVideoAsyncTaskEntity task = new ImageVideoAsyncTaskEntity();
task.setId(10L);
task.setTaskType(ImageVideoArchiveService.TASK_TYPE);
task.setArchivedVideosJson("[{\"objectKey\":\"result/image_video/10/video.mp4\"}]");
when(taskMapper.selectList(any())).thenReturn(List.of(task));
when(taskMapper.deleteById(10L)).thenReturn(1);
archiveService.cleanupExpiredWorkflowTasks();
verify(ossStorageService).deleteObject("result/image_video/10/video.mp4");
verify(taskMapper).deleteById(10L);
}
@Test
void keepsExpiredTaskWhenArchivedObjectDeletionFails() {
ImageVideoAsyncTaskMapper taskMapper = mock(ImageVideoAsyncTaskMapper.class);
OssStorageService ossStorageService = mock(OssStorageService.class);
ImageVideoArchiveService archiveService = new ImageVideoArchiveService(
taskMapper, ossStorageService, new ImageVideoProperties(), new ObjectMapper());
ImageVideoAsyncTaskEntity task = new ImageVideoAsyncTaskEntity();
task.setId(11L);
task.setTaskType(ImageVideoArchiveService.TASK_TYPE);
task.setArchivedVideosJson("[{\"objectKey\":\"result/image_video/11/video.mp4\"}]");
when(taskMapper.selectList(any())).thenReturn(List.of(task));
doThrow(new IllegalStateException("OSS unavailable"))
.when(ossStorageService).deleteObject("result/image_video/11/video.mp4");
archiveService.cleanupExpiredWorkflowTasks();
verify(taskMapper, never()).deleteById(11L);
}
@Test
void extractsNestedAndMultipleVideoUrlsWithoutDuplicates() {
Map<String, Object> result = Map.of(

View File

@@ -139,6 +139,69 @@ class ImageVideoAsyncTaskServiceTest {
verify(taskMapper).updateById(task);
}
@Test
void failedTaskIsReturnedOnceAndThenDeleted() {
ImageVideoAsyncTaskMapper taskMapper = mock(ImageVideoAsyncTaskMapper.class);
ImageVideoAsyncTaskService service = new ImageVideoAsyncTaskService(
taskMapper,
mock(ImageVideoCozeService.class),
mock(ImageVideoWorkflowConfigService.class),
mock(ImageVideoArchiveService.class),
new ObjectMapper(),
Runnable::run);
ImageVideoAsyncTaskEntity task = waitingWorkflowTask();
task.setStatus("FAILED");
task.setCozeStatus("FAIL");
task.setErrorMessage("model unavailable");
when(taskMapper.selectOne(any())).thenReturn(task);
var result = service.getTask(98L, 1L);
assertEquals("FAILED", result.getStatus());
assertEquals("model unavailable", result.getErrorMessage());
verify(taskMapper).deleteById(98L);
}
@Test
void recoverableFailedTaskIsNotDeleted() {
ImageVideoAsyncTaskMapper taskMapper = mock(ImageVideoAsyncTaskMapper.class);
ImageVideoAsyncTaskService service = new ImageVideoAsyncTaskService(
taskMapper,
mock(ImageVideoCozeService.class),
mock(ImageVideoWorkflowConfigService.class),
mock(ImageVideoArchiveService.class),
new ObjectMapper(),
Runnable::run);
ImageVideoAsyncTaskEntity task = waitingWorkflowTask();
task.setStatus("FAILED");
task.setCozeStatus("RUNNING");
task.setErrorMessage("Coze workflow finished with status RUNNING");
when(taskMapper.selectOne(any())).thenReturn(task);
var result = service.getTask(98L, 1L);
assertEquals("WAITING", result.getStatus());
verify(taskMapper).updateById(task);
verify(taskMapper, never()).deleteById(98L);
}
@Test
void cleanupRemovesFailedTasksThatWereNotPolled() {
ImageVideoAsyncTaskMapper taskMapper = mock(ImageVideoAsyncTaskMapper.class);
ImageVideoAsyncTaskService service = new ImageVideoAsyncTaskService(
taskMapper,
mock(ImageVideoCozeService.class),
mock(ImageVideoWorkflowConfigService.class),
mock(ImageVideoArchiveService.class),
new ObjectMapper(),
Runnable::run);
when(taskMapper.delete(any())).thenReturn(3);
service.cleanupFailedTasks();
verify(taskMapper).delete(any());
}
private ImageVideoAsyncTaskEntity waitingDouyinTask() {
ImageVideoAsyncTaskEntity task = new ImageVideoAsyncTaskEntity();
task.setId(79L);

View File

@@ -0,0 +1,70 @@
package com.nanri.aiimage.modules.permission.service;
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
import com.nanri.aiimage.modules.permission.mapper.PermissionMenuMapper;
import com.nanri.aiimage.modules.permission.mapper.UserColumnPermissionMapper;
import com.nanri.aiimage.modules.permission.model.dto.UserColumnPermissionUpdateRequest;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import com.nanri.aiimage.modules.permission.model.entity.PermissionMenuEntity;
import com.nanri.aiimage.modules.permission.model.entity.UserColumnPermissionEntity;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class PermissionMenuServiceTest {
@Test
void preservesExistingImageVideoPermissionDuringGenericReplacement() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(userMapper.selectById(7L)).thenReturn(new AdminUserEntity());
when(menuMapper.selectCount(any())).thenReturn(1L);
when(menuMapper.selectOne(any())).thenReturn(imageVideoPermission());
when(permissionMapper.selectCount(any())).thenReturn(1L);
UserColumnPermissionUpdateRequest request = new UserColumnPermissionUpdateRequest();
request.setColumnIds(List.of(2L));
service.updateUserColumnPermissions(7L, request);
ArgumentCaptor<UserColumnPermissionEntity> captor = ArgumentCaptor.forClass(UserColumnPermissionEntity.class);
verify(permissionMapper, times(2)).insert(captor.capture());
assertThat(captor.getAllValues())
.extracting(UserColumnPermissionEntity::getColumnId)
.containsExactly(2L, 75L);
}
@Test
void doesNotGrantImageVideoPermissionThroughGenericReplacement() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(userMapper.selectById(8L)).thenReturn(new AdminUserEntity());
when(menuMapper.selectCount(any())).thenReturn(1L);
when(menuMapper.selectOne(any())).thenReturn(imageVideoPermission());
when(permissionMapper.selectCount(any())).thenReturn(0L);
UserColumnPermissionUpdateRequest request = new UserColumnPermissionUpdateRequest();
request.setColumnIds(List.of(75L));
service.updateUserColumnPermissions(8L, request);
verify(permissionMapper, times(0)).insert(any(UserColumnPermissionEntity.class));
}
private PermissionMenuEntity imageVideoPermission() {
PermissionMenuEntity entity = new PermissionMenuEntity();
entity.setId(75L);
entity.setColumnKey("admin_image_video_tasks");
return entity;
}
}

View File

@@ -0,0 +1,89 @@
package com.nanri.aiimage.modules.pricetrack.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.config.TaskPressureProperties;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import com.nanri.aiimage.modules.pricetrack.mapper.PriceTrackShopCandidateMapper;
import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
import com.nanri.aiimage.modules.task.service.TaskDistributedLockService;
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
import com.nanri.aiimage.modules.task.service.TaskResultPayloadService;
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PriceTrackTaskServiceTest {
@Mock private FileTaskMapper fileTaskMapper;
@Mock private FileResultMapper fileResultMapper;
@Mock private PriceTrackShopCandidateMapper candidateMapper;
@Mock private ZiniaoShopSwitchService ziniaoShopSwitchService;
@Mock private OssStorageService ossStorageService;
@Mock private ObjectMapper objectMapper;
@Mock private SkipPriceAsinService skipPriceAsinService;
@Mock private PriceTrackExcelAssemblyService excelAssemblyService;
@Mock private PriceTrackTaskCacheService priceTrackTaskCacheService;
@Mock private PriceTrackLoopRunService priceTrackLoopRunService;
@Mock private TaskPressureProperties taskPressureProperties;
@Mock private TaskResultPayloadService taskResultPayloadService;
@Mock private TaskFileJobService taskFileJobService;
@Mock private TaskDistributedLockService taskDistributedLockService;
@InjectMocks private PriceTrackTaskService service;
@Test
void markDispatchFailedImmediatelyFinalizesUnqueuedTask() {
long taskId = 19788L;
long userId = 671L;
String error = "当前店铺 张志云 正在执行中,请等待店铺完成之后重试";
FileTaskEntity task = new FileTaskEntity();
task.setId(taskId);
task.setUserId(userId);
task.setModuleType("PRICE_TRACK");
task.setStatus("RUNNING");
task.setRequestJson("{}");
FileResultEntity result = new FileResultEntity();
result.setId(9001L);
result.setTaskId(taskId);
result.setModuleType("PRICE_TRACK");
result.setSourceFilename("张志云");
result.setSuccess(0);
TaskDistributedLockService.LockHandle lock = mock(TaskDistributedLockService.LockHandle.class);
when(taskDistributedLockService.acquire("PRICE_TRACK", taskId)).thenReturn(lock);
when(priceTrackTaskCacheService.getTaskCacheBatch(List.of(taskId))).thenReturn(Map.of(taskId, task));
when(fileResultMapper.selectList(any())).thenReturn(List.of(result));
service.markDispatchFailed(taskId, userId, error);
assertEquals("FAILED", task.getStatus());
assertEquals("张志云: " + error, task.getErrorMessage());
assertNotNull(task.getFinishedAt());
assertEquals(error, result.getErrorMessage());
verify(fileResultMapper).updateById(result);
verify(fileTaskMapper).updateById(task);
verify(priceTrackTaskCacheService).deleteTaskCache(taskId);
verify(priceTrackLoopRunService).syncLoopRunAfterChildTerminal(taskId);
verify(lock).close();
}
}

View File

@@ -1,9 +1,11 @@
package com.nanri.aiimage.modules.productrisk.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskRowDto;
import com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;
@@ -15,6 +17,9 @@ import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ProductRiskExcelAssemblyServiceTest {
@@ -47,6 +52,76 @@ class ProductRiskExcelAssemblyServiceTest {
}
}
@Test
void writesViolationDetailsAndMergesRepeatedAsinCells() throws Exception {
Path xlsx = Files.createTempFile("product-risk-details-", ".xlsx");
try {
ProductRiskRowDto row = row("B0D2LG8R11", "处理失败", Boolean.FALSE);
row.setViolationDetails(List.of(
detail("商品安全违规", "成功"),
detail("商品安全违规", "失败"),
detail("商品安全违规", "成功")));
ProductRiskRowDto secondRow = row("B0987884CV", "处理完成", Boolean.FALSE);
secondRow.setViolationDetails(List.of(
detail("商品安全违规", "成功"),
detail("商品详情页违规", "成功")));
service.writeWorkbook(
xlsx.toFile(),
"店铺A",
Map.of(ProductRiskCountryCode.DE.getSheetName(), List.of(row, secondRow)));
try (InputStream in = Files.newInputStream(xlsx);
Workbook workbook = new XSSFWorkbook(in)) {
Sheet sheet = workbook.getSheet(ProductRiskCountryCode.DE.getSheetName());
assertEquals("ASIN", sheet.getRow(0).getCell(6).getStringCellValue());
assertEquals("原因", sheet.getRow(0).getCell(7).getStringCellValue());
assertEquals("状态", sheet.getRow(0).getCell(8).getStringCellValue());
assertEquals("B0D2LG8R11", sheet.getRow(1).getCell(6).getStringCellValue());
assertEquals("商品安全违规", sheet.getRow(2).getCell(7).getStringCellValue());
assertEquals("失败", sheet.getRow(2).getCell(8).getStringCellValue());
assertTrue(sheet.getMergedRegions().contains(new CellRangeAddress(1, 3, 6, 6)));
assertTrue(sheet.getMergedRegions().contains(new CellRangeAddress(4, 5, 6, 6)));
assertEquals("处理失败", sheet.getRow(1).getCell(2).getStringCellValue());
assertEquals("B0987884CV", sheet.getRow(4).getCell(1).getStringCellValue());
assertEquals("B0987884CV", sheet.getRow(4).getCell(6).getStringCellValue());
assertEquals("商品详情页违规", sheet.getRow(5).getCell(7).getStringCellValue());
}
} finally {
Files.deleteIfExists(xlsx);
}
}
@Test
void violationDetailsUseLatestNonEmptyListAndIgnoreEmptyRetry() {
List<ProductRiskRowDto.ViolationDetail> original = List.of(detail("旧原因", "成功"));
List<ProductRiskRowDto.ViolationDetail> replacement = List.of(
detail("商品安全违规", "成功"),
detail("商品安全违规", "失败"));
List<ProductRiskRowDto.ViolationDetail> kept =
ProductRiskTaskService.mergeViolationDetails(original, List.of());
List<ProductRiskRowDto.ViolationDetail> replaced =
ProductRiskTaskService.mergeViolationDetails(original, replacement);
assertEquals(1, kept.size());
assertEquals("旧原因", kept.get(0).getReason());
assertEquals(2, replaced.size());
assertEquals("商品安全违规", replaced.get(1).getReason());
assertEquals("失败", replaced.get(1).getStatus());
assertNotSame(replacement.get(0), replaced.get(0));
}
@Test
void oldPayloadWithoutViolationDetailsRemainsCompatible() throws Exception {
ProductRiskRowDto row = new ObjectMapper().readValue(
"{\"shopName\":\"店铺A\",\"productAsinSku\":\"B0D2LG8R11\",\"status\":\"处理完成\"}",
ProductRiskRowDto.class);
assertNotNull(row.getViolationDetails());
assertTrue(row.getViolationDetails().isEmpty());
}
private static ProductRiskRowDto row(String productAsinSku, String status, Boolean done) {
ProductRiskRowDto row = new ProductRiskRowDto();
row.setShopName("店铺A");
@@ -55,4 +130,11 @@ class ProductRiskExcelAssemblyServiceTest {
row.setDone(done);
return row;
}
private static ProductRiskRowDto.ViolationDetail detail(String reason, String status) {
ProductRiskRowDto.ViolationDetail detail = new ProductRiskRowDto.ViolationDetail();
detail.setReason(reason);
detail.setStatus(status);
return detail;
}
}