Compare commits

...

3 Commits

Author SHA1 Message Date
irving
c29f76c2fc fix: random order clerk visibility rules
Some checks failed
Build and Push Backend / docker (push) Failing after 12s
2025-12-25 12:58:31 -05:00
irving
f300723fc0 fix: anonymize clerk random order views 2025-12-24 16:20:37 -05:00
hucs-dev
8dee4839e8 perf: 优化打赏动态页面接口 2025-12-22 18:16:55 +08:00
2 changed files with 216 additions and 4 deletions

View File

@@ -44,11 +44,9 @@ import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
import com.starry.admin.modules.weichat.entity.order.*;
import com.starry.admin.modules.weichat.service.NotificationSender;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import com.starry.admin.utils.DateRangeUtils;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.ConvertUtil;
@@ -323,6 +321,7 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getPlaceType, "2");
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getRefundType, OrderRefundFlag.NOT_REFUNDED.getCode());
lambdaQueryWrapper.selectAll(PlayOrderInfoEntity.class);
lambdaQueryWrapper.ne(PlayOrderInfoEntity::getOrderType, OrderConstant.OrderType.BLIND_BOX_PURCHASE.getCode());
lambdaQueryWrapper.orderByDesc(PlayOrderInfoEntity::getPurchaserTime);
// 查询陪聊表
lambdaQueryWrapper.selectAs(PlayClerkUserInfoEntity::getId, "clerkId")
@@ -477,6 +476,30 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
log.warn("Refund info missing for cancelled order, orderId={}", returnVo.getId());
}
}
// Privacy protection for random orders:
// 1) Pending random orders: hide customer info completely.
// 2) Random orders accepted by another clerk: hide customer info and mask status.
boolean isRandomOrder = OrderConstant.PlaceType.RANDOM.getCode().equals(returnVo.getPlaceType());
String orderStatus = returnVo.getOrderStatus();
String acceptBy = returnVo.getAcceptBy();
boolean isPending = OrderStatus.PENDING.getCode().equals(orderStatus);
boolean acceptedByOtherClerk = StringUtils.isNotEmpty(acceptBy) && !acceptBy.equals(clerkId) && !isPending;
if (isRandomOrder && isPending) {
returnVo.setWeiChatCode("");
returnVo.setCustomNickname("匿名用户");
returnVo.setCustomAvatar("");
returnVo.setCustomId("");
} else if (isRandomOrder && acceptedByOtherClerk) {
returnVo.setWeiChatCode("");
returnVo.setCustomNickname("匿名用户");
returnVo.setCustomAvatar("");
returnVo.setCustomId("");
// Hide concrete status when viewing another clerk's random order
returnVo.setOrderStatus("");
}
if (returnVo.getEstimatedRevenue() == null) {
returnVo.setEstimatedRevenue(BigDecimal.ZERO);
}
@@ -511,8 +534,21 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
.selectAs(PlayCustomLevelInfoEntity::getName, "customLevelName");
lambdaQueryWrapper.leftJoin(PlayCustomLevelInfoEntity.class, PlayCustomLevelInfoEntity::getId,
PlayCustomUserInfoEntity::getLevelId);
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()),
PlayClerkOrderListReturnVo.class, lambdaQueryWrapper);
IPage<PlayClerkOrderListReturnVo> page = this.baseMapper.selectJoinPage(
new Page<>(vo.getPageNum(), vo.getPageSize()),
PlayClerkOrderListReturnVo.class,
lambdaQueryWrapper);
for (PlayClerkOrderListReturnVo record : page.getRecords()) {
if (OrderConstant.PlaceType.RANDOM.getCode().equals(record.getPlaceType())
&& OrderStatus.PENDING.getCode().equals(record.getOrderStatus())) {
record.setCustomNickname("匿名用户");
record.setCustomAvatar("");
record.setCustomId("");
}
}
return page;
}
@Override

View File

