fix: mask random order details for non-owner clerks

This commit is contained in:
irving
2025-12-28 19:31:56 -05:00
parent c29f76c2fc
commit f39b560a05
2 changed files with 166 additions and 25 deletions

View File

@@ -122,25 +122,27 @@ public class WxOrderInfoController {
if (vo == null) { if (vo == null) {
throw new CustomException("订单不存在"); throw new CustomException("订单不存在");
} }
// Privacy protection: Hide customer info for pending random orders that current clerk hasn't accepted
String currentClerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId(); String currentClerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId();
if (OrderConstant.PlaceType.RANDOM.getCode().equals(vo.getPlaceType()) && OrderConstant.OrderStatus.PENDING.getCode().equals(vo.getOrderStatus())) { if (OrderConstant.PlaceType.RANDOM.getCode().equals(vo.getPlaceType())) {
// Random order pending - customer info already hidden by service layer boolean acceptedByCurrentClerk = StringUtils.isNotEmpty(vo.getAcceptBy())
vo.setWeiChatCode(""); && vo.getAcceptBy().equals(currentClerkId);
} else if (StringUtils.isNotEmpty(vo.getAcceptBy()) && !vo.getAcceptBy().equals(currentClerkId)) { if (!acceptedByCurrentClerk) {
// Order accepted by another clerk - hide WeChat and customer info maskRandomOrderForNonOwner(vo, currentClerkId);
vo.setWeiChatCode(""); }
vo.setCustomNickname("匿名用户");
vo.setCustomAvatar("");
vo.setCustomId("");
}
if(vo.getOrderStatus().equals("4")){
vo.setWeiChatCode("");
} }
return R.ok(vo); 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("");
}
}
/** /**
* 店员查询打赏动态 * 店员查询打赏动态
* *

View File

@@ -15,7 +15,9 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder; import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.common.task.OverdueOrderHandlerTask; 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;
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.PlayOrderContinueInfoEntity;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderContinueInfoService; 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.constant.Constants;
import com.starry.common.context.CustomSecurityContextHolder; import com.starry.common.context.CustomSecurityContextHolder;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
@@ -34,11 +40,21 @@ import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport { 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 MESSAGE_OPERATION_SUCCESS = "操作成功";
private static final String REVIEW_PENDING_STATE = "0"; private static final String REVIEW_PENDING_STATE = "0";
private static final String EXCLUDE_HISTORY_DISABLED = "0"; private static final String EXCLUDE_HISTORY_DISABLED = "0";
private static final String OTHER_CLERK_ID = "clerk-apitest-other"; 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; private static final int SINGLE_QUANTITY = 1;
@MockBean @MockBean
@@ -90,7 +106,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
@Test @Test
void selectRandomOrderByIdHidesCustomerContactWhenPending() throws Exception { void selectRandomOrderByIdHidesCustomerContactWhenPending() throws Exception {
String marker = "privacy-" + LocalDateTime.now().toString(); String marker = PRIVACY_MARKER_PREFIX + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker); String orderId = createRandomOrder(marker);
MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById") MvcResult result = mockMvc.perform(get("/wx/order/clerk/selectRandomOrderById")
@@ -153,7 +169,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
@Test @Test
void randomOrderAcceptedByAnotherClerkHidesSensitiveFields() throws Exception { void randomOrderAcceptedByAnotherClerkHidesSensitiveFields() throws Exception {
String marker = "privacy-accepted-" + LocalDateTime.now(); String marker = PRIVACY_MARKER_PREFIX + "accepted-" + LocalDateTime.now();
String orderId = createRandomOrder(marker); String orderId = createRandomOrder(marker);
ensureTenantContext(); ensureTenantContext();
@@ -177,13 +193,88 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
assertThat(data.path("weiChatCode").asText()).isEmpty(); assertThat(data.path("weiChatCode").asText()).isEmpty();
assertThat(data.path("customId").asText()).isEmpty(); assertThat(data.path("customId").asText()).isEmpty();
String nickname = data.path("customNickname").asText(); 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(); assertThat(data.path("customAvatar").asText()).isEmpty();
} }
@Test @Test
void queryByIdFromClerkControllerHidesCustomerInfoForPendingRandomOrders() throws Exception { void queryByIdFromClerkControllerHidesCustomerInfoForPendingRandomOrders() throws Exception {
String marker = "privacy-leak-" + LocalDateTime.now().toString(); String marker = PRIVACY_MARKER_PREFIX + "leak-" + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker); String orderId = createRandomOrder(marker);
// Access via the generic Clerk Order Detail endpoint (which the notification likely links to) // 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()); assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
String nickname = data.path("customNickname").asText(); 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(); 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 // For the owning clerk, customer info should not be forced to anonymous
String nickname = data.path("customNickname").asText(); String nickname = data.path("customNickname").asText();
assertThat(nickname).isNotEmpty(); assertThat(nickname).isNotEmpty();
assertThat(nickname).isNotIn("匿名用户", "匿名用户"); assertThat(nickname).isNotIn(ANON_NICKNAME, "匿名用户");
assertThat(data.path("weiChatCode").asText()) assertThat(data.path("weiChatCode").asText())
.withFailMessage("Owner clerk should see customer wechat for accepted random order") .withFailMessage("Owner clerk should see customer wechat for accepted random order")
.isNotEmpty(); .isNotEmpty();
@@ -271,7 +362,7 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
assertThat(data.path("weiChatCode").asText()).isEmpty(); assertThat(data.path("weiChatCode").asText()).isEmpty();
assertThat(data.path("customId").asText()).isEmpty(); assertThat(data.path("customId").asText()).isEmpty();
String nickname = data.path("customNickname").asText(); 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(); assertThat(data.path("customAvatar").asText()).isEmpty();
} }
@@ -304,12 +395,12 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.ACCEPTED.getCode()); 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 // We still expect customer info to not be forcibly anonymized by the random-order privacy rules
String nickname = data.path("customNickname").asText(); String nickname = data.path("customNickname").asText();
assertThat(nickname).isNotIn("匿名用户", "匿名用户"); assertThat(nickname).isNotIn(ANON_NICKNAME, "匿名用户");
} }
@Test @Test
void clerkOrderListHidesCustomerInfoForPendingRandomOrders() throws Exception { void clerkOrderListHidesCustomerInfoForPendingRandomOrders() throws Exception {
String marker = "privacy-list-" + LocalDateTime.now().toString(); String marker = PRIVACY_MARKER_PREFIX + "list-" + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker); String orderId = createRandomOrder(marker);
// Ensure the created pending random order appears in the clerk's own order list // 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())) { if (orderId.equals(node.path("id").asText())) {
found = true; found = true;
String nickname = node.path("customNickname").asText(); 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("customAvatar").asText()).isEmpty();
assertThat(node.path("customId").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("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID); payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
payload.put("commodityQuantity", SINGLE_QUANTITY); payload.put("commodityQuantity", SINGLE_QUANTITY);
payload.put("weiChatCode", "apitest-customer-wx"); payload.put("weiChatCode", CUSTOMER_WECHAT_CODE);
payload.put("excludeHistory", EXCLUDE_HISTORY_DISABLED); payload.put("excludeHistory", EXCLUDE_HISTORY_DISABLED);
payload.set("couponIds", mapper.createArrayNode()); payload.set("couponIds", mapper.createArrayNode());
payload.put("remark", remark); payload.put("remark", remark);
@@ -391,6 +482,54 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
return order.getId(); 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<Arguments> 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 { private void acceptOrder(String orderId) throws Exception {
mockMvc.perform(get("/wx/clerk/order/accept") mockMvc.perform(get("/wx/clerk/order/accept")
.param("id", orderId) .param("id", orderId)