From 21bbd0386deb9c7cf098413fa98b5471dc616ff3 Mon Sep 17 00:00:00 2001 From: irving Date: Fri, 5 Dec 2025 22:16:01 -0500 Subject: [PATCH] feat(media): refine clerk album review and tests --- docker/docker-compose-apitest.yml | 8 + .../weichat/controller/WxClerkController.java | 92 +++++- .../admin/api/WxClerkAlbumUpdateApiTest.java | 271 ++++++++++++++++++ .../api/WxClerkMediaControllerApiTest.java | 33 +-- ...layClerkDataReviewInfoServiceImplTest.java | 75 ++++- .../PlayClerkUserInfoServiceImplTest.java | 19 ++ .../service/MediaUploadServiceTest.java | 1 - 7 files changed, 469 insertions(+), 30 deletions(-) create mode 100644 play-admin/src/test/java/com/starry/admin/api/WxClerkAlbumUpdateApiTest.java diff --git a/docker/docker-compose-apitest.yml b/docker/docker-compose-apitest.yml index dd4abae..fb662b0 100644 --- a/docker/docker-compose-apitest.yml +++ b/docker/docker-compose-apitest.yml @@ -22,3 +22,11 @@ services: interval: 10s timeout: 5s retries: 10 + + redis-apitest: + image: redis:7-alpine + container_name: peipei-redis-apitest + restart: unless-stopped + command: ["redis-server", "--appendonly", "no"] + ports: + - "36379:6379" diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkController.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkController.java index da7b3ed..271d302 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkController.java +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxClerkController.java @@ -6,12 +6,16 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.starry.admin.common.aspect.ClerkUserLogin; import com.starry.admin.common.conf.ThreadLocalRequestDetail; import com.starry.admin.common.exception.CustomException; +import com.starry.admin.modules.clerk.enums.ClerkMediaReviewState; +import com.starry.admin.modules.clerk.enums.ClerkMediaUsage; import com.starry.admin.modules.clerk.module.entity.*; import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityEditVo; import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityQueryVo; import com.starry.admin.modules.clerk.service.*; import com.starry.admin.modules.clerk.service.impl.PlayClerkUserInfoServiceImpl; import com.starry.admin.modules.clerk.service.impl.PlayClerkUserReviewInfoServiceImpl; +import com.starry.admin.modules.media.enums.MediaOwnerType; +import com.starry.admin.modules.media.service.IPlayMediaService; import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; import com.starry.admin.modules.order.module.vo.PlayOrderCompleteVo; @@ -54,6 +58,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -121,6 +126,10 @@ public class WxClerkController { private SmsUtils smsUtils; @Resource private WxCustomMpService wxCustomMpService; + @Resource + private com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService clerkMediaAssetService; + @Resource + private IPlayMediaService mediaService; /** * 店员获取个人业绩信息 @@ -313,18 +322,77 @@ public class WxClerkController { @PostMapping("/user/updateAlbum") public R updateAlbum(@Validated @RequestBody PlayClerkUserAlbumVo vo) { PlayClerkUserInfoEntity userInfo = ThreadLocalRequestDetail.getClerkUserInfo(); - // PlayClerkDataReviewInfoEntity entity = - // playClerkDataReviewInfoService.queryByClerkId(userInfo.getId(), "2", "0"); - // if (entity != null) { - // throw new CustomException("已有申请未审核"); - // } - PlayClerkDataReviewInfoEntity entity = new PlayClerkDataReviewInfoEntity(); - entity.setClerkId(userInfo.getId()); - entity.setDataType("2"); - entity.setReviewState("0"); - entity.setDataContent(vo.getAlbum()); - playClerkDataReviewInfoService.create(entity); - return R.ok("提交成功,等待审核~"); + List requested = vo.getAlbum() == null ? new ArrayList<>() : vo.getAlbum().stream() + .filter(StrUtil::isNotBlank) + .map(String::trim) + .distinct() + .collect(Collectors.toList()); + + // 查询当前所有已审核通过的 PROFILE 媒资 + List approvedAssets = clerkMediaAssetService.listByState( + userInfo.getId(), + ClerkMediaUsage.PROFILE, + Collections.singletonList(ClerkMediaReviewState.APPROVED)); + + LinkedHashSet requestedSet = new LinkedHashSet<>(requested); + if (requestedSet.isEmpty()) { + throw new CustomException("最少上传一张照片"); + } + + // 计算哪些是新媒资(需走审核),哪些是纯删除/排序 + java.util.Set approvedIds = approvedAssets.stream() + .map(PlayClerkMediaAssetEntity::getMediaId) + .filter(StrUtil::isNotBlank) + .collect(java.util.stream.Collectors.toSet()); + java.util.Set newMediaIds = requestedSet.stream() + .filter(id -> !approvedIds.contains(id)) + .collect(java.util.stream.Collectors.toSet()); + + if (!newMediaIds.isEmpty()) { + // 新增媒资必须是当前店员本人名下、已就绪的媒资,才能进入审核流程 + java.util.List newMediaEntities = + mediaService.lambdaQuery() + .in(com.starry.admin.modules.media.entity.PlayMediaEntity::getId, newMediaIds) + .list(); + if (newMediaEntities.size() != newMediaIds.size()) { + throw new CustomException("存在未完成上传的照片/视频,请稍后重试"); + } + for (com.starry.admin.modules.media.entity.PlayMediaEntity media : newMediaEntities) { + if (!userInfo.getTenantId().equals(media.getTenantId()) + || media.getOwnerType() != MediaOwnerType.CLERK + || !userInfo.getId().equals(media.getOwnerId())) { + throw new CustomException("存在无效的照片/视频,请刷新后重试"); + } + if (!com.starry.admin.modules.media.enums.MediaStatus.READY.getCode().equals(media.getStatus())) { + throw new CustomException("存在未完成上传的照片/视频,请稍后重试"); + } + } + + // 只要存在新增媒资,则按原有逻辑走资料审核,由审核通过时统一生效 + PlayClerkDataReviewInfoEntity entity = new PlayClerkDataReviewInfoEntity(); + entity.setClerkId(userInfo.getId()); + entity.setDataType("2"); + entity.setReviewState("0"); + entity.setDataContent(new ArrayList<>(requestedSet)); + playClerkDataReviewInfoService.create(entity); + return R.ok("提交成功,等待审核~"); + } + + // 仅删除/排序:直接应用变更,不再生成审核记录 + // 先根据新的顺序更新 orderIndex + clerkMediaAssetService.reorder(userInfo.getId(), ClerkMediaUsage.PROFILE, new ArrayList<>(requestedSet)); + + // 再对不再保留的媒资执行软删除 + java.util.Set requestedOnly = new java.util.HashSet<>(requestedSet); + java.util.Set deletedMediaIds = approvedIds.stream() + .filter(id -> !requestedOnly.contains(id)) + .collect(java.util.stream.Collectors.toSet()); + for (String mediaId : deletedMediaIds) { + clerkMediaAssetService.softDelete(userInfo.getId(), mediaId); + mediaService.softDelete(MediaOwnerType.CLERK, userInfo.getId(), mediaId); + } + + return R.ok("修改成功"); } @ClerkUserLogin diff --git a/play-admin/src/test/java/com/starry/admin/api/WxClerkAlbumUpdateApiTest.java b/play-admin/src/test/java/com/starry/admin/api/WxClerkAlbumUpdateApiTest.java new file mode 100644 index 0000000..260fbb6 --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/api/WxClerkAlbumUpdateApiTest.java @@ -0,0 +1,271 @@ +package com.starry.admin.api; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.starry.admin.common.apitest.ApiTestDataSeeder; +import com.starry.admin.modules.clerk.enums.ClerkMediaReviewState; +import com.starry.admin.modules.clerk.enums.ClerkMediaUsage; +import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity; +import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; +import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService; +import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService; +import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService; +import com.starry.admin.modules.media.entity.PlayMediaEntity; +import com.starry.admin.modules.media.enums.MediaOwnerType; +import com.starry.admin.modules.media.enums.MediaStatus; +import com.starry.admin.modules.media.service.IPlayMediaService; +import com.starry.admin.modules.weichat.entity.clerk.MediaVo; +import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo; +import com.starry.admin.modules.weichat.service.WxTokenService; +import com.starry.admin.utils.SecurityUtils; +import com.starry.common.constant.Constants; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; + +/** + * 专门验证 /wx/clerk/user/updateAlbum 在“只有删除/排序”时不会创建新的审核记录, + * 并且会立即更新顾客端视图。 + */ +class WxClerkAlbumUpdateApiTest extends AbstractApiTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private WxTokenService wxTokenService; + + @Autowired + private IPlayClerkUserInfoService clerkUserInfoService; + + @Autowired + private IPlayClerkDataReviewInfoService dataReviewInfoService; + + @Autowired + private IPlayClerkMediaAssetService mediaAssetService; + + @Autowired + private IPlayMediaService mediaService; + + @Test + void reorderExistingApprovedMediaDoesNotCreateNewReviewAndUpdatesOrder() throws Exception { + ensureTenantContext(); + String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID; + String clerkToken = wxTokenService.createWxUserToken(clerkId); + clerkUserInfoService.updateTokenById(clerkId, clerkToken); + + PlayMediaEntity media1 = seedMedia(clerkId); + PlayMediaEntity media2 = seedMedia(clerkId); + PlayMediaEntity media3 = seedMedia(clerkId); + + seedApprovedAsset(clerkId, media1.getId(), 0); + seedApprovedAsset(clerkId, media2.getId(), 1); + seedApprovedAsset(clerkId, media3.getId(), 2); + + long reviewCountBefore = dataReviewInfoService.lambdaQuery() + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId) + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2") + .count(); + + // 仅调整顺序:album 只包含已审核通过的媒资 id,不引入任何新媒资 + List reordered = List.of(media3.getId(), media1.getId(), media2.getId()); + ObjectNode payload = objectMapper.createObjectNode(); + com.fasterxml.jackson.databind.node.ArrayNode albumArray = payload.putArray("album"); + reordered.forEach(albumArray::add); + + mockMvc.perform(post("/wx/clerk/user/updateAlbum") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken) + .contentType(MediaType.APPLICATION_JSON) + .content(payload.toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + + ensureTenantContext(); + long reviewCountAfter = dataReviewInfoService.lambdaQuery() + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId) + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2") + .count(); + assertThat(reviewCountAfter) + .as("reordering without introducing new media should not create review records") + .isEqualTo(reviewCountBefore); + + List assets = mediaAssetService + .listByState(clerkId, ClerkMediaUsage.PROFILE, + Collections.singletonList(ClerkMediaReviewState.APPROVED)); + PlayClerkMediaAssetEntity asset1 = assets.stream() + .filter(a -> media1.getId().equals(a.getMediaId())) + .max(java.util.Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime)) + .orElse(null); + PlayClerkMediaAssetEntity asset2 = assets.stream() + .filter(a -> media2.getId().equals(a.getMediaId())) + .max(java.util.Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime)) + .orElse(null); + PlayClerkMediaAssetEntity asset3 = assets.stream() + .filter(a -> media3.getId().equals(a.getMediaId())) + .max(java.util.Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime)) + .orElse(null); + + assertThat(asset3).as("asset for media3 should exist").isNotNull(); + assertThat(asset1).as("asset for media1 should exist").isNotNull(); + assertThat(asset2).as("asset for media2 should exist").isNotNull(); + + assertThat(asset3.getOrderIndex()).isEqualTo(0); + assertThat(asset1.getOrderIndex()).isEqualTo(1); + assertThat(asset2.getOrderIndex()).isEqualTo(2); + + PlayClerkUserInfoResultVo detail = clerkUserInfoService.buildCustomerDetail(clerkId, ""); + List customerMediaIds = detail.getMediaList().stream() + .map(MediaVo::getMediaId) + .collect(Collectors.toList()); + + int index3 = customerMediaIds.indexOf(media3.getId()); + int index1 = customerMediaIds.indexOf(media1.getId()); + int index2 = customerMediaIds.indexOf(media2.getId()); + + assertThat(index3).isGreaterThanOrEqualTo(0); + assertThat(index1).isGreaterThanOrEqualTo(0); + assertThat(index2).isGreaterThanOrEqualTo(0); + assertThat(index3).isLessThan(index1); + assertThat(index1).isLessThan(index2); + } + + @Test + void deleteAndReorderAlbumDoesNotCreateNewReviewAndIsImmediatelyVisible() throws Exception { + ensureTenantContext(); + String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID; + String clerkToken = wxTokenService.createWxUserToken(clerkId); + clerkUserInfoService.updateTokenById(clerkId, clerkToken); + + // 预置两条已审核通过的 PROFILE 媒资 + PlayMediaEntity media1 = seedMedia(clerkId); + PlayMediaEntity media2 = seedMedia(clerkId); + + seedApprovedAsset(clerkId, media1.getId(), 0); + seedApprovedAsset(clerkId, media2.getId(), 1); + + // 顾客端初始视图应包含两条媒资 + PlayClerkUserInfoResultVo beforeDetail = clerkUserInfoService.buildCustomerDetail(clerkId, ""); + List beforeIds = beforeDetail.getMediaList().stream() + .map(MediaVo::getMediaId) + .collect(Collectors.toList()); + assertThat(beforeIds).contains(media1.getId(), media2.getId()); + + // 记录当前相册审核记录数量 + long reviewCountBefore = dataReviewInfoService.lambdaQuery() + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId) + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2") + .count(); + + // 现在通过 updateAlbum 只保留 media2,相当于“删除 media1 + 不引入新媒资” + ObjectNode payload = objectMapper.createObjectNode(); + payload.putArray("album").add(media2.getId()); + + mockMvc.perform(post("/wx/clerk/user/updateAlbum") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken) + .contentType(MediaType.APPLICATION_JSON) + .content(payload.toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)); + + ensureTenantContext(); + long reviewCountAfter = dataReviewInfoService.lambdaQuery() + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId) + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2") + .count(); + assertThat(reviewCountAfter) + .as("Deleting/reordering without new media should not create new review records") + .isEqualTo(reviewCountBefore); + + // 资产表中仅剩 media2 且仍为 APPROVED 状态 + List assets = mediaAssetService + .listByState(clerkId, ClerkMediaUsage.PROFILE, + Collections.singletonList(ClerkMediaReviewState.APPROVED)); + List assetMediaIds = assets.stream() + .map(PlayClerkMediaAssetEntity::getMediaId) + .collect(Collectors.toList()); + assertThat(assetMediaIds) + .contains(media2.getId()) + .doesNotContain(media1.getId()); + + // 顾客端视图应立即反映删除结果 + PlayClerkUserInfoResultVo afterDetail = clerkUserInfoService.buildCustomerDetail(clerkId, ""); + List afterIds = afterDetail.getMediaList().stream() + .map(MediaVo::getMediaId) + .collect(Collectors.toList()); + assertThat(afterIds) + .contains(media2.getId()) + .doesNotContain(media1.getId()); + } + + @Test + void updateAlbumRejectsEmptyAlbumPayload() throws Exception { + ensureTenantContext(); + String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID; + String clerkToken = wxTokenService.createWxUserToken(clerkId); + clerkUserInfoService.updateTokenById(clerkId, clerkToken); + + ObjectNode payload = objectMapper.createObjectNode(); + payload.putArray("album"); // 空数组 + + MvcResult result = mockMvc.perform(post("/wx/clerk/user/updateAlbum") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken) + .contentType(MediaType.APPLICATION_JSON) + .content(payload.toString())) + .andExpect(status().isOk()) + .andReturn(); + + String body = result.getResponse().getContentAsString(); + com.fasterxml.jackson.databind.JsonNode root = new ObjectMapper().readTree(body); + assertThat(root.path("code").asInt()) + .as("empty album should be rejected, response=%s", body) + .isEqualTo(500); + assertThat(root.path("message").asText()) + .as("error message for empty album should be present, response=%s", body) + .isNotBlank(); + } + + private PlayMediaEntity seedMedia(String clerkId) { + String mediaId = "media-" + java.util.UUID.randomUUID().toString().substring(0, 16); + PlayMediaEntity entity = new PlayMediaEntity(); + entity.setId(mediaId); + entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); + entity.setOwnerType(MediaOwnerType.CLERK); + entity.setOwnerId(clerkId); + entity.setKind("image"); + entity.setStatus(MediaStatus.READY.getCode()); + entity.setUrl("https://oss.apitest/" + mediaId + ".png"); + mediaService.save(entity); + return entity; + } + + private void seedApprovedAsset(String clerkId, String mediaId, int orderIndex) { + PlayClerkMediaAssetEntity asset = new PlayClerkMediaAssetEntity(); + asset.setId("asset-" + mediaId); + asset.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); + asset.setClerkId(clerkId); + asset.setMediaId(mediaId); + asset.setUsage(ClerkMediaUsage.PROFILE.getCode()); + asset.setOrderIndex(orderIndex); + asset.setReviewState(ClerkMediaReviewState.APPROVED.getCode()); + asset.setDeleted(false); + mediaAssetService.save(asset); + } + + private void ensureTenantContext() { + SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); + } +} diff --git a/play-admin/src/test/java/com/starry/admin/api/WxClerkMediaControllerApiTest.java b/play-admin/src/test/java/com/starry/admin/api/WxClerkMediaControllerApiTest.java index bd2d79a..073f9a1 100644 --- a/play-admin/src/test/java/com/starry/admin/api/WxClerkMediaControllerApiTest.java +++ b/play-admin/src/test/java/com/starry/admin/api/WxClerkMediaControllerApiTest.java @@ -213,9 +213,23 @@ class WxClerkMediaControllerApiTest extends AbstractApiTest { assertThat(customerMediaIdsAfterFirst).containsAll(allMediaIds); List keptMedia = List.of(mediaIdA, mediaIdC); + + // 第二次提交:只删除与重新排序,不再生成新的资料审核记录,应直接生效 + long reviewCountBeforeSecond = dataReviewInfoService.lambdaQuery() + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, + ApiTestDataSeeder.DEFAULT_CLERK_ID) + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2") + .count(); submitAlbumUpdate(keptMedia, clerkToken); ensureTenantContext(); - approveLatestAlbumReview(); + long reviewCountAfterSecond = dataReviewInfoService.lambdaQuery() + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, + ApiTestDataSeeder.DEFAULT_CLERK_ID) + .eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2") + .count(); + assertThat(reviewCountAfterSecond) + .as("deleting/reordering album should not create another review record") + .isEqualTo(reviewCountBeforeSecond); List assetsAfterSecondApprove = mediaAssetService .listActiveByUsage(ApiTestDataSeeder.DEFAULT_CLERK_ID, ClerkMediaUsage.PROFILE); @@ -224,27 +238,14 @@ class WxClerkMediaControllerApiTest extends AbstractApiTest { .filter(a -> mediaIdA.equals(a.getMediaId())) .max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime)) .orElse(null); - PlayClerkMediaAssetEntity assetB = assetsAfterSecondApprove.stream() - .filter(a -> mediaIdB.equals(a.getMediaId())) - .max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime)) - .orElse(null); PlayClerkMediaAssetEntity assetC = assetsAfterSecondApprove.stream() .filter(a -> mediaIdC.equals(a.getMediaId())) .max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime)) .orElse(null); - PlayClerkMediaAssetEntity assetD = assetsAfterSecondApprove.stream() - .filter(a -> mediaIdD.equals(a.getMediaId())) - .max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime)) - .orElse(null); - - assertThat(assetA).isNotNull(); assertThat(assetA.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode()); - assertThat(assetC).isNotNull(); + assertThat(assetA.getOrderIndex()).isEqualTo(0); assertThat(assetC.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode()); - assertThat(assetB).isNotNull(); - assertThat(assetB.getReviewState()).isEqualTo(ClerkMediaReviewState.REJECTED.getCode()); - assertThat(assetD).isNotNull(); - assertThat(assetD.getReviewState()).isEqualTo(ClerkMediaReviewState.REJECTED.getCode()); + assertThat(assetC.getOrderIndex()).isEqualTo(1); PlayClerkUserInfoResultVo customerDetailAfterSecond = clerkUserInfoService.buildCustomerDetail(ApiTestDataSeeder.DEFAULT_CLERK_ID, ""); diff --git a/play-admin/src/test/java/com/starry/admin/modules/clerk/service/impl/PlayClerkDataReviewInfoServiceImplTest.java b/play-admin/src/test/java/com/starry/admin/modules/clerk/service/impl/PlayClerkDataReviewInfoServiceImplTest.java index c0c051e..45d4ed6 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/clerk/service/impl/PlayClerkDataReviewInfoServiceImplTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/clerk/service/impl/PlayClerkDataReviewInfoServiceImplTest.java @@ -1,5 +1,6 @@ package com.starry.admin.modules.clerk.service.impl; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -8,11 +9,15 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.starry.admin.common.exception.CustomException; import com.starry.admin.modules.clerk.enums.ClerkMediaUsage; import com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; +import com.starry.admin.modules.clerk.module.enums.ClerkDataType; +import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewReturnVo; import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo; import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService; import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService; @@ -49,7 +54,10 @@ class PlayClerkDataReviewInfoServiceImplTest { ReflectionTestUtils.setField(service, "playClerkUserInfoService", clerkUserInfoService); ReflectionTestUtils.setField(service, "clerkMediaAssetService", clerkMediaAssetService); ReflectionTestUtils.setField(service, "mediaService", mediaService); - doReturn(true).when(service).update(any(PlayClerkDataReviewInfoEntity.class)); + org.mockito.Mockito.lenient() + .doReturn(true) + .when(service) + .update(any(PlayClerkDataReviewInfoEntity.class)); } @Test @@ -105,6 +113,71 @@ class PlayClerkDataReviewInfoServiceImplTest { .hasMessageContaining("店员信息不存在"); } + @Test + void enrichDataContentWithMediaPreviewPopulatesCoverAndVideoUrls() { + PlayClerkDataReviewInfoServiceImpl impl = service; + + PlayClerkDataReviewReturnVo videoRow = new PlayClerkDataReviewReturnVo(); + videoRow.setId("row-video"); + videoRow.setDataTypeEnum(ClerkDataType.PHOTO_ALBUM); + videoRow.setDataContent(java.util.Collections.singletonList("media-video")); + + PlayClerkDataReviewReturnVo imageRow = new PlayClerkDataReviewReturnVo(); + imageRow.setId("row-image"); + imageRow.setDataTypeEnum(ClerkDataType.PHOTO_ALBUM); + imageRow.setDataContent(java.util.Collections.singletonList("media-image")); + + IPage page = new Page<>(1, 10); + page.setRecords(java.util.Arrays.asList(videoRow, imageRow)); + + PlayMediaEntity videoMedia = new PlayMediaEntity(); + videoMedia.setId("media-video"); + videoMedia.setKind(com.starry.admin.modules.media.enums.MediaKind.VIDEO.getCode()); + videoMedia.setUrl("https://oss/video.mp4"); + videoMedia.setCoverUrl("https://oss/video-cover.jpg"); + + PlayMediaEntity imageMedia = new PlayMediaEntity(); + imageMedia.setId("media-image"); + imageMedia.setKind(com.starry.admin.modules.media.enums.MediaKind.IMAGE.getCode()); + imageMedia.setUrl("https://oss/image.png"); + + when(mediaService.getById("media-video")).thenReturn(videoMedia); + when(mediaService.getById("media-image")).thenReturn(imageMedia); + + org.springframework.test.util.ReflectionTestUtils.invokeMethod( + impl, "enrichDataContentWithMediaPreview", page); + + assertThat(videoRow.getDataContent()).containsExactly("https://oss/video-cover.jpg"); + assertThat(videoRow.getMediaVideoUrls()).containsExactly("https://oss/video.mp4"); + + assertThat(imageRow.getDataContent()).containsExactly("https://oss/image.png"); + assertThat(imageRow.getMediaVideoUrls()).containsExactly((String) null); + } + + @Test + void enrichDataContentWithMediaPreviewFallsBackToLegacyUrlWhenMediaMissing() { + PlayClerkDataReviewInfoServiceImpl impl = service; + + String legacyUrl = "https://oss/legacy-only.png"; + PlayClerkDataReviewReturnVo row = new PlayClerkDataReviewReturnVo(); + row.setId("row-legacy"); + row.setDataTypeEnum(ClerkDataType.PHOTO_ALBUM); + row.setDataContent(java.util.Collections.singletonList(legacyUrl)); + + IPage page = new Page<>(1, 10); + page.setRecords(java.util.Collections.singletonList(row)); + + // 当无法通过 mediaId 查询到记录时,应当保持 dataContent 为原始 URL, + // 并且对应的 mediaVideoUrls 位置为 null。 + when(mediaService.getById(legacyUrl)).thenReturn(null); + + org.springframework.test.util.ReflectionTestUtils.invokeMethod( + impl, "enrichDataContentWithMediaPreview", page); + + assertThat(row.getDataContent()).containsExactly(legacyUrl); + assertThat(row.getMediaVideoUrls()).containsExactly((String) null); + } + private PlayClerkDataReviewInfoEntity buildReview(String id, String clerkId, String dataType, java.util.List payload) { PlayClerkDataReviewInfoEntity entity = new PlayClerkDataReviewInfoEntity(); diff --git a/play-admin/src/test/java/com/starry/admin/modules/clerk/service/impl/PlayClerkUserInfoServiceImplTest.java b/play-admin/src/test/java/com/starry/admin/modules/clerk/service/impl/PlayClerkUserInfoServiceImplTest.java index d3869cc..60e7fe0 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/clerk/service/impl/PlayClerkUserInfoServiceImplTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/clerk/service/impl/PlayClerkUserInfoServiceImplTest.java @@ -24,4 +24,23 @@ class PlayClerkUserInfoServiceImplTest { assertThat(merged.stream().map(MediaVo::getUrl)) .containsExactlyInAnyOrder("https://oss/2.png", "https://oss/1.png"); } + + @Test + void mergeLegacyAlbumFillsUsageStatusAndReviewStateForLegacyEntries() { + List legacy = Arrays.asList("https://oss/legacy-only.png"); + List destination = new ArrayList<>(); + + List merged = PlayClerkUserInfoServiceImpl.mergeLegacyAlbum(legacy, destination); + + assertThat(merged).hasSize(1); + MediaVo legacyVo = merged.get(0); + assertThat(legacyVo.getUrl()).isEqualTo("https://oss/legacy-only.png"); + assertThat(legacyVo.getId()) + .as("legacy media id should fallback to url for compatibility") + .isEqualTo("https://oss/legacy-only.png"); + assertThat(legacyVo.getUsage()).isEqualTo(com.starry.admin.modules.clerk.enums.ClerkMediaUsage.PROFILE.getCode()); + assertThat(legacyVo.getStatus()).isEqualTo(com.starry.admin.modules.media.enums.MediaStatus.READY.getCode()); + assertThat(legacyVo.getReviewState()) + .isEqualTo(com.starry.admin.modules.clerk.enums.ClerkMediaReviewState.APPROVED.getCode()); + } } diff --git a/play-admin/src/test/java/com/starry/admin/modules/weichat/service/MediaUploadServiceTest.java b/play-admin/src/test/java/com/starry/admin/modules/weichat/service/MediaUploadServiceTest.java index ecca67a..7ab9959 100644 --- a/play-admin/src/test/java/com/starry/admin/modules/weichat/service/MediaUploadServiceTest.java +++ b/play-admin/src/test/java/com/starry/admin/modules/weichat/service/MediaUploadServiceTest.java @@ -56,7 +56,6 @@ class MediaUploadServiceTest { MultipartFile file = buildImageFile("avatar.png", "image/png"); when(mediaService.normalizeAndSave(any())).thenAnswer(invocation -> invocation.getArgument(0)); - when(mediaService.updateById(any())).thenReturn(true); when(ossFileService.upload(any(), eq(clerk.getTenantId()), any())) .thenReturn("https://oss.mock/avatar.png");