@@ -181,6 +181,182 @@ class WxOrderInfoControllerApiTest extends WxCustomOrderApiTestSupport {
assertThat(data.path("customAvatar").asText()).isEmpty();
}
@Test
void queryByIdFromClerkControllerHidesCustomerInfoForPendingRandomOrders() throws Exception {
String marker = "privacy-leak-" + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker);
// Access via the generic Clerk Order Detail endpoint (which the notification likely links to)
MvcResult result = mockMvc.perform(get("/wx/clerk/order/queryById")
.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("weiChatCode").asText()).isEmpty();
assertThat(data.path("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
String nickname = data.path("customNickname").asText();
assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue();
assertThat(data.path("customAvatar").asText()).isEmpty();
}
@Test
void queryByIdForAcceptedRandomOrderOwnedByCurrentClerkRetainsCustomerInfoAndStatus() throws Exception {
String marker = "random-owned-" + LocalDateTime.now();
String orderId = createRandomOrder(marker);
// Mark the random order as accepted by the current test clerk
ensureTenantContext();
playOrderInfoService.lambdaUpdate()
.eq(PlayOrderInfoEntity::getId, orderId)
.set(PlayOrderInfoEntity::getAcceptBy, ApiTestDataSeeder.DEFAULT_CLERK_ID)
.set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.ACCEPTED.getCode())
.update();
MvcResult result = mockMvc.perform(get("/wx/clerk/order/queryById")
.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("acceptBy").asText()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
assertThat(data.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.ACCEPTED.getCode());
// 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(data.path("weiChatCode").asText())
.withFailMessage("Owner clerk should see customer wechat for accepted random order")
.isNotEmpty();
}
@Test
void queryByIdForRandomOrderAcceptedByAnotherClerkHidesCustomerInfoAndOrderStatus() throws Exception {
String marker = "random-other-clerk-" + LocalDateTime.now();
String orderId = createRandomOrder(marker);
ensureTenantContext();
playOrderInfoService.lambdaUpdate()
.eq(PlayOrderInfoEntity::getId, orderId)
.set(PlayOrderInfoEntity::getAcceptBy, OTHER_CLERK_ID)
.set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.ACCEPTED.getCode())
.update();
MvcResult result = mockMvc.perform(get("/wx/clerk/order/queryById")
.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("acceptBy").asText()).isEqualTo(OTHER_CLERK_ID);
// Order status must be masked for random orders owned by another clerk
assertThat(data.path("orderStatus").asText()).isEmpty();
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(data.path("customAvatar").asText()).isEmpty();
}
@Test
void queryByIdForNonRandomOrderAcceptedByAnotherClerkDoesNotMaskStatus() throws Exception {
String marker = "specified-other-clerk-" + LocalDateTime.now();
String orderId = createRandomOrder(marker);
// Re-tag the order as a specified order to ensure non-random logic
ensureTenantContext();
playOrderInfoService.lambdaUpdate()
.eq(PlayOrderInfoEntity::getId, orderId)
.set(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.SPECIFIED.getCode())
.set(PlayOrderInfoEntity::getAcceptBy, OTHER_CLERK_ID)
.set(PlayOrderInfoEntity::getOrderStatus, OrderConstant.OrderStatus.ACCEPTED.getCode())
.update();
MvcResult result = mockMvc.perform(get("/wx/clerk/order/queryById")
.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.SPECIFIED.getCode());
// Status should remain the real value for non-random orders
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("匿名用户", "匿名用户");
}
@Test
void clerkOrderListHidesCustomerInfoForPendingRandomOrders() throws Exception {
String marker = "privacy-list-" + LocalDateTime.now().toString();
String orderId = createRandomOrder(marker);
// Ensure the created pending random order appears in the clerk's own order list
ensureTenantContext();
playOrderInfoService.lambdaUpdate()
.eq(PlayOrderInfoEntity::getId, orderId)
.set(PlayOrderInfoEntity::getAcceptBy, ApiTestDataSeeder.DEFAULT_CLERK_ID)
.update();
ObjectNode payload = mapper.createObjectNode();
payload.put("pageNum", 1);
payload.put("pageSize", 10);
payload.put("orderStatus", OrderConstant.OrderStatus.PENDING.getCode());
payload.put("placeType", OrderConstant.PlaceType.RANDOM.getCode());
MvcResult result = mockMvc.perform(post("/wx/clerk/order/queryByPage")
.contentType(MediaType.APPLICATION_JSON)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.content(payload.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
JsonNode root = mapper.readTree(result.getResponse().getContentAsString());
JsonNode data = root.path("data");
JsonNode records = data.isArray() ? data : data.path("records");
assertThat(records.isArray()).isTrue();
assertThat(records.size()).isGreaterThan(0);
boolean found = false;
for (JsonNode node : records) {
assertThat(node.path("placeType").asText()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
assertThat(node.path("orderStatus").asText()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
if (orderId.equals(node.path("id").asText())) {
found = true;
String nickname = node.path("customNickname").asText();
assertThat(nickname.equals("匿名用户") || "匿名用户".equals(nickname)).isTrue();
assertThat(node.path("customAvatar").asText()).isEmpty();
assertThat(node.path("customId").asText()).isEmpty();
}
}
assertThat(found).as("Pending random order should appear in clerk list response").isTrue();
}
private String createRandomOrder(String remark) throws Exception {
ensureTenantContext();
resetCustomerBalance();