diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxOrderInfoController.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxOrderInfoController.java index cb87f5a..252349b 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxOrderInfoController.java +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxOrderInfoController.java @@ -122,25 +122,27 @@ public class WxOrderInfoController { if (vo == null) { throw new CustomException("订单不存在"); } - // Privacy protection: Hide customer info for pending random orders that current clerk hasn't accepted String currentClerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId(); - if (OrderConstant.PlaceType.RANDOM.getCode().equals(vo.getPlaceType()) && OrderConstant.OrderStatus.PENDING.getCode().equals(vo.getOrderStatus())) { - // Random order pending - customer info already hidden by service layer - vo.setWeiChatCode(""); - } else if (StringUtils.isNotEmpty(vo.getAcceptBy()) && !vo.getAcceptBy().equals(currentClerkId)) { - // Order accepted by another clerk - hide WeChat and customer info - vo.setWeiChatCode(""); - vo.setCustomNickname("匿名用户"); - vo.setCustomAvatar(""); - vo.setCustomId(""); - } - - if(vo.getOrderStatus().equals("4")){ - vo.setWeiChatCode(""); + if (OrderConstant.PlaceType.RANDOM.getCode().equals(vo.getPlaceType())) { + boolean acceptedByCurrentClerk = StringUtils.isNotEmpty(vo.getAcceptBy()) + && vo.getAcceptBy().equals(currentClerkId); + if (!acceptedByCurrentClerk) { + maskRandomOrderForNonOwner(vo, currentClerkId); + } } return R.ok(vo); } + private void maskRandomOrderForNonOwner(PlayOrderDetailsReturnVo vo, String currentClerkId) { + vo.setWeiChatCode(""); + vo.setCustomNickname("匿名用户"); + vo.setCustomAvatar(""); + vo.setCustomId(""); + if (StringUtils.isNotEmpty(vo.getAcceptBy()) && !vo.getAcceptBy().equals(currentClerkId)) { + vo.setOrderStatus(""); + } + } + /** * 店员查询打赏动态 * diff --git a/play-admin/src/test/java/com/starry/admin/api/WxOrderInfoControllerApiTest.java b/play-admin/src/test/java/com/starry/admin/api/WxOrderInfoControllerApiTest.java index a7d26b7..196f41d 100644 --- a/play-admin/src/test/java/com/starry/admin/api/WxOrderInfoControllerApiTest.java +++ b/play-admin/src/test/java/com/starry/admin/api/WxOrderInfoControllerApiTest.java @@ -15,7 +15,9 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.starry.admin.common.apitest.ApiTestDataSeeder; import com.starry.admin.common.task.OverdueOrderHandlerTask; +import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; import com.starry.admin.modules.order.module.constant.OrderConstant; +import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus; import com.starry.admin.modules.order.module.entity.PlayOrderContinueInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; import com.starry.admin.modules.order.service.IPlayOrderContinueInfoService; @@ -24,9 +26,13 @@ import com.starry.admin.modules.weichat.service.WxCustomMpService; import com.starry.common.constant.Constants; import com.starry.common.context.CustomSecurityContextHolder; import java.time.LocalDateTime; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; @@ -34,11 +40,21 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { + private enum RandomOrderOwnership { + UNACCEPTED, + OWNED_BY_CURRENT, + OWNED_BY_OTHER + } + private static final String MESSAGE_OPERATION_SUCCESS = "操作成功"; private static final String REVIEW_PENDING_STATE = "0"; private static final String EXCLUDE_HISTORY_DISABLED = "0"; private static final String OTHER_CLERK_ID = "clerk-apitest-other"; + private static final String CUSTOMER_AVATAR_URL = "https://example.com/avatar.png"; + private static final String ANON_NICKNAME = "匿名用户"; + private static final String CUSTOMER_WECHAT_CODE = "apitest-customer-wx"; + private static final String PRIVACY_MARKER_PREFIX = "privacy-"; private static final int SINGLE_QUANTITY = 1; @MockBean @@ -90,7 +106,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { @Test void selectRandomOrderByIdHidesCustomerContactWhenPending() throws Exception { - String marker = "privacy-" + LocalDateTime.now().toString(); + String marker = PRIVACY_MARKER_PREFIX + LocalDateTime.now().toString(); String orderId = createRandomOrder(marker); MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById") @@ -153,7 +169,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { @Test void randomOrderAcceptedByAnotherClerkHidesSensitiveFields() throws Exception { - String marker = "privacy-accepted-" + LocalDateTime.now(); + String marker = PRIVACY_MARKER_PREFIX + "accepted-" + LocalDateTime.now(); String orderId = createRandomOrder(marker); ensureTenantContext(); @@ -177,13 +193,88 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { assertThat(data.path("weiChatCode").asText()).isEmpty(); assertThat(data.path("customId").asText()).isEmpty(); String nickname = data.path("customNickname").asText(); - assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue(); + assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue(); + assertThat(data.path("customAvatar").asText()).isEmpty(); + } + + @ParameterizedTest + @MethodSource("randomOrderMaskingCases") + void selectRandomOrderByIdAppliesPrivacyRules(OrderStatus status, + RandomOrderOwnership ownership, + boolean expectCustomerMasked, + boolean expectStatusMasked) throws Exception { + String marker = PRIVACY_MARKER_PREFIX + "matrix-" + status.name() + "-" + ownership.name() + "-" + LocalDateTime.now(); + String orderId = createRandomOrder(marker); + + ensureTenantContext(); + customUserInfoService.lambdaUpdate() + .set(PlayCustomUserInfoEntity::getAvatar, CUSTOMER_AVATAR_URL) + .eq(PlayCustomUserInfoEntity::getId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID) + .update(); + updateRandomOrderStatusAndOwner(orderId, status, ownership); + + MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById") + .param("id", orderId) + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andReturn(); + + JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data"); + assertThat(data.path("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode()); + + if (expectCustomerMasked) { + assertMaskedCustomerInfo(data); + } else { + assertVisibleCustomerInfo(data); + } + + if (expectStatusMasked) { + assertThat(data.path("orderStatus").asText()).isEmpty(); + } else { + assertThat(data.path("orderStatus").asText()).isEqualTo(status.getCode()); + } + } + + @Test + void selectRandomOrderByIdForCancelledRandomOrderMasksCustomerAvatarWhenNotAccepted() throws Exception { + String marker = PRIVACY_MARKER_PREFIX + "cancelled-" + LocalDateTime.now().toString(); + String orderId = createRandomOrder(marker); + + ensureTenantContext(); + customUserInfoService.lambdaUpdate() + .set(PlayCustomUserInfoEntity::getAvatar, CUSTOMER_AVATAR_URL) + .eq(PlayCustomUserInfoEntity::getId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID) + .update(); + playOrderInfoService.lambdaUpdate() + .eq(PlayOrderInfoEntity::getId, orderId) + .set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.CANCELLED.getCode()) + .update(); + + MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById") + .param("id", orderId) + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andReturn(); + + JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data"); + assertThat(data.path("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode()); + assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode()); + assertThat(data.path("weiChatCode").asText()).isEmpty(); + assertThat(data.path("customId").asText()).isEmpty(); + String nickname = data.path("customNickname").asText(); + assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue(); assertThat(data.path("customAvatar").asText()).isEmpty(); } @Test void queryByIdFromClerkControllerHidesCustomerInfoForPendingRandomOrders() throws Exception { - String marker = "privacy-leak-" + LocalDateTime.now().toString(); + String marker = PRIVACY_MARKER_PREFIX + "leak-" + LocalDateTime.now().toString(); String orderId = createRandomOrder(marker); // Access via the generic Clerk Order Detail endpoint (which the notification likely links to) @@ -203,7 +294,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode()); String nickname = data.path("customNickname").asText(); - assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue(); + assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue(); assertThat(data.path("customAvatar").asText()).isEmpty(); } @@ -236,7 +327,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { // For the owning clerk, customer info should not be forced to anonymous String nickname = data.path("customNickname").asText(); assertThat(nickname).isNotEmpty(); - assertThat(nickname).isNotIn("匿名用户", "匿名用户"); + assertThat(nickname).isNotIn(ANON_NICKNAME, "匿名用户"); assertThat(data.path("weiChatCode").asText()) .withFailMessage("Owner clerk should see customer wechat for accepted random order") .isNotEmpty(); @@ -271,7 +362,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { assertThat(data.path("weiChatCode").asText()).isEmpty(); assertThat(data.path("customId").asText()).isEmpty(); String nickname = data.path("customNickname").asText(); - assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue(); + assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue(); assertThat(data.path("customAvatar").asText()).isEmpty(); } @@ -304,12 +395,12 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.ACCEPTED.getCode()); // We still expect customer info to not be forcibly anonymized by the random-order privacy rules String nickname = data.path("customNickname").asText(); - assertThat(nickname).isNotIn("匿名用户", "匿名用户"); + assertThat(nickname).isNotIn(ANON_NICKNAME, "匿名用户"); } @Test void clerkOrderListHidesCustomerInfoForPendingRandomOrders() throws Exception { - String marker = "privacy-list-" + LocalDateTime.now().toString(); + String marker = PRIVACY_MARKER_PREFIX + "list-" + LocalDateTime.now().toString(); String orderId = createRandomOrder(marker); // Ensure the created pending random order appears in the clerk's own order list @@ -348,7 +439,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { if (orderId.equals(node.path("id").asText())) { found = true; String nickname = node.path("customNickname").asText(); - assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue(); + assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue(); assertThat(node.path("customAvatar").asText()).isEmpty(); assertThat(node.path("customId").asText()).isEmpty(); } @@ -366,7 +457,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID); payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID); payload.put("commodityQuantity", SINGLE_QUANTITY); - payload.put("weiChatCode", "apitest-customer-wx"); + payload.put("weiChatCode", CUSTOMER_WECHAT_CODE); payload.put("excludeHistory", EXCLUDE_HISTORY_DISABLED); payload.set("couponIds", mapper.createArrayNode()); payload.put("remark", remark); @@ -391,6 +482,54 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { return order.getId(); } + private void updateRandomOrderStatusAndOwner(String orderId, OrderStatus status, RandomOrderOwnership ownership) { + ensureTenantContext(); + var updater = playOrderInfoService.lambdaUpdate() + .eq(PlayOrderInfoEntity::getId, orderId) + .set(PlayOrderInfoEntity::getOrderStatus, status.getCode()); + if (ownership == RandomOrderOwnership.OWNED_BY_CURRENT) { + updater.set(PlayOrderInfoEntity::getAcceptBy, ApiTestDataSeeder.DEFAULT_CLERK_ID); + } else if (ownership == RandomOrderOwnership.OWNED_BY_OTHER) { + updater.set(PlayOrderInfoEntity::getAcceptBy, OTHER_CLERK_ID); + } + updater.update(); + } + + private void assertMaskedCustomerInfo(JsonNode data) { + assertThat(data.path("weiChatCode").asText()).isEmpty(); + assertThat(data.path("customId").asText()).isEmpty(); + String nickname = data.path("customNickname").asText(); + assertThat(nickname.equals(ANON_NICKNAME) || "匿名用户".equals(nickname)).isTrue(); + assertThat(data.path("customAvatar").asText()).isEmpty(); + } + + private void assertVisibleCustomerInfo(JsonNode data) { + assertThat(data.path("weiChatCode").asText()).isNotEmpty(); + assertThat(data.path("customId").asText()).isEqualTo(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); + String nickname = data.path("customNickname").asText(); + assertThat(nickname).isNotEmpty(); + assertThat(nickname).isNotIn(ANON_NICKNAME, "匿名用户"); + assertThat(data.path("customAvatar").asText()).isEqualTo(CUSTOMER_AVATAR_URL); + } + + private static Stream randomOrderMaskingCases() { + return Stream.of( + Arguments.of(OrderStatus.PENDING, RandomOrderOwnership.UNACCEPTED, true, false), + Arguments.of(OrderStatus.CANCELLED, RandomOrderOwnership.UNACCEPTED, true, false), + Arguments.of(OrderStatus.REVOKED, RandomOrderOwnership.UNACCEPTED, true, false), + Arguments.of(OrderStatus.ACCEPTED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false), + Arguments.of(OrderStatus.IN_PROGRESS, RandomOrderOwnership.OWNED_BY_CURRENT, false, false), + Arguments.of(OrderStatus.COMPLETED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false), + Arguments.of(OrderStatus.CANCELLED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false), + Arguments.of(OrderStatus.REVOKED, RandomOrderOwnership.OWNED_BY_CURRENT, false, false), + Arguments.of(OrderStatus.ACCEPTED, RandomOrderOwnership.OWNED_BY_OTHER, true, true), + Arguments.of(OrderStatus.IN_PROGRESS, RandomOrderOwnership.OWNED_BY_OTHER, true, true), + Arguments.of(OrderStatus.COMPLETED, RandomOrderOwnership.OWNED_BY_OTHER, true, true), + Arguments.of(OrderStatus.CANCELLED, RandomOrderOwnership.OWNED_BY_OTHER, true, true), + Arguments.of(OrderStatus.REVOKED, RandomOrderOwnership.OWNED_BY_OTHER, true, true) + ); + } + private void acceptOrder(String orderId) throws Exception { mockMvc.perform(get("/wx/clerk/order/accept") .param("id", orderId)